Robinhood Chain Historical Data: Indexing, Retention and Reorgs
How Crius indexes Robinhood Chain from block 0, what history is retained, how latest, safe and finalized data differ, and how to handle reorgs in REST and WebSocket.
Crius 8 min read
Crius indexes Robinhood Chain from block 0 on its own archive nodes and keeps the full history (every trade, transfer, pool, launch and holder balance), so any endpoint can answer for any past block, not just recent ones. Every event also carries a finality level (latest, safe or finalized), and chain reorganizations are handled explicitly: REST simply reflects the canonical chain, and the WebSocket stream tells you exactly what to undo.
This post explains how that history is built, what you can rely on, and how to write consumers that stay correct when the chain changes under them.
What “full history” means
At time of writing (Oct 2026), the index covers:
| First block | 0 (start_block: 0 in /v1/status) |
| Head | about 96.7 million blocks, roughly 100 ms apart |
| Tokens | 2.38 million |
| Pools | 2.74 million (Uniswap v2, v3, v4 and launchpad curves) |
| Addresses | 28 million |
You can check these live:
curl https://api.crius.sh/v1/status{ "data": { "chains": { "robinhood": { "status": "live", "start_block": 0, "indexed_block": 96714858, "safe_block": 96707359, "finalized_block": 96703786, "lag_ms": 118, "tokens": 2376044, "pools": 2741336, "addresses": 28075209 } } }}Responses that depend on history say how complete it is: "history": "complete" means the answer covers everything since genesis. Every response’s meta.block is the block the answer reflects, so a cached result can always be compared with the chain.
Retention
Nothing is pruned. Trades, transfers, liquidity events, launches, graduations and holder balances are kept for the life of the chain, and historical queries work the same for last week and for the first day of mainnet. What is bounded is how much work one request may do: historical holder replays aren’t available for the few tokens with more than about 2 million transfers (the API answers window_too_large), and heavy analytics endpoints run a bounded number of computations at a time.
How the history was built
A chain index is two things: a fast backfill of everything that already happened, and a live indexer that never falls behind. Crius uses the same decoding code for both, so a trade decoded during the backfill is identical to one decoded live.
1. Receipts backfill. History was read with eth_getBlockReceipts, one call per block returning every transaction receipt and log, in parallel across many workers against an archive node running next to the indexer. The full backfill, blocks 0 to 91,351,250, took about five hours; a shorter gap fill then brought it up to the live head. The live indexer then restarted on top of the complete state, restoring 26 million addresses and 2.3 million tokens in under two and a half minutes.
2. Verification. Before history was trusted, an independent verifier compared the index with the chain itself: token balances and total supply via eth_call at past blocks, and holder snapshots replayed at historical heights. It ran 5.76 million checks with zero failures.
3. Call traces for internal transfers. Logs don’t capture ETH moved inside a transaction by a contract, such as a disperse contract funding 50 wallets or a router paying out. Those come from call traces (debug_traceBlockByNumber with the call tracer), which are several times more expensive to produce than receipts: a few hundred blocks per second per node, against thousands for receipts. The tracer runs two cursors: a live one right behind the head, and a history one walking backwards from the newest block. Recent months are therefore complete first, and at time of writing older blocks are still filling in. Wallet funding links, bundle detection and wallet connections use these internal transfers.
4. Data hygiene. Some swaps are economically meaningless: a swap of one wei for one wei “prices” a token at a whole ETH. Trades where either side is below 10,000 raw units still count as trades and volume but never set a price, so they can’t draw a wick on a chart or set an all-time high.
Finality: latest, safe, finalized
Robinhood Chain is an L2: the sequencer orders transactions immediately, batches are posted to Ethereum later, and Ethereum finalizes them after that. Crius exposes all three stages:
| Level | Meaning | Measured lag behind the head |
|---|---|---|
latest |
Included by the sequencer | under a second (lag_ms ~100 ms) |
safe |
The batch has been posted to Ethereum | about 4,700–7,500 blocks: roughly 8–13 minutes |
finalized |
Ethereum finalized that batch | about 8,000–11,000 blocks: roughly 14–19 minutes |
Those lags are from real readings of meta.safe_block and meta.finalized_block on the same day, at an average block time of 101–105 ms. They vary with how often batches are posted, so read them from meta rather than hard-coding them.
Every trade has a finality field, and every response’s meta has the current safe_block and finalized_block, so you can settle data yourself:
"""settled.py: only act on settled data from REST by comparing each trade's block with meta.safe_block."""import os, requests
API = "https://api.crius.sh/v1/robinhood"TOKEN = "0x0bd7d308f8e1639fab988df18a8011f41eacad73" # WETHs = requests.Session()s.headers["x-api-key"] = os.environ["CRIUS_API_KEY"]
body = s.get(f"{API}/token/{TOKEN}/trades", params={"limit": 100}, timeout=30).json()meta, trades = body["meta"], body["data"]settled = [t for t in trades if t["block"] <= meta["safe_block"]]print(f"head {meta['block']}, safe {meta['safe_block']} (-{meta['block'] - meta['safe_block']} blocks), " f"finalized {meta['finalized_block']} (-{meta['block'] - meta['finalized_block']} blocks)")print(f"{len(trades)} newest trades: {len(settled)} settled, {len(trades) - len(settled)} still 'latest'")print("finality labels:", {f: sum(t['finality'] == f for t in trades) for f in ("latest", "safe", "finalized")})head 96726864, safe 96722110 (-4754 blocks), finalized 96718762 (-8102 blocks)100 newest trades: 0 settled, 100 still 'latest'finality labels: {'latest': 100, 'safe': 0, 'finalized': 0}On a busy token, the most recent hundred trades all happened in the last few seconds, so none is settled yet. Older history is all finalized. A query for one day in September returns trades marked "finality": "finalized":
curl "https://api.crius.sh/v1/robinhood/token/0xc9675fe679c7d4e1b94416f66d61c96127eb964a/trades?from_ts=1791590400000&to_ts=1791676799999&limit=3" \ -H "x-api-key: $CRIUS_API_KEY"Which level should you use? Display and alerting: latest, since users expect sub-second freshness. Accounting, payouts, anything that moves money: safe at least, finalized if you can wait. Reports and backtests: everything older than a few minutes is already final.
Reorgs
A reorg replaces blocks that were already included. On a sequencer-ordered L2 they are rare and shallow, but “rare” isn’t “never”, and a consumer that can’t handle one will eventually double-count something.
In REST, nothing special is needed. Responses always reflect the current canonical chain. Replaced events disappear and their replacements appear; trade ids are block-tx-log, so a replaced trade’s id simply stops existing.
On the WebSocket, which pushes events as they happen, Crius tells you precisely what changed:
- A
reorgmessage:{"ch": "reorg", "data": {"common_ancestor": 96714000, "removed_blocks": [96714001, 96714002], "depth": 2}}. - Every affected event again, with
"removed": trueand the sameid, so you can undo it. - The replacement events, as normal.
Or opt out entirely: subscribe with "finality": "safe" and the server holds each event until its block is safe. You will never see a removal, at the cost of 8–13 minutes of delay.
A reorg-safe ledger
This consumer keeps trades provisional until their block is safe, then confirms them; if a reorg removes one first, it’s rolled back. It needs a key with the stream scope (Free plan and up):
// ledger.mjs: provisional on `latest`, confirmed once `safe`, rolled back on a reorg. Node 22+.const KEY = process.env.CRIUS_API_KEY;const TOKEN = process.argv[2] ?? "0x0bd7d308f8e1639fab988df18a8011f41eacad73"; // WETH: always busyconst RUN_MS = Number(process.argv[3] ?? 60_000);
const pending = new Map(); // trade id -> { block, value_usd }let confirmed = 0, confirmedUsd = 0, rolledBack = 0;// The stream sends `finality` when the safe block moves (every few minutes): start from the current value.const status = await (await fetch("https://api.crius.sh/v1/status")).json();let safeBlock = status.data.chains.robinhood.safe_block;
const ws = new WebSocket(`wss://api.crius.sh/v1/robinhood/stream?api_key=${KEY}`);ws.onopen = () => ws.send(JSON.stringify({ op: "subscribe", channels: [`trades:${TOKEN}`] }));ws.onmessage = ({ data }) => { const m = JSON.parse(data); switch (m.ch) { case "finality": // the safe / finalized blocks moved: settle everything at or below `safe` safeBlock = m.data.safe_block; for (const [id, t] of pending) { if (t.block <= safeBlock) { confirmed++; confirmedUsd += t.value_usd; pending.delete(id); } } break; case "reorg": // replaced blocks: the affected events follow with "removed": true console.log(`reorg at ${m.data.common_ancestor}, depth ${m.data.depth}`); break; default: if (!m.ch.startsWith("trades:")) break; if (m.data.removed) { pending.delete(m.data.id) && rolledBack++; break; } pending.set(m.data.id, { block: m.block, value_usd: m.data.value_usd }); }};
setTimeout(() => { console.log(`safe block ${safeBlock}: ${confirmed} trades confirmed ($${confirmedUsd.toFixed(0)}), ` + `${pending.size} still provisional, ${rolledBack} rolled back`); ws.close();}, RUN_MS);Run for 20 minutes on WETH, the busiest token on the chain, at time of writing:
$ node ledger.mjs 0x0bd7d308f8e1639fab988df18a8011f41eacad73 1200000safe block 96718762: 238 trades confirmed ($155129), 455 still provisional, 0 rolled backOf 693 trades received, 238 had reached a safe block by the end of the run and 455 were still inside the 8–13 minute window. No reorg occurred in that period, which is typical, but the code path costs nothing until it’s needed.
For a production consumer, add reconnects: keep the last seq per channel and send {"op": "resume", "seqs": {...}} after reconnecting to replay up to 60 seconds of missed messages. The stream example has a complete client.
Backfilling your own database
If you mirror Crius data into your own store, the safest pattern combines both interfaces:
- Backfill with REST in time windows (
from_ts/to_ts, paginated withmeta.next_cursor), oldest to newest. Everything older than the finalized block is immutable, so you can write it once. - Switch to the stream for new events, with
finality: "safe"if your store can’t handle removals. - Re-sync the recent window (anything newer than
meta.finalized_blockat backfill time) after the switch, to close the gap between the two.
Frequently asked questions
How far back does Crius have Robinhood Chain data?
To block 0. The index covers the entire chain, and "history": "complete" on a response confirms full coverage for that query.
What’s the difference between safe and finalized on Robinhood Chain?
safe means the batch containing the block has been posted to Ethereum (typically 8–13 minutes behind the head); finalized means Ethereum has finalized that batch (typically 14–19 minutes behind). The current values are in every response’s meta.
How does the WebSocket handle reorgs?
It sends a reorg message with the common ancestor and removed blocks, re-sends every affected event with "removed": true and the same id, then sends the replacement events. Subscribing with "finality": "safe" avoids reorged events altogether.
Is internal ETH (contract-to-wallet transfers) included in history?
Yes, recovered from call traces. Recent history is complete; at time of writing, the oldest part of the chain is still being traced, newest blocks first.
Can I query historical holders at a past block?
Yes: /holders/snapshot?at=block:<n>, a timestamp, or a market-cap level. See the token holders API guide.
Related
- Product overview: historical data API, WebSocket API, Robinhood Chain API, RPC vs indexed API
- Reference: finality and reorgs, stream: heartbeats, finality and reorgs, conventions
- Guides: WebSocket trading bot, token holders API
Build with Crius
Robinhood Chain data in one API
Free plan with 1M credits a month. Your key in under a minute.
More from the blog
- ExplainerMethodology
Robinhood Chain vs Ethereum Mainnet: Indexing Requirements
Why indexing Robinhood Chain differs from Ethereum: 100x more blocks, two clocks, staged finality, Nitro receipts and traces, with real index figures.
- GuideRobinhood Chain
Robinhood Chain API: The Complete Developer Guide for 2026
How to get Robinhood Chain prices, OHLCV candles, trades, holders, launches and live streams from one API, with tested code in cURL, Node.js and Python.
- PricesRobinhood Chain
How Robinhood Chain Stock Token Multipliers Affect Price Data
Robinhood stock tokens use an ERC-8056 uiMultiplier for splits and dividends. What it does to prices, balances and charts, and how to convert correctly.