Skip to main content
Twenty-one raw feeds today: four stream fills, three stream liquidations, three stream leverage and isolated-margin changes, two stream OHLC candles, three stream the fills a TWAP order executed, three stream the TWAP orders themselves, two stream the node’s own TWAP state machine, and one streams a coin’s full order book. Five of them are multi-wallet list feeds that follow up to 1,000 addresses on one subscription — below. Three deliver state on subscribe before the live stream starts: 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.
The 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’s coin must be the node’s own spelling of the market — BTC, xyz:GOLD, #28200 — because btc names no market there; see subscribing to the book.
  • candle’s and allCandles’ interval bites hardest: 1m is one minute and 1M is 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. 2m is not an interval at all and answers invalid argument interval.
A candle’s 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, and closedPnl 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.
This feed does not sequence on block height. Its seq is the replica block’s Unix timestamp in nanoseconds, because the replica stream does not expose its height outside the JSON payload. The number still only increases, and it is still your reconnect cursor — but never compare a leverageUpdates seq against one from a fill or book feed. See sequence numbers.
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:
A 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.
That frame is one 0.5 BTC trade. Both sides of a trade arrive as separate fills carrying the same 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.
On source: "mark", v is always the string "0" and n is not a trade count. A mark-price update carries no traded size, so there is no volume to report, and n counts mark-price observations in the bucket. Nothing errors: Hyperliquid client code ported onto a mark candle compiles, runs, and quietly produces zeros in volume and a wrong VWAP. If you want volume, use the default trade source.
Mark candles do not sequence on block height. Their seq is the replica block’s Unix timestamp in nanoseconds, exactly as on leverageUpdates — a trade candle on the same coin and interval carries a block height instead. Never compare the two, and never hold one “last seq” across both. See sequence numbers.

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, and interval 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.
twapStatuses is the one wallet-scoped family with no list variant. Five families have one; this sixth does not, and {"type":"twapStatusesByUsers","users":[…]} answers unknown subscription type — it is not a type the server knows, so nothing is subscribed and no slot is spent. To follow a set of wallets’ TWAP state today, either hold one twapStatusesByUser subscription per wallet against your subscription cap, or subscribe to the unfiltered twapStatuses and filter on state.user yourself. The unfiltered feed is the same Pro tier, so the second option reaches nothing the first could not.
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 carries channel, 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 for tradesByUsers 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.
One TWAP order produces many slices over its run. Group by 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.
Refusals are emitted, not dropped, because a refused TWAP leaves no other trace anywhere on the network. Filter on 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.
The node answers a refused TWAP with an outer status of ok. Acceptance is read from the inner response.data.status — {"running":{"twapId":N}} or {"error":"…"} — never from the outer status alone. A client reimplementing this against the raw node output and checking only the outer status will report TWAPs as started that never ran. An action whose outer status is err never reached the block and is omitted here; an inner status this server does not recognise fails the whole block closed rather than guessing.
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.
state.timestamp is placement, not activation. On a triggered TWAP it can be days stale: a captured terminated record carried a timestamp about 71 hours before the event with minutes of 30. Never derive an end time from timestamp + minutes for a triggered TWAP — only the node’s own terminal event says when a waiting TWAP ended.

Status is a union

status is a string on an ordinary transition and an object on a refusal:
Code that types status as a string breaks on refusals. An error status is a JSON object, not a string, and it can arrive mid-run on an already-activated TWAP that has partial executedSz. Check the JSON type before you compare. The server validates nothing here — it reads only state.user — so the vocabulary is the node’s to extend, and a status this page does not list is forwarded unchanged rather than dropping the block.

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.
Handle complete: false — do not assume the snapshot is the whole live population. When it is false the gateway is telling you its coverage is shorter than the window it would need, so a TWAP that activated before coverage_from_ms may be missing from the set. Keep the snapshot and apply the stream on top of it. How often it is false depends on how long the gateway has been running, so code for both values rather than for the one you happen to observe.This holds even when complete is true: a waitingForTrigger TWAP placed before coverage_from_ms and not yet activated is invisible to the snapshot until it activates or ends, because a waiting TWAP has no duration bound to age out of. So a twap_id the stream later names that is not in your set is expected, not an error — insert it rather than discarding the event.
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.
If the snapshot cannot be produced, the subscription is removed after its ack and a plain {"channel":"error","data":"<reason>"} follows — subscribe again rather than waiting. The reasons are listed with the other error strings.

Sequence

twapStatuses sequences on the source block height — not on nanoseconds. Its sibling twapOrders carries the replica block’s Unix timestamp in nanoseconds instead, so the two halves of the TWAP product sequence on different scales: never hold one cursor across them and never compare their seq. A height is around 1.1 × 10⁹ and a nanosecond stamp around 1.79 × 10¹⁸, so a capture tells them apart at a glance. See sequence numbers.
A frame is sent only for a block that carried a matching event, so seq 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 bearer subprotocol.
  • Position context on a market-wide feed. Their trades carries users — both sides’ addresses — but not dir, closedPnl, startPosition or fee. Those live on userFills, 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.
  • seq on 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.)