Documentation

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:

FramesMean frameTotal
l2Book1,67471,711 B120.04 MB
l2Diff1 snapshot + 1,674 updates592 B per update1.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:

ParameterValuesMeaning
coine.g. "BTC", "@107"required
nLevels1-1000, omit for 20levels per side
nSigFigs2-5price bucketing, as on the public Hyperliquid API
mantissa2 or 5, requires nSigFigs: 5finer bucketing
WARNING

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:

  • upd holds 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. px and sz are strings, n is a number.
  • del holds prices that are no longer in the book at your depth.
WARNING

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.

NOTE

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 del may 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.

Next steps

  • Examples: a client that keeps the book in sync from l2Diff.
  • Streams: every other channel on the connection.
  • Limits: depth, connections and keep-alive.