WebSocket stream
Real-time Robinhood Chain data over one WebSocket - trades, prices, live candles, new launches, graduations, new pools, LP and tax changes, wallet activity, with replay and reorg handling.
WS wss://api.crius.sh/v1/{chain}/stream
Plan: Free and above, key with the stream scope · Cost: 1 credit per connection
One connection carries many subscriptions. Messages are pushed as soon as a block is decoded, typically well under a second after it was produced.
| Plan | WebSocket connections | Subscriptions per connection |
|---|---|---|
| Anonymous (no key) | none | none |
| Free | 1 | 10 |
| Starter | 3 | 100 |
| Pro | 10 | 1,000 |
| Business | 50 | 10,000 |
| Enterprise | custom | custom |
See Plans & limits for the rest of each plan.
Connecting
Authenticate with the x-api-key header, or with ?api_key= where a client can’t set headers (browsers):
const ws = new WebSocket(`wss://api.crius.sh/v1/robinhood/stream?api_key=${key}`);The key is checked before the upgrade, like any REST call: no key returns 403 plan_required, and a key without the stream scope returns 403 scope_required.
On connect the server sends a greeting with your limits (max_subscriptions is your plan’s subscriptions per connection):
{"ch": "hello", "data": {"chain": "robinhood", "heartbeat_s": 15, "max_subscriptions": 10}}Operations
Send JSON messages with an op:
| Op | Message | Reply |
|---|---|---|
| Subscribe | {"op": "subscribe", "channels": ["trades:0x…", "launches:all"]} |
{"ch": "subscribed", "data": {"channels": […], "finality": "latest"}} |
| Unsubscribe | {"op": "unsubscribe", "channels": ["trades:0x…"]} |
{"ch": "unsubscribed", "data": {"channels": […]}} |
| Settled data only | {"op": "subscribe", "channels": […], "finality": "safe"} |
events are held until their block is safe |
| Resume after a reconnect | {"op": "resume", "seqs": {"trades:0x…": 90455}} |
missed messages (up to 60 s), then {"ch": "resumed"} |
| Ping | {"op": "ping"} |
{"ch": "pong", "ts": …} |
- Subscribe adds channels to the connection; the
subscribedandunsubscribedreplies list every channel the connection now has, not only the ones in the message. - Channel names are lowercased by the server, and messages carry the lowercase name in
ch. Send lowercase addresses so the names you match on are the ones you receive. - Price snapshot. For each
price:channel in asubscribemessage, the current price is sent right away, before thesubscribedreply:{"ch": "price:0x…", "snapshot": true, "data": {"usd": …, "eth": …, "liquidity_usd": …}}. A snapshot has noseq,blockorts, itschis the name exactly as you sent it, and it is only sent for tokens that have a price. - Finality.
"finality": "safe"(or"finalized", which behaves the same) applies to the whole connection, for every channel, from that message on; the reply says"safe". Without it, events are sent atlatest. - Resume.
seqsmaps a channel to the lastseqyou saw on it; you receive the newer messages still in the replay buffer (the last 60 seconds) for channels you’re subscribed to, then{"ch": "resumed"}.from_seq(a number) is used for subscribed channels missing fromseqs. Channels with neither are not replayed, and resume only covers channels already subscribed on this connection: sendsubscribefirst.
Errors
Errors arrive as messages on the error channel; the connection stays open.
data.code |
When | Other fields |
|---|---|---|
channel_denied |
A firehose: channel on a plan below Enterprise, or subscribing past max_subscriptions |
channels (the ones refused), message, max_subscriptions |
lagged |
Your client read too slowly and the server dropped messages for it | dropped (how many), hint: "resume with your last seq" |
bad_message |
The message wasn’t valid JSON, or had no op |
message |
unknown_op |
op is not subscribe, unsubscribe, resume or ping |
op |
{"ch": "error", "data": {"code": "lagged", "dropped": 120, "hint": "resume with your last seq"}}On lagged, send resume with your last seq per channel to get the dropped messages back (if they’re still in the 60-second buffer).
Channels
| Channel | Pushes |
|---|---|
trades:<token> |
Every trade of a token, in any pool |
trades:pool:<pool> |
Every trade in one pool (v2/v3 pool address or v4 pool id) |
price:<token> |
Price updates (usd, eth, liquidity_usd) when the token trades, at most every 100 ms per token; a snapshot is sent right after subscribing |
candles:<token>:<interval> |
The live candle for 1s, 30s, 1m, 5m, 15m, 1h, 4h, 6h or 1d, after every trade |
token:<token> |
Price and holder count (price_usd, holders), with the same timing as price: |
tax:<token> / tax:all |
Realized buy or sell tax changes of one token, or of every token |
lp:<token> / lp:all |
Liquidity added or removed in the token’s pools, or in every pool |
launches:<launchpad> / launches:all |
New launches (pons, flap, bankr, long, flywheel, standard_reserve) |
graduations:<launchpad> / graduations:all |
Graduations, with the new pool |
pools:new |
Every new Uniswap v2 / v3 / v4 pool, with hook permissions |
wallet:<address> |
Trades the wallet made and token transfers to or from it |
dev:<address> |
New tokens launched by a creator |
firehose:trades |
Every trade on the chain (Enterprise) |
finality, reorg and heartbeat messages reach every connection without subscribing.
Every message has the same envelope: ch, seq, block, ts (unix ms: the block’s time for chain events, the server’s time on finality, reorg and removed re-sends) and data. This is the trade tape sample’s trade 100410183-9-12 as the stream sends it:
{ "ch": "trades:0x322f0929c4625ed5bad873c95208d54e1c003b2d", "seq": 90455, "block": 100410183, "ts": 1793225424000, "data": { "id": "100410183-9-12", "type": "sell", "ts": 1793225424000, "block": 100410183, "token": "0x322f0929c4625ed5bad873c95208d54e1c003b2d", "symbol": "TSLA", "logo": null, "pool": "0x3ea70ce0b34473692549f97e630a1d82dc05cea94fa55f81b50f10c8c6d8186f", "dex": "uniswap_v4", "trader": "0x278d858f05b94576c1e6f73285886876ff6ef8d2", "signer": "0xe07ba3acee529ff1d4f7290cbae32fa065e18acd", "amount_base": 0.68873541086638403, "amount_quote": 0.358712450309036398, "price_usd": 402.641, "value_usd": 277.313, "tax_pct": 0 }}seq increases by one per channel. Keep the last seq you saw for each channel and send them in a resume op after reconnecting to receive what you missed (60-second replay buffer).
Message data per channel
| Channel | data fields |
|---|---|
trades:, trades:pool:, firehose:trades, and trades on wallet: |
id (block-tx-log), type (buy / sell), ts, block, token, symbol, logo, pool, dex, trader, signer, amount_base, amount_quote (human units), price_usd, value_usd, tax_pct (null when no tax was measured) |
Transfers on wallet: |
type: "transfer", token, from, to, amount, id. Mints and burns are not sent |
price: |
usd, eth, liquidity_usd |
candles: |
t (bucket start, unix ms), o, h, l, c, v (USD volume), n (trades) |
token: |
price_usd, holders (null until the token’s holder history is complete) |
tax: |
token, side (buy / sell), from, to (whole percent), source: "realized" |
lp: |
type (add_liquidity / remove_liquidity), pool, dex, provider, token0, token1, amount0, amount1 |
launches:, dev: |
address, launchpad, creator, curve, quote (the quote token’s symbol, e.g. ETH), tx |
graduations: |
token, launchpad, pool, position_id |
pools:new |
pool, dex, token0, token1, fee, tick_spacing, hook (null, or address and permissions, e.g. beforeSwap) |
A stream trade is a compact version of a trade tape row, with the same id. It doesn’t carry tx, tx_index, executor, via, fees, finality, quote, quote_symbol, value_eth, price_eth, eth_usd_at_trade, market_cap_usd_at_trade or the trader_label / signer_label / trader_app labels: fetch the trade tape for those.
Notes on timing:
price:andtoken:are only sent when the token trades, and at most once per 100 ms per token.candles:channels have no snapshot: the first message comes with the token’s next trade, and a bucket without trades sends nothing. A new candle opens at the previous close, and a small trade far from the token’s current price counts invandnwithout movingh,lorc.tax:fires when the realized tax on one side, rounded to a whole percent, differs from that side’s previous trade.
Heartbeats, finality and reorgs
{"ch": "heartbeat", "ts": …, "uptime_s": …}arrives every 15 seconds (uptime_sis how long this connection has been open); reconnect if you hear nothing for 45 seconds.{"ch": "finality", "seq": …, "block": 0, "ts": …, "data": {"safe_block": …, "finalized_block": …}}arrives on every connection whenever the safe or finalized block moves.- If blocks are replaced, a
{"ch": "reorg", "data": {"common_ancestor", "removed_blocks", "depth"}}message comes first (removed_blockslists the replaced blocks that had messages,depthhow many there are), then every affected event is re-sent on its channel with"removed": trueadded to itsdata(sameid, a newseq), then the replacement events as their blocks are decoded. - With
"finality": "safe", events are held until thefinalitymessage that makes their block safe, then delivered in order. Theremovedre-sends of a reorg are held the same way, so keep handlingremoved: true.
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