# 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.

Source: https://docs.crius.sh/guides/stream-trades-websocket/ · Updated 2026-10-28

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.

[GET `/token/{address}/trades`](https://docs.crius.sh/reference/trades/trade-tape/)

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](https://docs.crius.sh/reference/trades/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.

```json
{"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](https://docs.crius.sh/reference/stream/) 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

| Field | Meaning |
| --- | --- |
| `id` | `block-tx-log`, the same id the REST trade tape uses: use it to de-duplicate and to undo reorged trades |
| `type` | `buy` or `sell`, from the token's side |
| `token`, `symbol`, `logo` | The token traded |
| `pool`, `dex` | Pool address (v2/v3) or pool id (v4), and `uniswap_v2`, `uniswap_v3`, `uniswap_v4` or a launch curve |
| `trader` | The wallet whose balance changed, not the router or bundler that sent the transaction |
| `signer` | The address that signed the transaction |
| `amount_base`, `amount_quote` | Token and quote amounts in human units |
| `price_usd`, `value_usd` | USD price per token and USD value of the trade at that block |
| `tax_pct` | Token tax measured on this trade, when there was one |
| `removed` | `true` 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.

Example [Anonymous · no key · 2 credits](https://docs.crius.sh/reference/trades/trade-tape/)

### 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**

```bash
curl "https://api.crius.sh/v1/robinhood/token/0x322f0929c4625ed5bad873c95208d54e1c003b2d/trades?limit=2"
```

**JavaScript**

```js
const res = await fetch("https://api.crius.sh/v1/robinhood/token/0x322f0929c4625ed5bad873c95208d54e1c003b2d/trades?limit=2");
const { data, meta } = await res.json();
```

**Python**

```python
import requests

r = requests.get(
    "https://api.crius.sh/v1/robinhood/token/0x322f0929c4625ed5bad873c95208d54e1c003b2d/trades?limit=2",
)
body = r.json()
print(body["data"])
```

**Real response 200 Captured Oct 28, 2026 · 98 lines (arrays trimmed)**

```json
{
  "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](https://docs.crius.sh/reference/trades/trade-tape/) Runs from your browser on the anonymous plan [Data looks wrong? Report it](mailto:support@crius.sh?subject=Wrong%20data%20in%20docs%20example%3A%20The%20last%20two%20TSLA%20trades%2C%20for%20backfilling%20a%20stream&body=Page%3A%20https%3A%2F%2Fdocs.crius.sh%2Fguides%2Fstream-trades-websocket%2F%23backfill%0ARequest%3A%20GET%20%2Fv1%2Frobinhood%2Ftoken%2F0x322f0929c4625ed5bad873c95208d54e1c003b2d%2Ftrades%3Flimit%3D2%0A%0AWhat's%20wrong%20or%20missing%3F%0A%0A)

## 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.

**Node.js**

```js
// 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();
```

**Python**

```python
# 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.
    

Keep the key on a server

The browser WebSocket API can’t set headers, so a browser client would have to put the key in the URL, where anyone can read it. Run the stream client on your backend and forward trades to your pages, for example over Server-Sent Events, as in the [live candles guide](https://docs.crius.sh/guides/stream-live-candles/).

## 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](https://docs.crius.sh/plans-and-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`.

## Related

[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. Read](https://docs.crius.sh/reference/stream/) [Docs **Real-time stream example** A complete reconnecting WebSocket client for the Crius stream in Node.js - subscribe to new launches, trades and prices on Robinhood Chain, resume after disconnects and handle reorgs. Read](https://docs.crius.sh/examples/stream/) [Guide **Token trades** Get every buy and sell of a Robinhood Chain token with USD value, price, gas, tax and the real trader behind routers and smart wallets. Real examples, no key. Read](https://docs.crius.sh/guides/get-token-trades/) [Guide **Stream live candles** Keep a Robinhood Chain candlestick chart live: load candles over REST, update the last bar from the Crius WebSocket, with a runnable Node.js relay. Read](https://docs.crius.sh/guides/stream-live-candles/) [Guide **Price alerts** Build Robinhood Chain price alerts on the Crius WebSocket price channel, with a runnable Node.js alert bot that checks liquidity and token security first. Read](https://docs.crius.sh/guides/build-price-alerts/) [Concept **Finality & reorgs** Finality levels on Robinhood Chain (latest, safe, finalized), how the Crius API marks them, and how chain reorganizations are handled in REST and WebSocket. Read](https://docs.crius.sh/concepts/finality/)
