CRIUS Docs
API status Get API key
Real-time

Stream Robinhood Chain trades over WebSocket

Stream every trade of a Robinhood Chain token, pool or wallet over the Crius WebSocket, with Node.js and Python clients that reconnect, resume and backfill.

Updated 3 min read
Short answer

Open wss://api.crius.sh/v1/robinhood/stream with a Free (or higher) key that has the stream scope and send a subscribe op for trades: plus the token address. Each trade arrives, typically well under a second after its block, with type, trader, amount_base, price_usd and value_usd. trades:pool: follows one pool and wallet: one wallet; a connection costs 1 credit.

The Crius WebSocket stream pushes every decoded Robinhood Chain trade as soon as its block is indexed, typically well under a second after the block was produced. You open one connection, subscribe to trades:<token>, trades:pool:<pool> or wallet:<address>, and each trade arrives as a JSON message with its side, amounts, USD price and value and the wallet that really traded. It needs a Free key with the stream scope, and the REST trade tape fills any gap the stream can’t replay.

Subscribe to a token’s trades

Connect, wait for the hello greeting (it tells you max_subscriptions for your plan), then subscribe. Channel names are lowercase, so lowercase addresses before you subscribe and match on the ch field of each message.

{"op": "subscribe", "channels": ["trades:0x322f0929c4625ed5bad873c95208d54e1c003b2d"]}

The server answers {"ch": "subscribed", "data": {"channels": [...], "finality": "latest"}}, and from then on every trade of Tesla’s tokenized stock (TSLA) arrives in the standard envelope: ch, seq, block, ts and data. The stream reference shows a full message.

Channel What you get
trades:<token> Every trade of one token, in any pool
trades:pool:<pool> Every trade in one pool (v2/v3 address or v4 pool id)
wallet:<address> The wallet’s trades plus its plain token transfers (type: "transfer")
firehose:trades Every trade on the chain (Enterprise plan)

What a trade message contains

FieldMeaning
idblock-tx-log, the same id the REST trade tape uses: use it to de-duplicate and to undo reorged trades
typebuy or sell, from the token's side
token, symbol, logoThe token traded
pool, dexPool address (v2/v3) or pool id (v4), and uniswap_v2, uniswap_v3, uniswap_v4 or a launch curve
traderThe wallet whose balance changed, not the router or bundler that sent the transaction
signerThe address that signed the transaction
amount_base, amount_quoteToken and quote amounts in human units
price_usd, value_usdUSD price per token and USD value of the trade at that block
tax_pctToken tax measured on this trade, when there was one
removedtrue only on a re-sent trade whose block was reorged out

Backfill and cross-check with the trade tape

The stream only replays the last 60 seconds. For anything older, and to get detail the stream message doesn’t carry, read the REST trade tape: it uses the same id, so the two merge cleanly.

The last two TSLA trades, for backfilling a stream

My stream client was offline. What did Tesla's tokenized stock (0x322f0929c4625ed5bad873c95208d54e1c003b2d) trade while I was gone?

GET/v1/robinhood/token/0x322f0929c4625ed5bad873c95208d54e1c003b2d/trades?limit=2
curl "https://api.crius.sh/v1/robinhood/token/0x322f0929c4625ed5bad873c95208d54e1c003b2d/trades?limit=2"
Real response 200 Captured Oct 28, 2026 · 98 lines (arrays trimmed)
{
"data": [
{
"amount_base": 0.68873541086638403,
"amount_quote": 0.358712450309036398,
"block": 100410183,
"dex": "uniswap_v4",
"eth_usd_at_trade": 2356.79,
"executor": "0xe07ba3acee529ff1d4f7290cbae32fa065e18acd",
"fees": {
"gas": {
"gas_price_gwei": 0.021016,
"gas_used": 160574,
"gas_used_l1": 0,
"l1_eth": 0,
"l2_eth": 0.00000337462,
"total_eth": 0.00000337462,
"usd": 0.00795329
},
"token_tax": {
"pct": 0
}
},
"finality": "latest",
"id": "100410183-9-12",
"market_cap_usd_at_trade": 4636638,
"pool": "0x3ea70ce0b34473692549f97e630a1d82dc05cea94fa55f81b50f10c8c6d8186f",
"price_eth": 0.170843,
"price_usd": 402.641,
"quote": "0x117cc2133c37b721f49de2a7a74833232b3b4c0c",
"quote_symbol": "SPY",
"signer": "0xe07ba3acee529ff1d4f7290cbae32fa065e18acd",
"signer_label": null,
"trader": "0x278d858f05b94576c1e6f73285886876ff6ef8d2",
"trader_app": null,
"trader_label": null,
"ts": 1793225424000,
"tx": "0xe45e6dd2072db6c23bbae5aa7719cf43ba81a7f23a0f7d94addf2a4362d819c5",
"tx_index": 9,
"type": "sell",
"value_eth": 0.117665,
"value_usd": 277.313,
"via": []
},
{
"amount_base": 2.131024263465401912,
"amount_quote": 1.110019178795529442,
"block": 100410039,
"dex": "uniswap_v4",
"eth_usd_at_trade": 2356.79,
"executor": "0xe07ba3acee529ff1d4f7290cbae32fa065e18acd",
"fees": {
"gas": {
"gas_price_gwei": 0.02139,
"gas_used": 160578,
"gas_used_l1": 0,
"l1_eth": 0,
"l2_eth": 0.00000343476,
"total_eth": 0.00000343476,
"usd": 0.00809503
},
"token_tax": {
"pct": 0
}
},
"finality": "latest",
"id": "100410039-2-2",
"market_cap_usd_at_trade": 4637151,
"pool": "0x3ea70ce0b34473692549f97e630a1d82dc05cea94fa55f81b50f10c8c6d8186f",
"price_eth": 0.170862,
"price_usd": 402.686,
"quote": "0x117cc2133c37b721f49de2a7a74833232b3b4c0c",
"quote_symbol": "SPY",
"signer": "0xe07ba3acee529ff1d4f7290cbae32fa065e18acd",
"signer_label": null,
"trader": "0x278d858f05b94576c1e6f73285886876ff6ef8d2",
"trader_app": null,
"trader_label": null,
"ts": 1793225409000,
"tx": "0x70aa3910a6109eb864a3464d828d6ae2c026c3dfe5ca69d5653f4c765a97e569",
"tx_index": 2,
"type": "sell",
"value_eth": 0.36411,
"value_usd": 858.133,
"via": []
}
],
"meta": {
"block": 100410278,
"block_age_ms": 1180,
"block_ts": 1793225433000,
"chain": "robinhood",
"finalized_block": 100401329,
"lag_ms": 83,
"next_cursor": "100410039-2",
"safe_block": 100405028
}
}

What this tells you

  • Both trades are sells in the same Uniswap v4 pool, quoted in the S&P 500 tokenized stock (quote_symbol: "SPY"): tokenized stocks trade against each other, not only against USDG.
  • id: "100410183-9-12" is block 100410183, transaction 9, log 12. A stream message for this trade has the same id, so a client that stores ids never counts it twice.
  • trader (0x278d858f05b94576c1e6f73285886876ff6ef8d2) differs from signer and executor (0xe07ba3acee529ff1d4f7290cbae32fa065e18acd): another address signed and sent the transaction, and Crius reports the wallet whose balance actually changed.
  • value_usd is 277.313 and 858.133, at price_usd around 402.6. finality: "latest": neither block was safe yet when the call was made.
  • meta.next_cursor pages further back; from_ts and to_ts (unix ms) bound the window, which is how the client below fills a gap.
Trade tape reference Runs from your browser on the anonymous plan Data looks wrong? Report it

A complete client

Both clients follow the same loop: subscribe, remember the last seq per channel, resume after a reconnect, backfill from REST when the outage was longer than the replay buffer, and drop trades that come back with removed: true. Set CRIUS_API_KEY to a key with the stream scope.

// trades-stream.mjs: node trades-stream.mjs (Node.js 22+, built-in WebSocket)
const KEY = process.env.CRIUS_API_KEY;
const API = "https://api.crius.sh/v1/robinhood";
const TOKEN = "0x322f0929c4625ed5bad873c95208d54e1c003b2d"; // TSLA, Tesla tokenized stock
const CHANNELS = [`trades:${TOKEN}`];
const MIN_USD = 100; // only print trades of $100 or more
const lastSeq = {}; // channel -> last seq seen
const seen = new Map(); // trade id -> trade (de-duplication and reorg undo)
let lastTs = 0; // chain time of the newest trade we hold
let lastMsgAt = 0; // wall-clock time of the last message
let backoff = 1000;
function onTrade(t, source) {
if (seen.has(t.id)) return;
seen.set(t.id, t);
if (seen.size > 50_000) seen.delete(seen.keys().next().value);
lastTs = Math.max(lastTs, t.ts);
if (t.value_usd >= MIN_USD) {
console.log(`${source} ${new Date(t.ts).toISOString()} ${t.type} $${t.value_usd} @ $${t.price_usd} by ${t.trader}`);
}
}
async function backfill() {
if (!lastTs) return;
const res = await fetch(`${API}/token/${TOKEN}/trades?from_ts=${lastTs}&limit=500`, { headers: { "x-api-key": KEY } });
const { data } = await res.json();
for (const t of (data ?? []).reverse()) onTrade(t, "rest"); // the tape is newest first
}
function connect() {
const ws = new WebSocket(`wss://api.crius.sh/v1/robinhood/stream?api_key=${KEY}`);
let idle;
const touch = () => {
clearTimeout(idle);
idle = setTimeout(() => ws.close(), 45_000); // heartbeats arrive every 15 s
};
ws.onopen = () => {
backoff = 1000;
touch();
const offline = lastMsgAt ? Date.now() - lastMsgAt : 0;
ws.send(JSON.stringify({ op: "subscribe", channels: CHANNELS }));
if (Object.keys(lastSeq).length) ws.send(JSON.stringify({ op: "resume", seqs: lastSeq }));
if (offline > 55_000) backfill().catch(console.error); // longer than the 60 s replay buffer
};
ws.onmessage = (e) => {
touch();
lastMsgAt = Date.now();
const m = JSON.parse(e.data);
if (m.seq) lastSeq[m.ch] = m.seq;
if (m.ch === "error") {
if (m.data.code === "lagged") ws.send(JSON.stringify({ op: "resume", seqs: lastSeq }));
else console.error(m.data);
return;
}
if (m.ch === "reorg") return console.warn(`reorg: blocks ${m.data.removed_blocks.join(", ")} replaced`);
if (!m.ch.startsWith("trades:")) return; // hello, subscribed, heartbeat, finality, resumed
if (m.data.removed) seen.delete(m.data.id); // undo whatever you did for this trade
else onTrade(m.data, "live");
};
ws.onclose = () => {
clearTimeout(idle);
setTimeout(connect, backoff + Math.random() * 500);
backoff = Math.min(backoff * 2, 30_000);
};
ws.onerror = () => ws.close();
}
connect();
# trades_stream.py: pip install websockets requests; python trades_stream.py
import asyncio, json, os, random, time
import requests
import websockets
KEY = os.environ["CRIUS_API_KEY"]
API = "https://api.crius.sh/v1/robinhood"
TOKEN = "0x322f0929c4625ed5bad873c95208d54e1c003b2d" # TSLA, Tesla tokenized stock
CHANNELS = [f"trades:{TOKEN}"]
last_seq, seen = {}, {}
state = {"last_ts": 0, "last_msg": 0.0}
def on_trade(t, source):
if t["id"] in seen:
return
seen[t["id"]] = t
state["last_ts"] = max(state["last_ts"], t["ts"])
print(source, t["type"], f"${t['value_usd']}", f"@ ${t['price_usd']}", t["trader"])
def backfill():
if not state["last_ts"]:
return
r = requests.get(f"{API}/token/{TOKEN}/trades", params={"from_ts": state["last_ts"], "limit": 500},
headers={"x-api-key": KEY}, timeout=10)
for t in reversed(r.json().get("data") or []): # the tape is newest first
on_trade(t, "rest")
async def run():
backoff = 1
while True:
try:
async with websockets.connect(f"wss://api.crius.sh/v1/robinhood/stream?api_key={KEY}") as ws:
backoff = 1
offline = time.time() - state["last_msg"] if state["last_msg"] else 0
await ws.send(json.dumps({"op": "subscribe", "channels": CHANNELS}))
if last_seq:
await ws.send(json.dumps({"op": "resume", "seqs": last_seq}))
if offline > 55: # longer than the 60 s replay buffer
await asyncio.to_thread(backfill)
while True:
m = json.loads(await asyncio.wait_for(ws.recv(), timeout=45))
state["last_msg"] = time.time()
if m.get("seq"):
last_seq[m["ch"]] = m["seq"]
ch, data = m["ch"], m.get("data") or {}
if ch == "error":
if data.get("code") == "lagged":
await ws.send(json.dumps({"op": "resume", "seqs": last_seq}))
else:
print("error", data)
elif ch.startswith("trades:"):
if data.get("removed"):
seen.pop(data["id"], None) # undo whatever you did for this trade
else:
on_trade(data, "live")
except (OSError, asyncio.TimeoutError, websockets.WebSocketException) as e:
print("reconnecting:", e)
await asyncio.sleep(backoff + random.random() / 2)
backoff = min(backoff * 2, 30)
asyncio.run(run())

Keep the stream correct

  1. Watch the heartbeat. A heartbeat message arrives every 15 seconds. If nothing arrives for 45 seconds, close the socket and reconnect with backoff, as both clients do.

  2. Resume, then backfill. After reconnecting, subscribe again and send resume with the last seq per channel. The server replays up to 60 seconds. If you were away longer, call /trades?from_ts= with your newest trade’s ts; the id check drops anything you already have.

  3. Handle lagged. If your client reads too slowly, the server sends {"ch": "error", "data": {"code": "lagged", ...}} with the number of dropped messages. Send resume with your last seq values to recover them.

  4. Undo reorgs by id. A reorg message lists removed_blocks; each affected trade is then re-sent with removed: true and its replacement follows. Subscribing with "finality": "safe" holds trades until their block is safe, at the cost of a short delay; keep the removed handling anyway, since reorg re-sends are held and delivered the same way.

Frequently asked questions

Which plan do I need for the trades WebSocket?

The Free plan or higher, with a key that has the stream scope (keys get it by default). Free allows 1 connection with 10 subscriptions, Starter 3 connections with 100, Pro 10 with 1,000. Each connection costs 1 credit. Every trade on the chain (firehose:trades) is Enterprise only. See Plans & limits.

Can I stream all Robinhood Chain trades, not just one token?

firehose:trades carries every trade on the chain and needs the Enterprise plan. On other plans, subscribe to the tokens, pools (trades:pool:<pool>) or wallets (wallet:<address>) you care about, up to your plan's subscription limit.

What happens to my stream if I disconnect?

The server keeps 60 seconds of messages. Keep the last seq of each channel and send {"op": "resume", "seqs": {...}} after you subscribe again; you get what you missed, then {"ch": "resumed"}. After a longer outage, fill the gap from the REST trade tape with from_ts.

Are trades on the stream final?

They are pushed at latest, as soon as the block is decoded. If a block is replaced you get a reorg message, then each affected trade again with "removed": true. Subscribe with "finality": "safe" to receive trades only once their block is safe.

Is the stream message the same as the REST trade?

It carries the core fields with the same names and the same id (block-tx-log), so you can merge both. The REST trade tape adds detail the stream leaves out, such as fees, executor, quote_symbol, via and finality.

Get your free Crius API key

Every example on this page works with a free key: higher rate limits than the anonymous plan, holders, wallets and the WebSocket stream. No card needed.

  • Robinhood Chain (4663) only, from our own archive nodes
  • REST + WebSocket
  • Pay on-chain or by card