Documentation

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.

StreamWhat it carries
l2BookThe full book by price level, up to 1000 levels per side
l2DiffThe same book as a snapshot plus incremental changes
bboBest bid and offer only
tradesExecuted trades
l4BookThe book order by order, with owner addresses
bookDiffsRaw book diffs from the node
orderUpdatesStatus changes of orders
allMidsMid 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} ] ] } }
FieldMeaning
levels[0]bids, best price first
levels[1]asks, best price first
pxprice of the level, as a string
sztotal size resting at that level
nhow many orders make up that level
timeserver timestamp in milliseconds
nLevelshow 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.

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.

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

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.

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.

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"] } ] }
FieldMeaning
dataa list — one frame can carry several trades
sideB for a buy, A for a sell
px / szprice and size of the trade
hashtransaction hash
tidtrade id
timetimestamp in milliseconds
usersthe 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.

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.

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.

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

FieldType
timeunix milliseconds, 13 digits — 1788943509915
px, sz, limitPx, origSzstrings, never numbers
ninteger — how many orders are on the level
oid, tid, heightintegers
user, hash, cloid0x 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.

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

Limits

What is capped today.

Examples

Working code for each of the patterns above.

External references