method; server frames carry a channel. The error strings on this page are exact — match them verbatim; they will not change.
Subscribe
The acknowledgement echoes your subscription back and adds a 16-character hexid identifying that subscription on this connection.
id is stable: the same type and arguments always hash to the same id. It is the first 16 hex characters of a SHA-256 over the type and its arguments, not a full digest. On the fill feeds argument values are matched case-insensitively and case-folded before hashing, so BTC and btc are one subscription. l4Book’s coin is one exception — it is case-sensitive, hashed as sent, and must be the node’s own spelling (BTC, xyz:GOLD, #28200); see Order book (L4). candle’s and allCandles’ interval is the other: it is hashed as sent, so 1m and 1M are two different subscriptions carrying two different intervals — one minute and 30 days. A candle’s coin is not an exception; it folds like any other market name, and the frame’s s echoes the market as the data spells it. candle also takes an optional source, which defaults to trade before the id is hashed, so omitting it and sending "source":"trade" are one subscription with one id, not two.
Arguments are strings or arrays of strings. The five multi-wallet feeds declare users as a list of up to 1,000; every other argument accepts at most one value. A users list is hashed as a set — sorted, case-folded and deduplicated — so ["0xb","0xa"], ["0xA","0xb"] and ["0xa","0xb","0xa"] all produce one id and one subscription. A one-element list hashes identically to the same argument sent as a bare string, which is also how a bare string is accepted: it becomes a one-item list.
id.
Unsubscribe
Send the same subscription object back with"method":"unsubscribe". The acknowledgement carries the same id the subscribe returned, so you can match the teardown to the setup. The slot frees immediately. Because a users list is a set, an unsubscribe listing the same wallets in any order releases the subscription.
Ping
An application-level heartbeat, independent of the protocol-level ping frames your client library may already send. It is the only liveness probe a browser has.Data frames
channel is the subscription type, seq is a monotonic source position — the block height on every feed but the leverageUpdates and twapOrders families and mark-source candle — and data is an array of [wallet, fill] pairs on the fill feeds. The liquidation, leverage, TWAP-order and TWAP-status feeds put their own objects in data, candle puts a single OHLC object there, and allCandles an array of them; see feeds and the fill object for every shape. There is no subscription id on a data frame.
Order book frames
l4Book speaks two frames of its own, both on channel l4Book and both carrying the coin as you sent it. The full contract — field meanings, the data layout, and the client rule — is on Order book (L4).
Snapshot — exactly one, first, with seq equal to its height; data is the node’s export of the whole book:
height and prev chain the blocks, and seq equals height on every one:
l4Book has no error frame of its own. A book subscription that is dropped is reported on the ordinary error channel, without the coin on it — see when a book subscription is dropped:
TWAP status frames
twapStatuses and twapStatusesByUser speak two frames. The full contract — the event shape, the status union and the client rule for keeping the set — is on TWAP statuses.
Snapshot — at most one, first, carrying "snapshot": true and data as an object. seq is the block height the gateway built it at, and channel is the type you subscribed to, so a by-user snapshot says twapStatusesByUser:
twaps is the last status event for one live twap_id, identical in shape to an element of a live frame’s data. complete says whether the gateway’s coverage reaches back far enough to hold every activated TWAP; coverage_from_ms is the oldest block time it has applied. A false here is not an error — see the snapshot on subscribe.
Live — one per block that carried a matching event, with data as an array of status events:
seq are released after it, leaving no gap.
If the snapshot fails, the subscription is removed after its ack and one {"channel":"error","data":"<reason>"} follows; resubscribe. The reasons are below.
Sequence numbers
seq is a monotonic source position — the place in the source stream the frame was produced from. It does not restart on reconnect, it is not per subscription, and it does not start at 1.
What the number is depends on the feed:
candle and allCandles appear in both rows: they are the only feeds whose seq depends on an argument. Its default source: "trade" is built from fills and carries a block height; source: "mark" is built from the replica-command stream and carries a nanosecond timestamp. Two candle subscriptions on one connection can therefore be sequencing on different kinds of number on the same candle channel, and data frames carry no subscription id to tell them apart — see candles.
Both only ever increase, and both are valid reconnect cursors for their own feed. What you must not do is compare one against the other, or hold a single “last seq” across a mixture of the two — a nanosecond timestamp is about nine orders of magnitude larger than a block height (a height is around 1.1 × 10⁹, a nanosecond stamp around 1.79 × 10¹⁸), so a shared counter would read every fill frame after a leverage frame as a replay.
A filtered feed sends no frame for a block that matched nothing, so tradesByCoin on a quiet coin legitimately skips hundreds of numbers between frames. That is the feed working.
What is broken is seq going backwards: the chain only moves forward, so a value below the last one you applied means frames were replayed or reordered.
Because a data frame carries no subscription id, key any per-stream tracking on what the frame itself identifies: the channel plus the coin or wallet of its first fill. Two tradesByCoin subscriptions on one connection both arrive on channel tradesByCoin, and because seq is now a shared source position they will both report the same number for the same block — so tracking on the channel alone sees that as a repeat, which is exactly the fault this page tells you to act on. Key per subscription, not per channel.
That recipe works on the by-coin and by-user feeds, where every frame on one subscription carries the same coin or the same wallet. It does not work on the multi-wallet feeds: on a …ByUsers subscription the wallet in the first entry changes from frame to frame, so keying on it manufactures a new stream almost every frame and never catches a rewind. Nothing in a frame distinguishes two tradesByUsers lists sharing a connection. So hold at most one subscription of a given list feed per connection — then the channel is the stream key for it and the ordinary rule applies. A second list of the same type belongs on a second connection, within your connection cap.
seq is also your reconnect cursor — record the last one you applied and pass it back on resubscribe; see Reconnecting.
On l4Book, seq equals height on the snapshot and on every update, so the two agree. Gate the book on prev all the same: it names the exact predecessor block, which seq alone does not — see seq versus prev.
Reconnecting
Addsequence to the subscription and the server replays what you missed instead of starting you over:
seq greater than yours, then continues live.
sequence is control data: it steers the subscribe without being hashed into the subscription id, so reconnecting with a cursor resumes the same subscription rather than opening a second one. The same is true of snapshot.
Retention is 10,000 messages per feed per connection manager, and an empty feed group stays warm for 60 seconds. Past either, the subscription is dropped:
In every case, subscribe again without
sequence to rebuild from a fresh snapshot.
Starting without a snapshot
To begin at live updates and skip initial state, pass"snapshot": false:
l4Book this gives you raw diffs, which do not constitute a book unless you already hold compatible state for that coin. On twapStatuses and twapStatusesByUser it gives you state changes with no live set under them, so you learn about a TWAP only when it next changes state.
Omitting snapshot defaults to sending one, unless you supplied a sequence. Three feeds have state to send — l4Book, twapStatuses and twapStatusesByUser; on every other feed a subscribe starts at the next matching block either way.
Errors on an open socket
{"channel":"error","data":"<string>"} arrives on an open socket and never closes it — the connection keeps serving whatever it was already serving. This is the opposite of a refused handshake, which ends the connection before it exists.
A
twapStatuses or twapStatusesByUser subscription whose snapshot fails is dropped the same way, after its ack, with one of these on the error channel. The frame names no feed, so a connection holding both TWAP-status subscriptions cannot tell which one ended from the frame alone:
In every case the subscription no longer exists — nothing is retried for you and no live frames follow. Subscribe again rather than waiting.
A book subscription that starts and then fails — an unknown coin, a snapshot that could not be fetched, a buffer that never filled — is dropped and reported here, on the ordinary
error channel, as {"channel":"error","data":"<reason>"}. The frame does not name the coin, so a connection holding several books cannot tell which one ended from the frame alone. The reasons are listed under when a book subscription is dropped.
Closing
Close the socket from your side with a normal closure (1000); the connection is returned to your account’s pool at once. A silent disconnect — a laptop lid, a dropped network — holds its slot until keepalive notices, roughly 40 seconds, which is why a fast reconnect after a crash can meet a 429 briefly.
