l2Diff: the incremental book
The Hyperliquid l2Diff channel on Supanode: the same order book as l2Book, sent as one snapshot and then only the levels that changed. About 113x less traffic at 1000 levels, rebuilt book identical level for level.
// updated 2026-09-24
l2Diff carries the same order book as l2Book, with the same parameters, built by the same flush. The difference is delivery: one snapshot when you subscribe, and after that only the levels that changed. Subscribe to it instead of l2Book whenever you want depth. At 1000 levels it is roughly a hundred times less traffic, and the book you rebuild from it is identical, level for level.
Why it exists
A 1000-level BTC book is about 72 KB of JSON, and it is rebuilt about 14 times a second, one flush per block. Resending it whole is about 1 MB/s (8 Mbit/s) for a single coin on a single subscriber.
Measured over 120 s on one connection carrying both channels, BTC at nLevels: 1000:
| Frames | Mean frame | Total | |
|---|---|---|---|
l2Book | 1,674 | 71,711 B | 120.04 MB |
l2Diff | 1 snapshot + 1,674 updates | 592 B per update | 1.06 MB |
113× less over the run, 121× less per update. A second server on a second node, same run: 104× and 111×. The book rebuilt from those 1,674 updates matched all 1,674 l2Book frames.
Subscribe
{ "method": "subscribe", "subscription": { "type": "l2Diff", "coin": "BTC", "nLevels": 1000 } }
The parameters are exactly those of l2Book:
| Parameter | Values | Meaning |
|---|---|---|
coin | e.g. "BTC", "@107" | required |
nLevels | 1-1000, omit for 20 | levels per side |
nSigFigs | 2-5 | price bucketing, as on the public Hyperliquid API |
mantissa | 2 or 5, requires nSigFigs: 5 | finer bucketing |
The validator is strict about two things. Passing nLevels: 20 explicitly is rejected: omit the field to get the default. And mantissa without nSigFigs: 5 is rejected too.
Unsubscribe with the same subscription object and "method": "unsubscribe".
The two frames
data holds exactly one of two keys, Snapshot or Updates.
Snapshot is the whole book at your depth. It is sent when you subscribe, and again whenever the server cannot produce a correct diff for you (see Continuity):
{"channel":"l2Diff","data":{"Snapshot":{
"coin":"BTC","time":1789558634545,"height":1149553971,"nLevels":1000,
"levels":[
[{"px":"75899.0","sz":"1.85926","n":11}, {"px":"75898.0","sz":"0.4","n":2}],
[{"px":"75900.0","sz":"5.68367","n":38}, {"px":"75901.0","sz":"0.9","n":3}]
]}}}
levels is [bids, asks], bids descending and asks ascending, exactly as in l2Book. px and sz are decimal strings. n is the number of resting orders at that level.
Updates carry only what moved since the previous frame on this subscription:
{"channel":"l2Diff","data":{"Updates":{
"coin":"BTC","time":1789558634744,"height":1149553974,"prevHeight":1149553971,
"nLevels":1000,
"bids":{"upd":[["75899.0","2.10012",13]],"del":["75898.0"]},
"asks":{"upd":[["75900.0","5.1",35],["75902.0","0.25",1]],"del":[]}
}}}
Each side carries two lists:
updholds levels to insert or overwrite, addressed by price. Each one is a positional triple[px, sz, n], not an object: at 1000 levels the field names would be about 60% of the frame.pxandszare strings,nis a number.delholds prices that are no longer in the book at your depth.
A level has two shapes. In a snapshot it is an object, {"px":"75899.0","sz":"1.85926","n":11}. In an update it is an array, ["75899.0","2.10012",13]. In del it is a bare price string. Write one parser per shape.
nLevels is always echoed back. nSigFigs and mantissa are echoed on both frames when you set them.
Continuity
A coin is not dirty at every block, so height skips legitimately. height - 1 proves nothing. prevHeight is the check.
If prevHeight does not equal the last height you applied, you missed a frame. Your book is stale. Resubscribe (unsubscribe, then subscribe) to get a fresh Snapshot.
The server also sends a Snapshot unasked in three cases:
- your first frame on the subscription;
- a slow consumer that fell behind the broadcast: the frames it missed are gone, so a diff would be wrong;
- a coin the server has no prior book for.
Treat any Snapshot as "replace everything you have for this subscription".
Depth is part of the diff
Levels are truncated to your nLevels before the comparison. That has two consequences:
- a price in
delmay simply have been pushed past your depth by a better level, not cancelled or filled; - two subscribers at different depths receive different diffs for the same book. That is correct behaviour.
Silence is normal
A coin with nothing to report sends nothing at all. There are no heartbeat or keep-alive frames on this channel. On a quiet market you can go seconds without a frame: the book has not changed, the connection is fine. Send {"method":"ping"} (answered with {"channel":"pong"}) if you want to check liveness.
Use height, not time
time is the block time, and a block is applied over several flushes. Two consecutive frames can carry the same time with different books. Use height and prevHeight as the frame identity and for ordering. l2Book has no height field, which is one more reason to prefer this channel. Flushing once per block is on the roadmap.