WebSocket Client
The SDK exposes a single WebSocket client with functional subscription methods:Connection
The WebSocket auto-connects when the SDK is initialized. You can also manually manage the connection:Auto-Connect Behavior
By default, the WebSocket connection is established automatically when you initialize the SDK:- Runs asynchronously without blocking SDK initialization
- Logs connection errors to console but doesn’t throw
- Allows you to call SDK methods immediately while connection is established
- Automatically retries if initial connection fails
Manual Connection Management
You may want to manually control the WebSocket connection in these scenarios: 1. Delayed Connection If you don’t need real-time data immediately:Connection Methods
() => Promise<void>
Manually connect to the WebSocket server.Returns: Promise that resolves when connected, rejects on error.Note: Does nothing if already connected. Safe to call multiple times.
() => void
Disconnect from the WebSocket server.Returns: void (synchronous)Note: Does nothing if already disconnected. Clears all subscriptions.
() => WebSocketStatus
Get current connection status.Returns: One of:
"connected", "connecting", "disconnected", "reconnecting"Reconnection and resync
sdk.ws auto-reconnects with full-jitter exponential backoff (base 1s, capped at 30s) and retries indefinitely by default. getStatus() returns "reconnecting" while backing off, and Ping/Pong heartbeats are handled automatically.
To react to reconnects, pass a ws config to the SDK — new MonacoSDK({ ..., ws: { onStatusChange, onResync } }). The constructor forwards it to the WebSocket client it builds internally, so you no longer need to drop to the low-level createMonacoWebSocket(baseUrl, options) factory (which remains available and additionally takes the session keypair). The same fields are accepted either way:
onStatusChange(status)— fires on every transition, including"reconnecting".onResync({ code, slowClient })— fires after each automatic reconnect, after sending authentication/subscription requests, without waiting for acknowledgements or snapshots. A channel with a subscribe-time snapshot can deliver a fresh baseline over the socket on reconnect, so the REST refetch below is for the channels that have none — and the same(id, version)rule reconciles either source. Refetch REST snapshots and merge them into local state on(id, version): order events, theorderssnapshot frame, and the REST endpoints all carry the same counter, so keep applying events as they arrive and take the snapshot for an order whensnapshot.version >= local.version, else keep local. Use>=, not>— on the stream equal versions mean the same sequencer step, not identical state (one step emits anOrderPlacedand then its fills; a batch shares one step across all items), and the REST row is that step’s final state. Two things the resting snapshot cannot tell you, because they stopped resting during the gap: fills, which you recover fromsdk.profile.getUserTrades(execution-time ordered, so a gap fill is at the top whatever the order’s age — the order list iscreatedAt-ordered and would bury it); and cancels or expiries, signalled by an order being absent from the snapshot that local state still thinks is resting. Snapshot absence is not a tombstone — persistence may instead lag a newer local resting event — so retain the local row and resolve it withsdk.trading.getOrder(orderId), then apply the same version rule. Treat an absentversionas unknown, never zero, and fall back to buffer-and-replay for those rows. The market-maker runbook has the full recipe.slowClientistruewhen the server shed the connection with close code1013(the client fell behind — expect a gap).onError({ code, message, channel })— fires for everyErrorframe the server sends;channelis present only on a per-channel failure. See Server error frames.onReauthenticationRequired({ code, message, closeCode })— fires once when the connection lost its authentication for good and the SDK stopped reconnecting. See Session loss (close code 1008).autoReconnect,maxReconnectAttempts,reconnectBaseDelayMs, andconnectionTimeoutMstoggle reconnection and tune the retry cap, backoff base, and per-attempt timeout.
Subscribe-time snapshots
Most channels deliver their current state once, as aSnapshot frame the server injects after the subscribe ack and before that subscription’s first live event. Pass an onSnapshot callback to receive it as one array — a complete baseline, straight off the socket, with no REST read.
onSnapshot is always the last argument, and always optional — existing calls that omit it are unaffected. On a channel that also takes a tradingPairId filter, it follows the filter:
- The array is the whole channel, once. It is not fanned through the event handler — snapshot rows and live events are different shapes, so a handler that reduced both would apply the same state twice.
- Empty means empty. A user with no open positions gets
onSnapshot([]), not a skipped call, so you can clear stale state rather than guess. - It fires again after every reconnect. The resubscribe is a new subscription server-side, so a fresh snapshot follows. Apply it according to that channel’s semantics: complete maps such as positions or instruments can replace their prior set; the persisted resting-orders snapshot must be version-ranked against held events, with omitted resting rows resolved by
getOrderrather than deleted on absence alone. - It is scoped to the exact channel you subscribed to. Two pair-scoped subscriptions each receive only their own baseline.
- It is withheld rather than truncated. If any row of the frame cannot be parsed, nothing is delivered and your existing state stands. The array is contractually complete, so a short one would have you delete whatever the unparseable row described — and a resting order or an unchanged market has no later event to undo that.
One baseline per subscription, not per call. The client opens one wire subscription per channel however many handlers you register on it, and the server snapshots only genuinely new subscriptions. A second call made while that subscription’s first snapshot is still pending joins the handler set and receives the upcoming frame. A call added after the frame arrived has no cached baseline and waits until the next reconnect.
instruments is the exception — it caches its baseline and replays it locally, filtered to the active markets so the replay matches what a real snapshot would carry. Subscribe once per channel and fan out in your own code, or read the baseline over REST for an extra consumer added later.onResync would otherwise prompt. Orders can still require targeted getOrder calls for locally resting rows the persisted snapshot omits.
A callback on a Not yet sent channel is simply never invoked — it is inert, not an error, and the live stream is unaffected. Registering one now is safe, and it starts working when the server arm ships provided that arm follows the convention every current one does: a flat array of the channel’s own payload shape. The SDK’s channel-coverage lock fails until a newly-shipped arm is reclassified, so that shape is confirmed rather than assumed.
orderbook, ohlcv, trades and movements take no onSnapshot: they stream events rather than state. For orderbook the first live frame is already the full book; for the others, use the REST history reads.
Every row is the channel’s own terminal-state payload — the same shape as its live event’s data — with one exception. orders snapshot rows describe a resting order, not a change, so OrderSnapshotItem carries status and the cumulative fill totals and carries none of the per-fill metadata (tradeId, executionPrice, lastFillQuantity, role, fee):
| null fields are always present on the wire and carry null when unset; clientOrderId, selfTradePreventionMode, parentOrderId and version are the ones the server omits outright. OrderSnapshotItem carries no postOnly: track that as order-level state from the OrderPlaced acknowledgement, since a snapshot row cannot tell you the flag either way.
version is the one field here that is not resting state. It is the same counter the live order events and the REST endpoints carry, and it is what a reconnecting client ranks this frame against — compare with >=, not >, and treat an absent value as unknown rather than zero. See reconnection and resync for the full rule.
Slow-client disconnect (close code 1013)
Every broadcast-ring gap that one of the socket’s subscriptions owns closes the whole socket with code1013 and reason slow consumer: stream gapped, resync required, including sockets holding only positions, balances, account, instruments, orderbook, ohlcv, or market-stat subscriptions. The ring is not key-aware: a lost terminal or isolated-key update need not have a later replacement. There is no separate JSON error frame for lag. A gap closes the socket only if one of its subscriptions owns it: orders, conditionalOrders, twapOrders, positions, balances, account, instruments, liquidations, and single-market marketStats subscriptions own every gap, while other channels own only gaps reaching events published after their Subscribed acknowledgement (see the reference).
With automatic reconnect enabled, sdk.ws opens a new socket, sends its authentication/subscription requests, and invokes onResync with slowClient: true. The callback does not wait for Authenticated, Subscribed, or Snapshot frames. Reconciliation is application code: the SDK does not automatically refetch REST state.
A reconnect starts a new live stream; it does not replay missed NATS events. Consume subscribe snapshots where supported (balances, orders, conditionalOrders, positions, instruments, account, twapOrders, marketStats, marketStatsAll), reconciling complete sets and per-key revisions. Other channels have no subscribe snapshot: explicitly refresh the relevant REST state/history and reconcile events arriving during that read. Quiet keys may never produce a fresh live baseline; a flat margin account is covered by the account snapshot, which includes it even though the live producer never emits for it. See the protocol recovery contract for snapshot limits, missing terminal updates, and additive-history gaps. Public trades and movements have no universal WS replay cursor or guarantee of complete recovery beyond their REST history/pagination limits.
Session loss (close code 1008)
When the server finds that an authenticated connection’s session was revoked or expired (or the account was gated), it sends anError frame with code SESSION_INVALID and closes with 1008. When it rejects the Authenticate handshake itself, it sends AUTH_FAILED and closes. Reconnecting with the same session would only be rejected again, so after either the SDK does not reconnect automatically. It:
- forgets the rejected session keypair, so a later
connect()does not replay it; - keeps your subscription handlers;
- reports
"disconnected"throughonStatusChange; - calls
onReauthenticationRequired({ code, message, closeCode })once.
sdk.login(...) (or setSessionKeypair on a client from createMonacoWebSocket) reconnects, authenticates with the new session, resubscribes every channel you still hold, and then fires onResync with the lost connection’s close code so you can reconcile what was missed (also when autoReconnect is off). If you already supplied a new session before the close arrived — for example from onError — the SDK keeps it and reconnects with it instead of calling onReauthenticationRequired:
connect() yourself first: a socket opened without a session re-sends every held channel unauthenticated, the server rejects the user-specific ones with AUTH_REQUIRED, and authenticating on that open socket afterwards does not resubscribe them (see AUTH_REQUIRED below).
The server also rejects a handshake with AUTH_FAILED when it could not verify the session because of an internal error; that case stops reconnecting too, and calling login again (or re-applying the same session with setSessionKeypair) retries it. Every other abnormal close keeps the backoff reconnect — including a 1008 for exceeding the inbound message rate, which carries MESSAGE_RATE_LIMIT rather than SESSION_INVALID, and a 1013 slow-client disconnect.
Server error frames
The server reports failures asError frames carrying a machine-readable code and a human-readable message. They match no subscription, so they reach no channel handler — the ws config’s onError callback is how they reach your app.
A per-channel failure also carries channel: the channel string the SDK sent, exactly as sent (a string longer than 128 bytes, never a valid channel, is echoed as its first 128 UTF-8 bytes, cut at a character boundary). The Subscribed / Unsubscribed acknowledgements list only the channels a request changed, so a rejected channel is missing from the ack and reported by its own Error frame instead; channel is how you pair the two. The SDK sends every held channel in one Subscribe on reconnect, so it is also how you tell which one failed and act on that channel alone. Connection-wide failures have no channel.
Always keep a
default branch: a code added server-side after your SDK version is delivered as-is rather than dropped.
Error Handling
WebSocket connection errors are handled gracefully:Best Practices
1. Let Auto-Connect Work In most cases, let the SDK handle connection automatically:WebSocket Types
Orders Channel
Subscribe to real-time order events for your account. Requires authentication.Event Timeline
Events are emitted in this order:- OrderPlaced → Order validated, persisted, funds locked (taker only)
- Match Events → Emitted after OrderPlaced
- Taker:
OrderMatched/OrderPartiallyFilled/OrderFilled - Each Maker:
OrderPartiallyFilled/OrderFilled
- Taker:
- OrderCancelled → For an
IOCorder’s unfilled remainder after a partial fill (anFOKthat can only partially fill for want of liquidity is rejected, not cancelled — its candidate fills are discarded). Also, after the taker’s own fan-out, one terminalOrderCancelledper resting order swept by self-trade prevention — sent to that order’s owner, carryingterminalReasonSELF_TRADE_PREVENTIONandstpCounterpartyOrderId. A maker subscriber therefore sees a terminal frame for an order it never cancelled - OrderRejected → For orders rejected after acceptance (an IOC/FOK that cannot fill for want of liquidity; a
MARKETorder whose liquidity is invalidated by maker-risk revalidation). An IOC/FOK stopped by self-trade prevention instead isOrderCancelled, even at zero fills. A plainMARKETsubmit with no liquidity is rejected before acceptance and emits nothing. - OrderExpired → For GTC orders reaching expiration
Taker vs Maker
Events contain different fields based on your role:Order Event Types
Every order event also carriesdata.version?: number, the same opaque, compare-only revision as REST reads and the orders subscription snapshot. Compare only states of the same order: higher means newer, gaps are normal, and absent means unknown, never zero. Merge a snapshot when snapshot.version >= local.version; it wins ties because several live events can share one sequencer step. See the reconciliation recipe.
Event value fields typed as KnownAlias | (string & {}) preserve unfamiliar server strings; keep a default branch when narrowing them. Each named event variant keeps its closed eventType discriminant.
Every order event’s data also carries an optional clientOrderId?: string (wire client_order_id) when the order was placed with one — echoed on placement, taker and maker fills, cancellation, rejection and expiry, so an event can be matched to the request that placed the order. A maker fill carries the resting order’s own handle. It is omitted when the order carried none.
selfTradePreventionMode? is a common order-event field, not a cancellation-only one: the requested override rides the OrderPlaced acknowledgement, fills in both roles, OrderRejected, OrderCancelled, OrderPartiallyCancelled, OrderExpired and the orders snapshot frame. It echoes what the request carried and is absent when it carried none — it is never the resolved wallet or platform default, so absence does not mean CANCEL_MAKER was not applied.
A TWAP child order reports parentOrderId?: string (wire parent_order_id), the same id getOrder returns, so you can group a TWAP’s children from the socket alone. It rides a resting child’s maker fills, OrderCancelled, OrderPartiallyCancelled, OrderExpired and its orders snapshot row. It is omitted for an order that is not a TWAP child, and by servers that predate the field.
An order admitted post-only likewise reports postOnly?: boolean (wire post_only) on its OrderPlaced acknowledgement, its fills in both roles, OrderPartiallyCancelled, OrderCancelled and OrderExpired — so a market maker driving state off this channel can confirm the flag was honoured without a round trip to getOrder, and keeps it across an in-place reduction rather than losing it mid-lifecycle. The maker fill matters most: an admitted post-only order can never produce a taker fill (a crossing one is refused at admission and emits nothing), so every fill a post-only order ever has is a maker fill — which now carries the flag. Two frames never carry it, and neither is reachable by a post-only order: OrderRejected, which only describes a MARKET order whose apparent liquidity is invalidated by maker-risk revalidation after acceptance, or an IOC/FOK unfilled for want of liquidity (one stopped by self-trade prevention is cancelled, not rejected — and a plain MARKET submit with no liquidity is refused before acceptance and emits nothing at all); and the OrderCancelled that reports the remainder of an IOC or market order after a partial fill, which is a different payload from the resting-order cancellation named above and declares no postOnly at all. Post-only is refused on MARKET and IOC/FOK alike, so a post-only order reaches neither. The key is only ever present as true; an order that is not post-only omits it rather than sending postOnly: false, exactly as the REST read model does, so data.postOnly ?? false reads the same as REST on every carrying variant.
Absent means “not post-only” — or an order placed before the flag was persisted, and a maker fill emitted by a server predating the maker-fill support — never “the flag was lost”. For those pre-field orders, the order-level value learned from the OrderPlaced acknowledgement (or a REST read) remains the authority; for everything since, ?? false is safe on any carrying variant, whichever event arrived last.
The structural snippets in this section assume these type imports:
- OrderPlaced
- OrderMatched
- OrderFilled
- OrderPartiallyFilled
- OrderCancelled
- OrderPartiallyCancelled
- OrderRejected
- OrderExpired
Orderbook Channel
Subscribe to orderbook updates. No authentication required.Magnitude Parameter
The magnitude parameter groups orderbook levels by price intervals:OHLCV Channel
Subscribe to real-time candlestick updates. No authentication required.priceType argument ("trade" | "mark" | "*") selects the price series. Omitting it — or passing "trade" — subscribes to the trade-derived candles exactly as above; "mark" subscribes to the close-only mark-price series for margin pairs (each candle’s open = high = low = close = the minute’s closing mark, volume 0), and "*" receives both. The mark series is published on the 1m interval only — a "mark" or "*" subscription at any other interval succeeds but receives no mark frames. The argument appends a fifth token to the ohlcv channel string, so existing four-token subscriptions are unchanged and never receive the mark series. When you pass "*", read event.priceType (known series "trade" and "mark", with unfamiliar strings passed through) on each frame to tell the two streams apart.
OHLCV Event Structure
priceType identifies a concrete series, never the "*" subscription selector. Legacy frames may omit it. Keep a default branch for an unfamiliar series, interval, or trading mode.
Candlestick Structure
Trades Channel
Subscribe to trade executions. No authentication required.Trade Event Structure
Movements Channel
New in v0.5.4 - Real-time balance movement tracking
Movement Event Structure
Example: Track Deposits
Example: Monitor Order Unlocks
sdk.ws.orders (the lifecycle event) and sdk.ws.balances (the resulting balance — a versioned spot row for a spot leg, an unversioned margin-collateral frame for a perp fill), and read its ledger entries — including fee and rebate rows — from movement history with sdk.profile.getPaginatedUserMovements().
React Hook
For React applications, use the dedicated hook:Conditional Orders Channel
Stream TP/SL conditional-order lifecycle events. Requires authentication. Authentication is checked at subscribe time — unauthenticated subscribe attempts are rejected. Ensure the SDK is authenticated before callingsdk.ws.conditionalOrders(...).
Subscribe to Conditional Orders
Conditional Order Event Structure
triggeredOrder, the summary of what the triggered order has done — it is a REST-only field on getConditionalOrder, listConditionalOrders and the conditional detail of a listing row. triggeredOrderId is the id alone, and no current frame on this channel carries it either: the live reason: "triggered" event is emitted from the conditional’s pre-trigger model, so it announces the transition without the id it produced, and the subscribe-time snapshot lists only the non-terminal states (PENDING_PARENT, ACTIVE, TRIGGERING), never a TRIGGERED row. Neither a live frame nor a reconnect will hand you the close’s identity — read it from REST, along with the close’s status, filled quantity and average fill price. The close’s own reverse pointer, Order.conditionalOrderId, is REST-only on the same terms: no order event and no order snapshot row carries it either, so a close first seen over the socket acquires it on the next REST read. See Trigger Price Is Not Fill Price.
Conditional-order version is present on REST reads and subscribe-time snapshots only; live conditional events never carry it. It is an opaque, compare-only revision for the same conditional order, with absent meaning unknown rather than zero. Seed from REST or the subscription snapshot and compare versioned snapshots with snapshot.version >= local.version; apply live events as state updates without version ranking. Do not compare versions across different orders.
Value unions are what the server sends today, not a guarantee
@0xmonaco/core parses this frame leniently: an unrecognized orderType, timeInForce, state, side, positionSide, conditionType, triggerSource, associationType, or reason is passed through as-is rather than rejected. Throwing inside the message handler would drop the whole update with nothing surfaced to the consumer, and a dropped frame is the worse failure — the position, liquidation, and market-stats parsers already make the same trade. Only eventType stays strict, so a mis-routed frame is still rejected.
Match these fields with a default branch rather than relying on exhaustiveness:
reasonis free-form on the wire. Its canonical live values arecreated,activated,cancelled,parent_cancelled,triggered,failed, andoco_cancelled, plusarmedandratchetedon aTRAILING_STOProw (tracking began; the trigger moved — eachratchetedframe carries the newtriggerPriceandwatermarkPrice) — exported asConditionalOrderEventReasonfor narrowing — and the subscribe-time snapshot reuses the same shape withsnapshot. Any value the server adds arrives unchanged.timeInForceis the fullTimeInForceenum, not theGTC/IOCsubset the creation paths validate: it mirrors an unconstrained server column that maps exhaustively over every variant, so a value written by another path reaches the wire as-is.orderTypeisLIMITorMARKETin practice — both creation paths reject anything else — but that rests on validation rather than an exhaustive mapping, so a relaxed validation would surface the raw variant (e.g.STOPLOSSrather thanSTOP_LOSS).
Lifecycle and recipients
The triggered order itself emits regular
OrderEvent messages on the orders / userOrders channels. To follow a TP/SL all the way through to fill, subscribe to both channels.
For higher-level usage, see Order Management and sdk.positions.attachPositionTpSl.
Positions Channel
Stream live position state — size, mark price, unrealized PnL, liquidation price, isolated margin, and leverage. Requires authentication; the signed session-key handshake is sent before the subscribe. Pass an optional trailingtradingPairId to filter to one pair; omit it to stream every pair.
Detected lag closes the whole socket with 1013 under the slow-client rule. There is no wire sequence number — treat updatedAt as monotonic per positionId, ignore an event older than the state you already hold, and refetch a snapshot on reconnect (see Reconnect and resync).
Subscribe to Positions
Position Event Structure
position_update frame now carries a position’s scope and margin requirements, so the common case — knowing which risk bucket a position sits in, whether it is cross or isolated, and what it requires at the current mark — no longer needs a REST read. positionId, marginAccountId, tradingPairId, riskBucketId, marginMode, status, side, size, entryPrice and isolatedMargin are the same values on the subscribe-time snapshot, on the live frames, and on GET /api/v1/positions.
The frame is still not a superset of the REST position. indexPrice, realizedPnl, netRealizedPnl, exitPrice, fundingPaid, feesPaid and cumFees are REST-only, so a consumer that needs any of those still reads REST.
The six re-derived values (markPrice, unrealizedPnl, liquidationPrice, leverage, maintenanceMarginRequired, initialMarginRequired) are recomputed per payload rather than read from the versioned row, so take them from the newest frame, as the merge rule below already requires. Five of them are priced at whichever mark the answering surface holds, so a snapshot row and a live frame can differ by one oracle tick — the snapshot is a database baseline and prices its whole row from a single mark, which keeps each row internally consistent. leverage is the exception: it is opening notional over posted margin, which no reprice touches, and it is in this group because the live surfaces derive it from engine state while the persisted row serves its stored leverage column, so the two can disagree at an unchanged version.
A position with no risk bucket — one opened before risk buckets existed — reports neither riskBucketId nor marginMode. Do not infer either field from the other: they are resolved from different sources — the id from the position, the mode from the matching engine when it holds the position and from the risk-bucket record otherwise — so an id can arrive with no mode, on a terminal row whose record is unavailable, on a read taken while the engine is unreachable, or from a producer older than the field. Treat a missing marginMode as unknown and keep the mode you already hold; defaulting it to CROSS renders an isolated position with the wrong backing.
updatedAt is when the position was last mutated — a fill, collateral transfer, funding settlement or close — carried from the producer’s own record of that mutation. Two frames for a position nothing has changed repeat one timestamp, and a valuation-only oracle reprice does not move it. It is display metadata: reconcile on version, never on updatedAt.
The match across surfaces is exact for an open position. Two cases are served from the persistence clock instead and differ by the persistence lag: a terminal row — the live frame carries the engine’s close instant while a terminal REST read carries the stored one — and any position read after a matching-engine restart. Treat it as accurate to that lag.
One further case is not bounded by that lag. While the matching engine is unreachable, a REST read of an open position falls back to that row’s lifecycle timestamps — close, then last funding, then open — so a position filled repeatedly since it opened reports its opening instant until funding settles. A live position_update frame is unaffected, being built from engine state, so a REST value older than a frame you already hold is expected in that window and the frame is the fresher one.
version is the position’s opaque, non-negative durable row revision. The subscribe-time snapshot and a ranked live frame for identical position state carry the same value, so a client reconciles on positionId + version. Never use updatedAt, arrival order, or receiver backlog to establish ordering. Not every live frame is ranked — see the note below for which are, and treat an unranked frame as unranked rather than assuming it is comparable with a snapshot row.
The merge rule is per field group, not per frame — a frame is never discarded wholesale:
isolatedMargin gets its own column because it is a versioned field that the subscribe-time snapshot does not read from the versioned row: the snapshot overlays it from an independent live matching-engine read, so it can reflect a later command than the row’s version names. Take it from the newest payload rather than skipping it on an equal version, or you can retain stale backing. Sourcing it atomically with the row is tracked separately.
Note the asymmetry between the Equal and Lower rows. An equal version means the same command, so its valuation fields are a legitimately newer sample of the same position state. A lower version means an older command, so nothing it carries is newer — including its mark. Arrival order is only a fallback where there is no version to compare at all; it must never override a positive version, for the same reason the first paragraph rules it out as an ordering signal.
Discarding a whole frame on an equal version is the one mistake this field is likely to cause: it freezes displayed unrealized PnL until the position’s next real mutation, which for an idle position is the next hourly funding settlement.
Only a POSITIVE version is rankable, and “unranked” has two wire spellings that mean exactly the same thing:
- On the WebSocket — live frames and the subscribe-time snapshot alike — the key is absent. The snapshot maps the persisted
0sentinel to an absent key before it reaches the wire, so apositionspayload never carries a literal0. - On REST/gRPC, where the field is a required int64 and cannot be omitted, it is the literal
0.
- The emit path wrote no position row at that sequence, so there is no versioned row to point at.
- The path wrote the row but re-reads live risk state after its command was sequenced, so a later command could overtake the read. This is the
fundingandmargin_added/margin_reducedcase: the mutation did advance the persisted version. - The frame is a different projection of the row than persistence stores. Terminal frames are this case: they report
size0, andLIQUIDATEDfor a liquidation or ADL, while the stored row keeps its historical size and recordsCLOSED.
The version advances on sequenced mutations: open, add, reduce, partial and full close, funding, margin transfer, liquidation and ADL — exactly when the row’s versioned content changed at that sequence, including a change that landed and was undone within one persistence batch. A sequenced command carries its whole margin account’s open positions, but a position the command left unchanged keeps its version (and its
updatedAt): an untouched sibling of a filled position does not move. A ranked live frame is stamped with its command’s sequence, so a frame for such a sibling can carry a higher version than the REST row while describing identical content — rank it as newer and apply it; nothing changes. Advancing still never misses a change: every mutation of the row’s versioned fields stamps a new version.maintenanceMarginRequired and initialMarginRequired sit in the valuation column for the same reason markPrice does: both are computed from the current mark, so they move on an oracle tick with no mutation behind them.A valuation-only oracle reprice — the overwhelming majority — persists nothing and advances no version, which is the whole point of the carve-out. A state-transitioning oracle command (a fresh maintenance-margin breach, or a recovery out of one) does persist the affected account’s snapshot, so any open position whose versioned content it changed is restamped like any other sequenced mutation. Both cases are safe to merge with the rules above; the distinction matters only if you are reasoning about how often the version moves.Which live frames are ranked. A frame carries a version only when it is a faithful projection of the row persisted at that sequence. In practice that is the fills that leave a position open, built from the matching engine’s own captured post-command state.Two kinds of frame are deliberately unranked:- Frames that re-read live risk state after their command was already sequenced, because a later command can overtake that read and the frame would then carry one command’s state under another’s version. That is the
fundingframes and themargin_added/margin_reducedcollateral-transfer frames. Ranking them needs an atomic post-settlement capture, tracked separately. - Terminal frames — a full close, liquidation or ADL — because they project the row differently from persistence: the frame reports
size0, andLIQUIDATEDfor a liquidation or ADL, while the stored row keeps its historical size and recordsCLOSED. Ranking two different contents under one version would make your final state depend on which arrived first.
status CLOSED or LIQUIDATED, size 0) before the position is dropped, so a subscriber sees the close rather than the position going silent. Positions closed by the liquidation worker or auto-deleveraging now also emit a terminal LIQUIDATED event — reason "liquidation" for a maintenance-margin liquidation and "adl" for an auto-deleverage close — and a counterparty whose position survives or is only partially reduced by such a close receives a live OPEN refresh. A funding settlement emits a position_update with reason "funding" carrying the post-settlement state (status OPEN, unchanged size), so a live position change can be attributed to funding rather than a trade.
Liquidations Channel
Stream liquidation alerts — the authenticated account crossed a maintenance threshold and a liquidation record was opened or advanced. Requires authentication; the signed session-key handshake is sent before the subscribe. Pass an optional trailingtradingPairId to filter to one pair; omit it to receive account-scoped alerts (which arrive on the bare liquidations channel) as well as pair-scoped ones.
Lag policy is Disconnect: a subscriber that falls behind has its whole connection closed with 1013 (try again later), taking every other subscription on that socket down with it — see Slow-client disconnect. There is no wire sequence number, so treat a 1013 close as the gap signal and resync by refetching a snapshot — see Reconnect and resync.
Subscribe to Liquidations
Liquidation Event Structure
status: "COMPLETED", accountState: "Normal", and the episode’s liquidationId. The same COMPLETED / Normal pair also closes an episode that was liquidated to completion and left the account healthy, so the pair marks the episode’s end, not a recovery — do not infer from it whether a liquidation executed. Let the terminal alert supersede any UNRESOLVED snapshot row for the same liquidationId. See Recovered episodes.
Account Channel
Stream account-level margin health for the authenticated user — equity, collateral, free collateral, margin ratio, and maintenance requirement, per margin account. Requires authentication; the signed session-key handshake is sent before the subscribe.The server-side producer that emits live
account_update frames now runs on every network, mainnet included. Enablement is a deployment switch rather than a delivery guarantee, so confirm you are receiving frames before you build on a live stream. The subscribe-time snapshot below is served by the WebSocket API rather than that producer, so it arrives regardless. See the WebSocket reference.marginAccountId, never as an additive delta. Dedupe LIVE frames per margin account by updatedAt, newest wins; use sequence only to break ties between frames carrying the same updatedAt (those come from the same producer run). A snapshot row is not ranked this way — any live frame supersedes it, see Account Snapshot. Never treat sequence as a cross-restart order: it resets to 0 when the producer restarts, which can happen while your socket stays connected. It may also skip values because publishing and delivery are at-most-once. A sequence gap alone is not a replay cursor; an explicit 1013 close still requires reconciliation of potentially missing account keys. A subscribe-time snapshot arrives ahead of the live frames, so no REST call is needed for a baseline — see Account Snapshot below.
Subscribe to Account Updates
Account Snapshot
On subscribe, before any live frame, the server sends one row per margin account you own in the application you authenticated under — including flat accounts, which the live producer never emits for because it enumerates only accounts holding an open position or a resting order reserve. 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 bothequity and freeCollateral, with a zero maintenanceRequirement and a zero marginRatio. 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. Owning no margin account delivers an empty array rather than no callback at all, so “you have none” is distinguishable from “this channel has no snapshot”.
A live frame always supersedes a snapshot row for the same marginAccountId, whatever the timestamps say. Use updatedAt to order live frames against each other — not to rank a snapshot row against one. The snapshot is a PostgreSQL-backed baseline stamped when the state was persisted, while a live frame is stamped when the engine sampled it; under persistence lag a later-written row can carry older state, so the two clocks are not comparable. Treat the snapshot as a seed that the first live frame for that account replaces. That includes frames emitted before your subscription activated: the connection buffers them while the snapshot read runs and delivers them afterwards, so one can supersede a newer snapshot row. A later frame normally corrects it within the ~30s keepalive; the exception is an account that went flat in that window whose final frame never reached you, which leaves a stale active account until you reconnect and re-seed. A durable per-account version would fence this — until it exists, reconnecting is the recovery.
The live stream is slightly wider than the snapshot: account_update frames are delivered per user, so if the same wallet trades through more than one application you also receive live frames for those accounts. Each frame is full current health, so such an account baselines itself from its next frame (within the ~30s keepalive) even though the snapshot does not list it.
A snapshot row is the live payload with sequence omitted. The counter belongs to the matching engine’s producer process and the WebSocket API cannot mint one; a 0 would be worse than absent, because it is indistinguishable from a restarted producer’s first frame. Its absence costs you nothing, because a snapshot row is never ranked against a live frame by updatedAt or sequence in the first place — any live frame for that account supersedes it.
Account Event Structure
TWAP Orders Channel
Stream live progress for your TWAP parent orders. Requires authentication. Subscribe to every TWAP parent you own, or scope to one market with the optionaltradingPairId. The channel is private — only the owner receives events — and delivers the full parent snapshot on every lifecycle transition (creation, each placed or skipped slice, cancellation, completion, the passive-child mutations of a PASSIVE parent, and the activation or expiry of a conditional one), tagged with the reason for the update, so a client renders live progress without polling.
Subscribe to TWAP Orders
TWAP Order Event Structure
Subscribe-time snapshot
Pass anonSnapshot callback — the third argument, after the optional tradingPairId — to receive the current baseline as one TwapOrderEventData[], injected ahead of the live frames so no REST read is needed to seed state:
reason: "snapshot". The baseline carries only the owned non-terminal parents — PENDING (awaiting its start window, or, for a conditional parent, still awaiting its price trigger — which can be indefinite) and ACTIVE (triggered/started and slicing) — optionally scoped to the filtered pair; an owner with no live parents receives onSnapshot([]), never a skipped call. Terminal parents (COMPLETED / CANCELLED) are history and never appear — read those from listTwapOrders. The shared rules — one baseline per wire subscription, all-or-nothing delivery, and a fresh baseline after every reconnect — are covered under Subscribe-time snapshots.
Treat the baseline as a seed, not an authoritative replacement set. Like the persisted orders snapshot it is PostgreSQL-backed, so a parent that reached a terminal state just before you connected can still arrive as ACTIVE/PENDING if that transition has not yet been persisted — and its terminal event, sent before this subscription existed, does not re-fire. Absence from the baseline is likewise not a tombstone. So merge the snapshot into local state rather than replacing it wholesale (the setTwapParents above is the first-connection seed), and reconcile a parent that then stays live with no further frames against the REST TWAP read.
Each child order is a regular order — its fills flow through the standard Orders Channel above. The persisted child order records the parent’s ID in its parentOrderId field, so a parent’s children can be tied together in order history (the field is on the order read model, not on the live fill event).
Market Stats Channel
Stream periodic market statistics. Public — no session handshake required. Detected lag closes the whole socket with 1013 under the slow-client rule. Each frame carries the market’s full current stats, so consume it as a terminal-state snapshot rather than a delta. Two subscriptions:sdk.ws.marketStats(pairId, handler) for one market’s full stats, and sdk.ws.marketStatsAll(handler) for a slower-cadence reduced summary of every published market in a single frame (a market that has not yet assembled its first frame is omitted; the subscribe-time snapshot carries the same set). Perp-only fields (index/mark price, open interest, funding) are omitted for spot markets and surface as undefined.
Both accept an optional onSnapshot that delivers the current baseline before the live frames: marketStats yields a one-element MarketStatsData[] with that pair’s latest complete frame, and marketStatsAll yields a MarketStatsSummary[] with one summary per active market with retained data, deterministic by trading-pair id. Prefer it on reconnect over waiting for the next periodic push. A per-market subscription whose market has not yet produced a frame — or on a transient read failure — receives a SNAPSHOT_UNAVAILABLE signal rather than a fabricated one; a later published live frame seeds it once the market produces one (a market that never publishes stays unseeded). marketStatsAll can likewise return SNAPSHOT_UNAVAILABLE for a brief window during initial startup or after a server restart (until the producer commits its first full generation), or on a transient snapshot read failure, rather than a partial list; handle the coded error as a resync signal (the next marketStatsAll push delivers the baseline) rather than assuming onSnapshot always fires. Where a deployment does not run the market-stats snapshot service, neither form sends a snapshot or a SNAPSHOT_UNAVAILABLE signal at all — the first live push seeds state, so never block on onSnapshot firing.
Subscribe to Market Stats
Market Stats Event Structures
Instruments Channel
Stream market lifecycle and configuration changes: new listings, delistings, and edits to tick size, quantity step, order-size bounds, or category. Public — no session handshake required. Detected lag closes the whole socket with 1013 under the slow-client rule. On reconnect, apply the new subscribe snapshot; if unavailable, reconcile over REST rather than waiting for another event from every market. Every frame carries the market’s full current configuration as a terminal-state snapshot — never an additive delta — withchangedFields naming which fields the mutation touched, for handlers that want to react selectively.
The current active-pair list arrives before the live diffs, so a subscriber always starts from a complete baseline. How it is delivered depends on whether you pass onSnapshot: with it, the list arrives as one InstrumentEventData[]; without it, as one listing event per market through the ordinary handler. Prefer onSnapshot if you keep a market map — it is the only form that tells you where the baseline ends, which is what lets you replace the map after a reconnect and drop markets delisted while the socket was down. A subscriber joining a channel this client already holds is replayed the current baseline from a local cache instead, through whichever form it registered (the server snapshots only genuinely new wire subscriptions).
Omit tradingPairId to receive every market, or pass one to filter.
Subscribe to Instruments
Instrument Event Structure
baseIconUrl and quoteIconUrl apply to every market and are always present (unlike the perp-only fields, which are omitted for spot), each null when that asset has no icon — join a position’s tradingPairId to the instrument row to render both icons without a REST call. halt / unhalt are reserved for a future market-regime producer and are not emitted by admin trading-pair mutations. For the raw wire protocol (snake_case keys, the Snapshot envelope), see the WebSocket reference.
Complete Example
React Integration
Best Practices
1. Use Trading Pair IDs
All WebSocket channels use trading pair IDs (UUIDs), not symbols:2. Always Clean Up
Store unsubscribe functions and call them when done:3. Authenticate for Orders
The orders channel requires authentication:4. Handle Reconnection
sdk.ws reconnects automatically (unlimited retries by default). Poll getStatus() if you need to surface it:
onResync hook and an onStatusChange callback, pass a ws config to the SDK (new MonacoSDK({ ..., ws })) or construct the client with createMonacoWebSocket — see Reconnection and resync.
