To get Robinhood Chain data for a DeFi strategy, read Uniswap v3 and v4 Swap logs and pool state from an archive RPC endpoint, stream new events over WebSocket, and backfill gaps with chunked eth_getLogs. Add an indexer for months of history, and use the Robinhood assets API and Chainlink feeds for Stock Tokens.
The hard part is speed. On 23 September 2026 we measured 1,000 blocks in 101 seconds, about 10 blocks per second, with roughly 1,600 Uniswap-v3-style Swap events in that window. A 500-block eth_getLogs window covers under a minute of chain time, and the public RPC returned HTTP 429 after about six quick calls and kept only about 10 minutes of historical state.
Every address below comes from Robinhood or Uniswap deployment docs, the Robinhood assets API, or the Uniswap v3 factory, and was checked with a live call.
| Data you need | Where it lives | How to get it | Freshness |
|---|---|---|---|
| Uniswap v3 swaps | Swap logs on each pool contract | eth_getLogs backfill, then WSS logs subscription | Every block (about 0.1 s, measured) |
| Uniswap v4 swaps | Swap logs on the PoolManager, keyed by PoolId | Same, filtered on topic1 = PoolId | Every block |
| Current pool price | v3 slot0(), v4 StateView.getSlot0() | eth_call | Latest block |
| Pool price at a past block | Same contracts, archive state | eth_call with blockNumber on an archive endpoint | Any historical block |
| Stock Token addresses | Robinhood assets registry | GET api.robinhood.com/rhj/assets | On request |
| Stock Token multiplier | uiMultiplier() on each token | eth_call | Latest block |
| Stock Token prices | One Chainlink feed per token | latestRoundData() | 24/5, holds last value when markets close |
| Internal calls, execution path | Node traces | debug_traceTransaction with callTracer | Per transaction |
| Block timestamps | Block headers (logs return 0x0) | eth_getBlockByNumber | Per block |
| Months of decoded history | Indexers, Dune | Goldsky, Envio, Ponder, Dune SQL | Depends on the indexer |
Robinhood Chain Data at a Glance
Robinhood Chain is the Arbitrum Nitro Layer 2 that Robinhood launched on mainnet on 1 July 2026. Reading its data has nothing to do with the Robinhood brokerage API or the Robinhood Crypto trading API. For background, see what Robinhood Chain is.
| Property | Value |
|---|---|
| Chain ID | 4663 (0x1237) mainnet, 46630 testnet |
| Stack | Arbitrum Nitro, settles to Ethereum using blobs |
| Gas token | ETH (live gas tracker) |
| Client | nitro/v3.12.0-rc.3 |
| Block rate | About 10 blocks/s measured on 23 Sep 2026. 0.18 s average since block 1 (30 Apr 2026) |
| Transaction ordering | First-come, first-served at the sequencer |
| Public RPC | https://rpc.mainnet.chain.robinhood.com, rate-limited, "not for production", not archive (about 10 minutes of state, measured) |
| Public sequencer feed | wss://feed.mainnet.chain.robinhood.com (Nitro feed protocol, not JSON-RPC) |
| Explorer | robinhoodchain.blockscout.com |
| Multicall3 | 0xca11bde05977b3631167028862be2a173976ca11. Robinhood's own Multicall: 0x2cAC2D899eCC914d704FeaAE33ac1bF36277DaD1 |
Robinhood's docs give no official block time. Arbitrum describes "configurable block times... to achieve 100ms latency", and the numbers above are our measurements.
Four Nitro behaviours break Ethereum parsers:
- Logs return
blockTimestamp: "0x0", so take timestamps from block headers and cache them per block. - Receipts add
gasUsedForL1andl1BlockNumber, and blocks addl1BlockNumberandsendCount, which strict schema validators must allow. - Each block opens with an internal transaction of type 106. Skip types 100 to 106 when counting user transactions.
- Solidity
block.numberreturns an L1 estimate. UseArbSys(0x64).arbBlockNumber()for the L2 block.
Connect With viem
viem ships a robinhood chain definition (checked on viem 2.56.8) with chain ID 4663 and Multicall3 preconfigured. You need Node.js with ESM for top-level await, TypeScript, viem 2.x and a Dwellir API key on a paid plan, because the Free plan excludes eth_getLogs.
WSS comes first in the fallback transport, so viem's watch* actions open real eth_subscribe subscriptions. HTTPS on the same archive backend comes second. The public RPC is a last resort, and it cannot serve the historical reads later in this guide.
// config.ts
import { createPublicClient, fallback, http, webSocket } from 'viem'
import { robinhood } from 'viem/chains' // chain ID 4663, Multicall3 included
export const HTTP_URL = 'https://api-robinhood-mainnet-archive.n.dwellir.com/YOUR_API_KEY'
export const WSS_URL = 'wss://api-robinhood-mainnet-archive.n.dwellir.com/YOUR_API_KEY'
export const client = createPublicClient({
chain: robinhood,
transport: fallback([
// First transport is WSS, so watch* actions subscribe instead of polling
webSocket(WSS_URL, { keepAlive: true, reconnect: true }),
// Same archive backend over HTTPS if the socket is down
http(HTTP_URL, { batch: true }),
// Last resort: rate-limited, no archive, no debug_*, errors above 10,000 matched logs
http('https://rpc.mainnet.chain.robinhood.com'),
]),
})
// Chain 4663 addresses from Robinhood/Uniswap docs, the assets API and factory.getPool
export const POOL_MANAGER = '0x8366a39cc670b4001a1121b8f6a443a643e40951' // Uniswap v4
export const STATE_VIEW = '0xf3334192d15450cdd385c8b70e03f9a6bd9e673b' // Uniswap v4
export const WETH_USDG_V3 = '0x69BfaF19C9f377BB306a89aEd9F6B07e2c1a8d9a' // v3, 0.05% fee
export const NVDA = '0xd0601CE157Db5bdC3162BbaC2a2C8aF5320D9EEC' // Stock Token
Stateful filters from eth_newFilter live on one backend, so keep them on one WSS connection or preserve the DWSESSION cookie over HTTP. eth_getLogs with explicit block ranges is simpler and survives reconnects.
Backfill With eth_getLogs, Then Stream
A subscription only delivers events from the moment it opens. Everything earlier, and everything missed during a reconnect, comes from eth_getLogs, and at about 10 blocks per second the range limits decide how you build that backfill.
The block-rate arithmetic
At the measured rate of roughly 850,000 blocks per day, each Dwellir eth_getLogs range cap covers this much for one filter:
| Plan | Block range cap | Chain time per call | Calls per day of history | Calls per 30 days |
|---|---|---|---|---|
| Developer ($49/mo, 100 RPS) | 500 blocks | About 50 seconds | About 1,700 | About 51,000 |
| Growth ($299/mo, 500 RPS) | 10,000 blocks | About 17 minutes | About 85 | About 2,550 |
| Scale ($999/mo, 5,000 RPS) | 10,000 blocks | About 17 minutes | About 85 | About 2,550 |
Dwellir bills one credit per RPC response with no compute-unit weighting, so a 30-day single-pool backfill on Developer costs about 51,000 credits. Parallel windows up to your plan's RPS shorten the wall-clock time.
The public RPC limits result count instead: a query matching more than 10,000 logs fails with "logs matched by query exceeds limit of 10000". At about 1,600 Swap events per 1,000 blocks, an unfiltered v3 Swap topic query hits that cap after roughly 6,000 blocks. It is also not an archive node, so it cannot serve state reads older than about 10 minutes.
Deep history is where RPC stops being the right tool. At the 0.18 s average, the chain has produced roughly 70 million blocks, about 140,000 calls per filter in 500-block windows. Use an indexer or Envio HyperSync for that, and keep RPC for the live edge and point-in-time reads.
Adaptive backfill with a checkpoint
The loop halves the window when the provider rejects a range or result count, grows it back after each success, and checkpoints the last fully processed block, so a crash costs at most one window of rework.
// backfill.ts
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
import { client, WETH_USDG_V3 } from './config'
import { V3_SWAP, type SwapLog } from './swaps'
const MAX_SPAN = 500n // Dwellir Developer cap. Use 10_000n on Growth or Scale.
const FILE = './checkpoint.json'
export const loadCheckpoint = (fallback: bigint): bigint =>
existsSync(FILE) ? BigInt(JSON.parse(readFileSync(FILE, 'utf8')).block) : fallback
export const saveCheckpoint = (block: bigint) =>
writeFileSync(FILE, JSON.stringify({ block: block.toString() }))
// Range and result-count errors are worded differently by each provider
const isRangeError = (err: unknown) =>
/range|limit|exceed|too many|too large/i.test(String((err as Error)?.message ?? err))
export async function backfill(from: bigint, to: bigint, onLogs: (logs: SwapLog[]) => void) {
let span = MAX_SPAN
let cursor = from
while (cursor <= to) {
const end = cursor + span - 1n < to ? cursor + span - 1n : to
try {
const logs = await client.getLogs({
address: WETH_USDG_V3, // filter by pool: v3 forks emit the same Swap topic
event: V3_SWAP,
fromBlock: cursor,
toBlock: end,
strict: true,
})
onLogs(logs)
saveCheckpoint(end) // last fully processed block
cursor = end + 1n
span = span * 2n > MAX_SPAN ? MAX_SPAN : span * 2n // grow back after a dense stretch
} catch (err) {
if (span === 1n || !isRangeError(err)) throw err
span /= 2n // rejected: halve the window and retry the same start block
}
}
}
Stream over WebSocket and resume from the checkpoint
Dwellir's WSS endpoint supports eth_subscribe for newHeads and logs. newPendingTransactions returns a subscription ID but delivers nothing. viem reconnects the socket but does not replay missed events, and a silent reconnect may never reach onError. So subscribe first, backfill from the checkpoint, drain the buffer, then go live, and keep a periodic eth_getLogs sweep from the checkpoint as the safety net.

// stream.ts
import { client, WETH_USDG_V3 } from './config'
import { V3_SWAP, type SwapLog } from './swaps'
import { backfill, loadCheckpoint, saveCheckpoint } from './backfill'
const seen = new Set<string>() // bound this to recent blocks in production
function handle(log: SwapLog) {
const key = `${log.blockHash}:${log.logIndex}` // blockHash, not blockNumber, so reorgs are safe
if (log.removed) {
seen.delete(key) // orphaned by a reorg: undo anything derived from it
return
}
if (seen.has(key)) return // overlap between backfill and live stream
seen.add(key)
saveCheckpoint(log.blockNumber)
// strategy logic goes here
}
export async function run(): Promise<void> {
const buffer: SwapLog[] = []
let live = false
let restarting = false
// 1. Subscribe first, so events that land during the backfill are buffered
const unwatch = client.watchContractEvent({
address: WETH_USDG_V3,
abi: [V3_SWAP],
eventName: 'Swap',
strict: true,
onLogs: (logs) => {
if (live) logs.forEach(handle)
else buffer.push(...logs)
},
onError: () => {
if (restarting) return
restarting = true
unwatch()
setTimeout(run, 2_000) // resubscribe, then backfill from the checkpoint
},
})
// 2. Fill the gap from the checkpoint block. Dedupe covers overlap within a run;
// keep strategy handlers idempotent across restarts
const head = await client.getBlockNumber()
await backfill(loadCheckpoint(head - 500n), head, (logs) => logs.forEach(handle))
// 3. Drain what arrived during the backfill, then handle events as they come
buffer.forEach(handle)
live = true
}
run()
Add a staleness watchdog with client.watchBlockNumber. At about 10 blocks per second, a few seconds without a new head points to a stalled socket, and tearing it down triggers the same backfill path. For gaps the watchdog cannot see, such as a quick silent reconnect, run backfill(checkpoint, head) on a timer every minute or so; dedupe makes the overlap harmless. The WebSockets guide covers reconnection and dedupe in more depth.
For the earliest view of ordering, the Nitro sequencer feed streams ordered transactions in the Nitro feed protocol rather than JSON-RPC. Dwellir offers authenticated access; see the sequencer feed guide in the Robinhood Chain docs.
Reorg safety with safe and finalized
Robinhood Chain finality has three stages: soft confirmation in under a second when the sequencer orders the transaction, the batch posted to Ethereum minutes later, and Ethereum finality about 13 minutes after that. A soft-confirmed block reorgs only if the sequencer posts a different ordering, which is rare. In one observation on 23 September 2026, safe lagged latest by about 12 minutes and finalized by about 19.
Act on latest for trading signals. For a ledger or backtest dataset, commit only up to safe or finalized, or treat the last 20 minutes as provisional and reconcile on logs with removed: true.
const [safe, finalized] = await Promise.all([
client.getBlock({ blockTag: 'safe' }),
client.getBlock({ blockTag: 'finalized' }),
])
console.log(safe.number, finalized.number) // commit your dataset up to one of these
What is blockchain finality explains the two tags.
Reading Uniswap v3 and v4 Pools
Uniswap carries the bulk of on-chain AMM volume on Robinhood Chain: we counted 5,830 v4 Swap events across 847 pools in 3,000 blocks. Lighter (order book), Arcus (off-chain matching engine), Rialto, and RFQ routes through 0x, 1inch Fusion and LiFi are also live but need their own integrations.
| Contract | Address |
|---|---|
| v4 PoolManager | 0x8366a39cc670b4001a1121b8f6a443a643e40951 |
| v4 StateView | 0xf3334192d15450cdd385c8b70e03f9a6bd9e673b |
| V4Quoter | 0x8dc178efb8111bb0973dd9d722ebeff267c98f94 |
| UniswapV3Factory | 0x1f7d7550b1b028f7571e69a784071f0205fd2efa |
| QuoterV2 (v3) | 0x33e885ed0ec9bf04ecfb19341582aadcb4c8a9e7 |
| UniswapV2Factory | 0x8bceaa40b9acdfaedf85adf4ff01f5ad6517937f |
| WETH (18 decimals) | 0x0Bd7D308f8E1639FAb988df18A8011f41EAcAD73 |
| USDG (6 decimals) | 0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168 |
| v3 WETH/USDG 0.05% pool | 0x69BfaF19C9f377BB306a89aEd9F6B07e2c1a8d9a |
| v3 NVDA/USDG 0.05% pool | 0xd4EB21209C4D6093f80B5b84f5C45cc093EA14a3 |
Swap events and the sign flip
| Uniswap v3 | Uniswap v4 | |
|---|---|---|
| Emitted by | Each pool contract | The PoolManager only |
| Filter on | Pool address | topic1 = PoolId |
| topic0 | 0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67 | 0x40e9cecb9f5f1f1c5b9c97dec2917b7ee92e57ba5563708daca94dd84ad7112f |
| Amount types | int256 | int128, plus a uint24 fee field |
| Sign perspective | The pool's | The swapper's |
amount0 > 0 means | token0 went into the pool | The swapper received token0 |
This is the key gotcha. The v4 natspec calls the amounts the "delta of the pool balance", which is misleading. In v3, a positive amount0 meant token0 entered the pool on 539 of 539 consecutive swaps we checked. In v4, a negative amount0 meant the swapper paid token0 into the pool on 4,982 of 4,983. Reuse v3 decoding on v4 and every buy becomes a sell.
A second v3-style factory at 0x5481864ddd46a2d798df0925c23b7846e776e5e3 is not Uniswap. Other v3 forks emit the identical Swap topic, so filter by pool address or compare factory() on the emitting contract to the UniswapV3Factory address.

Decode swaps and compute price
The price of token0 in token1 is (sqrtPriceX96 / 2^96)^2 × 10^(decimals0 - decimals1). token0 is the lower address, so read token0() rather than assuming. In the WETH/USDG pool, WETH is token0 and the 10^12 adjustment gives USDG per WETH.
// swaps.ts
import { formatUnits, parseAbi, parseAbiItem } from 'viem'
export const V3_SWAP = parseAbiItem(
'event Swap(address indexed sender, address indexed recipient, int256 amount0, int256 amount1, uint160 sqrtPriceX96, uint128 liquidity, int24 tick)'
)
export const V4_SWAP = parseAbiItem(
'event Swap(bytes32 indexed id, address indexed sender, int128 amount0, int128 amount1, uint160 sqrtPriceX96, uint128 liquidity, int24 tick, uint24 fee)'
)
export const v3PoolAbi = parseAbi([
'function token0() view returns (address)',
'function token1() view returns (address)',
'function factory() view returns (address)',
'function slot0() view returns (uint160 sqrtPriceX96, int24 tick, uint16 observationIndex, uint16 observationCardinality, uint16 observationCardinalityNext, uint8 feeProtocol, bool unlocked)',
])
export type SwapLog = {
blockNumber: bigint
blockHash: `0x${string}`
logIndex: number
transactionHash: `0x${string}`
removed: boolean
args: { amount0: bigint; amount1: bigint; sqrtPriceX96: bigint }
}
// Normalise both versions to the pool's perspective: positive = entered the pool
export const poolDeltas = (version: 'v3' | 'v4', amount0: bigint, amount1: bigint) =>
version === 'v3'
? { in0: amount0, in1: amount1 } // v3 already reports the pool's side
: { in0: -amount0, in1: -amount1 } // v4 reports the swapper's side: flip it
// token1 per 1 token0 in human units. Fine for signals; use bigint math for accounting
export function sqrtPriceToPrice(sqrtPriceX96: bigint, dec0: number, dec1: number) {
const ratio = Number(sqrtPriceX96) / 2 ** 96
return ratio * ratio * 10 ** (dec0 - dec1)
}
export function describeSwap(in0: bigint, in1: bigint, dec0: number, dec1: number) {
const abs = (x: bigint) => (x < 0n ? -x : x)
const a0 = Number(formatUnits(abs(in0), dec0))
const a1 = Number(formatUnits(abs(in1), dec1))
return { side: in0 > 0n ? 'sold token0' : 'bought token0', a0, a1, execPrice: a1 / a0 }
}
Applied to recent swaps on the WETH/USDG pool:
// v3-swaps.ts
import { erc20Abi } from 'viem'
import { client, WETH_USDG_V3 } from './config'
import { V3_SWAP, v3PoolAbi, poolDeltas, describeSwap, sqrtPriceToPrice } from './swaps'
const [token0, token1] = await Promise.all([
client.readContract({ address: WETH_USDG_V3, abi: v3PoolAbi, functionName: 'token0' }),
client.readContract({ address: WETH_USDG_V3, abi: v3PoolAbi, functionName: 'token1' }),
])
const [dec0, dec1] = await Promise.all(
[token0, token1].map((address) =>
client.readContract({ address, abi: erc20Abi, functionName: 'decimals' })
)
)
const head = await client.getBlockNumber()
const logs = await client.getLogs({
address: WETH_USDG_V3, event: V3_SWAP, fromBlock: head - 499n, toBlock: head, strict: true,
})
for (const { args, blockNumber } of logs) {
const { in0, in1 } = poolDeltas('v3', args.amount0, args.amount1)
const s = describeSwap(in0, in1, dec0, dec1)
// execPrice = what this trade paid; pool price = where the pool ended up after it
console.log(blockNumber, s.side, s.a0, s.a1, s.execPrice, sqrtPriceToPrice(args.sqrtPriceX96, dec0, dec1))
}
Current v4 price with StateView
v4 pools have no contract of their own, so live state comes from StateView's getSlot0(poolId), which returns sqrtPriceX96, tick, protocolFee and lpFee. The PoolId is keccak256(abi.encode(PoolKey)) over currency0, currency1, fee, tickSpacing and hooks.
// v4-pools.ts
import { encodeAbiParameters, keccak256, parseAbi, parseAbiParameters, type Address } from 'viem'
import { client, POOL_MANAGER, STATE_VIEW } from './config'
import { V4_SWAP, poolDeltas } from './swaps'
const stateViewAbi = parseAbi([
'function getSlot0(bytes32 poolId) view returns (uint160 sqrtPriceX96, int24 tick, uint24 protocolFee, uint24 lpFee)',
])
// PoolId for a known PoolKey. currency0 = 0x000...000 means native ETH
export const poolIdOf = (c0: Address, c1: Address, fee: number, tickSpacing: number, hooks: Address) =>
keccak256(encodeAbiParameters(
parseAbiParameters('address, address, uint24, int24, address'),
[c0, c1, fee, tickSpacing, hooks],
))
// Take a PoolId from a recent swap. Every v4 pool emits through the PoolManager
const head = await client.getBlockNumber()
const [swap] = await client.getLogs({
address: POOL_MANAGER, event: V4_SWAP, fromBlock: head - 100n, toBlock: head, strict: true,
})
if (!swap) throw new Error('no v4 swaps in the last 100 blocks')
const { in0 } = poolDeltas('v4', swap.args.amount0, swap.args.amount1)
console.log(swap.args.id, in0 > 0n ? 'swapper paid token0' : 'swapper paid token1', swap.args.fee)
const [sqrtPriceX96, tick, protocolFee, lpFee] = await client.readContract({
address: STATE_VIEW, abi: stateViewAbi, functionName: 'getSlot0', args: [swap.args.id],
})
console.log({ sqrtPriceX96, tick, protocolFee, lpFee })
// From here on, filter one pool with topic1 = PoolId
const poolSwaps = await client.getLogs({
address: POOL_MANAGER, event: V4_SWAP, args: { id: swap.args.id }, fromBlock: head - 499n, toBlock: head,
})
console.log(poolSwaps.length)
A PoolId's currencies, and so its decimals, come from the Initialize event: Initialize(bytes32 indexed id, address indexed currency0, address indexed currency1, uint24 fee, int24 tickSpacing, address hooks, uint160 sqrtPriceX96, int24 tick). Build the PoolId-to-PoolKey map once from an indexer or HyperSync, then keep it current with a PoolManager logs subscription on Initialize.
Historical price at a past block
Backtests need pool state as it was. Pass blockNumber to any read, which requires an archive node. Dwellir's Robinhood endpoint serves archive state for fixed block parameters. The public RPC does not: on 23 September 2026, historical eth_call there failed with "historical state … is not available" for blocks more than about 6,100 back (roughly 10 minutes), while a read 1,000 blocks back succeeded.
// historical.ts
import { client, WETH_USDG_V3 } from './config'
import { v3PoolAbi, sqrtPriceToPrice } from './swaps'
const past = (await client.getBlockNumber()) - 36_000n // about an hour back at ~10 blocks/s. Needs an archive node: the public RPC keeps ~10 minutes of state
const [[sqrtPriceX96, tick], block] = await Promise.all([
client.readContract({ address: WETH_USDG_V3, abi: v3PoolAbi, functionName: 'slot0', blockNumber: past }),
client.getBlock({ blockNumber: past }), // timestamps come from headers, not logs
])
// token0 = WETH (18), token1 = USDG (6), so this prints USDG per WETH
console.log(new Date(Number(block.timestamp) * 1000).toISOString(), tick, sqrtPriceToPrice(sqrtPriceX96, 18, 6))
The same blockNumber parameter works on StateView.getSlot0 for v4 pools. If the fallback transport drops to the public RPC, reads this far back will fail.
Stock Token Data: Multipliers, Addresses, Prices
Robinhood Stock Tokens are 18-decimal ERC-20s. Splits and dividends go through ERC-8056 Scaled UI Amount: raw balances stay unchanged and an on-chain multiplier moves instead. Each token exposes uiMultiplier() (1e18 = 1.0), balanceOfUI(), newUIMultiplier() and effectiveAt(), and emits UIMultiplierUpdated(oldMultiplier, newMultiplier, effectiveAtTimestamp). Its parameter types are undocumented, so take the event from the verified ABI on Blockscout rather than hardcoding topic0.
Same-ticker impostor tokens exist, so never trust an on-chain symbol(). Resolve addresses from the Robinhood registry at api.robinhood.com/rhj/assets (195 assets on 23 September 2026), which returns tokenSymbol, deployments, currentMultiplier, pendingMultiplier and tradingCapabilities per asset.
// stock-tokens.ts
import { formatUnits, parseAbi } from 'viem'
import { client, NVDA } from './config'
type Asset = {
tokenSymbol: string
deployments: { chainId: number | string; contractAddress: string }[]
currentMultiplier: unknown
pendingMultiplier: unknown
}
// 1. Build an address -> asset map for chain 4663 from the registry
const body = await (await fetch('https://api.robinhood.com/rhj/assets')).json()
const assets: Asset[] = Array.isArray(body) ? body : (Object.values(body).find(Array.isArray) as Asset[])
const registry = new Map<string, Asset>()
for (const asset of assets)
for (const d of asset.deployments ?? [])
if (Number(d.chainId) === 4663) registry.set(d.contractAddress.toLowerCase(), asset)
const entry = registry.get(NVDA.toLowerCase())
if (!entry) throw new Error('not in the Robinhood registry: treat as an impostor')
// 2. Read the ERC-8056 multiplier on-chain (1e18 = 1.0)
const uiMultiplier = await client.readContract({
address: NVDA,
abi: parseAbi(['function uiMultiplier() view returns (uint256)']),
functionName: 'uiMultiplier',
})
console.log(entry.tokenSymbol, formatUnits(uiMultiplier, 18), entry.currentMultiplier, entry.pendingMultiplier)
// NVDA's uiMultiplier read 1.000775159164630595 on 23 September 2026
A non-null pendingMultiplier, or a newUIMultiplier() with a future effectiveAt(), means a corporate action is scheduled, so reprice positions around that timestamp.
For prices, Chainlink is the official oracle, with one AggregatorV3Interface feed per Stock Token. The feed price is the underlying price times uiMultiplier, a total-return price, so do not apply the multiplier a second time. Feeds update 24/5 and hold the last value while markets are closed. Check oraclePaused(), set during corporate actions, and the sequencer uptime feed before trusting a price. Take feed addresses from Chainlink's Robinhood tokenized equity feeds page, not third-party lists.
The NVDA/USDG v3 pool at 0xd4EB21209C4D6093f80B5b84f5C45cc093EA14a3 gives a second, market-driven price via the swap and slot0 code above.
Tracing for Execution and MEV Analysis
Logs show what a swap did. Traces show how: which router called which pool, what reverted, and which contracts a searcher touched in the same block. Dwellir's Robinhood endpoint enables debug_traceTransaction, debug_traceBlockByNumber, debug_traceBlockByHash and debug_traceCall on all paid plans, but not Parity-style trace_*. The public RPC rejects debug_* entirely.
viem's public schema does not type debug_* methods, so declare the one you need:
// trace.ts
import { createPublicClient, http, rpcSchema, type Address, type Hash, type Hex } from 'viem'
import { robinhood } from 'viem/chains'
import { client, HTTP_URL, POOL_MANAGER } from './config'
import { V4_SWAP } from './swaps'
type CallFrame = {
type: string; from: Address; to?: Address; input: Hex; gasUsed: Hex; error?: string; calls?: CallFrame[]
}
type DebugSchema = [{
Method: 'debug_traceTransaction'
Parameters: [Hash, { tracer: 'callTracer' }]
ReturnType: CallFrame
}]
const tracer = createPublicClient({
chain: robinhood,
transport: http(HTTP_URL),
rpcSchema: rpcSchema<DebugSchema>(),
})
// Trace the transaction behind a recent v4 swap
const head = await client.getBlockNumber()
const [swap] = await client.getLogs({ address: POOL_MANAGER, event: V4_SWAP, fromBlock: head - 100n, toBlock: head })
if (!swap) throw new Error('no v4 swaps in the last 100 blocks')
const root = await tracer.request({
method: 'debug_traceTransaction',
params: [swap.transactionHash, { tracer: 'callTracer' }],
})
function walk(frame: CallFrame, depth = 0) {
const note = frame.error ? ` REVERTED: ${frame.error}` : ''
console.log(`${' '.repeat(depth)}${frame.type} ${frame.to} ${frame.input.slice(0, 10)}${note}`)
frame.calls?.forEach((child) => walk(child, depth + 1))
}
walk(root)
For block-level analysis, debug_traceBlockByNumber with the same callTracer config returns one trace per transaction. Skip the type 106 internal transaction that opens each block.
Ordering on Robinhood Chain is first-come, first-served at the sequencer, so a higher fee does not reorder transactions, and eth_maxPriorityFeePerGas returns 0. Dwellir's endpoint exposes no pending-transaction feed or txpool_* methods for mempool watching; ordered transactions are visible through the sequencer feed. To study searcher behaviour, trace the blocks after a large swap, find transactions that touch the same pool and check who sent them.
RPC vs Indexer: Which to Use
A Robinhood Chain pipeline draws on three layers: an RPC node for live swaps, point-in-time state and traces; indexers for decoded history; and data APIs such as the Robinhood assets API and Chainlink feeds for reference data.

| Tool | Robinhood Chain support | What you get | Best for |
|---|---|---|---|
| Dwellir RPC | Mainnet archive, HTTPS and WSS, debug_* | Logs, state at any block, traces, newHeads and logs subscriptions, sequencer feed access | Live signals, point-in-time reads, execution analysis |
| Public RPC | Mainnet, rate-limited, "not for production" | Recent state only (about 10 minutes measured), no debug_*, 10,000-log result cap | Quick checks, last-resort fallback |
| Goldsky | Mainnet and testnet, slug robinhood-chain | Subgraphs, Turbo pipelines to Postgres, ClickHouse or Kafka, Edge RPC | Managed pipelines into your own database |
| Envio | Yes | HyperIndex, plus HyperSync at https://robinhood.hypersync.xyz | Fast historical log scans for backtests |
| SQD | Yes, private enterprise dataset | Portal API, decoded tables | Teams already on an SQD enterprise contract |
| The Graph | Substreams and Firehose; Subgraph Studio not listed in the networks registry | Substreams modules | Existing Substreams users. Check current status first |
| Ponder | Any RPC, self-hosted TypeScript | Your own indexer and schema | Full control. Set ethGetLogsBlockRange to your provider's cap and use an archive RPC with WSS |
| Blockscout Pro API | https://api.blockscout.com/4663/api/v2 | Explorer data over REST | Light lookups. Free tier 100K credits/day at 5 RPS, getLogs capped at 1,000 records |
| Dune | robinhood schema: blocks, transactions, logs, traces, decoded tables | SQL | Research and dashboards, not real-time execution |
Two questions decide it. If the strategy acts on data within seconds, read it from an RPC connection, because every other layer sits downstream of a node. If a query spans weeks across hundreds of pools, use HyperSync, an indexer or Dune, because chunked eth_getLogs is the slow path. Ponder sits in between when you want to own the indexer and feed it from your own archive endpoint.
For endpoint pricing, limits and methods side by side, see the best Robinhood Chain RPC providers.
FAQ
How can I get data from Robinhood Chain for my DeFi strategy?
Read Uniswap v3 and v4 Swap logs and pool state from an archive RPC endpoint, stream new events over WebSocket, and backfill gaps with chunked eth_getLogs. For months of history use an indexer such as Goldsky or Envio, and for Stock Token addresses and prices use the Robinhood assets API and Chainlink feeds.
Does Robinhood Chain support eth_getLogs, and what are the limits?
Yes, with limits set by the endpoint. The public RPC errors once a query matches more than 10,000 logs, while Dwellir caps the range at 500 blocks on Developer and 10,000 blocks on Growth and Scale, and the Free plan excludes eth_getLogs. At about 10 blocks per second, 500 blocks is roughly 50 seconds of chain time, so backfills need adaptive chunking.
Is the public Robinhood Chain RPC good enough for a trading bot?
No. Robinhood's docs call it rate-limited and not for production, and in our tests it returned HTTP 429 after about six quick calls and rejected debug_traceTransaction. It is not an archive node either: historical eth_call failed for blocks more than about 6,100 back (roughly 10 minutes), so use it only for quick checks or as a last-resort fallback.
How do I get real-time Uniswap swap data on Robinhood Chain?
Subscribe to logs over WebSocket, filtering v3 on the pool address and v4 on the PoolManager address with topic1 set to the PoolId. v4 amounts use the swapper's sign convention, the opposite of v3, so flip the signs before reusing v3 logic. Persist a checkpoint and backfill with eth_getLogs after every reconnect.
How do I get Robinhood Stock Token prices on-chain?
Use the Chainlink feed for each Stock Token, listed in Chainlink's tokenized equity feeds documentation. The feed price already includes the token's uiMultiplier, so do not apply it again. Feeds run 24/5 and hold the last value while markets are closed, so check oraclePaused(), which is set during corporate actions, and the sequencer uptime feed before trusting a price.
Can I get historical Robinhood Chain data for backtesting?
Yes, from an archive endpoint: eth_call at a past block returns a pool's slot0 or StateView getSlot0 as it was then, and eth_getLogs returns historical Swap events in chunks. The public RPC is not an archive node and served only about the last 10 minutes of state in our test. For months of history across many pools, Envio HyperSync, an indexer or Dune's robinhood schema is faster than chunked RPC scans.
What is the best way to index Robinhood Chain?
Goldsky and Envio support Robinhood Chain directly, with Envio HyperSync for fast historical log scans. Ponder runs self-hosted against any archive RPC with WebSocket, The Graph supports it through Substreams, SQD's dataset is enterprise-only, and Dune suits SQL analytics. Pair an indexer for history with a direct RPC connection for the live edge.
Does Robinhood Chain support WebSockets, archive and trace methods?
Through providers, yes. Dwellir's endpoint serves archive state, WebSocket eth_subscribe for newHeads and logs, and debug_traceTransaction, debug_traceBlockByNumber, debug_traceBlockByHash and debug_traceCall, but not Parity-style trace_*, txpool_* or a pending-transaction feed. The public RPC exposes no debug methods and is not an archive node.
Next Steps
Start with config.ts and the v3 swap script on the WETH/USDG pool. If the signs and the USDG-per-WETH price look right there, the v4 code and the backfill loop reuse the same helpers. Then choose a history source: 500-block windows for recent days, HyperSync or an indexer for the months before.
Endpoints, the sequencer feed guide and method details are in the Robinhood Chain docs and on the Robinhood Chain network page, and plan limits are on the pricing page. To run it in production, create a Dwellir API key or contact the Dwellir team about dedicated capacity for a trading workload.
Sources
Chain measurements and live contract calls taken on 23 September 2026. Every snippet except trace.ts was run against mainnet that day (historical.ts inside the public RPC's state window); trace.ts needs a paid Dwellir key.
- Robinhood Chain: connecting, transaction finality, contracts, building with Stock Tokens
- Uniswap: Uniswap/contracts deployments/4663.md, v4 deployments
- Chainlink: Robinhood tokenized equity feeds
- Explorer: Robinhood Chain Blockscout
- Dwellir: Robinhood Chain docs, pricing


