All Blog Posts
How to Get Robinhood Chain Data for DeFi Strategies: RPC, Logs, and Indexers

How to Get Robinhood Chain Data for DeFi Strategies: RPC, Logs, and Indexers

By Ben Chatwin 27min read

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 needWhere it livesHow to get itFreshness
Uniswap v3 swapsSwap logs on each pool contracteth_getLogs backfill, then WSS logs subscriptionEvery block (about 0.1 s, measured)
Uniswap v4 swapsSwap logs on the PoolManager, keyed by PoolIdSame, filtered on topic1 = PoolIdEvery block
Current pool pricev3 slot0(), v4 StateView.getSlot0()eth_callLatest block
Pool price at a past blockSame contracts, archive stateeth_call with blockNumber on an archive endpointAny historical block
Stock Token addressesRobinhood assets registryGET api.robinhood.com/rhj/assetsOn request
Stock Token multiplieruiMultiplier() on each tokeneth_callLatest block
Stock Token pricesOne Chainlink feed per tokenlatestRoundData()24/5, holds last value when markets close
Internal calls, execution pathNode tracesdebug_traceTransaction with callTracerPer transaction
Block timestampsBlock headers (logs return 0x0)eth_getBlockByNumberPer block
Months of decoded historyIndexers, DuneGoldsky, Envio, Ponder, Dune SQLDepends 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.

PropertyValue
Chain ID4663 (0x1237) mainnet, 46630 testnet
StackArbitrum Nitro, settles to Ethereum using blobs
Gas tokenETH (live gas tracker)
Clientnitro/v3.12.0-rc.3
Block rateAbout 10 blocks/s measured on 23 Sep 2026. 0.18 s average since block 1 (30 Apr 2026)
Transaction orderingFirst-come, first-served at the sequencer
Public RPChttps://rpc.mainnet.chain.robinhood.com, rate-limited, "not for production", not archive (about 10 minutes of state, measured)
Public sequencer feedwss://feed.mainnet.chain.robinhood.com (Nitro feed protocol, not JSON-RPC)
Explorerrobinhoodchain.blockscout.com
Multicall30xca11bde05977b3631167028862be2a173976ca11. 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 gasUsedForL1 and l1BlockNumber, and blocks add l1BlockNumber and sendCount, 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.number returns an L1 estimate. Use ArbSys(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.

TYPESCRIPT
// 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:

PlanBlock range capChain time per callCalls per day of historyCalls per 30 days
Developer ($49/mo, 100 RPS)500 blocksAbout 50 secondsAbout 1,700About 51,000
Growth ($299/mo, 500 RPS)10,000 blocksAbout 17 minutesAbout 85About 2,550
Scale ($999/mo, 5,000 RPS)10,000 blocksAbout 17 minutesAbout 85About 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.

TYPESCRIPT
// 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.

Four-step ingest loop for Robinhood Chain: subscribe over WSS and buffer, backfill with eth_getLogs from the checkpoint, drain the buffer with dedupe, then go live; on any drop, stall or timer, repeat from step 1.
TYPESCRIPT
// 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.

TYPESCRIPT
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.

ContractAddress
v4 PoolManager0x8366a39cc670b4001a1121b8f6a443a643e40951
v4 StateView0xf3334192d15450cdd385c8b70e03f9a6bd9e673b
V4Quoter0x8dc178efb8111bb0973dd9d722ebeff267c98f94
UniswapV3Factory0x1f7d7550b1b028f7571e69a784071f0205fd2efa
QuoterV2 (v3)0x33e885ed0ec9bf04ecfb19341582aadcb4c8a9e7
UniswapV2Factory0x8bceaa40b9acdfaedf85adf4ff01f5ad6517937f
WETH (18 decimals)0x0Bd7D308f8E1639FAb988df18A8011f41EAcAD73
USDG (6 decimals)0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168
v3 WETH/USDG 0.05% pool0x69BfaF19C9f377BB306a89aEd9F6B07e2c1a8d9a
v3 NVDA/USDG 0.05% pool0xd4EB21209C4D6093f80B5b84f5C45cc093EA14a3

Swap events and the sign flip

Uniswap v3Uniswap v4
Emitted byEach pool contractThe PoolManager only
Filter onPool addresstopic1 = PoolId
topic00xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca670x40e9cecb9f5f1f1c5b9c97dec2917b7ee92e57ba5563708daca94dd84ad7112f
Amount typesint256int128, plus a uint24 fee field
Sign perspectiveThe pool'sThe swapper's
amount0 > 0 meanstoken0 went into the poolThe 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.

The same trade, selling 1 WETH for 2,728 USDG, logs amount0 +1.0 WETH on Uniswap v3 (pool's side) but -1.0 WETH on Uniswap v4 (swapper's side). Flip v4 amounts before reusing v3 logic.

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.

TYPESCRIPT
// 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:

TYPESCRIPT
// 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.

TYPESCRIPT
// 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.

TYPESCRIPT
// 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.

TYPESCRIPT
// 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:

TYPESCRIPT
// 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.

Which access method fits each Robinhood Chain data horizon: live edge via eth_subscribe, recent events via chunked eth_getLogs, state at a block via archive eth_call, execution path via debug_traceTransaction, deep history via an indexer or HyperSync.
ToolRobinhood Chain supportWhat you getBest for
Dwellir RPCMainnet archive, HTTPS and WSS, debug_*Logs, state at any block, traces, newHeads and logs subscriptions, sequencer feed accessLive signals, point-in-time reads, execution analysis
Public RPCMainnet, rate-limited, "not for production"Recent state only (about 10 minutes measured), no debug_*, 10,000-log result capQuick checks, last-resort fallback
GoldskyMainnet and testnet, slug robinhood-chainSubgraphs, Turbo pipelines to Postgres, ClickHouse or Kafka, Edge RPCManaged pipelines into your own database
EnvioYesHyperIndex, plus HyperSync at https://robinhood.hypersync.xyzFast historical log scans for backtests
SQDYes, private enterprise datasetPortal API, decoded tablesTeams already on an SQD enterprise contract
The GraphSubstreams and Firehose; Subgraph Studio not listed in the networks registrySubstreams modulesExisting Substreams users. Check current status first
PonderAny RPC, self-hosted TypeScriptYour own indexer and schemaFull control. Set ethGetLogsBlockRange to your provider's cap and use an archive RPC with WSS
Blockscout Pro APIhttps://api.blockscout.com/4663/api/v2Explorer data over RESTLight lookups. Free tier 100K credits/day at 5 RPS, getLogs capped at 1,000 records
Dunerobinhood schema: blocks, transactions, logs, traces, decoded tablesSQLResearch 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.

read another blog post