l4Book, twapStatuses and twapStatusesByUser. Each is a separate subscription, and you can hold as many as your plan’s per-connection cap allows on one socket. Every frame carries a monotonic seq — see sequence numbers for what the number means on each feed.
type is case-sensitive — tradesByCoin works, TRADESBYCOIN answers unknown subscription type. Argument values are case-insensitive almost everywhere — including candle’s coin, which folds like any other market name. Two arguments are the exception, and both are hashed exactly as sent:
l4Book’scoinmust be the node’s own spelling of the market —BTC,xyz:GOLD,#28200— becausebtcnames no market there; see subscribing to the book.candle’s andallCandles’intervalbites hardest:1mis one minute and1Mis 30 days. Both are valid, so upper-casing it does not produce an error — it produces a working subscription delivering candles 43,200× wider than you asked for.2mis not an interval at all and answersinvalid argument interval.
coin is not in that list. It folds — btc and BTC are one subscription and one feed group — and the frame answers both with "s":"BTC", so s is always a market name that exists.
Send only the arguments a type declares: an unrecognised extra field is refused, not ignored and not folded into the subscription id, so {"type":"trades","note":"x"} answers {"channel":"error","data":"unknown argument note"} and subscribes nothing — no stream starts and no slot is spent. sequence and snapshot are the exception: they are recognised control data and steer the subscribe without changing its id, and candle’s optional source is a declared argument that is filled in with its default before the id is hashed, so omitting it changes nothing — see reconnecting.
Re-subscribing to something you already hold is idempotent for an identical subscription object — the same id comes back, no second stream starts, and nothing is charged against your cap.
tradesByUser, liquidationsByUser, leverageUpdatesByUser, twapSliceFillsByUser, twapOrdersByUser and twapStatusesByUser accept any address, and so do the five …ByUsers list variants. So does Hyperliquid’s own websocket — it has no authentication at all — but it allows at most ten unique users across every user-specific subscription per IP, and that ceiling is the thing these feeds exist to lift: here one subscription follows up to 1,000 wallets. trades, tradesByUsers, allCandles, twapSliceFills and twapSliceFillsByUsers answer feed not in tier below Starter, and the liquidation, leverage, TWAP-order, TWAP-status and l4Book feeds — list variants included — answer it below Pro. Check connected.data.limits.feeds for what your key may subscribe to.Data frames
This is the frame shape on the seven fill feeds —trades, tradesByCoin, tradesByUser, tradesByUsers, twapSliceFills, twapSliceFillsByUser and twapSliceFillsByUsers. The liquidation, leverage, TWAP-order and TWAP-status feeds carry the same envelope with their own object in data — below. l4Book frames carry data as an object — a snapshot or a block of diffs — and are described on their own page. A twapStatuses snapshot frame also carries data as an object and adds "snapshot": true — below; its live frames are ordinary arrays. candle frames also carry data as a single object, not an array: a client that reaches straight for data.forEach or data[0] breaks on them — below.
channel is the subscription type that produced the frame. data is an array of [wallet, fill] pairs — one entry per fill, with the address that made it in the first position. A single frame can carry many fills; both sides of a trade are separate fills, so most frames carry at least two pairs. The only top-level keys are channel, seq and data — there is no subscription id on a data frame, which matters as soon as you hold two subscriptions on one channel; see sequence numbers.
The fill object
The fill is passed through as Hyperliquid’s node produced it — nothing is added or renamed, andclosedPnl and startPosition come from the node, not from our own accounting. Fields marked always appear on every fill; the rest appear only when the order carried them (a census of 2,066 live fills).
Liquidations
liquidations, liquidationsByUser and liquidationsByUsers are filtered out of the same fill stream, so they sequence on the source block height exactly as the trade feeds do. data is an array of liquidation objects — not [wallet, fill] pairs — one per liquidated side of a trade.
Numeric values keep the JSON type and precision the node sent. The filter on
liquidationsByUser and liquidationsByUsers is the liquidated account only — a liquidator watching its own activity sees its side of the trade through tradesByUser.
Leverage and margin
leverageUpdates, leverageUpdatesByUser and leverageUpdatesByUsers carry accepted leverage and isolated-margin state changes, read from the node’s replica-command stream rather than from fills. Three action types share the feed; update_type selects the variant.
asset is a number, not a coin name. The feed does not guess a ticker without a matching metadata snapshot, so resolve it yourself against Markets & symbols if you need a symbol.
Rejected actions never appear. Requests are paired with the block’s resps.Full by bundle hash and action index, and user is taken from the accepted response rather than from the broadcaster — which is also what leverageUpdatesByUsers matches its list against — which matters because the broadcaster can be an agent wallet acting for the account. A missing, malformed, hash-mismatched or cardinality-mismatched response fails closed for that whole block rather than reporting a request as an applied change.
Candles
candle streams live OHLC candles for one market on one interval. It is on every plan, and it takes three arguments: coin, interval, and an optional source.
source defaults to trade, so omitting it resolves to exactly the subscription above — same stream, same id. The ack echoes your own bytes back, which means an omitted source stays omitted in the echo even though the subscription it created is the explicit one:
source that is neither trade nor mark answers {"channel":"error","data":"invalid argument source"}.
Trade candles — source: "trade" (the default)
Built from fills, the way Hyperliquid’s own candle channel is: o/h/l/c are traded prices, v is traded base volume and n is the number of trades. seq is the source block height, the same number the fill and book feeds carry.
tid, and the counterparty fill is in that same block — v is "0.5" and n is 1 because volume is counted once per tid, not once per fill. A client summing sz over the raw fill feed itself would read 1.0 there and be exactly double Hyperliquid.
Mark candles — source: "mark"
Built from the mark prices in the node’s replica-command stream rather than from fills. The frame shape is identical, but two fields do not mean what they mean above.
The candle object
Prices and volume stay decimal strings at the precision the node sent, never JSON numbers — the same rule as the fill object.
Intervals
Fourteen, case-sensitive, andinterval is mandatory:
1m, 3m, 5m, 15m, 30m, 1h, 2h, 4h, 8h, 12h, 1d, 3d, 1w, 1M
Anything else answers invalid argument interval. Note 1m (one minute) against 1M (30 days).
Live only
There is no historical snapshot: a subscription starts delivering the bucket that is open when it starts, so the first candle you receive is partial — it carries only what happened after you subscribed, not the whole interval. The same is true across a processor restart. Treat the first frame you receive as incomplete rather than as a closed candle; for candles that already closed, the REST API’s/pulse/market/candles/recent/{symbol} is the passthrough to use.
Multi-wallet subscriptions
Five of the six by-user feeds have a list variant that follows a set of wallets on one subscription. It carries exactly what its singular feed carries — the same[wallet, fill] pairs, liquidation objects or replica updates, under the same seq — filtered to any wallet in the list. A copy-trading bot, a whale-alert channel or a builder front-end streaming fills for all of its users holds one subscription rather than one per wallet. The singular feeds are unchanged.
The list is the required
users argument, sent as a JSON array of strings — not a comma-separated string — and matched case-insensitively like every other wallet argument. A bare string is accepted too and becomes a one-item list.
The list is a set
The subscription id is hashed from the sorted, case-folded, deduplicated list, so["0xb","0xa"], ["0xA","0xb"] and ["0xa","0xb","0xa"] are one subscription. Re-sending the list in another order acks the same id, holds no second slot against your cap and never duplicates a frame. The ack echoes the list as you sent it, not as the server normalised it — match on the id, not on the echo.
An unsubscribe with the same set in any order releases it. To change the set, unsubscribe the old list and subscribe the new one: a list that differs by one wallet is a different subscription.
Every matched event appears once, in block order, however many listed wallets it involves. When two listed wallets are counterparties on one trade, the data array carries that trade’s two fills — one per wallet, as the node reports them — each exactly once.
Telling two lists apart
You cannot. A data frame carrieschannel, seq and data and no subscription id, and the wallet in the first entry changes from frame to frame — so the per-stream keying recipe the singular feeds use has nothing stable to key on here, and two tradesByUsers lists on one connection are indistinguishable. Hold at most one subscription of a given list feed per connection; put a second list of the same type on a second connection, within your connection cap. With one list per channel, the channel is the stream key and seq tracking works as on every other feed.
The cap, and what it costs you
A list holds at most 1,000 distinct wallets, on every plan. Deduplication happens before the cap is checked, so repeated entries do not count against it. One subscription of 1,000 wallets is one subscription against your per-connection cap. That is the point of these feeds: the 1,000-wallet ceiling is the same on every tier, so a plan buys you more lists, not longer ones. The per-connection subscription cap is not the same on every tier — it is 2 on Free, 25 on Starter and 100 on Pro and Enterprise, and it is the number a list is charged one of; see plan limits. Each list variant sits at or above the tier of the unfiltered firehose it filters, so a list never delivers a client more than its tier could already stream. A wallet has to be listed in the form the feed matches on — the fill’s wallet fortradesByUsers and twapSliceFillsByUsers, the liquidated account for liquidationsByUsers, and the accepted response’s user for leverageUpdatesByUsers and twapOrdersByUsers — exactly as on the singular feeds. Nothing validates that an entry is an address; an entry matching no wallet simply never matches.
When a list is refused
Nothing is subscribed when any of these fires, and each arrives as{"channel":"error","data":"<string>"} on the open socket:
See the wire protocol for the rest.
TWAP slice fills
twapSliceFills, twapSliceFillsByUser and twapSliceFillsByUsers carry every fill the node attributes to a TWAP order, filtered out of the same fills stream by the fill’s twapId. data is the node’s own [wallet, fill] pairs, byte for byte the objects the trade feeds send — a client that already reads trades reads these unchanged — so the fill object above documents every field.
twapId to follow one order, and sum sz to track its executed size. twapSliceFillsByUser takes a required user, matched case-insensitively like the trade feeds, and filters to the wallet the slice filled for — twapSliceFillsByUsers does the same over a list of up to 1,000 — the counterparty of a slice is an ordinary fill with twapId: null and stays on the trade feeds. A fill with no twapId key at all is never a slice.
This feed reports execution only. A TWAP that never fills a slice never appears here, and there is no placed or cancelled event — those are on
twapOrders, and the order’s own lifecycle is on twapStatuses. twapId here equals twap_id on both of those, so subscribing and joining on it gives you an order’s terms, its state and its executions side by side.TWAP orders
twapOrders, twapOrdersByUser and twapOrdersByUsers carry TWAP placements, refusals and cancellations, read from the node’s replica-command stream. update_type selects the variant:
Every variant carries
time, user, asset and tx_index.
update_type if you only want orders that ran.
asset is the node’s numeric asset id, builder-dex encoded ids included, as on leverage and margin — no coin name is guessed. twap_id is the join key to twapSliceFills, whose fills carry it as twapId.
twapOrdersByUser takes a required user — and twapOrdersByUsers a list of up to 1,000 — matched against the accepted response’s account, so an agent-signed action reaches the account it applied to rather than the broadcaster.
This feed cannot see a TWAP finish or be terminated by the node — it reports what an account asked for and whether the node took it, and nothing after that. Those states are on twapStatuses, which carries the node’s own state machine and joins here on twap_id; executed size is on twapSliceFills. The twapCancel response shapes follow Hyperliquid’s exchange-endpoint documentation and have not yet been confirmed against a captured block; the twapOrder shapes have.
TWAP statuses
twapStatuses and twapStatusesByUser carry the node’s own TWAP state machine — the third and last view of one TWAP. A subscription opens with a snapshot of the TWAPs that are live right now and then streams every status change as its block lands.
user is required on twapStatusesByUser and matched case-insensitively, like every wallet argument on this socket. It is matched against the event’s own state.user.
The three views of one TWAP
All three join on the same node id, and only the spelling differs: it is
twap_id on twapStatuses and on twapOrders, and twapId inside the fill object on twapSliceFills (data[i][1].twapId). Treat it as an opaque number — this server never re-encodes it on any of the three.
One join does not exist: a twapOrders event with update_type: "rejected" carries no twap_id at all, because a refused order never got one. A refusal is visible only on twapOrders and can be joined to nothing.
The live frame
channel is the subscribed type, seq is the source block height, and data is an array of status events — one frame per block that carried at least one matching event. Events are forwarded as the node wrote them; the only field this server reads is state.user, for the by-user filter.
The
state object:
Sizes are decimal strings and
minutes is a number, always — the same rule as the fill object.
Status is a union
status is a string on an ordinary transition and an object on a refusal:
The snapshot on subscribe
A fresh subscribe answers with a snapshot of the live TWAP set before any live frame. It carries"snapshot": true, and data is an object rather than an array:
channel is the type you subscribed to, so a twapStatusesByUser snapshot says twapStatusesByUser and its twaps are that account’s alone.
The maintenance rule is one line: apply each stream event onto the entry with the same
twap_id, inserting one you have not seen, and drop the entry when its status is terminal.
snapshot and sequence steer the subscribe exactly as they do elsewhere — omitting both sends a snapshot, "snapshot": false skips it, a sequence cursor implies no snapshot, and the two together are refused. See reconnecting.
Unlike
l4Book, these feeds have no buffer warm-up: the subscribe ack is sent first and the snapshot follows essentially immediately, rather than after the server has collected a span of blocks. Frames buffered above the snapshot’s seq are released after it, so there is no hole between the two.{"channel":"error","data":"<reason>"} follows — subscribe again rather than waiting. The reasons are listed with the other error strings.
Sequence
A frame is sent only for a block that carried a matching event, soseq increases strictly but skips heights — on twapStatusesByUser it will skip a great many. Across a reconnect the server drops any block at or below the last height it published, so a gateway that resumes behind the cursor replays nothing. That guard is deliberately released in one case: when the gateway reports the resume position as pruned, the server clears its last-applied height and rejoins live, and monotonicity then rests on the gateway resuming ahead of where it left off rather than on the guard.
Hyperliquid compatibility
The frame shapes —subscribe / unsubscribe with a subscription object, subscriptionResponse acks that echo it, ping / pong, and a fill object identical to Hyperliquid’s — are deliberately Hyperliquid’s own, so client code written against their websocket is familiar here. The differences are the ones the product exists for:
- Authentication on the upgrade. Hyperliquid’s socket has none; ours needs your key, in a header or the
bearersubprotocol. - Position context on a market-wide feed. Their
tradescarriesusers— both sides’ addresses — but notdir,closedPnl,startPositionorfee. Those live onuserFills, which is per-user and subject to the ten-user ceiling below. Every feed here carries all of them. - Liquidations and leverage changes are feeds of their own, not something you filter per account — and both take any address.
seqon every data frame — a monotonic source position, and your reconnect cursor.
What each socket is for
Hyperliquid’s websocket is built to watch your own account and a few markets, and it is free, unauthenticated and closer to the matching engine than anything downstream of it can be. It also carries far more market-data channels than this one — mids, BBO, asset contexts, order updates, ledger and TWAP events among them. (Candles are no longer one of them:candle here is trade-based on the same field names as theirs.)
This socket is built to watch everyone at once. That is the whole of the difference:
Hyperliquid’s own limits are 1,000 websocket subscriptions and ten unique users per IP. The ten-user ceiling is the one that decides things: tracking wallets past that number is not slow on their socket, it is not possible.
Enriched cohort feeds are coming. They are not available yet, and this page will document them when they are. (Replaying what you missed after a disconnect is already here — that is the
sequence cursor, not a future feed.)
