# Streams

> Every Hyperliquid WebSocket stream on Supanode: the full book, incremental updates, best bid and offer, trades, the per-order book, raw book diffs, order statuses and mid prices.

Eight streams, all on one connection. Almost every subscribe message uses the same envelope:

```json
{ "method": "subscribe", "subscription": { "type": "STREAM_NAME", "coin": "BTC" } }
```

The two exceptions are `allMids`, which covers every market and takes no `coin`, and `orderUpdates`, which follows an address rather than a market.

| Stream | What it carries |
|---|---|
| [`l2Book`](#l2book) | The full book by price level, up to 1000 levels per side |
| [`l2Diff`](#l2diff) | The same book as a snapshot plus incremental changes |
| [`bbo`](#bbo) | Best bid and offer only |
| [`trades`](#trades) | Executed trades |
| [`l4Book`](#l4book) | The book order by order, with owner addresses |
| [`bookDiffs`](#bookdiffs) | Raw book diffs from the node |
| [`orderUpdates`](#orderupdates) | Status changes of orders |
| [`allMids`](#allmids) | Mid price for every market |

## l2Book

The whole book, aggregated by price level.

```json
{ "method": "subscribe", "subscription": { "type": "l2Book", "coin": "BTC", "nLevels": 100 } }
```

A frame:

```json
{ "channel": "l2Book",
  "data": { "coin": "BTC", "time": 1787495871629, "nLevels": 20,
            "levels": [ [ {"px":"77488","sz":"18.36223","n":1} ],
                        [ {"px":"77492","sz":"0.51","n":2} ] ] } }
```

| Field | Meaning |
|---|---|
| `levels[0]` | bids, best price first |
| `levels[1]` | asks, best price first |
| `px` | price of the level, as a string |
| `sz` | total size resting at that level |
| `n` | how many orders make up that level |
| `time` | server timestamp in milliseconds |
| `nLevels` | how many levels this frame carries per side |

**Depth.** `nLevels` is optional and defaults to 20. The maximum is **1000**.

<Warning>
**`time` is not a unique identifier.** Two frames inside the same block can carry the same `time` and still differ, because they are two consecutive states of the book. Use the arrival order to tell frames apart — or `l2Diff` and `l4Book`, which carry a `height` that does identify the state.
</Warning>

<Tip>
**Book prices are strings without a trailing zero.** In `l2Book` we send `"78714"` where the public Hyperliquid API sends `"78714.0"`. Parse to a number and the two are identical; compare them as strings and they are not.
</Tip>

## l2Diff

The same book, sent as one snapshot followed by changes. This is the channel to use when you want depth: at 1000 levels the incremental stream carries the same book for a fraction of the bytes of a full frame on every block.

```json
{ "method": "subscribe", "subscription": { "type": "l2Diff", "coin": "BTC", "nLevels": 1000 } }
```

`data` always holds **exactly one** of two keys:

- **`Snapshot`** — the whole book, arranged as one `levels` list of two halves exactly like `l2Book`. It arrives first, and again whenever the feed needs to restart your view.
- **`Updates`** — what changed since the previous state, plus a `prevHeight`.

Inside `Updates`, changes come separately for `bids` and `asks`. Each side has `upd` for levels that changed and `del` for levels that are gone.

<Warning>
**A level has two different shapes.** In a snapshot a level is an object — `{"px":"77488","sz":"18.36","n":1}`. In an update the same level is a three-value array — `["77488","18.36",1]`, in the order price, size, order count. And `del` is neither: it is a plain list of price strings. Write one parser per shape, or the second frame will corrupt your book.
</Warning>

<Warning>
**Heights are not consecutive.** `prevHeight` is not `height` minus one: a block that changed nothing inside your window is never sent. So compare `prevHeight` against the height you last applied — never count blocks or assume a step of one.
</Warning>

An update, with one level removed on the ask side:

```json
{ "channel": "l2Diff",
  "data": { "Updates": { "coin": "BTC", "time": 1788944469884,
    "height": 1141112013, "prevHeight": 1141112009, "nLevels": 5,
    "bids": { "upd": [["79626","6.55144",24], ["79625","0.02308",4]], "del": [] },
    "asks": { "upd": [["79627","3.61784",13], ["79632","1.31325",2]], "del": ["79628"] } } } }
```

**The rule for the client:** if `prevHeight` does not match the height of the last update you applied, your copy of the book is wrong. Stop applying updates and wait for the next snapshot.

A snapshot can also arrive in the middle of a stream, on the same connection. Treat it as a fresh start: throw away the book you were keeping and rebuild from it.

## bbo

Best bid and best offer only, pushed as the top of the book changes. The lightest stream here — use it when you need a price, not a book.

```json
{ "method": "subscribe", "subscription": { "type": "bbo", "coin": "BTC" } }
```

A frame:

```json
{ "channel": "bbo",
  "data": { "coin": "BTC", "time": 1788944430011,
            "bid": {"px":"79613","sz":"1.90375","n":6},
            "ask": {"px":"79614","sz":"13.83401","n":54} } }
```

`bid` and `ask` are single objects, not lists, in the same `px` / `sz` / `n` shape as a level of `l2Book`.

<Warning>
**This frame differs from the public Hyperliquid API.** The public API returns one field, `bbo`, holding a list of two. We return two separate fields, `bid` and `ask`. Code written against their documentation will not find `data["bbo"]` here. Every other stream on this page keeps their shape.
</Warning>

## trades

Executed trades as they are matched.

```json
{ "method": "subscribe", "subscription": { "type": "trades", "coin": "BTC" } }
```

A frame:

```json
{ "channel": "trades",
  "data": [ { "coin": "BTC", "side": "B", "px": "79614.0", "sz": "0.00035",
              "hash": "0xe28cff12f5267aeae406044403fac902054d00f890...",
              "time": 1788944433313, "tid": 1066813006098562,
              "users": ["0x807e5727654ec56e3c7bb3f3779a7a7787ada4cd",
                        "0x2cffce91b4e0c81df18726ff66b31b2b1545e1ad"] } ] }
```

| Field | Meaning |
|---|---|
| `data` | a list — one frame can carry several trades |
| `side` | `B` for a buy, `A` for a sell |
| `px` / `sz` | price and size of the trade |
| `hash` | transaction hash |
| `tid` | trade id |
| `time` | timestamp in milliseconds |
| `users` | the two addresses on the two sides of the trade |

## l4Book

The book order by order rather than aggregated by price: every resting order with its price, size, id and the address that owns it. This is the deepest view of the market that exists.

```json
{ "method": "subscribe", "subscription": { "type": "l4Book", "coin": "BTC" } }
```

The first message is a full snapshot, then changes stream on every block. There is no depth parameter here — `nLevels` does not apply to `l4Book`, which always carries the whole book order by order.

A snapshot, cut down to one order per side — a real one carries hundreds or thousands:

```json
{ "channel": "l4Book",
  "data": { "Snapshot": { "coin": "LTC", "time": 1788944447014, "height": 1141111700,
    "levels": [
      [ {"user":"0x4e60e3a4...","coin":"LTC","side":"B","limitPx":"54.717","sz":"7.31",
         "oid":540105017942,"timestamp":1788944446488,"orderType":"Limit","tif":"Alo",
         "origSz":"7.31","cloid":"0xf80bd700...","reduceOnly":false,
         "isTrigger":false,"triggerPx":"0.0","triggerCondition":"N/A",
         "isPositionTpsl":false,"children":[]} ],
      [ {"user":"0x4e60e3a4...","coin":"LTC","side":"A","limitPx":"54.723","sz":"7.31",
         "oid":540105019037,"timestamp":1788944446690,"orderType":"Limit","tif":"Alo",
         "origSz":"7.31","cloid":"0x0e0cd700...","reduceOnly":false,
         "isTrigger":false,"triggerPx":"0.0","triggerCondition":"N/A",
         "isPositionTpsl":false,"children":[]} ] ] } } }
```

`levels[0]` is bids and `levels[1]` is asks, the same way round as `l2Book`. Each order carries who placed it (`user`), its id (`oid`) and client id (`cloid`), price and size (`limitPx`, `sz`, `origSz`), when it was placed, its type and time-in-force, and its trigger fields.

An update:

```json
{ "channel": "l4Book",
  "data": { "Updates": { "time": 1788944447092, "height": 1141111701,
    "order_statuses": [],
    "book_diffs": [ { "user": "0xdf83170d...", "oid": 540105022216,
                      "px": "54.742", "coin": "LTC",
                      "raw_book_diff": { "new": { "sz": "54.8" } } } ] } } }
```

Like `l2Diff`, `data` holds either a `Snapshot` or `Updates`, and both carry a `height`.

<Tip>
**Subscribe one market at a time.** The opening snapshot of a liquid market is several megabytes and takes a few seconds to arrive — that is the snapshot, not a stall. Let it finish before you subscribe to the next market, and keep the number of simultaneous `l4Book` markets to the ones you actually display.
</Tip>

<Note>
**Trigger orders are not in `l4Book`.** Stop and take-profit orders do not rest in the book until they fire, so they are not part of this stream.
</Note>

## bookDiffs

The raw book diffs the node itself produces, before any aggregation.

```json
{ "method": "subscribe", "subscription": { "type": "bookDiffs", "coin": "BTC" } }
```

## orderUpdates

Status changes of orders — placed, filled, cancelled. This stream follows **an address, not a market**, so there is no `coin` here.

```json
{ "method": "subscribe", "subscription": { "type": "orderUpdates", "user": "0xYOUR_ADDRESS" } }
```

## allMids

One frame carrying the mid price of every market, so you do not need a subscription per coin.

```json
{ "method": "subscribe", "subscription": { "type": "allMids" } }
```

## Field types

| Field | Type |
|---|---|
| `time` | unix milliseconds, 13 digits — `1788943509915` |
| `px`, `sz`, `limitPx`, `origSz` | strings, never numbers |
| `n` | integer — how many orders are on the level |
| `oid`, `tid`, `height` | integers |
| `user`, `hash`, `cloid` | `0x` strings, lower case |

## Spot markets

Spot works the same way as perpetuals: put `@<index>` from `spotMeta` where the symbol goes. The book, the incremental channel, best bid and offer, trades and the per-order book all take spot markets.

<Tip>
**Prices and sizes are always strings, and the number of decimals depends on the market.** BTC arrives as `79499`, ETH as `2511.9`, DOGE as `0.09117`. Parse to a number rather than assuming a format.
</Tip>

## Several markets on one connection

Send one subscribe message per market. Each market streams as its own frame, and the `coin` field tells you which is which. There is no need to open a connection per market.

## Next steps

<CardGroup cols={2}>
  <Card title="Limits" icon="gauge" href="https://supanode.xyz/docs/hyperliquid/websocket/limits">
    What is capped today.
  </Card>
  <Card title="Examples" icon="code" href="https://supanode.xyz/docs/hyperliquid/websocket/examples">
    Working code for each of the patterns above.
  </Card>
</CardGroup>

## External references

- [Hyperliquid WebSocket subscriptions](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/websocket/subscriptions)
