How to buy Fast Feed access
Generate an API key, deposit USDG, and buy a ticket each round from the Tickets contract to access the Fast Feed on Arbitrum One, automate renewals with the reference bot, and troubleshoot purchase reverts.
This guide is for searchers, propAMM operators, market makers, and anyone else who wants to read onchain data faster. It shows you how to subscribe to the on and keep your access across rounds.
It covers the full purchase path: hash your API key, deposit USDG, read the round state, call purchaseTickets, and automate the purchase with the reference bot. To open the connection and read the messages, see how to consume the Fast Feed.
The Fast Feed is a paid WebSocket stream that publishes each transaction as soon as the executes it, before its block reaches the free sequencer feed. The introduction to the Fast Feed explains what it publishes, who it serves, and how rounds and prices work.
The Fast Feed admits one WebSocket connection per ticket, and a ticket is valid for one round. To keep a connection open, you buy a new ticket every round from the Tickets contract on Arbitrum One.
Prerequisites
- You know how Fast Feed rounds and prices work. If not, read the introduction to the Fast Feed and the introduction to PGA, which sets the round cadence.
- An account on Arbitrum One that holds USDG to pay for tickets and ETH to pay for gas.
- An Arbitrum One JSON-RPC endpoint.
- Foundry's
cast, or any EVM client, to send transactions. The examples usecastand leave out--rpc-url $RPC_URL --private-key $PRIVATE_KEYto stay short.
Addresses and endpoints
| Item | Arbitrum One | Arbitrum Sepolia |
|---|---|---|
| Fast Feed endpoint | wss://arb1-txfeed.arbitrum.io/ | wss://sepolia-rollup-txfeed.arbitrum.io |
Tickets (proxy) | 0x451DD62C26b753d07d6104aaC6865AaE73a6A598 | 0x6f940179448e0a1C7BD8cfCB787555eb85cFC879 |
| Payment token | USDG: 0x004B506865409877C9fA29bfb1ebA929984B9bbC | TestToken (TEST): 0xca202b90454Db321256Cd84531Dd4929561d1e00 |
USDG has 6 decimals, so the 17 USD minimum price is 17000000 in the token's smallest unit. The first round started on September 23, 2026 at 17:00 UTC.
The examples below use $FEED_URL, $TICKETS, and $USDG for these values:
export FEED_URL=wss://arb1-txfeed.arbitrum.io/
export TICKETS=0x451DD62C26b753d07d6104aaC6865AaE73a6A598
export USDG=0x004B506865409877C9fA29bfb1ebA929984B9bbCTo test on Arbitrum Sepolia, export the Sepolia values instead. $USDG then holds the TestToken (TEST) address, so the commands in this guide work without changes:
export FEED_URL=wss://sepolia-rollup-txfeed.arbitrum.io
export TICKETS=0x6f940179448e0a1C7BD8cfCB787555eb85cFC879
export USDG=0xca202b90454Db321256Cd84531Dd4929561d1e00On Arbitrum Sepolia, a round lasts 10 minutes. TEST has 6 decimals and the minimum price is 17000000, as on Arbitrum One. Only Offchain Labs can mint TEST. To get some, post your Arbitrum Sepolia address in the Arbitrum Discord.
Step 1: Generate an API key and its hash
Your API key never goes onchain. You register its Keccak-256 hash with the ticket purchase, and you present the raw key when you connect to the Fast Feed. Anyone who holds the key can use your tickets, so treat it like a password.
-
Generate a random key and keep it private:
openssl rand -hex 32 -
Hash it:
cast keccak "<your-api-key>"Save the 32-byte output. You pass it as
apiKeyHashin every purchase.
The hash covers the UTF-8 bytes of the key string exactly as you send it in the Authorization header when you connect. Hash the exact string you send, with no 0x prefix and no surrounding whitespace.
You can bind several tickets to one key, in the same round or across rounds. The Fast Feed allows as many open connections per key as that key has active tickets.
Step 2: Deposit USDG
The contract debits ticket purchases from an internal balance, not from your wallet. Fund that balance before you buy.
-
Approve the contract to pull USDG.
amountis in the token's smallest unit, so 100 USDG is100000000, enough for five tickets at the 17 USDG minimum price:cast send $USDG "approve(address,uint256)" $TICKETS <amount> -
Move the tokens into your internal balance:
cast send $TICKETS "depositToken(uint256)" <amount> -
Confirm the balance:
cast call $TICKETS "tokenBalance(address)(uint256)" <your-address>
Deposit enough for several rounds. The price can rise by up to about two times between rounds, as the rounds and pricing section explains. You can withdraw an unused balance at any time with withdrawToken(amount).
Step 3: Read the round state
A purchase must name the round and the price you expect. The contract reverts if either value has changed, so you never pay a new round's higher price by accident. Read the live values right before you buy:
cast call $TICKETS "roundNumber()(uint256)"
cast call $TICKETS "currentPrice()(uint256)"
cast call $TICKETS "roundEnd()(uint256)"
cast call $TICKETS "grandfatherPeriodEnd()(uint256)"
cast call $TICKETS "grandfatherCount(address)(uint256)" <your-address>| View | What it tells you |
|---|---|
roundNumber | The current round. Pass it as expectedRound. |
currentPrice | The price of every ticket in this round. Pass it as expectedPrice. |
roundEnd | Unix time when the round ends and the tickets you buy now become active. |
grandfatherPeriodEnd | Unix time when the grandfather period ends. Before it, only buyers from the previous round can buy. |
grandfatherCount | How many tickets you can still buy during the grandfather period. It equals the tickets you held last round, minus those bought this round. |
ticketsSoldThisRound | Tickets sold so far. Compare it with maxTicketsPerRound to see how much room is left. |
Round state updates lazily: the first state-changing call in a new round . View functions compute the rolled-forward values on the fly, so the values you read are current.
Step 4: Buy tickets
Call purchaseTickets with the values from step 3:
cast send $TICKETS \
"purchaseTickets(uint256,uint256,uint256,bytes32)" \
<expectedRound> <expectedPrice> <numTicketsDesired> <apiKeyHash>The contract fills up to numTicketsDesired, capped by the room left in the round, and debits expectedPrice × filled from your internal balance. If the round is close to its cap, you receive and pay for fewer tickets than you asked for. The call reverts only if the round is already sold out.
Each purchase emits a TicketsPurchased(buyer, round, apiKeyHash, price, numTickets, numTicketsDesired) event. Read numTickets from the receipt to learn how many tickets you got.
Your tickets become active when the round ends. Until then, tickets from the previous round stay active. If you hold tickets in both rounds under the same key, your connection stays open across the round boundary.
Step 5: Keep the connection across rounds
Tickets bought in round N are active for round N+1 only, so an uninterrupted connection needs a purchase in every round:
- Buy the next round's ticket before
roundEnd, with the sameapiKeyHash. During the grandfather period you can renew up to the number of tickets you held. - Shortly after a round ends, the endpoint closes every connection whose key holds no ticket in the new round. A key with a ticket in both rounds keeps its connection.
If your connection closes, reconnect and continue from the current head.
To make the purchase step automatic, run the reference bot described in the next section.
Automate the purchase with the reference bot
Buying by hand every round does not scale. Offchain Labs publishes a TypeScript purchase bot in the feed-ticket-contracts repository. Each round, the bot:
- Reads the price, round boundaries, and your grandfather count.
- Skips the round if the price exceeds
MAX_PRICE_PER_TICKET. - Buys up to your grandfather count during the grandfather period, then buys the rest after the period ends.
- Before each transaction, estimates gas and waits until
gas × maxFeePerGasfits withinMAX_TRANSACTION_FEE. - Sleeps past the round boundary, with random jitter so that bots do not all act at once.
Configure it with environment variables:
| Variable | Required | Description |
|---|---|---|
RPC_URL | Yes | Arbitrum One JSON-RPC endpoint. |
PRIVATE_KEY | Yes | 0x-prefixed private key of the account that deposited USDG. |
TICKETS_ADDRESS | Yes | Tickets contract address. |
TICKETS_PER_ROUND | Yes | Number of tickets to buy each round. |
MAX_PRICE_PER_TICKET | Yes | Highest price per ticket you accept, in the token's smallest unit. |
MAX_TRANSACTION_FEE | Yes | Highest total gas fee per purchase, in wei. |
API_KEY_HASH | Yes | Keccak-256 hash of your API key, from step 1. |
MAX_SCHEDULE_JITTER_MS | No | Upper bound of the random delay added at round boundaries. Default 30000. |
GAS_POLL_INTERVAL_MS | No | How often to re-check gas while waiting for it to fit the budget. Default 10000. |
BOUNDARY_BUFFER_MS | No | Delay after a round or grandfather boundary before acting, so the chain has passed it. Default 2000. |
PRIORITY_FEE_PER_GAS | No | priority fee per gas, in wei. Default 0. |
BASE_FEE_BOOST_PERCENT | No | Percentage added to the latest base fee when computing maxFeePerGas. Default 20. |
Run it with Docker:
git clone https://github.com/OffchainLabs/feed-ticket-contracts.git
cd feed-ticket-contracts/bot
docker build -t purchase-bot .
docker run --rm \
-e RPC_URL=$RPC_URL \
-e PRIVATE_KEY=$PRIVATE_KEY \
-e TICKETS_ADDRESS=$TICKETS \
-e TICKETS_PER_ROUND=1 \
-e MAX_PRICE_PER_TICKET=<amount> \
-e MAX_TRANSACTION_FEE=<wei> \
-e API_KEY_HASH=<apiKeyHash> \
purchase-botTwo behaviors to plan for:
- The bot does not deposit funds. Keep the internal balance topped up with
depositToken, as in step 2. A purchase that finds an empty balance reverts, and the bot skips that round. - The bot exits on RPC or network errors. It treats a contract revert as a skipped round and continues, but any transport failure ends the process. Run it under a supervisor that restarts it, such as Docker
--restart=always, Kubernetes, or systemd.
Troubleshoot
Purchase reverts
| Revert | Cause | Fix |
|---|---|---|
RoundNumberMismatch(expected, actual) | The round advanced between your read and your transaction. | Re-read roundNumber and currentPrice, then retry. |
IncorrectTicketPrice(expected, actual) | The price changed, which happens at a round boundary. | Re-read currentPrice and decide whether the new price fits your budget. |
MaxTicketsSold() | The round is sold out. | Wait for the next round. Buy earlier in the round, or hold tickets so you can buy during the grandfather period. |
InsufficientTokenBalance(balance, required) | Your internal balance is below expectedPrice × numTicketsDesired. | Deposit more USDG with depositToken. |
NotEnoughGrandfatheredTickets(held, requested) | You asked for more tickets than your grandfather count during the grandfather period. | Request at most grandfatherCount, or wait until grandfatherPeriodEnd. |
BeforeFirstRoundStart() | The first round has not started. | Wait for the configured first round start. |
ZeroTicketsRequested() | numTicketsDesired was 0. | Request at least one ticket. |
FAQ
Which token pays for tickets? USDG on Arbitrum One. The token is fixed when the contract is deployed and cannot change. On Arbitrum Sepolia, tickets cost TEST, a test token that only Offchain Labs can mint. Ask for it in the Arbitrum Discord.
When does my ticket start working? At the end of the round in which you buy it. It stays active until the end of the next round.
What happens to money I do not spend?
It stays in your internal balance. Withdraw it at any time with withdrawToken(amount).
Next step
Once your ticket is active, consume the Fast Feed: open the WebSocket connection, read each message, and handle rejected or closed connections.
How is this guide?
Introduction to the Fast Feed
Learn what the Fast Feed publishes, who it serves, and how paid subscription access works.
Consume the Fast Feed
Open an authenticated WebSocket to the Fast Feed on Arbitrum One, read the fields of each message, reconnect after a drop, and troubleshoot rejected or closed connections.