Skip to main content

Endpoint

TypeScript SDK helpers and React hooks are available for WebSocket subscriptions. Use them when you want typed channel helpers, reconnect handling, and cleanup helpers. See TypeScript WebSockets and React hooks.

Connection Setup

  • Open one connection to the WebSocket endpoint and reuse it for all subscriptions.
  • Subscribe with channel strings; unsubscribe with the same channel strings.
  • Channel parameters use trading pair UUIDs, not symbols.
  • Public market channels do not require authentication.
  • Account channels require a session-key WebSocket authentication message before subscribing.
  • Use * only where listed to leave a filter open.

Message Format

Subscribe and unsubscribe with the same channel strings.
The server answers each Subscribe with one Subscribed acknowledgement listing only the channels that request changed — the ones it actually added, in the order you sent them.
  • A channel the connection already holds is a no-op — it is not re-subscribed, no snapshot is replayed, and it is left out of the ack.
  • A channel that is malformed, fails validation, requires authentication you have not completed, or exceeds the per-connection subscription cap gets its own Error frame (with a machine-readable code and the rejected channel) and is left out of the ack. The ack still follows, so a partially-accepted request yields the error frames first and then the accepted subset.
  • "channels": [] therefore means the request added nothing, whatever the reason: every channel was rejected, you already held them all, some mix of the two, or the request named no channels at all.
A per-channel Error frame names the channel it concerns in channel, echoed exactly as you sent it — including a string the server could not parse — so you can pair each rejection with your request without reading the human-readable message. A channel longer than 128 bytes (never a valid channel) is echoed as its first 128 UTF-8 bytes, cut at a character boundary:
INVALID_SUBSCRIPTION, AUTH_REQUIRED, SUBSCRIPTION_LIMIT, and SNAPSHOT_UNAVAILABLE carry a channel. A SNAPSHOT_UNAVAILABLE channel was accepted — it is in the ack and its live frames flow — so its channel tells you which baseline to refetch. Connection-wide failures (AUTH_FAILED, CONNECTION_LIMIT, INVALID_MESSAGE, SESSION_INVALID, MESSAGE_RATE_LIMIT) concern no single channel and have no channel key. Do not read the ack as the connection’s full subscription list; it is the delta for that one request. Track what you hold client-side. Authenticated channels require a session-key handshake first. The signature covers WS-AUTH\n<public_key>\n<timestamp> with the session private key.
timestamp is epoch milliseconds and is bound into the signature. The server rejects a handshake whose timestamp is outside a ±30s clock-skew window (stale or future-dated), so sign with the current time and re-sign on every reconnect — never cache and replay an old Authenticate payload.
Events are delivered in this envelope:

Heartbeat (Ping/Pong)

The connection uses application-level heartbeats. A client may send {"type":"Ping"} and the server replies {"type":"Pong"}. The server may also send {"type":"Ping"}, and the client must reply with {"type":"Pong"} to keep the connection alive.

Slow-client disconnect (close code 1013)

When the server detects that a connection’s broadcast receiver missed events that one of its subscriptions owns (see below), it closes the whole connection, including every other subscription on it, with code 1013 (“try again later”) and reason slow consumer: stream gapped, resync required. This close frame is the machine-readable resync signal; no additional JSON Error frame precedes it. Sending the close is best-effort: a broken socket may surface only an abnormal transport close, and still releases server resources. A gap closes the connection only if one of its subscriptions owns it. A subscription to orders, conditional_orders, twap_orders, positions, balances, account, instrument, liquidations, copy, or a single market’s market_stats:<tradingPairId> owns every gap, because its subscribe snapshot may not yet reflect an event that the gap overwrote. A subscription to any other channel (trades, orderbook, ohlcv, movements, all-markets market_stats) owns only gaps that reach events published after its Subscribed acknowledgement. An earlier gap just means its stream starts after it. For example, a public market-data connection that falls behind while its handshake or an earlier Subscribe is still being processed keeps streaming instead of reconnecting into the same backlog. The ring drops arbitrary events, not superseded values for each key. A lost position close, candle, balance, or instrument update may have no later replacement. Even a connection holding only absolute-state channels must reconnect and reconcile. Healthy peers continue consuming independently; NATS delivery remains at-most-once and is not replayed on reconnect. Reconnect, authenticate, and resubscribe before reconciling REST reads with incoming events. Subscribe snapshots exist for balances, orders, conditional_orders, twap_orders, liquidations, positions, instrument, account, copy, and market_stats (per-market and all-markets); they describe current state, not every missed lifecycle transition. Apply each channel’s baseline semantics: complete maps such as positions and instruments can replace their prior set. For persisted resting orders, merge by per-order version; snapshot absence is not a tombstone because persistence can lag a newer local event. Retain such local rows and resolve them with getOrder before applying the version rule. An order version is not a contiguous replay cursor. See the SDK reconciliation guidance. The remaining channels do not provide a subscribe-time snapshot — the append-only streams (trades, movements), ohlcv (whose live events upsert the candle for the current period rather than appending), and orderbook (whose first live frame is already the full book). Applications must explicitly refresh the relevant REST state/history, including quiet keys that may never emit again. A per-market market_stats subscription whose market has not yet produced a frame (or on a transient matching-engine read failure), and the all-markets channel for a brief window during initial startup or after a server restart (until the producer commits its first full generation, or on the same read failure), return SNAPSHOT_UNAVAILABLE instead of a baseline — treat it as a transient resync signal, not a failure: keep the subscription open (no auth or session reset), and let a later published live frame seed the baseline once one is produced. Account keepalives cover active accounts only, so a flat account stays silent on the live stream — the account subscribe snapshot includes it, and so establishes a complete baseline for the authenticated application. It is not a full replacement set for the channel: live account delivery is user-scoped and can carry accounts from other applications (see Account), which the snapshot deliberately omits. Merge it per margin_account_id rather than replacing the whole channel map, or those cross-application keys are discarded. A resumed stream alone does not establish a complete baseline for the other channels. For additive history such as public trades and movements, use the available REST history and its documented pagination/retention limits. There is no universal WebSocket replay cursor or guarantee that an arbitrary outage can be reconstructed completely. Surface an unrecovered gap when the required history is unavailable; do not assume a fresh current-state snapshot reproduces missed events. The TypeScript SDK automatically reconnects when enabled and invokes onResync({ code: 1013, slowClient: true }) after sending authentication/subscription requests. It does not wait for acknowledgements or snapshots and does not automatically perform REST refetches; the application owns recovery.

Inbound message rate (close code 1008)

Every application frame you send — Authenticate, Subscribe, Unsubscribe, Ping, or anything unparseable — draws from a per-connection budget. The defaults are 20 messages per second sustained with a burst of 1,200; both values are operator-configurable, so a deployment may run different numbers. Protocol-level Ping frames count too (each costs the server a Pong write), and so does every fragment of a fragmented message; protocol Pong (your reply to the server heartbeat) and Close are not counted. Frames are also metered by size: a per-connection byte budget admits a full re-subscribe frame at connect plus normal traffic, but refuses a burst of very large frames even well under 20 per second. The first frame past either budget receives an Error frame with code MESSAGE_RATE_LIMIT, and the connection is then closed with 1008 (“policy violation”) and reason message rate limit exceeded (or message byte limit exceeded when the byte budget ran out). The budget is per connection, so it is unaffected by other connections from the same account. A normal client never approaches it: subscribe to everything you need in as few Subscribe frames as possible (one frame carries many channels) and let the ~25 s heartbeat idle. The TypeScript SDK sends every held channel in a single Subscribe frame when the socket opens and on every reconnect; a channel added while the socket is already open is sent as its own frame, and the default burst (1,200) is sized above the default subscription cap (1,024 channels) so even adding a full subscription set one channel at a time on a live connection stays within budget. The SDK reconnects through 1008 with backoff and re-sends its authentication and subscriptions; an application that re-subscribes in a hot loop on every reconnect will trip the cap again, so treat MESSAGE_RATE_LIMIT in onError as a signal to slow down, not to retry immediately.

Connect

To stop a stream, send Unsubscribe with the same channel string and close the socket when no subscriptions remain.
Unsubscribed mirrors Subscribed: it lists only the channels that request actually removed. A channel the connection was not subscribed to is an idempotent no-op and is left out, and a channel string the server cannot parse gets an INVALID_SUBSCRIPTION Error frame whose channel is that string, exactly as you sent it.

Channel Catalog

Public Channels

Authenticated Channels

Channel Details

Trades

Executed trades for one pair, including price, quantity, maker side, and execution time.
UUID | *
required
Trading pair ID. Use * only when you intentionally want trades for every pair.
Example

Orderbook

Live bid/ask depth with optional aggregation and base or quote quantity formatting.
UUID | *
required
Trading pair ID. Use * for every pair.
SPOT | MARGIN | *
Trading mode filter. Omit it for all modes, or use * when keeping later segments.
number | *
Price-level grouping. Supported values are 0.0001, 0.001, 0.01, 0.1, 1, 10, 100, 1000, 10000, and *.
base | quote | *
Quantity denomination. base returns base-token quantities; quote returns quote-token notional quantities.
Example

OHLCV

Live candle updates for charting and market analytics.
UUID | *
required
Trading pair ID. Use * for every pair.
SPOT | MARGIN | *
required
Trading mode filter.
1m | 5m | 15m | 1h | 4h | 1d | *
required
Candle interval.
trade | mark | *
Optional price series. Omit it (the four-token form) or use trade for the trade-derived candles. mark selects the close-only mark-price series (open = high = low = close = mark, zero volume), published on 1m for margin pairs. Use * to receive both series. A four-token subscription receives only the trade series.
Example

Market stats

Periodic market statistics, public (no session handshake). A per-market subscription delivers frames with index / mark / mid prices, best bid/ask, open interest and its limit, last trade price, 24h volume / high / low and absolute and percentage price change, funding state (current rate, estimated next rate, next funding timestamp, funding interval, last funding time, premium, and clamp bounds), and a market_status label. Subscribing to the bare market_stats channel (or market_stats:*) instead delivers, on a slower cadence, a single frame carrying a reduced per-market summary for every published market — a market that has not yet assembled its first frame is omitted, and the subscribe-time snapshot carries the same set — including a market_status label (TRADABLE, POST_ONLY, HALTED, NOT_READY, or UNKNOWN). A market’s per-market frame and its all-markets row carry the same label. SPOT markets flow through both channels carrying orderbook, last trade, 24h stats, and a TRADABLE / HALTED label; the perp-only fields (index/mark price, open interest and limit, funding rate/timestamp/interval/last funding time/premium, base interest rate, funding clamps) are omitted for spot. On subscribe, before any live frame, the server sends a Snapshot frame carrying the channel’s current baseline as an array: for a per-market subscription, one element with that market’s latest complete frame (the same shape as a live per-market data); for the all-markets channel, one reduced summary per active market with retained data, ordered by trading-pair id (the same shape as the live markets elements). A per-market subscription whose market has not yet produced a frame — or on a transient matching-engine read failure — receives a SNAPSHOT_UNAVAILABLE error instead of a fabricated frame, and the subscription stays active so a later published live frame seeds it once the market produces one. The all-markets channel returns SNAPSHOT_UNAVAILABLE in the same way for a brief window during initial startup or after a server restart (until the producer commits its first full generation), and on any transient snapshot read failure against the matching engine, rather than a partial baseline; treat it as a resync signal and let the next periodic all-markets push deliver the baseline. Both channels stay active through it. Where a deployment does not run the market-stats snapshot service, neither channel sends a snapshot or a SNAPSHOT_UNAVAILABLE frame at all — the first live push seeds state, so do not wait on either.
UUID
Trading pair ID for a per-market subscription. Omit the segment (or use *) for the all-markets summary.
Example
Frames are routed by channel — there is no inner event-type discriminator. Every decimal price/quantity is a JSON string; funding_timestamp and last_funding_time are numeric millisecond epochs; funding_interval_seconds is a whole number of seconds; keys are snake_case; optional fields are omitted (not null) when absent. Per-market frame (market_stats:{trading_pair_id}) — the envelope data carries a trading_pair_id and a nested data object. Perp-only fields (index_price, mark_price, open_interest, open_interest_limit, open_interest_headroom, current_funding_rate, funding_rate, funding_timestamp, funding_interval_seconds, last_funding_time, premium, base_interest_rate, funding_clamp_small/funding_clamp_big) are omitted for spot markets. market_status is present on every frame, perp and spot: TRADABLE, POST_ONLY, HALTED, NOT_READY, or UNKNOWN for perps, TRADABLE or HALTED for spot. funding_interval_seconds is the funding settlement interval. last_funding_time is when the latest funding window settled, as a millisecond epoch (REST getFundingState returns the same instant as an RFC 3339 string); it is omitted until the market’s first settlement. open_interest is in base units; open_interest_limit (the market’s gross open-interest cap) and open_interest_headroom (open_interest_limit − open_interest × mark_price, never below zero) are USD notional and present only when the market has a cap. Until a perp’s open interest is known, open_interest and open_interest_headroom are omitted (never zero) while every other field is still sent; once known, every frame carries the most recent value. At zero headroom, orders that open exposure are refused until open interest or the mark falls:
All-markets summary frame (market_stats / market_stats:*) — the envelope data carries a markets array of reduced per-market summaries, each with a market_status label. index_price, funding_rate, premium and open_interest_notional are perp-only and omitted for spot. funding_rate is the estimated next-window rate (current_funding_rate is the last settled one). open_interest_notional is open_interest × mark_price, falling back to the last trade price when there is no mark, in quote units; it is omitted until both open interest and a price are known, never sent as zero:

Instrument

Trading-pair lifecycle and configuration changes, public (no session handshake): new listings, delistings, and non-activation edits to tick size, quantity step, order-size bounds, or category. Perp leverage and margin-rate bounds are carried in every frame’s full config (see the payload below) but are payload only — an edit to those margin parameters alone does not emit a frame. Every frame also carries the base and quote asset icon URLs, so a client can join a position’s trading_pair_id to the instrument row and render both icons without a REST call. A per-market subscription (instrument:{trading_pair_id}) delivers frames for one pair; the bare instrument channel (or instrument:*) streams every market. A single subscribe-time Snapshot frame carries the configuration of every listed market as an array of per-market baselines (each element a listing), and arrives before the live Event diffs — a raw client must handle the Snapshot frame, not only Event, to receive that baseline (see the two envelopes below). The snapshot includes inactive (delisted) markets, each flagged is_active: false, so positions, orders and history on a delisted market can still resolve its metadata; filter on is_active for the tradable set. Markets not yet launched are not included. A per-market snapshot for a delisted market returns that market’s row.
UUID
Trading pair ID for a per-market subscription. Omit the segment (or use *) to stream every market.
Example
Detected lag closes the whole connection with 1013; see Slow-client disconnect. Each live Event frame carries one market’s full current configuration, not just the changed fields, and the subscribe-time Snapshot frame carries an array of those same per-market configs (one element per listed market, active or not) — so consume each event, and each element of the snapshot array, as a per-market terminal-state snapshot, never an additive delta; changed_fields names which fields the mutation touched, for callers that want to react selectively. Frames are routed by channel. Every decimal value is a JSON string; keys are snake_case; the perp-only fields (min_leverage, max_leverage, initial_margin_rate, maintenance_margin_rate) are omitted (not null) for spot markets, which have no leverage or margin parameters. The base_icon_url and quote_icon_url fields differ: they apply to every market and are always present, carrying an explicit null when an asset has no icon configured (never a synthesized URL) rather than being omitted. Every market’s frame also carries its asset identity, always present: base_asset_id / quote_asset_id (UUID strings), base_token / quote_token (ticker symbols), base_asset_name / quote_asset_name, and base_decimals / quote_decimals (JSON integers), named as on the REST TradingPair. change is one of listing (on a live frame, a market became tradable — created active or re-activated; every snapshot element is a listing baseline whatever its is_active), delisting (deactivated), halt / unhalt (trading halted / resumed — reserved for a future market-regime producer, not emitted by admin trading-pair edits), or config_change (a non-activation edit). The payload nests under the channel envelope’s data, which carries a trading_pair_id and a nested data object:
The subscribe-time baseline arrives as a single Snapshot frame — type: "Snapshot", with a server-capture timestamp — whose data is an array of per-market InstrumentEventData (each with change: "listing"), not the per-pair { trading_pair_id, data } wrapper the Event frames use. Live changes then follow as the Event frames above. Handle both type values — a loop that ignores every non-Event message never receives the baseline:
@0xmonaco/types ships the InstrumentEvent, InstrumentEventData, and InstrumentChangeKind types for this channel, and @0xmonaco/core exposes the typed sdk.ws.instruments(handler, tradingPairId?) subscription, which parses the snake_case wire keys onto those types and fans the Snapshot baseline out as one event per market. A raw WebSocket client reads the snake_case wire keys directly, or maps them onto InstrumentEventData itself (trading_pair_id → tradingPairId, is_active → isActive, tick_size → tickSize, base_icon_url → baseIconUrl, quote_icon_url → quoteIconUrl, base_asset_id → baseAssetId, base_decimals → baseDecimals, min_leverage → minLeverage, changed_fields → changedFields, and so on; that type describes the ergonomic shape, not these raw keys).

Orders

Order lifecycle events for the authenticated user, including placement, fills, cancellations, rejections, and expirations.
UUID | *
Trading pair filter. Omit it by using orders for all of the authenticated user’s orders.
SPOT | MARGIN | *
Trading mode filter. Use orders:{trading_pair_id}:* to filter by pair across all modes.
Example

Balances

Balance state changes for the authenticated user.
Emits the authenticated user’s balance state changes, including available, locked, total, reason, and reference fields.

Movements

Ledger-style balance movement entries for the authenticated user.
Emits entries for deposits (spot and margin-routed), withdrawals, funding settlements, and the balance a cancelled or expired order unlocks. Fills and fees are not emitted here: a fill’s ledger entries are written durably and read back through movement history (GET /api/v1/accounts/movements), while its live signal is a balance_update on the balances channel — a versioned spot row for a spot-owned leg, and for a margin fill an unversioned frame carrying the margin account’s post-fill collateral.

Positions

Margin position changes, for cross and isolated risk buckets alike, including size, status, mark price, PnL, and liquidation fields.
UUID
Optional trading pair filter. Omit it for all positions owned by the authenticated user.
Example
A fill sends a live OPEN frame for every open position in each risk bucket it touched, taker and makers alike: the bucket of the position each side’s order is bound to. In a cross risk bucket that is every position of the bucket, in any pair, because a trade in one pair moves the liquidation price of the others; an isolated risk bucket holds a single position. Positions in the account’s other risk buckets are not re-sent, even in the same pair. Each position is sent at most once per fill, and every frame carries the fill’s version. Each frame names its own trading_pair_id, so positions:{trading_pair_id} receives the frames for positions in that pair even when the trade was in another one. Liquidation slices and auto-deleveraging refresh the liquidated account’s risk bucket and each counterparty’s risk bucket the same way. Collateral transfers use margin_added or margin_reduced. A direct isolated-position margin add or reduction sends that position with the transfer’s version. Adding margin to a copy position sends the isolated child plus every open position in its copy cross risk bucket, also versioned. Cross-risk-bucket ALLOCATE/RELEASE, copy-follow creation or allocation growth, and copy-stop return refresh every open position in the affected cross risk bucket without a version because those commands do not write position rows. Wallet IN/OUT, plain isolated-risk-bucket allocation without a position collateral delta, and empty scopes send no position frame. Successful frames use the command’s captured post-command state and are released only after its durable acknowledgement. Where enabled, the channel also carries position_valuation frames: about every five seconds, each open position’s mark_price, unrealized_pnl, liquidation_price (null when none can be derived), initial_margin_required and maintenance_margin_required, with valued_at, the time the server read it. A frame is sent when any of the five changed, and otherwise periodically, normally at least once a minute. It carries no size, status or version: apply it only to a position you hold as OPEN whose updated_at is earlier than the frame’s valued_at (a valuation read before the position’s last mutation is stale), ignore it for an unknown or terminal one, and never close a position from one. See Position Valuation Frames. Non-zero funding payments emit ranked OPEN frames from state captured inside the funding command; zero payments emit no position frame. Full closes, liquidation victims/backstop exits and ADL closes normally emit ranked terminal frames with lifetime reduced quantity — the sum of every reducing execution — and the exact terminal classification: ordinary closes and flattened ADL counterparties are CLOSED, while liquidation exits and the bankrupt ADL side are LIQUIDATED. After restart, a terminal frame rebuilt from pre-upgrade reduction history omits version when that history lacks the size or margin fields needed to reconstruct complete lifetime values; incomplete size history carries only the known partial total. Reconcile that frame by full field comparison. Detect terminal state from status, never from size === "0".

Conditional Orders

Take-profit, stop-loss and trailing-stop lifecycle changes for the authenticated user. A trailing stop additionally emits reason: "armed" when tracking begins and reason: "ratcheted" on every trigger move, each frame carrying the new triggerPrice and watermarkPrice (see Trailing Stop).
UUID
Optional trading pair filter. conditional-orders with a hyphen is also accepted.
Example

TWAP Orders

Owner-scoped TWAP parent-order updates. Every mutation emits one twap_order_update carrying the full parent snapshot (configuration plus progress: executed quantity/notional, average fill price, slice counters, next-slice cursor, state, execution_style, the conditional-trigger fields, and any terminal reason such as end-of-window shortfall or trigger expired). The reason field names the mutation: created, slice_placed, slice_skipped, cancelled, or completed for every parent; passive_child_placed, child_repriced, and passive_fill for a PASSIVE parent working a resting post-only child (its deadline sweeps and crossing takes still arrive as slice_placed); and triggered or trigger_expired for a conditional parent. Two of the passive reasons are looser than they read: child_repriced is also emitted by the cancel-only fallback, with no child left resting, and the maker fill that completes a parent reports completed rather than passive_fill. Only the authenticated owner’s parents are ever delivered. See TWAP orders for the execution mechanics, including passive execution. On subscribe, before any live frame, the server sends a Snapshot frame on the twap_orders channel whose data is an array with one TwapOrderEventData row per owned non-terminal parent — PENDING (awaiting its start window, or, for a conditional parent, still awaiting its price trigger) or ACTIVE (triggered and slicing) — honouring the optional pair filter. Each row is the same shape as a live twap_order_update data, with reason: "snapshot". An owner with no live parents receives an empty array, not no frame. Terminal parents (COMPLETED / CANCELLED) are excluded as history — read those from the REST TWAP order listing. The baseline reflects persisted state, so a parent that reached a terminal state just before the subscription opened can still appear ACTIVE/PENDING until that transition is persisted, and its terminal event — sent before the subscription existed — does not re-fire; consume the snapshot as a seed to merge and reconcile against the REST read, not as an authoritative replacement set. A repository read failure returns SNAPSHOT_UNAVAILABLE and leaves the subscription active (live diffs still flow); treat it as a transient resync signal, not a failure. Live updates and snapshot rows can carry an optional version, derived from the sequencer step that wrote that parent. Reconcile only rows with the same twap_order_id: accept >=, reject lower versions, and let the snapshot win an equal-version tie. Gaps are normal. An absent version means unknown legacy state, not zero, and versions from different parents are unrelated.
UUID
Optional trading pair filter. Omit it for all TWAP parents owned by the authenticated user.
Example

Liquidations

Liquidation-risk alerts and liquidation status changes for the authenticated user.
UUID
Optional trading pair filter for liquidation alerts owned by the authenticated user.
Example

Subscribe-time snapshot

On subscribe, before any live alert, the server sends a Snapshot frame on the bare liquidations channel whose data is an array with one LiquidationEventData row per in-flight liquidation record the authenticated user owns — one the matching engine is still actively liquidating (status: "UNRESOLVED"). These are exactly the records the live channel keeps reconciling: the engine emits a liquidation_alert on each transition, including the terminal one. A COMPLETED record, and a BAD_DEBT record (an insolvent account whose insurance resolution is a settlement-side process the live channel does not report), are excluded. Each row is the same shape as a live liquidation_alert data. An owner with no in-flight liquidation receives an empty array, not no frame. A liquidation record is account-scoped and carries no trading pair, so a snapshot row’s position_id, trading_pair_id, liquidation_price, and mark_price are always null — the live alert fills those from the live position the snapshot lacks. risk_bucket_state is Liquidating on every row: an in-flight record is a risk bucket under active liquidation, and the record does not persist the exact live bucket state, so the next live alert carries the precise value. Because a snapshot row cannot be attributed to a pair, a pair-scoped liquidations:{trading_pair_id} subscription baselines with an empty array; subscribe to the bare liquidations channel for the account-wide baseline. The baseline reflects persisted state, so consume it as a seed to merge and reconcile against a REST read, not as an authoritative replacement set: a record that terminalized just before the subscription opened can still appear non-terminal until that transition is persisted, and its terminal alert — sent before the subscription existed — does not re-fire. A repository read failure returns SNAPSHOT_UNAVAILABLE and leaves the subscription active (live alerts still flow); treat it as a transient resync signal, not a failure.

Recovered episodes

A liquidation episode that recovers ends with a terminal liquidation_alert too, not only one that liquidates to completion. When a breached risk bucket heals back over its maintenance requirement — on a liquidation step, or with no step at all on an oracle mark, a funding settlement, or an auto-deleveraging close that lifts a counterparty’s bucket — the episode’s record closes as status: "COMPLETED" and the owner receives one alert with that status and risk_bucket_state: "Normal", carrying the same liquidation_id as the earlier alerts for the episode. The alert describes the recovered bucket’s own position, so a pair-scoped liquidations:{trading_pair_id} subscription on that bucket’s pair receives it; a bucket with no open position carries null position fields. Treat it as the episode’s terminal frame: it supersedes any in-flight (UNRESOLVED) row a subscribe-time snapshot seeded.

Copy

Copy trading events for the authenticated follower: every change to one of your copy relationships, and every leader fill a relationship could not mirror. There is no per-pair variant. The leader of a relationship receives nothing on this channel.
Two event types share the channel, named by the frame’s event_type:
  • copy_relationship_update: the relationship’s full current state after a change. change says why the frame was sent: created, updated, close_only, stopped, skip_recorded, or reactivated. status is ACTIVE, CLOSE_ONLY, or STOPPED, and STOPPED is terminal.
  • copy_skip: one leader fill the relationship did not mirror. reason is INSUFFICIENT_MARGIN, BELOW_MIN, LEVERAGE_MISMATCH, MARKET_STATE, LEADER_EQUITY, or NO_LIQUIDITY. Only INSUFFICIENT_MARGIN advances consecutive_skips.
Both payloads name the leader by leader_wallet, their canonical lowercase wallet address; the channel never carries an internal leader id. version is the durable-log sequence of the step that produced the frame. For one relationship, a higher version is newer. cum_net_realized and hwm are the relationship’s realized PnL net of fees since started_at and its high-water mark, which is the basis for the leader’s profit share. They are not the balance of the copy risk bucket. Subscribe snapshot. The snapshot is an array of your ACTIVE and CLOSE_ONLY relationships in the authenticated application. Each row has the copy_relationship_update payload shape with change: "snapshot". Skips are events, not state, so the snapshot carries none. Payout events are planned for a later release and are not sent today. Every decimal value is a JSON string, and wire keys are snake_case:

Account

Margin-account health for the authenticated user: equity, collateral, free collateral, margin ratio, and maintenance requirement. There is no per-pair variant — one subscription covers every margin account the user owns. Frames flow while an account is active (it holds an open position or a resting order reserve): when its last position and reserve clear, one final frame carries the released state (maintenance requirement back to zero, free collateral restored), and the channel then stays silent for that account until it re-enters the active set. Collateral moves while flat (deposits, withdrawals) surface on the movements channel, not here. The producer samples on a ~2s tick, so a position or reserve that opens and fully closes inside one tick is below the sampling cadence — no frame marks it, and the account’s previous state stands until its next activity.
The server-side producer that emits live account_update frames runs on every deployment, mainnet included. Enablement is a deployment switch rather than a delivery guarantee, so confirm you are receiving frames before depending on the live stream. The subscribe-time snapshot below is served by the WebSocket API rather than that producer, so it is unaffected by the switch.
Detected lag closes the whole connection with 1013; see Slow-client disconnect. A frame is published when the account’s health changes and republished unchanged as a keepalive after a bounded quiet interval (~30s), so treat a repeat as a keepalive, not a state transition. Each frame carries the account’s full current health, so consume it as a terminal-state snapshot per margin account, not an additive delta. Dedupe LIVE frames per account by updated_at, newest wins; sequence (a plain JSON number) only breaks ties between frames carrying the same updated_at (those come from the same producer run). A snapshot row is not ranked this way — any live frame supersedes it. sequence resets to 0 on a producer restart (which can happen while your socket stays connected), may skip values, and is never a gap detector or a cross-restart order. A subscription made while an account is flat receives no LIVE frame for it until the account re-activates, because keepalives cover only active accounts — but the subscribe-time snapshot below includes it, so the baseline is complete. Every decimal value is a JSON string; wire keys are snake_case (like every other raw frame on this page), and the payload nests under the channel envelope’s data:

Subscribe-time snapshot

On subscribe, before any live frame, the server sends a Snapshot frame on the account channel whose data is an array with one row per margin account the authenticated user owns in the session’s application — including flat accounts, which the live producer never emits for. Each row’s updated_at is the instant that account’s state was persisted, not the time the snapshot was read, so rows of one frame legitimately differ. Live delivery is per user and carries no application filter, so a wallet trading through more than one application also receives live frames for its other accounts. Those are terminal-state payloads, so such an account baselines itself from its next frame (within the ~30s keepalive) despite not appearing in this list. Each row carries that account’s current persisted health. An account that is flat and otherwise empty — no positions and no open risk bucket — reports its deposited collateral as both equity and free_collateral, with maintenance_requirement and margin_ratio at 0. An account that is flat but still holds an open risk bucket keeps that bucket’s allocated collateral and unsettled realized PnL, so its equity need not equal its collateral. A user who owns no margin account receives "data": [], not a missing frame. A snapshot row is the live data payload with sequence omitted:
sequence is the live producer’s process-local per-account counter and the WebSocket API is a different service, which cannot mint one; 0 is not available either, because it is indistinguishable from a restarted producer’s first frame and would make a consumer applying the dedupe rule discard real live frames. Absent means unknown — never read it as zero. Ranking still works without it, but not by comparing a snapshot row’s updated_at against a live frame’s. A live frame always supersedes a snapshot row for the same margin account, whatever the timestamps say: the snapshot is stamped when its state was persisted (database write time) and a live frame when the engine sampled it, so under persistence lag a later-written row can carry older state. Use updated_at to order live frames against each other — newest wins — with sequence breaking ties inside one producer run, which a snapshot is not part of. A durable per-account version would make the two surfaces genuinely comparable; until then the snapshot is a seed, not an authority. That rule also applies to live frames emitted before your subscription activated: the connection buffers them while the snapshot read runs and delivers them after it, so one can supersede a newer snapshot row. A later frame normally corrects this within the ~30s keepalive. The exception is an account that went flat during that window and whose final frame never reached you — the producer emits one, but it derives departures from in-process state, so a producer restart across the transition emits none — in which case you keep a stale active account until you reconnect and re-seed. Reconnecting and re-seeding is the recovery for this, as it is for dropped events. margin_ratio is the maintenance margin required divided by equity; it is 0 for an account with no open positions. It saturates to a maximum-distress sentinel (a very large decimal string, Decimal::MAX ~7.9e28) whenever the ratio cannot be computed or represented: a maintenance requirement still owed at zero or negative equity, and equally at near-zero positive (dust) equity where the true ratio exceeds the decimal range. Treat a value at or above the liquidation threshold as maximal distress and clamp it for display. equity is mark-inclusive (collateral + unrealized PnL + funding). The typed sdk.ws.account(handler) helper (see TypeScript WebSockets) subscribes and hands you the camelCased AccountEvent shape. To consume the channel over the raw WebSocket protocol above instead, send a Subscribe frame naming the account channel and read the snake_case wire keys directly — or map them yourself onto the AccountEvent type in @0xmonaco/types (event_type → eventType, margin_account_id → marginAccountId, free_collateral → freeCollateral, and so on; that type describes the ergonomic shape the helper exposes, not these raw keys).