Hyperliquid WebSocket 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-24
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.0","sz":"18.36223","n":1} ],
[ {"px":"77492.0","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. It is the block time, and a block is applied over several flushes, so two frames can carry the same time and still differ. Use the arrival order to tell frames apart - or l2Diff and l4Book, which carry a height that does identify the state.
l2Diff
The same book as l2Book, same parameters, sent as one snapshot and then only the levels that changed. At 1000 levels that is about 113× less traffic, and the rebuilt book is identical level for level. Use it whenever you want depth.
{ "method": "subscribe", "subscription": { "type": "l2Diff", "coin": "BTC", "nLevels": 1000 } }
data holds either a Snapshot (the whole book, shaped like l2Book) or Updates (per side, upd as [px, sz, n] triples and del as bare prices). If an update's prevHeight does not match the last height you applied, you missed a frame: resubscribe to get a fresh snapshot.
Frame formats, continuity rules and the traffic measurements are on their own page: l2Diff: the incremental book.
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.