CRIUS Docs
API status Get API key
API reference

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.

Updated 6 min read

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 subscribed and unsubscribed replies 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 a subscribe message, the current price is sent right away, before the subscribed reply: {"ch": "price:0x…", "snapshot": true, "data": {"usd": …, "eth": …, "liquidity_usd": …}}. A snapshot has no seq, block or ts, its ch is 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 at latest.
  • Resume. seqs maps a channel to the last seq you 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 from seqs. Channels with neither are not replayed, and resume only covers channels already subscribed on this connection: send subscribe first.

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: and token: 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 in v and n without moving h, l or c.
  • 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_s is 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_blocks lists the replaced blocks that had messages, depth how many there are), then every affected event is re-sent on its channel with "removed": true added to its data (same id, a new seq), then the replacement events as their blocks are decoded.
  • With "finality": "safe", events are held until the finality message that makes their block safe, then delivered in order. The removed re-sends of a reorg are held the same way, so keep handling removed: 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