CRIUS
Historical dataWebSocketMethodologyRobinhood Chain

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:

Terminal window
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" # WETH
s = 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":

Terminal window
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:

  1. A reorg message: {"ch": "reorg", "data": {"common_ancestor": 96714000, "removed_blocks": [96714001, 96714002], "depth": 2}}.
  2. Every affected event again, with "removed": true and the same id, so you can undo it.
  3. 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 busy
const 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 1200000
safe block 96718762: 238 trades confirmed ($155129), 455 still provisional, 0 rolled back

Of 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:

  1. Backfill with REST in time windows (from_ts / to_ts, paginated with meta.next_cursor), oldest to newest. Everything older than the finalized block is immutable, so you can write it once.
  2. Switch to the stream for new events, with finality: "safe" if your store can’t handle removals.
  3. Re-sync the recent window (anything newer than meta.finalized_block at 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.

Build with Crius

Robinhood Chain data in one API

Free plan with 1M credits a month. Your key in under a minute.

Related APIs

More from the blog