Robinhood Chain Guide: Tokens & Events (2026)

Build on Robinhood Chain: read Stock Tokens, index Transfer events at 100 ms blocks, handle Arbitrum Nitro quirks. Measured on a live endpoint.

Robinhood Chain Guide: Tokens & Events (2026)

Robinhood Chain is an Arbitrum Nitro Layer 2 on Ethereum (chain ID 4663) built for tokenized stocks, ETFs and other real-world assets. For a developer it behaves like any EVM chain, with three things that change how you write code: blocks about every 100 ms, Stock Tokens that are plain ERC-20 contracts and finality that arrives in stages. We measured all three on a live endpoint on 25 September 2026 and re-checked them on public endpoints on 24 October.

This guide is the layer under the setup page. Endpoint URLs, MetaMask and the one-minute connect snippets live on the Robinhood Chain RPC endpoint page. Here we cover what you need once you are connected: reading Stock Tokens safely, indexing Transfer events at Robinhood volume, the Nitro differences that break naive code, WebSocket streams and when a trade counts as settled.


What Robinhood Chain is, for a developer

Robinhood Chain runs the Arbitrum Nitro stack. Execution happens on the L2, transaction data lands on Ethereum as blobs and gas is paid in ETH. According to Robinhood’s documentation, contracts written in Solidity or Vyper deploy without modification and standard tooling works as on Ethereum.

PropertyMainnetTestnet
Chain ID4663 (0x1237)46630
StackArbitrum NitroArbitrum Nitro
Gas tokenETHETH
Data availabilityEthereum blobsEthereum blobs
Explorerrobinhoodchain.blockscout.comexplorer.testnet.chain.robinhood.com

If you already build on Arbitrum One, the mental model carries over; the Arbitrum RPC guide covers the shared Nitro behaviour in more depth. What is different is the workload. Robinhood Chain is where Robinhood puts Stock Tokens, so the traffic is mostly token transfers, led by USDG and WETH, alongside Stock Token trades that settle through RFQ, around the clock.

Libraries already know the chain. viem ships robinhood and robinhoodTestnet in viem/chains, so you do not need a custom chain definition:

import { createPublicClient, http } from 'viem'
import { robinhood } from 'viem/chains'

const client = createPublicClient({
  chain: robinhood,
  transport: http('https://endpoints.boltrpc.io/rpc/YOUR_API_KEY/robinhood'),
})

const block = await client.getBlock()
console.log('L2 block:', block.number)
// Nitro adds l1BlockNumber to every block; viem's Block type does not declare it
const { l1BlockNumber } = block as unknown as { l1BlockNumber: `0x${string}` }
console.log('L1 block (Ethereum):', BigInt(l1BlockNumber))

Measured on BoltRPC: what the chain looks like from an endpoint

We ran every example in this guide against the BoltRPC Robinhood Chain endpoint on 25 September 2026. These are the numbers that shaped the code below.

Measurement (25 September 2026)Result
Block production365 blocks in 36.9 seconds: one block every 0.101 s
Chain time in 10,000 blocksabout 17 minutes
ERC-20 Transfer events, 3,000 blocks (about 5 minutes)32,999 events across 798 contracts
Busiest contracts in that windowUSDG 7,636 transfers, WETH 7,505
USDG Transfer events, 200,000 blocks (about 5.6 hours)585,340 events, fetched in 3 calls
Transactions in 200 consecutive blocks1,291: 990 type 2, 200 type 0x6a, 99 legacy, 2 type 4
WebSocket log stream, 10 seconds298 USDG transfers pushed
safe and finalized tags behind headabout 9 minutes and 15 minutes
debug_traceTransaction with callTracerreturns the full call tree
Archive readsUSDG balance at block 432 and 433 (0 before, 50 USDG after the first transfer)
eth_estimateGas, plain ETH transfer22,495 gas (Ethereum: 21,000)

The chain moves, so treat these as a snapshot. On public endpoints on 24 October we saw 0.104 to 0.106 s per block, 14,299 ERC-20 transfers in 3,000 blocks (USDG and WETH still first), 1,330 transactions in 200 blocks with one 0x6a in each and a plain transfer estimated at 21,257 gas.

Two things stand out. A block range that feels small on Ethereum covers very little time here. A single stablecoin can produce hundreds of thousands of events in an afternoon. Both change how you page through history.


Five differences from Ethereum that break naive code

Robinhood’s docs list the Nitro differences. These five are the ones we see bite first.

1. block.number inside a contract is the L1 block

Inside Solidity, block.number returns an estimate of the Ethereum block number, updated periodically. It is not a per-block counter on Robinhood Chain. Contracts that need the L2 height call the ArbSys precompile at 0x0000000000000000000000000000000000000064 (arbBlockNumber()).

Over RPC it is the other way round: eth_blockNumber returns the L2 height. Every block object also carries an extra l1BlockNumber field with the Ethereum block it was built against. In our run the head was L2 block 72,211,004 on L1 block 26,054,335. Store both if you reconcile against Ethereum events.

2. One system transaction opens every block

Each of the 200 blocks we sampled carried exactly one transaction of type 0x6a. In a 20 block spot check it was always the first one. That is the ArbOS internal transaction Nitro inserts at the start of every block. It is not a user action, so filter it out before you count transactions, compute fees or feed a dashboard:

const block = await client.getBlock({ includeTransactions: true })
const userTxs = block.transactions.filter((tx) => tx.typeHex !== '0x6a')

Without includeTransactions: true the block holds transaction hashes only, and the filter keeps everything.

3. Fees have two parts

A transaction pays L2 execution gas plus an L1 data fee for posting its bytes to Ethereum. eth_estimateGas returns a figure that already covers both: a plain ETH transfer estimated at 22,495 gas on 25 September and 21,257 on 24 October, where Ethereum charges 21,000. The extra moves with the price of L1 data. Always estimate rather than hard-coding a gas limit copied from Ethereum. For fee breakdowns, the ArbGasInfo precompile exposes the current L1 and L2 components.

4. Randomness and coinbase are constants

block.prevrandao and block.difficulty return a constant. block.coinbase is the network fee account. Neither works as a source of entropy or as a proposer address. Use a verifiable randomness oracle if a contract needs it.

5. Contracts can be larger

Robinhood Chain allows 96 KB of runtime code (Ethereum allows 24 KB) and 192 KB of init code. A contract that fails to deploy on Ethereum for size reasons can deploy here unchanged.


Reading Stock Tokens

Stock Tokens are standard ERC-20 contracts. symbol, decimals, balanceOf and Transfer events work exactly as they do for any token. Each Stock Token has a canonical address in Robinhood’s onchain asset registry, published on the token contracts page.

The canonical address is the part to take seriously. In the 20,000 blocks before our 24 October check, 11 other contracts emitted Transfer events under the exact symbol USDG. They use 18 decimals where the canonical USDG uses 6, so an app that trusts the symbol would show a balance a trillion times off, or worse, price a fake token as a real one. Robinhood’s docs say it plainly: a token with a matching ticker but a different address is not a Robinhood Stock Token.

Check the address, never the symbol:

import { createPublicClient, http, erc20Abi, getAddress } from 'viem'
import { robinhood } from 'viem/chains'

const client = createPublicClient({
  chain: robinhood,
  transport: http('https://endpoints.boltrpc.io/rpc/YOUR_API_KEY/robinhood'),
})

// Canonical addresses from docs.robinhood.com/chain/contracts
const CANONICAL = new Map([
  ['USDG', getAddress('0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168')],
  ['WETH', getAddress('0x0Bd7D308f8E1639FAb988df18A8011f41EAcAD73')],
])

async function readToken(address, holder) {
  const [symbol, decimals, balance] = await Promise.all([
    client.readContract({ address, abi: erc20Abi, functionName: 'symbol' }),
    client.readContract({ address, abi: erc20Abi, functionName: 'decimals' }),
    client.readContract({ address, abi: erc20Abi, functionName: 'balanceOf', args: [holder] }),
  ])
  const canonical = CANONICAL.get(symbol) === getAddress(address)
  return { symbol, decimals, balance, canonical }
}

Run against the canonical contract and one of the lookalikes, the same holder address returns { symbol: 'USDG', decimals: 6, canonical: true } for one and { symbol: 'USDG', decimals: 18, canonical: false } for the other. Load the full list of Stock Token addresses from the registry at startup and keep it in the same allowlist.

Common patterns on top of these reads: portfolio views that value NVDA, AAPL or SPY tokens in a user’s wallet, lending markets that accept Stock Tokens as collateral, index baskets that value each component from its price feed, plus swap flows through RFQ. Robinhood’s own example quotes NVDA against USDG through an RFQ and settles onchain.


Indexing Transfer events at 100 ms blocks

This is where Robinhood Chain differs most from the chains most indexers were tuned on. 10,000 blocks is 17 minutes of history. Our 200,000 block USDG backfill on 25 September covered about 5.6 hours and returned 585,340 events; the same span on 24 October still held 231,499. A pipeline that pages in fixed 2,000 block windows would make about 100 calls for that afternoon alone.

The BoltRPC gateway helps you size the window. It has no block-range cap, but one response has a size limit, and a filter that would exceed it is refused with an error that names the range that fits (docs). On 24 October a single 100,000-block USDG query returned 110,024 logs in one call. This is what a wider query returned on 25 September:

query returned more than 250000 results (~305,948 logs expected for 100000 blocks;
narrow to <= 73541 blocks for this filter)

By 24 October the gateway stated the limit in bytes instead (on Ethereum: query returned more than 268435456 bytes (... narrow to <= 4914 blocks for this filter)), with the same hint at the end. Read that number and retry with it, instead of halving blindly. Treat it as a ceiling, not a promise: in our test a window just under the hinted size still hit the request timeout. The pager below does exactly that. It halves only when an error carries no hint:

import { createPublicClient, http, parseAbiItem } from 'viem'
import { robinhood } from 'viem/chains'

const client = createPublicClient({
  chain: robinhood,
  transport: http('https://endpoints.boltrpc.io/rpc/YOUR_API_KEY/robinhood', {
    timeout: 60_000, // a wide eth_getLogs answer carries 100k+ logs
    maxResponseBodySize: 256 * 1024 * 1024, // viem caps response bodies by default
  }),
})

const transfer = parseAbiItem('event Transfer(address indexed from, address indexed to, uint256 value)')

async function* transfers(address, fromBlock, toBlock, window = 100_000n) {
  let start = fromBlock
  while (start <= toBlock) {
    const end = start + window - 1n > toBlock ? toBlock : start + window - 1n
    try {
      const logs = await client.getLogs({ address, event: transfer, fromBlock: start, toBlock: end })
      yield { start, end, logs }
      start = end + 1n
    } catch (err) {
      // The gateway names the window that fits: "narrow to <= 73541 blocks for this filter"
      const hint = /narrow to <= (\d+) blocks/.exec(err.details ?? err.message)
      window = hint ? BigInt(hint[1]) : window / 2n
      if (window < 1n) throw err
    }
  }
}

const USDG = '0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168'
const head = await client.getBlockNumber()
for await (const { start, end, logs } of transfers(USDG, head - 200_000n, head)) {
  console.log(`${start}-${end}: ${logs.length} logs`)
}

Two client settings matter as much as the window. viem’s HTTP transport defaults to a 10 second timeout and a 10 MB response cap. A 200,000 log answer is larger and takes longer than that. Without the two options above, viem aborts the request on its side, the pager falls back to halving and the backfill takes many more calls. With them, our 25 September run took 3.

Production rules that follow from the numbers:

  • Filter by address. An unfiltered Transfer query covered 798 contracts in five minutes on 25 September. Index the tokens you care about, each with its own cursor.
  • Store the cursor per contract, not per chain. USDG and a small Stock Token move at very different rates, so their safe window sizes differ by orders of magnitude.
  • Deduplicate on (transactionHash, logIndex) so a retried window never double counts.
  • Keep reorg handling in the tail. Treat anything newer than the safe block as provisional and re-read that tail on each pass (see finality below).

The general pattern, storage and retry logic are in our production eth_getLogs pipeline guide.


Live data over WebSocket

At 10 blocks per second, polling eth_blockNumber once a second misses nine heads out of ten and still spends a request every second. A WebSocket subscription pushes each head and each matching log as it lands. We compared the two approaches on other chains in WebSocket vs polling, measured.

import { createPublicClient, webSocket, parseAbiItem } from 'viem'
import { robinhood } from 'viem/chains'

const client = createPublicClient({
  chain: robinhood,
  transport: webSocket('wss://endpoints.boltrpc.io/ws/YOUR_API_KEY/robinhood'),
})

client.watchBlockNumber({
  onBlockNumber: (n) => console.log('head', n), // replace with your state update
})

client.watchEvent({
  address: '0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168', // canonical USDG
  event: parseAbiItem('event Transfer(address indexed from, address indexed to, uint256 value)'),
  onLogs: (logs) => console.log(logs.length, 'USDG transfers'), // replace with your handler
})

In a 10 second run on 25 September the log subscription pushed 298 USDG transfers; on 24 October, 80 to 241. Two habits keep a UI calm at that rate: batch updates and render once per animation frame or once per second. Subscribe to logs for the specific token addresses a view needs rather than every Transfer on the chain.


Finality: when a trade counts as settled

Per Robinhood’s finality documentation, a transaction moves through three stages:

StageWhat happenedTypical time
Soft confirmationThe sequencer ordered and executed it, you have a receiptsub-second
Posted to EthereumThe batch holding it sits in the L1 inbox, ordering is fixedminutes
Ethereum finalityThe L1 block with that batch is finalizedabout 13 minutes after posting

The standard block tags map roughly onto these stages. latest is the sequencer’s head. Per Arbitrum’s docs, safe follows the Ethereum block that two thirds of validators have attested to, which is usually resistant to reorgs, and finalized follows the Ethereum block that is finalized. When we measured, safe trailed the head by about 8 to 10 minutes and finalized by about 15.

async function settlement(hash) {
  const receipt = await client.getTransactionReceipt({ hash })
  const [safe, finalized] = await Promise.all([
    client.getBlock({ blockTag: 'safe' }),
    client.getBlock({ blockTag: 'finalized' }),
  ])
  if (receipt.blockNumber <= finalized.number) return 'final on Ethereum'
  if (receipt.blockNumber <= safe.number) return 'posted to Ethereum'
  return 'soft confirmation (sequencer)'
}

Use soft confirmations for interface feedback, safe for crediting internal balances and finalized for anything irreversible, such as releasing funds off-chain or booking a trade in a ledger. Withdrawals to Ethereum through the canonical bridge are a separate matter: they carry Arbitrum’s 7-day challenge period regardless of finality.


Debug traces and archive history

Two capabilities matter for RWA products in particular, because regulators, auditors and users all ask what happened and when.

Debug. debug_traceTransaction with the callTracer returns the full call tree of a transaction: every internal call, value transfer and revert. That is how you explain a failed RFQ settlement or trace a Stock Token moving through a vault. On Nitro the trace also carries beforeEVMTransfers and afterEVMTransfers, which show the fee payment and the gas refund around the execution.

curl https://endpoints.boltrpc.io/rpc/YOUR_API_KEY/robinhood \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"debug_traceTransaction","params":["0xYOUR_TX_HASH",{"tracer":"callTracer"}]}'

Archive. Balance and state reads at any historical block. We read the canonical USDG contract at block 432 and 433: the first recipient held 0 before the first transfer and 50 USDG after it. Portfolio history, cost basis, dividend eligibility snapshots and tax reports all need the balance at a past block, which a pruned node cannot answer. On BoltRPC the same endpoint serves live reads and full history, so the backfill above and the live stream share one URL and one key.


From testnet to mainnet

The testnet (chain ID 46630) runs the same Nitro stack with ETH as gas. viem’s robinhoodTestnet chain points at Robinhood’s public testnet RPC. Build and test there, then switch two values for production: the chain import (robinhood) and the transport URL (https://endpoints.boltrpc.io/rpc/YOUR_API_KEY/robinhood). Everything in this guide, from the token allowlist to the pager and the finality check, runs unchanged.

Robinhood’s own docs note that its public endpoints are rate-limited and not meant for production traffic. For production, a dedicated key gives you the WebSocket stream, archive reads and debug methods on one URL.


Why BoltRPC for Robinhood Chain

BoltRPC runs production RPC infrastructure with automatic failover. For Robinhood Chain that means:

  • One endpoint for live and history: full archive plus debug methods on the same key.
  • WebSocket included on every plan, for heads and filtered log streams.
  • A gateway that sizes your queries: a refused wide eth_getLogs call names a block window to retry with.
  • Request-unit pricing by method weight, so a Transfer backfill and a balance lookup are each billed for what they are; see pricing.
  • The same key on 35+ networks, including Arbitrum, if your app spans more than one chain.

Setup takes a minute on the Robinhood Chain endpoint page.


FAQ

What is Robinhood Chain?

Robinhood Chain is an Ethereum Layer 2 built by Robinhood on the Arbitrum Nitro stack, designed for tokenized stocks, ETFs and other real-world assets. It uses chain ID 4663, pays gas in ETH and posts its data to Ethereum as blobs.

Does MetaMask support Robinhood Chain?

Yes. Add it as a custom network with chain ID 4663, ETH as currency and your RPC URL; the full settings are on the Robinhood Chain endpoint page.

What is the Robinhood Chain block time?

About 100 milliseconds. We measured one block every 0.101 seconds on 25 September 2026 and 0.104 to 0.106 seconds on 24 October.

How long until a Robinhood Chain transaction is final?

The sequencer confirms it in under a second. The batch reaches Ethereum within minutes. Full Ethereum finality follows about 13 minutes after posting. Use the safe and finalized block tags to check which stage a transaction has reached.

Are Robinhood Stock Tokens ERC-20 tokens?

Yes. Stock Tokens are standard ERC-20 contracts, so balances, transfers and events work with any ERC-20 tooling. Always verify the contract address against Robinhood’s onchain asset registry, because other contracts can reuse the same ticker.

Build on Robinhood Chain

Archive, debug and WebSocket are included on Robinhood Chain. Run the pager above against your own key and watch the Usage page count each window.

Start your free 14-day trial

Questions we get about this.

Content is for informational purposes only and does not constitute financial, legal or technical advice. Code examples and configurations are provided as-is. Verify against official documentation and test in your own environment before deploying to production.

More from the blog.

All articles →
Pricing

Alchemy Alternative: What Switching Really Costs

Switching from Alchemy pays off for standard JSON-RPC at volume. A worked 50M-call bill, what you give up and the migration steps.

Oct 24, 2026 · 18 min read
RPC

eth_getLogs Block Range Limits by Chain, Measured

No standard eth_getLogs block range limit exists. We measured 15 chains on 24 October 2026, from 10 blocks to no cap, and what each -32005 error means.

Oct 24, 2026 · 32 min read
RPC

Migrating from Grove Portal: A Practical RPC Move

Grove has wound down and its rpc.grove.city endpoints no longer resolve. Map your endpoints, check chain coverage and move a production app without a rewrite.

Oct 24, 2026 · 7 min read