CRIUS
WebSocketStreamingReal-timeReference

Robinhood Chain WebSocket API: Live Trades, Prices and Reconnects

Stream live Robinhood Chain trades, prices, candles, launches and new pools over WebSocket: channels, resume after reconnects, reorgs and finality.

Crius 8 min read

Short answer: connect to wss://api.crius.sh/v1/robinhood/stream, send {"op":"subscribe","channels":["trades:<token>"]}, and you receive every swap of that token within about a block of it landing on Robinhood Chain. Every message carries a per-channel sequence number, so after a dropped connection you send {"op":"resume"} with your last numbers and the server replays what you missed.

This is the protocol reference in tutorial form: connecting, the channels, message format, heartbeats, resuming, reorgs, finality and plan limits. If you want a complete bot built on top of it, read Building a trading bot with WebSocket streams after this one. All examples were run against the live stream in October 2026.

Connecting

The stream needs an API key with the stream scope; every plan’s default keys have it. Browsers can’t set headers on a WebSocket, so the key can go in the query string:

wss://api.crius.sh/v1/robinhood/stream?api_key=YOUR_KEY

Server-side clients can send the usual x-api-key header instead, which keeps the key out of URLs and proxy logs.

On connect, the server sends a hello:

{"ch": "hello", "data": {"chain": "robinhood", "heartbeat_s": 15, "max_subscriptions": 1000}}

max_subscriptions is your plan’s channel limit for this connection.

Subscribing

{"op": "subscribe", "channels": ["trades:0xc967…964a", "price:0xc967…964a", "candles:0xc967…964a:1m", "pools:new"]}

The reply confirms the full set and the finality mode:

{"ch": "subscribed", "data": {"channels": ["trades:0xc967…964a", "price:0xc967…964a", "candles:0xc967…964a:1m", "pools:new"], "finality": "latest"}}

price: channels also get an immediate snapshot so you don’t wait for the next trade:

{"ch": "price:0xc967…964a", "snapshot": true, "data": {"usd": 0.00118745, "eth": 5.01799e-7, "liquidity_usd": 82356.4}}

{"op": "unsubscribe", "channels": [...]} removes channels. Channel names are case-insensitive.

Channels

Channel What it carries
trades:<token> Every swap of the token: side, amounts, USD value, price, pool, trader
trades:pool:<pool> Every swap in one pool
price:<token> Price in USD and ETH plus liquidity, at most 10 per second per token
candles:<token>:<interval> The live candle for 1s … 1d, updated with each trade
token:<token> Price and holder count
launches:all, launches:<launchpad> New token launches (Pons, Flap.sh and others)
graduations:all, graduations:<launchpad> Bonding-curve graduations to a DEX pool
pools:new Every new pool on Uniswap v2, v3 and v4
lp:<token>, lp:all Liquidity adds and removes
tax:<token>, tax:all Changes in a token’s measured buy or sell tax
wallet:<address> Trades and transfers of one wallet
dev:<address> Launches by a creator address
firehose:trades Every swap on the chain (Enterprise)

Which channels for which app

You are building Subscribe to
A price ticker or watchlist price:<token> for each token (or POST /prices polling for long lists)
A token page or chart candles:<token>:1m, trades:<token>, price:<token>
A new-token sniper or alert bot launches:all, pools:new, then trades:<token> for tokens that pass your checks
A graduation tracker graduations:all or graduations:pons
Wallet or copy-trading alerts wallet:<address> for each tracked wallet
Rug and risk monitoring lp:<token> for liquidity removals, tax:<token> for tax changes
A creator watchlist dev:<address> for each creator you follow

Two habits keep subscriptions efficient. Subscribe to specific tokens rather than firehose:trades unless you really need every swap on the chain, and unsubscribe from a token’s channels when your user leaves its page; the subscription limit is per connection, and cleaning up keeps you well inside it.

Message format

Every data message has the same envelope:

{"ch": "trades:0xc967…964a", "seq": 217, "block": 96714838, "ts": 1792839562000, "data": {"type": "sell", "value_usd": 34.7421, "price_usd": 0.00117581, "…": "…"}}
  • ch: the channel.
  • seq: increases by one per message on that channel, with no gaps. This is what makes resuming exact.
  • block and ts: the block the event is from and its time.
  • data: the payload.

Live candle messages carry one bar, and you replace your last bar when t matches:

{"ch": "candles:0xc967…964a:1m", "data": {"t": 1792839660000, "o": 0.00118786, "h": 0.00118791, "l": 0.00117547, "c": 0.00117581, "v": 422.529, "n": 5}}

Heartbeats and dead connections

The server sends {"ch":"heartbeat"} every 15 seconds. Use it as a watchdog: if nothing at all arrives for three heartbeat intervals, the connection is dead even if the socket hasn’t noticed, so close it and reconnect. Mobile networks and NAT gateways drop idle TCP connections silently, and this is the only reliable way to notice.

Resuming after a reconnect

Keep the last seq per channel. When you reconnect, subscribe again, then ask for everything after those numbers:

{"op": "resume", "seqs": {"trades:0xc967…964a": 219, "candles:0xc967…964a:1m": 219, "price:0xc967…964a": 9}}

The server replays the messages you missed, then answers {"ch":"resumed"}. The replay buffer holds the last 60 seconds, which covers ordinary drops and redeploys. After a longer outage, refetch current state over REST (prices, recent trades, candles) and continue from the live stream.

Here is a complete client with exponential backoff, the heartbeat watchdog and resume. In testing we forced a disconnect 20 seconds in; it reconnected, resumed from the saved sequence numbers and carried on without gaps.

const KEY = process.env.CRIUS_API_KEY;
const URL = `wss://api.crius.sh/v1/robinhood/stream?api_key=${KEY}`;
const TOKEN = "0xc9675fe679c7d4e1b94416f66d61c96127eb964a";
const CHANNELS = [`trades:${TOKEN}`, `price:${TOKEN}`, `candles:${TOKEN}:1m`, "pools:new"];
const lastSeq = {};
let backoff = 500;
function connect() {
const ws = new WebSocket(URL); // Node 22+ and browsers have WebSocket built in
let alive;
const watchdog = () => { clearTimeout(alive); alive = setTimeout(() => ws.close(), 45_000); };
ws.onopen = () => {
backoff = 500;
ws.send(JSON.stringify({ op: "subscribe", channels: CHANNELS }));
if (Object.keys(lastSeq).length) ws.send(JSON.stringify({ op: "resume", seqs: lastSeq }));
watchdog();
};
ws.onmessage = (ev) => {
watchdog();
const m = JSON.parse(ev.data);
if (m.seq) lastSeq[m.ch] = m.seq;
if (m.ch === "reorg") return rollback(m.data.removed_blocks);
if (m.data?.removed) return undo(m);
if (m.ch === "error") return console.warn(m.data);
handle(m); // your application logic
};
ws.onclose = () => {
clearTimeout(alive);
setTimeout(connect, backoff);
backoff = Math.min(backoff * 2, 30_000);
};
}
connect();

The same subscription in Python with the websockets package:

import asyncio, json, os, websockets
URL = f"wss://api.crius.sh/v1/robinhood/stream?api_key={os.environ['CRIUS_API_KEY']}"
TOKEN = "0xc9675fe679c7d4e1b94416f66d61c96127eb964a"
last_seq = {}
async def run():
async with websockets.connect(URL, ping_interval=None) as ws:
await ws.send(json.dumps({"op": "subscribe", "channels": [f"trades:{TOKEN}"]}))
if last_seq:
await ws.send(json.dumps({"op": "resume", "seqs": last_seq}))
while True:
m = json.loads(await asyncio.wait_for(ws.recv(), timeout=45))
if "seq" in m:
last_seq[m["ch"]] = m["seq"]
if m["ch"].startswith("trades:"):
print("trade", m["block"], m["data"]["type"], m["data"]["value_usd"])
asyncio.run(run())

If your client falls too far behind, the server drops messages for it and sends {"ch":"error","data":{"code":"lagged","dropped":N,"hint":"resume with your last seq"}}. Do what it says: send a resume. If it keeps happening, move slow work out of the message handler into a queue.

Reorgs

Robinhood Chain, like any chain, occasionally replaces its most recent blocks. When that happens you receive:

{"ch": "reorg", "data": {"common_ancestor": 96714800, "removed_blocks": [96714801, 96714802], "depth": 2}}

followed by every message from the removed blocks re-sent with "removed": true. Undo whatever those messages did: subtract the trade from your volume, drop the launch from your list. The replacement blocks then stream normally. If your application can’t undo, use finality instead.

Finality: latest, safe or finalized

By default you get events at latest, as soon as the block is applied. Subscribe with a finality mode to receive only events that can no longer be reorganised:

{"op": "subscribe", "channels": ["trades:0xc967…964a"], "finality": "safe"}

On Robinhood Chain, safe means the block’s batch has been posted to Ethereum. While we wrote this, the safe head was about 5,600 blocks (roughly 10 minutes) behind the latest block, and the finalized head about 9,300 blocks behind. safe is right for accounting and payouts; latest is right for trading and alerts, together with reorg handling. Events are held, not dropped, until their block becomes safe, and finality messages tell you how far the safe head has moved.

Plan limits

Plan Connections (plan allowance) Subscriptions per connection
Free 1 10
Starter 3 100
Pro 10 1,000
Business 50 10,000
Enterprise 200 50,000

Opening a connection costs one credit; the messages themselves are not metered, so a busy subscription costs the same as a quiet one. The subscription limit is enforced per connection: channels beyond it are refused with an error that states the maximum. firehose:trades, every swap on the chain, is Enterprise only. Streaming needs a key: anonymous connections are refused.

Frequently asked questions

How fast are WebSocket messages? Messages are sent as soon as Crius applies the block, typically within a fraction of a second of it being produced. Robinhood Chain produces a block about every 108 ms.

Do WebSocket trades include the same fields as REST trades? The core fields are the same: side, amounts, USD value, price, pool, DEX and trader. The REST endpoint adds per-trade extras such as gas and fee details, so fetch a trade over REST if you need those.

Can I use the stream from a browser? Technically yes, with the key in the api_key query parameter, but anything shipped to a browser is public. Origin-restricted keys are read-only and can’t stream, so for a public web app run a small relay on your own server that holds the stream key and forwards messages to your users.

What happens during a server deploy? Your connection closes and you reconnect, which takes a second or two. With resume, nothing is lost because the replay buffer covers the gap.

How do I watch every new token? Subscribe to launches:all for launchpad tokens and pools:new for tokens that list directly on a DEX. See the Pons launchpad guide for launch tracking in depth.

Next steps

Questions go to support@crius.sh.

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