> ## Documentation Index
> Fetch the complete documentation index at: https://docs.0xmonaco.com/llms.txt
> Use this file to discover all available pages before exploring further.

# WebSocket API

> Listen to live market data and authenticated account events over raw WebSockets

## Endpoint

```text theme={null}
wss://ws-staging.apimonaco.xyz/ws
```

<Note>
  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](/sdk/typescript/websockets) and [React hooks](/sdk/react/intro).
</Note>

## 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.

```json theme={null}
{
  "type": "Subscribe",
  "channels": ["trades:550e8400-e29b-41d4-a716-446655440000"]
}
```

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.

```json theme={null}
{
  "type": "Subscribed",
  "channels": ["trades:550e8400-e29b-41d4-a716-446655440000"]
}
```

* 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:

```json theme={null}
{
  "type": "Error",
  "code": "AUTH_REQUIRED",
  "message": "Channel 'orders' requires authentication. Please authenticate first.",
  "channel": "orders"
}
```

`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.

```json theme={null}
{
  "type": "Authenticate",
  "public_key": "<session_public_key>",
  "timestamp": 1710000000000,
  "signature": "<ed25519_signature>"
}
```

<Note>
  `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.
</Note>

Events are delivered in this envelope:

```json theme={null}
{
  "type": "Event",
  "channel": "trades:550e8400-e29b-41d4-a716-446655440000",
  "data": {}
}
```

### 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.

```json theme={null}
{ "type": "Ping" }
```

### 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.

The same close, with the same reason, is also sent when events may have been lost before they reached the WebSocket server: the matching engine dropped a frame under load, a publish failed, or the server's NATS connection dropped, reconnected, or fell behind. Which events were lost is unknown, so every connection on that server that holds at least one subscription and was open when the events were lost is closed, whatever it subscribes to; a connection with no subscription, or one opened after the loss (for example during a matching engine restart), stays open. These closes are spaced at least about one second apart, so a burst of losses closes a connection once. Periodic frames whose next frame restates their full state (`market_stats` and position valuation frames) never cause one: a later frame repairs a lost one.

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. A per-market `trades` subscription also gets one: the market's newest 50 trades, a recent-history baseline rather than current state (see [Trades](#trades)). So does an `ohlcv` subscription to one market, interval and price series: its newest 350 candles, also a recent-history baseline (see [OHLCV](#ohlcv)). 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](/sdk/typescript/websockets#reconnection-and-resync).

The remaining channels do **not** provide a subscribe-time snapshot — the append-only `movements` stream 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 whose collateral does not move 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](#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. The `trades` snapshot covers only the newest 50 trades, and the `ohlcv` snapshot only the newest 350 candles; neither is a gap-free replay. 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

<Tabs>
  <Tab title="Browser">
    ```typescript theme={null}
    const pairId = "550e8400-e29b-41d4-a716-446655440000";
    const ws = new WebSocket("wss://ws-staging.apimonaco.xyz/ws");

    ws.addEventListener("open", () => {
      ws.send(JSON.stringify({
        type: "Subscribe",
        channels: [
          `trades:${pairId}`,
          `orderbook:${pairId}:SPOT:0.01:base`,
        ],
      }));
    });

    ws.addEventListener("message", (message) => {
      const payload = JSON.parse(message.data);
      if (payload.type !== "Event") return;

      console.log(payload.channel, payload.data);
    });
    ```
  </Tab>

  <Tab title="Node.js">
    ```typescript theme={null}
    import WebSocket from "ws";

    const pairId = "550e8400-e29b-41d4-a716-446655440000";
    const ws = new WebSocket("wss://ws-staging.apimonaco.xyz/ws");

    ws.on("open", () => {
      ws.send(JSON.stringify({
        type: "Subscribe",
        channels: [`trades:${pairId}`],
      }));
    });

    ws.on("message", (message) => {
      const payload = JSON.parse(message.toString());
      if (payload.type === "Event") {
        console.log(payload.channel, payload.data);
      }
    });
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    import asyncio
    import json
    import websockets

    PAIR_ID = "550e8400-e29b-41d4-a716-446655440000"
    URL = "wss://ws-staging.apimonaco.xyz/ws"

    async def main():
        async with websockets.connect(URL) as ws:
            await ws.send(json.dumps({
                "type": "Subscribe",
                "channels": [f"trades:{PAIR_ID}"],
            }))

            async for message in ws:
                payload = json.loads(message)
                if payload.get("type") == "Event":
                    print(payload["channel"], payload["data"])

    asyncio.run(main())
    ```
  </Tab>
</Tabs>

To stop a stream, send `Unsubscribe` with the same channel string and close the socket when no subscriptions remain.

```json theme={null}
{
  "type": "Unsubscribe",
  "channels": ["trades:550e8400-e29b-41d4-a716-446655440000"]
}
```

`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.

```json theme={null}
{
  "type": "Unsubscribed",
  "channels": ["trades:550e8400-e29b-41d4-a716-446655440000"]
}
```

## Channel Catalog

### Public Channels

| Channel | Pattern | Event | Use for |
| - | - | - | - |
| Trades | `trades:{trading_pair_id}` | `trade` | Live executions for one pair or every pair |
| Orderbook | `orderbook:{trading_pair_id}:{mode}:{magnitude}:{denomination}` | `orderbook_update` | Live bid/ask depth |
| OHLCV | `ohlcv:{trading_pair_id}:{mode}:{interval}[:{price_type}]` | `ohlcv_update` | Live candle updates (trade or `mark` series) |
| Market stats (per-market) | `market_stats:{trading_pair_id}` | — (routed by `channel`) | Periodic per-market statistics: prices, best bid/ask, open interest, 24h stats, funding state |
| Market stats (all markets) | `market_stats` / `market_stats:*` | — (routed by `channel`) | Slower-cadence reduced summary for every published market (a market that has not yet assembled its first frame is omitted) |
| Instrument | `instrument:{trading_pair_id}` / `instrument` / `instrument:*` | — (routed by `channel`) | Trading-pair lifecycle + config: listings, delistings, and tick/step/order-size/category edits; each frame carries the full market config (perp leverage/margin included) and the base/quote asset icon URLs |

### Authenticated Channels

| Channel | Pattern | Event | Use for |
| - | - | - | - |
| Orders | `orders:{trading_pair_id}:{mode}` | order lifecycle | User order placement, fill, cancel, reject, and expiry events |
| Balances | `balances` | `balance_update` | Balance state changes |
| Movements | `movements` | `user_movement` | Ledger entries for deposits, withdrawals, funding settlements, and the unlocks a cancelled or expired order releases — **not** fills or fees |
| Positions | `positions:{trading_pair_id}` | `position_update`, `position_valuation` | Perp position changes, and periodic mark-derived valuations of open positions |
| Conditional Orders | `conditional_orders:{trading_pair_id}` | `conditional_order_update` | Take-profit, stop-loss and trailing-stop lifecycle updates |
| TWAP Orders | `twap_orders:{trading_pair_id}` | `twap_order_update` | TWAP parent-order progress: creation, slice placements/skips, passive-child mutations, conditional activation/expiry, cancellation, completion |
| Liquidations | `liquidations:{trading_pair_id}` | `liquidation_alert` | Liquidation-risk alerts and status changes |
| Account | `account` | `account_update` | Margin-account health: equity, collateral, margin ratio, maintenance requirement |
| Copy | `copy` | `copy_relationship_update`, `copy_skip` | Your copy trading relationships as a follower: status changes and leader fills that were not mirrored |

## Channel Details

### Trades

Executed trades for one pair, including price, quantity, maker side, and execution time.

```text theme={null}
trades:{trading_pair_id}
```

<ParamField path="trading_pair_id" type="UUID | *" required>
  Trading pair ID. Use `*` only when you intentionally want trades for every pair.
</ParamField>

**Example**

```text theme={null}
trades:550e8400-e29b-41d4-a716-446655440000
```

#### Subscribe-time snapshot

On subscribe to a single market, before any live trade, the server sends a `Snapshot` frame whose `data` is an array of that market's newest **50** trades, **newest first** — the same trades, in the same order, as the first page of the REST trades read with `pageSize` 50. Each row is the live trade frame's inner `data` object (`trade_id`, `price`, `quantity`, `quantity_raw`, `maker_side`, `executed_at`); the market is the channel's, so the `event_type`, `trading_pair_id` and `trading_mode` envelope is not repeated per row. Decimal values are JSON strings. A market with no trades, or an unknown `trading_pair_id`, gets an empty array. A `trades:*` subscription gets no snapshot. A read failure returns `SNAPSHOT_UNAVAILABLE` and leaves the subscription active.

```json theme={null}
{
  "type": "Snapshot",
  "channel": "trades:550e8400-e29b-41d4-a716-446655440000",
  "data": [
    {
      "trade_id": "123e4567-e89b-12d3-a456-426614174001",
      "price": "35000.5",
      "quantity": "0.5",
      "quantity_raw": "50000000",
      "maker_side": "SELL",
      "executed_at": "2026-10-09T12:34:57.120Z"
    },
    {
      "trade_id": "123e4567-e89b-12d3-a456-426614174000",
      "price": "35000",
      "quantity": "1.25",
      "quantity_raw": "125000000",
      "maker_side": "BUY",
      "executed_at": "2026-10-09T12:34:56.789Z"
    }
  ],
  "timestamp": "2026-10-09T12:35:00.002Z"
}
```

The snapshot is a recent-history baseline, not a gap-free replay. It reads recorded trades, which can trail the live stream by a few seconds, so a trade can appear both in the snapshot and in an early live frame, and a trade executed just before you subscribed can be in neither. Dedupe on `trade_id` when merging the snapshot with live frames, and use the REST trades read for anything older or for a complete history.

### Orderbook

Live bid/ask depth with optional aggregation and base or quote quantity formatting.

```text theme={null}
orderbook:{trading_pair_id}
orderbook:{trading_pair_id}:{mode}
orderbook:{trading_pair_id}:{mode}:{magnitude}
orderbook:{trading_pair_id}:{mode}:{magnitude}:{denomination}
```

<ParamField path="trading_pair_id" type="UUID | *" required>
  Trading pair ID. Use `*` for every pair.
</ParamField>

<ParamField path="mode" type="SPOT | MARGIN | *">
  Trading mode filter. Omit it for all modes, or use `*` when keeping later segments.
</ParamField>

<ParamField path="magnitude" type="number | *">
  Price-level grouping. Supported values are `0.0001`, `0.001`, `0.01`, `0.1`, `1`, `10`, `100`, `1000`, `10000`, and `*`.
</ParamField>

<ParamField path="denomination" type="base | quote | *">
  Quantity denomination. `base` returns base-token quantities; `quote` returns quote-token notional quantities.
</ParamField>

**Example**

```text theme={null}
orderbook:550e8400-e29b-41d4-a716-446655440000:SPOT:0.01:base
orderbook:550e8400-e29b-41d4-a716-446655440000:SPOT:*:quote
```

### OHLCV

Live candle updates for charting and market analytics.

```text theme={null}
ohlcv:{trading_pair_id}:{mode}:{interval}
ohlcv:{trading_pair_id}:{mode}:{interval}:{price_type}
```

<ParamField path="trading_pair_id" type="UUID | *" required>
  Trading pair ID. Use `*` for every pair.
</ParamField>

<ParamField path="mode" type="SPOT | MARGIN | *" required>
  Trading mode filter.
</ParamField>

<ParamField path="interval" type="1m | 5m | 15m | 1h | 4h | 1d | *" required>
  Candle interval.
</ParamField>

<ParamField path="price_type" type="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.
</ParamField>

**Example**

```text theme={null}
ohlcv:550e8400-e29b-41d4-a716-446655440000:SPOT:1m
ohlcv:550e8400-e29b-41d4-a716-446655440000:MARGIN:1m:mark
```

#### Subscribe-time snapshot

On subscribe to one market, interval and price series, before any live candle, the server sends a `Snapshot` frame whose `data` is an array of that series' newest **350** candles, **oldest first** — the same candles, in the same order, as the REST candlesticks read with `limit` 350 and the same interval and `priceType`, including its flat candles for periods with no trades. Each row is the live candle frame's inner `data` object (`open`, `high`, `low`, `close`, `volume`, `quote_volume`, `trades_count`, `period_start`, `period_end`); the channel fixes the market, interval and series, so the envelope is not repeated per row. Decimal values are JSON strings, `period_start` and `period_end` are Unix milliseconds, and `period_end` is `period_start` plus the interval, as on a live candle. `quote_volume` is `null` for a candle whose quote volume was not recorded.

```json theme={null}
{
  "type": "Snapshot",
  "channel": "ohlcv:550e8400-e29b-41d4-a716-446655440000:SPOT:1m",
  "data": [
    {
      "open": "35000.00000000",
      "high": "35020.50000000",
      "low": "34990.00000000",
      "close": "35010.00000000",
      "volume": "1.25000000",
      "quote_volume": "43760.12500000",
      "trades_count": 7,
      "period_start": 1760011920000,
      "period_end": 1760011980000
    },
    {
      "open": "35010.00000000",
      "high": "35015.00000000",
      "low": "35005.00000000",
      "close": "35012.00000000",
      "volume": "0.40000000",
      "quote_volume": "14004.40000000",
      "trades_count": 3,
      "period_start": 1760011980000,
      "period_end": 1760012040000
    }
  ],
  "timestamp": "2026-10-09T12:14:05.002Z"
}
```

* The four-token form is the `trade` series. A `mode` of `*` is served.
* A `*` market, interval or price type gets no snapshot, because one frame carries one series.
* An unknown `trading_pair_id`, a `mode` that is not the market's (`SPOT` on a margin market, or `MARGIN` on a spot market), and `mark` on a spot market get an empty array: no live candle comes for them.
* `mark` is served on every interval listed above, but live mark updates stream on `1m` only, so a `mark` snapshot on another interval is a baseline with no live updates after it.
* A read failure returns `SNAPSHOT_UNAVAILABLE` and leaves the subscription active.

The snapshot is a recent-history baseline, not a gap-free replay. It reads recorded candles, which trail the live candles: the newest `1m` candle is the last closed minute, and longer intervals can lag, so the snapshot's newest candle can be partial or missing. A live candle is the full current state of its period. Upsert by `period_start` when merging: a live candle replaces the snapshot candle with the same `period_start`, or appends a newer one. Use the REST candlesticks read for anything older.

### 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.

```text theme={null}
market_stats:{trading_pair_id}
market_stats
market_stats:*
```

<ParamField path="trading_pair_id" type="UUID">
  Trading pair ID for a per-market subscription. Omit the segment (or use `*`) for the all-markets summary.
</ParamField>

**Example**

```text theme={null}
market_stats:550e8400-e29b-41d4-a716-446655440000
```

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:

```json theme={null}
{
  "channel": "market_stats:550e8400-e29b-41d4-a716-446655440000",
  "data": {
    "trading_pair_id": "550e8400-e29b-41d4-a716-446655440000",
    "data": {
      "symbol": "BTC/USDC",
      "trading_pair_id": "550e8400-e29b-41d4-a716-446655440000",
      "index_price": "64000.0",
      "mark_price": "64010.5",
      "mid_price": "64008.0",
      "best_bid_price": "64005.0",
      "best_ask_price": "64011.0",
      "open_interest": "128.5",
      "open_interest_limit": "10000000",
      "open_interest_headroom": "1774650.75",
      "last_trade_price": "64009.0",
      "daily_base_token_volume": "842.1",
      "daily_quote_token_volume": "53900000.0",
      "daily_price_low": "62800.0",
      "daily_price_high": "64850.0",
      "daily_price_change": "1200.0",
      "daily_price_change_percent": "1.91",
      "current_funding_rate": "0.0001",
      "funding_rate": "0.00012",
      "funding_timestamp": 1735790400000,
      "premium": "0.00005",
      "base_interest_rate": "0",
      "funding_clamp_small": "0.0005",
      "funding_clamp_big": "0.0005",
      "market_status": "TRADABLE",
      "funding_interval_seconds": 3600,
      "last_funding_time": 1735786800000
    }
  }
}
```

**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:

```json theme={null}
{
  "channel": "market_stats",
  "data": {
    "markets": [
      {
        "trading_pair_id": "550e8400-e29b-41d4-a716-446655440000",
        "symbol": "BTC/USDC",
        "last_trade_price": "64009.0",
        "mark_price": "64010.5",
        "daily_price_change": "1200.0",
        "daily_price_change_percent": "1.91",
        "daily_base_token_volume": "842.1",
        "daily_quote_token_volume": "53900000.0",
        "daily_price_high": "64850.0",
        "daily_price_low": "62800.0",
        "open_interest": "128.5",
        "current_funding_rate": "0.0001",
        "funding_timestamp": 1735790400000,
        "market_status": "TRADABLE",
        "index_price": "64005.2",
        "funding_rate": "0.00012",
        "premium": "0.00005",
        "open_interest_notional": "8225349.25"
      }
    ]
  }
}
```

### Instrument

Trading-pair lifecycle and configuration changes, public (no session handshake): new listings, delistings, non-activation edits to tick size, quantity step, order-size bounds, or category, and edits to a perp market's margin ladder. Each perp frame carries the whole ladder as `risk_tiers` (see the payload below). Fee edits do 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.

```text theme={null}
instrument:{trading_pair_id}
instrument
instrument:*
```

<ParamField path="trading_pair_id" type="UUID">
  Trading pair ID for a per-market subscription. Omit the segment (or use `*`) to stream every market.
</ParamField>

**Example**

```text theme={null}
instrument:550e8400-e29b-41d4-a716-446655440000
```

Detected lag closes the whole connection with 1013; see [Slow-client disconnect](#slow-client-disconnect-close-code-1013). 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`, `risk_tiers`) are **omitted** (not `null`) for spot markets, which have no leverage or margin parameters. `risk_tiers` is the market's margin ladder, ascending by `tier_level`, with the same fields and values as the REST perp config's `riskTiers`: `tier_level` (a JSON integer), `lower_bound_notional`, `max_leverage`, `initial_margin_ratio`, `maintenance_margin_ratio`, `initial_margin_deduction`, `maintenance_margin_deduction` and `max_position_notional`, with `max_leverage` and `max_position_notional` omitted when the tier has none. `max_leverage`, `initial_margin_rate` and `maintenance_margin_rate` at the top level are tier 1's values. 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). A margin-ladder edit that changes any tier, or the position cap that bounds the top tier, emits a `config_change` whose `changed_fields` names `risk_tiers`, plus `max_leverage`, `initial_margin_rate` and `maintenance_margin_rate` when tier 1 changed. Saving an unchanged ladder emits nothing. The payload nests under the channel envelope's `data`, which carries a `trading_pair_id` and a nested `data` object:

```json theme={null}
{
  "type": "Event",
  "channel": "instrument:550e8400-e29b-41d4-a716-446655440000",
  "data": {
    "trading_pair_id": "550e8400-e29b-41d4-a716-446655440000",
    "data": {
      "change": "config_change",
      "trading_pair_id": "550e8400-e29b-41d4-a716-446655440000",
      "symbol": "BTC/USDC",
      "is_active": true,
      "tick_size": "0.1",
      "quantity_step_size": "0.001",
      "min_order_size": "0.001",
      "max_order_size": "1000",
      "market_type": "margin",
      "category": "crypto",
      "base_asset_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
      "quote_asset_id": "6ba7b811-9dad-11d1-80b4-00c04fd430c8",
      "base_token": "BTC",
      "quote_token": "USDC",
      "base_asset_name": "Bitcoin",
      "quote_asset_name": "USD Coin",
      "base_decimals": 8,
      "quote_decimals": 6,
      "base_icon_url": "https://cdn.example/btc.svg",
      "quote_icon_url": "https://cdn.example/usdc.svg",
      "min_leverage": "1",
      "max_leverage": "20",
      "initial_margin_rate": "0.05",
      "maintenance_margin_rate": "0.03",
      "risk_tiers": [
        {
          "tier_level": 1,
          "lower_bound_notional": "0",
          "max_leverage": "20",
          "initial_margin_ratio": "0.05",
          "maintenance_margin_ratio": "0.03",
          "initial_margin_deduction": "0",
          "maintenance_margin_deduction": "0",
          "max_position_notional": "1000000"
        },
        {
          "tier_level": 2,
          "lower_bound_notional": "1000000",
          "max_leverage": "10",
          "initial_margin_ratio": "0.1",
          "maintenance_margin_ratio": "0.05",
          "initial_margin_deduction": "50000",
          "maintenance_margin_deduction": "20000",
          "max_position_notional": "5000000"
        }
      ],
      "changed_fields": ["tick_size"],
      "updated_at": "2026-01-02T03:04:05Z"
    }
  }
}
```

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:

```json theme={null}
{
  "type": "Snapshot",
  "channel": "instrument",
  "data": [
    {
      "change": "listing",
      "trading_pair_id": "550e8400-e29b-41d4-a716-446655440000",
      "symbol": "BTC/USDC",
      "is_active": true,
      "tick_size": "0.1",
      "quantity_step_size": "0.001",
      "min_order_size": "0.001",
      "max_order_size": "1000",
      "market_type": "margin",
      "category": "crypto",
      "base_asset_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
      "quote_asset_id": "6ba7b811-9dad-11d1-80b4-00c04fd430c8",
      "base_token": "BTC",
      "quote_token": "USDC",
      "base_asset_name": "Bitcoin",
      "quote_asset_name": "USD Coin",
      "base_decimals": 8,
      "quote_decimals": 6,
      "base_icon_url": "https://cdn.example/btc.svg",
      "quote_icon_url": null,
      "min_leverage": "1",
      "max_leverage": "20",
      "initial_margin_rate": "0.05",
      "maintenance_margin_rate": "0.03",
      "risk_tiers": [
        {
          "tier_level": 1,
          "lower_bound_notional": "0",
          "max_leverage": "20",
          "initial_margin_ratio": "0.05",
          "maintenance_margin_ratio": "0.03",
          "initial_margin_deduction": "0",
          "maintenance_margin_deduction": "0",
          "max_position_notional": "1000000"
        },
        {
          "tier_level": 2,
          "lower_bound_notional": "1000000",
          "max_leverage": "10",
          "initial_margin_ratio": "0.1",
          "maintenance_margin_ratio": "0.05",
          "initial_margin_deduction": "50000",
          "maintenance_margin_deduction": "20000",
          "max_position_notional": "5000000"
        }
      ],
      "changed_fields": [],
      "updated_at": "2026-01-02T03:04:05Z"
    }
  ],
  "timestamp": "2026-01-02T03:04:05Z"
}
```

`@0xmonaco/types` ships the `InstrumentEvent`, `InstrumentEventData`, and `InstrumentChangeKind` types for this channel, and `@0xmonaco/core` exposes the typed [`sdk.ws.instruments(handler, tradingPairId?)`](/sdk/typescript/websockets#instruments-channel) 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`, `risk_tiers` → `riskTiers` (and each tier's keys likewise, `tier_level` → `tierLevel`), `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.

```text theme={null}
orders
orders:{trading_pair_id}:{mode}
```

<ParamField path="trading_pair_id" type="UUID | *">
  Trading pair filter. Omit it by using `orders` for all of the authenticated user's orders.
</ParamField>

<ParamField path="mode" type="SPOT | MARGIN | *">
  Trading mode filter. Use `orders:{trading_pair_id}:*` to filter by pair across all modes.
</ParamField>

**Example**

```text theme={null}
orders
orders:550e8400-e29b-41d4-a716-446655440000:SPOT
```

### Balances

Balance state changes for the authenticated user.

```text theme={null}
balances
```

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.

```text theme={null}
movements
```

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`](#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.

```text theme={null}
positions
positions:{trading_pair_id}
```

<ParamField path="trading_pair_id" type="UUID">
  Optional trading pair filter. Omit it for all positions owned by the authenticated user.
</ParamField>

**Example**

```text theme={null}
positions:550e8400-e29b-41d4-a716-446655440000
```

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](/sdk/typescript/websockets#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](/developers/trade#trailing-stop)).

```text theme={null}
conditional_orders
conditional_orders:{trading_pair_id}
```

<ParamField path="trading_pair_id" type="UUID">
  Optional trading pair filter. `conditional-orders` with a hyphen is also accepted.
</ParamField>

**Example**

```text theme={null}
conditional_orders:550e8400-e29b-41d4-a716-446655440000
```

### 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](/trading-mechanics/twap-orders) for the execution
mechanics, including [passive execution](/trading-mechanics/twap-orders#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.

```text theme={null}
twap_orders
twap_orders:{trading_pair_id}
```

<ParamField path="trading_pair_id" type="UUID">
  Optional trading pair filter. Omit it for all TWAP parents owned by the authenticated user.
</ParamField>

**Example**

```text theme={null}
twap_orders:550e8400-e29b-41d4-a716-446655440000
```

### Liquidations

Liquidation-risk alerts and liquidation status changes for the authenticated user.

```text theme={null}
liquidations
liquidations:{trading_pair_id}
```

<ParamField path="trading_pair_id" type="UUID">
  Optional trading pair filter for liquidation alerts owned by the authenticated user.
</ParamField>

**Example**

```text theme={null}
liquidations:550e8400-e29b-41d4-a716-446655440000
```

#### 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.

```text theme={null}
copy
```

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`, `NO_LIQUIDITY`, or `MARGIN_MODE_CONFLICT`. 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**:

```json theme={null}
{
  "channel": "copy",
  "data": {
    "event_type": "copy_skip",
    "user_id": "22222222-2222-2222-2222-222222222222",
    "data": {
      "relationship_id": "33333333-3333-3333-3333-333333333333",
      "leader_wallet": "0x2c7536e3605d9c16a7a3d7b1898e529396a65c23",
      "leader_trade_id": "66666666-6666-6666-6666-666666666666",
      "leader_order_id": "77777777-7777-7777-7777-777777777777",
      "trading_pair_id": "11111111-1111-1111-1111-111111111111",
      "reason": "INSUFFICIENT_MARGIN",
      "requested_quantity": "0.5",
      "consecutive_skips": 3,
      "version": 918343
    }
  }
}
```

### 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 after that the account sends no keepalive while it stays flat. A flat account still gets a frame after its collateral moves: a transfer between the spot wallet and margin, an on-chain deposit into margin or a withdrawal from it, a transfer between your margin accounts, closing a sub-account, a copy-trading allocation, or an insurance payout. Moves that land before the producer's next tick collapse into one frame carrying the latest state, and that frame normally arrives on the next tick, within \~2s of the move. The producer reads at most 512 flat accounts per tick, so a burst wider than that spreads over the following ticks. The frame carries the account's current collateral on the same per-account `sequence` as every other frame, so the usual ordering rule applies. On-chain deposits and withdrawals also appear on the [`movements`](#movements) channel. 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.

<Note>
  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.
</Note>

```text theme={null}
account
```

Detected lag closes the whole connection with 1013; see [Slow-client disconnect](#slow-client-disconnect-close-code-1013). 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`:

```json theme={null}
{
  "channel": "account",
  "data": {
    "event_type": "account_update",
    "user_id": "550e8400-e29b-41d4-a716-446655440000",
    "data": {
      "margin_account_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
      "sequence": 42,
      "equity": "10250.75",
      "collateral": "10000.00",
      "free_collateral": "8420.10",
      "margin_ratio": "0.12",
      "maintenance_requirement": "1230.09",
      "updated_at": "2026-08-25T12:00:00Z"
    }
  }
}
```

#### 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 emits for only when their collateral moves. 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**:

```json theme={null}
{
  "type": "Snapshot",
  "channel": "account",
  "data": [
    {
      "margin_account_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
      "equity": "10000.00",
      "collateral": "10000.00",
      "free_collateral": "10000.00",
      "margin_ratio": "0",
      "maintenance_requirement": "0",
      "updated_at": "2026-08-25T12:00:00Z"
    }
  ],
  "timestamp": "2026-08-25T12:00:01Z"
}
```

`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](#slow-client-disconnect-close-code-1013).

`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](/sdk/typescript/websockets#account-channel)) 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).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.