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.
// updated 2026-09-09
Eight streams, all on one connection. Almost every subscribe message uses the same envelope:
{ "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 | The full book by price level, up to 1000 levels per side |
l2Diff | The same book as a snapshot plus incremental changes |
bbo | Best bid and offer only |
trades | Executed trades |
l4Book | The book order by order, with owner addresses |
bookDiffs | Raw book diffs from the node |
orderUpdates | Status changes of orders |
allMids | Mid price for every market |
l2Book
The whole book, aggregated by price level.
{ "method": "subscribe", "subscription": { "type": "l2Book", "coin": "BTC", "nLevels": 100 } }
A frame:
{ "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.
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.
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.
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.
{ "method": "subscribe", "subscription": { "type": "l2Diff", "coin": "BTC", "nLevels": 1000 } }
data always holds exactly one of two keys:
Snapshot— the whole book, arranged as onelevelslist of two halves exactly likel2Book. It arrives first, and again whenever the feed needs to restart your view.Updates— what changed since the previous state, plus aprevHeight.
Inside Updates, changes come separately for bids and asks. Each side has upd for levels that changed and del for levels that are gone.
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.
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.
An update, with one level removed on the ask side:
{ "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.
{ "method": "subscribe", "subscription": { "type": "bbo", "coin": "BTC" } }
A frame:
{ "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.
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.
trades
Executed trades as they are matched.
{ "method": "subscribe", "subscription": { "type": "trades", "coin": "BTC" } }
A frame:
{ "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.
{ "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:
{ "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:
{ "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.
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.
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.
bookDiffs
The raw book diffs the node itself produces, before any aggregation.
{ "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.
{ "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.
{ "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.
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.
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.