# Market stream

> Subscribe to orderbooks, quotes, trades, market status, newly active cycles, and resolved markets, with recovery guidance for interrupted streams.

**WebSocket origin:** `wss://app.agara.xyz` — connect to
`/trade/v1/market-stream`.

**Public — no token required.** A connection can carry up to 64 subscriptions
across many markets. Application messages must be JSON text frames and cannot
exceed 64 KiB. Binary application frames are rejected.

For per-user order, fill, position, redemption, and collateral events, see the
[account stream](/docs/account-stream) — same protocol, authed.

To follow halts, resumptions, and resolution for a market, use
[`market_status` by condition](#market_status). To receive newly active markets
and resolved-market data for a recurring event, use
[`market_status` by event](#event-scoped-lifecycle-updates) on this same socket.

## Connect [#connect]

Any WebSocket client works. No subprotocol negotiation is required.

This endpoint allows **300 connection attempts per IP per 60 seconds**. Reuse
connections and reconnect with backoff; see [stream connection limits](/docs/rate-limits#stream-connection-attempts).

The HTTP upgrade can fail before a socket exists:

| HTTP response                      | What to do                                                                                   |
| ---------------------------------- | -------------------------------------------------------------------------------------------- |
| `401` typed authentication failure | Remove or correct an invalid `Authorization` header; the public stream itself needs no token |
| `426 websocket_upgrade_required`   | Send a valid WebSocket upgrade request                                                       |
| `429 rate_limited`                 | Wait for `Retry-After`, then reconnect                                                       |
| `503 dependency_unavailable`       | Back off with jitter and reconnect                                                           |

These are [HTTP Problem Details](/docs/errors), not WebSocket frames.

```bash
export AGARA_BASE_URL="https://app.agara.xyz"
export AGARA_WS_URL="wss://app.agara.xyz"
export AGARA_CHAIN_ID="8453"
export AGARA_COLLATERAL_ADDRESS="0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
export AGARA_CONDITIONAL_TOKENS_ADDRESS="0xe12D566cE5Dd8d817488E85a81386878649e3A18"
export AGARA_CTF_EXCHANGE_ADDRESS="0xD06f3fA925D35077A11dB42c6614D684f10B2aD6"
```

Connect with any WebSocket library and send JSON frames:

```python
import asyncio, json, os, websockets

URL = f"{os.environ['AGARA_WS_URL']}/trade/v1/market-stream"

async def main():
    async with websockets.connect(URL) as ws:
        await ws.send(json.dumps({
            "op": "subscribe",
            "channels": [
                { "name": "orderbook", "token_id": "21742…36455" },
                { "name": "best_quote", "token_id": "21742…36455" },
                { "name": "market_status", "condition_id": "0x2174…" },
            ],
        }))
        async for raw in ws:
            print(json.loads(raw))

asyncio.run(main())
```

```bash
wscat -c "$AGARA_WS_URL/trade/v1/market-stream"
# then paste:
> { "op": "subscribe", "channels": [{ "name": "best_quote", "token_id": "21742…36455" }] }
```

### API trading connection layout [#api-trading-connection-layout]

Use this public socket for book, quote, trade, and market-state updates. Open a
second connection to the [account stream](/docs/account-stream) for your own
orders, fills, positions, and collateral. If either connection is rebuilding,
pause quoting until both public and account state are current. See the full
[API trading recovery procedure](/docs/api-traders/recovery).

## Channels [#channels]

| Channel         | Subject                             | What you get                                                                                                                    |
| --------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `orderbook`     | `token_id`                          | Full-depth book updates. Current v1 sends a replacement snapshot for every book change; clients must still route on `data.kind` |
| `best_quote`    | `token_id`                          | Top-of-book bid/ask, emitted only on real moves                                                                                 |
| `trades`        | `condition_id`                      | One frame per executed trade across all outcomes of the market                                                                  |
| `market_status` | Either `condition_id` or `event_id` | Market lifecycle events by condition; current-market changes and completed resolutions by event                                 |

All channels require a specific subject id. There is no firehose.

## Client → server [#client--server]

All client frames have an `op`. A protocol error can close the connection; use
the `action` field and close code in the [error contract](#error).

### `subscribe` [#subscribe]

```json
{ "op": "subscribe", "channels": [
    { "name": "orderbook",     "token_id":     "21742…36455", "depth": 100 },
    { "name": "best_quote",    "token_id":     "21742…36455" },
    { "name": "trades",        "condition_id": "0x2174…" },
    { "name": "market_status", "condition_id": "0x2174…" }
] }
```

`depth` is accepted for `orderbook`, but v1 always sends full depth. Slice the
book locally. The complete subscription identity includes `depth`, so use the
same value when unsubscribing.

Each public entry is acknowledged separately:

```json
{
  "op": "subscribed",
  "channel": "orderbook",
  "token_id": "21742…36455"
}
```

An exact repeat is idempotent and produces another acknowledgement. Public
acknowledgement is queued before `orderbook` and `best_quote` subject
validation, so it can be followed by `stream_subject_not_found` or
`market_closed`. Before quoting, wait for the initial orderbook `snapshot`, then
fetch the REST book while buffering and use its `hash` as the sequence baseline;
neither the acknowledgement nor the initial WebSocket snapshot alone is
permission to quote. `trades` and `market_status` subscriptions are
acknowledged without checking that the condition exists.

### `unsubscribe` [#unsubscribe]

Mirror the exact subscription shape:

```json
{
  "op": "unsubscribe",
  "channels": [
    { "name": "orderbook", "token_id": "21742…36455", "depth": 100 }
  ]
}
```

When the subscription exists, the server stops it and replies with
`unsubscribed`. An unmatched unsubscribe produces no acknowledgement or error.

### `ping` and `list` [#ping-and-list]

```json
{ "op": "ping" }
{ "op": "list" }
```

`ping` returns `{ "op": "pong", "server_time": "..." }`. `list`
returns the active subscriptions:

```json
{
  "op": "subscription_list",
  "channels": [
    { "channel": "orderbook", "token_id": "21742…36455" },
    { "channel": "market_status", "condition_id": "0x2174…" }
  ]
}
```

## Server → client [#server--client]

Every server frame has an `op`. Subject-bearing frames carry exactly
one of `token_id`, `condition_id`, or `event_id` — clients route on
`channel` and read the relevant field.

| `op`                | When                                                                                                                                                       |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `subscribed`        | Ack for a `subscribe` entry                                                                                                                                |
| `unsubscribed`      | Ack for an `unsubscribe` entry                                                                                                                             |
| `update`            | One event for a channel you subscribed to                                                                                                                  |
| `error`             | A request, subscription, or connection failed. Carries an `action` telling you what to do; `reconnect` errors close the connection (see [`error`](#error)) |
| `sequence_reset`    | You missed messages on this channel — discard local state and follow the [recovery table](#sequence_reset)                                                 |
| `pong`              | Reply to your `ping`                                                                                                                                       |
| `heartbeat`         | Server-pushed every \~10 s                                                                                                                                 |
| `subscription_list` | Reply to your `list`                                                                                                                                       |

### `update` [#update]

```json
{
  "op": "update",
  "channel": "orderbook",
  "token_id": "21742…36455",
  "sequence": 12346,
  "data": {
    "kind": "snapshot",
    "bids": [[60, 12000], [59, 5000]],
    "asks": [[63, 4000]],
    "tick_size": 1,
    "price_scale": 100,
    "size_scale": 100
  }
}
```

For `orderbook`, the first `update` carries a snapshot. Current v1 also sends a
full replacement snapshot for each later book change. Keep routing on
`data.kind`: replace the ladder for `snapshot`, and apply changed levels if a
`delta` is received. `best_quote` sends the current quote as soon as the
exchange has reported the book, and both sides are `null` when that book is
empty. For
`trades` and `market_status`, the first `update` is the next event after
subscribe — there is no replay.

### `sequence` [#sequence]

`sequence` on Engine-backed `update` messages is a global engine-event watermark. It tells you
which engine event the frame includes, but it is not a unique frame ID.

Event-scoped `market_status` updates have no `sequence` or `version` field.
Do not compare their timestamps with Engine sequence numbers.

**Within a single (channel, subject) pair**, frames you receive are
monotonic except when a `sequence_reset` interrupts. Equal values can occur
when a snapshot and another frame include the same event. A backwards step
means local state is no longer trustworthy; rebuild that subject.

**Across channels or subjects**, one engine event can produce several frames
with the same value, and arrival order is not guaranteed. You can group equal
watermarks and order different watermarks, but sorting alone cannot create a
strict cross-channel frame timeline.

The counter is gappy within a channel: an `orderbook` subscriber sees
`1000, 1003, 1007, …` because the gaps are events on other channels.

### `sequence_reset` [#sequence_reset]

```json
{
  "op": "sequence_reset",
  "channel": "orderbook",
  "token_id": "21742…36455",
  "reason": "lagged"
}
```

**You missed messages** for that channel + subject. `reason` tells you
which of the two causes it was — recovery is identical either way:

* `"lagged"` — **your client wasn't reading fast enough.** The subscription
  remains active, but some messages were dropped. Move slow work out of the
  read loop before reconnecting.
* `"stream_reset"` — **the live stream was interrupted and is reconnecting.** The subscription remains
  active, but you still need to rebuild local state.

Your subscription is still active. What to do depends on the channel:

| Channel                      | What the next `update` will be                                                   | What you should do                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| ---------------------------- | -------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `orderbook`                  | Current v1 sends a replacement snapshot; clients must still route on `data.kind` | Discard your local book and wait for the first post-reset `snapshot` while buffering. Its arrival proves the live path is active again, but it is not a state fence. Continue buffering, fetch the REST orderbook, parse REST `hash` as the baseline sequence, and discard buffered frames with `sequence <= hash`. Process newer frames in arrival order: a `snapshot` replaces the whole baseline, while a `delta` changes the current baseline. Use the same snapshot-then-REST order after resubscribing. Another reset restarts recovery |
| `best_quote`                 | The current top-of-book, with both sides `null` when the book is empty           | Discard your local quote and take the next update as current. It carries the best prices only, so use the `orderbook` channel or the REST orderbook when you need depth                                                                                                                                                                                                                                                                                                                                                                       |
| `trades`                     | The next trade whenever it happens — public trades during the gap are lost       | Resume from the next trade; the API does not currently replay the public tape                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `market_status` by condition | The next lifecycle event whenever it happens — events during the gap are lost    | Keep buffering lifecycle frames, read the current market through REST, then apply the entire ordered buffer. Poll until REST catches up if they disagree; never let a stale REST response override a later stop event. Updates carry a watermark, but the channel has no snapshot, replay, or subscription-time baseline marker, so the absence of a frame does not prove that no lifecycle event was missed                                                                                                                                  |
| `market_status` by event     | The next current-market change or completed resolution — no replay               | Buffer updates and refresh the event through REST. These updates have no sequence counter. Preserve a newer current market when an older market resolves; refresh again if REST and the updates disagree                                                                                                                                                                                                                                                                                                                                      |
| `account_events`             | The next account event whenever it happens — events during the gap are lost      | Buffer new events and redo the account REST seed: fully paginated orders, portfolio open orders, positions, summary, trades, and activities. Correlate order transitions by `order_id` or `order_hash`, process every positive-sequence transition, deduplicate fills only by `fill_id`, and deduplicate `sequence: 0` rejections by `order_id`. Confirm each transition through REST before rebuilding the order views, and refetch positions and summary after every fill, position, or collateral event                                    |

For `account_events`, positions and any trades page can be partial even with a
`200` response. Retry the whole positions read while `unavailable_exchanges`
includes `AGARA`. Check that field on every trades page; if a page includes
`AGARA`, discard the entire accumulated walk and restart without a cursor. For
a multi-exchange seed, require the array to be empty on every page.

If another `sequence_reset` arrives while rebuilding an orderbook or best
quote, discard that attempt and start the channel recovery again.

A live account frame can arrive before REST reflects it. For a fill, first wait
until its `fill_id` appears through the order's trades endpoint, then wait until
the order endpoint reflects all listed fills or a later terminal transition.
For another order transition, wait until the order endpoint reflects that
event or a later state. Only then rebuild open orders and inventory. Follow the
full [account reconnect procedure](/docs/account-stream#reconnect-strategy).

If you see `sequence_reset` repeatedly under steady load, it's almost
always slow read on your side — profile your read loop before
assuming a server-side fault.

### `error` [#error]

An error frame contains a nested typed failure and a required socket action:

```json
{
  "op": "error",
  "failure": {
    "code": "stream_subject_unavailable",
    "title": "Stream subject temporarily unavailable",
    "detail": "This stream subject is temporarily unavailable.",
    "recovery": {
      "strategy": "none"
    }
  },
  "action": "resubscribe",
  "channel": "orderbook",
  "token_id": "21742…36455"
}
```

Switch on `failure.code`, not `title` or `detail`. `failure.detail` is optional
and is omitted when no safe public detail exists; clients must not require it.
The optional subject fields identify the affected subscription. Unlike an HTTP
Problem Details body, a WebSocket failure has no `status`, `type`, or
`request_id`.

Two independent instructions are present:

* `failure.recovery.strategy` tells you how to correct the underlying failure.
* `action` tells you whether to keep the socket, resubscribe one subject, or
  reconnect the entire socket.

For a recognized code, act on `action` first. If the code is unknown, do not
honor either automatic instruction, even when `action` or `strategy` is a value
your client recognizes. Pause the affected work, log the frame, reconcile
state, and update your client. An actual socket close remains authoritative.
For a recognized code, `action: "none"` does not mean the socket stays open:
protocol and authentication failures can still carry a close code.

| Code                            | Stream  | Recovery                 | Action        |  Close | What to do                                                                                                    |
| ------------------------------- | ------- | ------------------------ | ------------- | -----: | ------------------------------------------------------------------------------------------------------------- |
| `dependency_unavailable`        | both    | `retry`                  | `reconnect`   | `1013` | Back off, reconnect, resubscribe, and reseed                                                                  |
| `forbidden_scope`               | account | `none`                   | `none`        | `1008` | Create a PAT with `account:stream`                                                                            |
| `identity_token_expired`        | account | `refresh_identity_token` | `reconnect`   | `4001` | Refresh once, then reconnect and reseed                                                                       |
| `internal_error`                | both    | `none`                   | `reconnect`   | `1011` | Reconnect and reseed; retain diagnostics from surrounding HTTP requests                                       |
| `invalid_identity_token`        | account | `none`                   | `none`        | `1008` | Replace the identity token before opening a new socket                                                        |
| `invalid_message`               | both    | `none`                   | `none`        | `1008` | Correct the operation, channel, or field values before reconnecting                                           |
| `invalid_personal_access_token` | account | `none`                   | `none`        | `1008` | Replace the PAT before opening a new socket                                                                   |
| `malformed_websocket_message`   | both    | `none`                   | `none`        | `1007` | Send valid JSON text on a new socket                                                                          |
| `market_closed`                 | market  | `none`                   | `none`        |      — | Stop quoting that market; other subscriptions remain available                                                |
| `server_restarting`             | both    | `retry`                  | `reconnect`   | `1012` | Reconnect after a short backoff, then reseed                                                                  |
| `slow_consumer`                 | both    | `none`                   | `reconnect`   | `4008` | Make the read loop drain frames promptly before reconnecting                                                  |
| `stream_subject_not_found`      | market  | `none`                   | `none`        |      — | Correct the outcome token; the socket stays open                                                              |
| `stream_subject_unavailable`    | market  | `none`                   | `resubscribe` |      — | Resubscribe that subject, then follow its channel-specific recovery above; not every channel sends a baseline |
| `subscription_limit_exceeded`   | market  | `none`                   | `none`        |      — | Remove subscriptions or open another connection                                                               |
| `unsupported_websocket_data`    | both    | `none`                   | `none`        | `1003` | Send JSON text, not a binary application frame                                                                |
| `wallet_setup_incomplete`       | account | `none`                   | `none`        | `1008` | Complete AGARA wallet setup before opening a new socket                                                       |
| `websocket_message_too_large`   | both    | `none`                   | `none`        | `1009` | Keep each frame and message at or below 64 KiB                                                                |
| `websocket_protocol_error`      | both    | `none`                   | `none`        | `1002` | Correct the WebSocket protocol violation before reconnecting                                                  |

#### Knowing a feed stopped [#knowing-a-feed-stopped]

A subscription normally goes quiet only because the market itself is
quiet — no new orders, no trades. **You can't tell a quiet market from
a stopped feed by silence alone**, so never infer health from the gap
between updates. The `heartbeat` confirms the *connection* is alive; it
does not promise every subscription is still flowing.

Instead, rely on the frames above — the server tells you when a feed
actually stops:

| You receive                               | Scope            | Subscription still live? | What you do                                                                                                            |
| ----------------------------------------- | ---------------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------- |
| `sequence_reset`                          | one subject      | yes                      | discard local state and follow the channel recovery table above                                                        |
| `error` · `action: "resubscribe"`         | one subject      | no                       | resubscribe that subject, then follow its channel-specific recovery above; do not assume the next update is a baseline |
| `error` · `failure.code: "market_closed"` | one subject      | no                       | stop subscribing to that subject; the market has finished                                                              |
| `error` · `action: "reconnect"`           | whole connection | no                       | reopen the socket, resubscribe everything                                                                              |

As a backstop against a connection that stops delivering frames without
closing, keep your own staleness timer:
send a `ping` on an interval and, if no frame of any kind arrives within
a short window, reconnect. See [Reconnect strategy](#reconnect-strategy).

If the market-data feed goes down entirely, every connection receives
`error` with `action: "reconnect"` (`failure.code:
"dependency_unavailable"`) and closes with `1013`; new connections are
refused with `503` until it recovers.
There's nothing to do differently — your normal reconnect-with-backoff
loop is the recovery path: keep retrying and the handshake succeeds
once the feed is back. A brief blip won't trigger this; you'll just get
a `sequence_reset` and the feed continues. A transport failure can close the
socket without an error frame; treat that as reconnect-and-reseed.

## Channel payloads [#channel-payloads]

### `orderbook` [#orderbook]

The first `update` after subscribe is a snapshot:

```json
{ "kind": "snapshot",
  "bids": [[60, 10000], [59, 5000]],
  "asks": [[62, 8000], [63, 4000]],
  "tick_size": 1,
  "price_scale": 100,
  "size_scale": 100 }
```

Current v1 sends another full snapshot after each book change. Replace the
entire local ladder on every snapshot. The wire contract also supports a
`delta`; if one is received, apply its changed levels instead of replacing the
book:

```json
{ "kind": "delta",
  "bids": [[60, 12000]],
  "asks": [[62, 0]],
  "price_scale": 100,
  "size_scale": 100 }
```

Each level is `[price, size]` as integers in engine units. Divide
`price` by `price_scale` for collateral units and `size` by `size_scale` for
shares (`60 / 100 = 0.60`, `12000 / 100 = 120` shares); the scales
travel on every frame. `tick_size` is an integer in the same units as
`price` — divide it by `price_scale` too (`1 / 100 = 0.01`). Size `0`
removes the level in a delta. `kind` distinguishes the two shapes — route on
it for every frame rather than assuming later updates are deltas.

### `best_quote` [#best_quote]

```json
{ "bid": { "price": 60, "size": 12000 },
  "ask": { "price": 62, "size": 8000 },
  "price_scale": 100,
  "size_scale": 100 }
```

`price`/`size` are integers in engine units — apply `price_scale` /
`size_scale` the same way as orderbook levels. Either side is
`null` when that side of the book is empty, and both are `null` when the whole
book is. Frames are emitted whenever the top of the book moves, the move to an
empty book included. You receive no frame at all only before the exchange has
reported this market's book for the first time.

### `trades` [#trades]

```json
{ "kind": "trade",
  "fill_id": "42",
  "taker_token_id": "21742633...",
  "maker_token_id": "21742633...",
  "side": "BUY",
  "price": 60,
  "size": 12000,
  "price_scale": 100,
  "size_scale": 100,
  "settlement_mode": "NORMAL" }
```

One frame per executed trade across both outcomes of the binary market —
route on `taker_token_id` if you only care about a specific side.
Fields:

| Field                               | Meaning                                                                                                                                                                                                                                                                            |
| ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `fill_id`                           | Stable identifier for the trade                                                                                                                                                                                                                                                    |
| `taker_token_id` / `maker_token_id` | Outcome tokens involved on each side. Equal on `"NORMAL"` trades; different on `"MINT"` / `"MERGE"` (where one side bought / sold YES and the other NO via the collateral pair)                                                                                                    |
| `side`                              | `"BUY"` or `"SELL"` — the taker's side; the resting maker did the opposite on `"NORMAL"` trades                                                                                                                                                                                    |
| `price`                             | The **taker's** price per share for `taker_token_id`, in engine units. Divide by `price_scale` to get collateral units (`60 / 100 = 0.60`). See the note below for `"MINT"` / `"MERGE"` trades                                                                                     |
| `size`                              | Trade size in engine units. Divide by `size_scale` to get shares                                                                                                                                                                                                                   |
| `price_scale` / `size_scale`        | Self-describing scales — they travel with the event so old clients don't need an out-of-band lookup                                                                                                                                                                                |
| `settlement_mode`                   | `"NORMAL"` for a regular book crossing; `"MINT"` when paired buys of opposite outcomes minted new shares; `"MERGE"` when paired sells of opposite outcomes burned shares back to cash. Tape / chart UIs typically filter to `"NORMAL"`; volume aggregators usually want all three. |

The tape is always **taker-perspective**: `price` is what the taker
paid (or received) for `taker_token_id`.

* On `"NORMAL"` trades both sides trade the same token at the same
  price, so `price` is simply the execution price.
* On `"MINT"` / `"MERGE"` trades the taker and maker hold *opposite*
  outcomes (YES vs NO). The price you'd compute from the maker's side
  is the complement — if the maker's NO traded at `0.40`, the taker's
  YES `price` is `0.60`. They sum to one collateral unit, the value of a YES+NO
  pair. So `price` for these trades is `1 − maker_price`; the tape
  reports the taker's number directly so you never have to do that
  subtraction yourself.

No user identifiers are emitted. For per-account fill events with
`order_id`, fees, and your role (taker vs maker), use the
[account stream](/docs/account-stream).

#### How YES and NO match [#how-yes-and-no-match]

In a binary market the YES and NO order books are linked, so a trade
can print even when no one is on the other side of *your* book:

* A **BUY YES** and a **BUY NO** match when their prices sum to at
  least one collateral unit — together they fund a full YES+NO pair, which the
  market mints into the two new positions (`settlement_mode: "MINT"`).
* A **SELL YES** and a **SELL NO** match the same way in reverse,
  burning a pair back to cash (`settlement_mode: "MERGE"`).

Orders match at the best effective price across both outcomes, with older
resting orders first at the same effective price. Orderbook snapshots include
complementary liquidity at `1 − its quoted price`, with quantities at the same
price added together. See [Cross-orderbook matching](/docs/cross-orderbook-matching)
for worked examples.

This matching is enabled when the market's `cross_match_enabled` flag is set
(see [`market_status`](#market_status)). When it is off, the two books trade
independently, snapshots contain only the requested outcome's orders, and
trades use `"NORMAL"` settlement.

### `market_status` [#market_status]

Choose the subscription scope for the updates you need. You can subscribe to
both scopes as separate entries on the same connection.

| What you need                                             | Subscribe with | Updates                                                                                              |
| --------------------------------------------------------- | -------------- | ---------------------------------------------------------------------------------------------------- |
| Status of a market you already know                       | `condition_id` | Creation, halt, resume, proposal, resolution, and policy changes                                     |
| Newly active cycles and resolved-market data for an event | `event_id`     | `current_market_changed` and `market_resolution_completed`, including market and replacement details |

Get these IDs from [Markets](/docs/markets#follow-newly-active-and-resolved-markets).
There is no separate new-market channel or subscription to all markets. Use the
REST event listing to discover new events, then subscribe to the ones you follow.

#### Condition-scoped status updates [#condition-scoped-status-updates]

Subscribe with `condition_id` to follow a specific market:

```json
{
  "op": "subscribe",
  "channels": [{ "name": "market_status", "condition_id": "0x2174…" }]
}
```

For example, a resolution arrives in this envelope:

```json
{
  "op": "update",
  "channel": "market_status",
  "condition_id": "0x2174…",
  "sequence": 142,
  "data": { "kind": "market_resolved", "winning_token_id": "21742633…" }
}
```

| `kind`                | Carries                                                                                                                   |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `market_created`      | `num_outcomes` (currently `2`), `tick_size`, `price_scale`, `size_scale`, `min_price`, `max_price`, `cross_match_enabled` |
| `market_halted`       | —                                                                                                                         |
| `market_resumed`      | —                                                                                                                         |
| `outcome_proposed`    | `proposed_token_id`                                                                                                       |
| `market_resolved`     | `winning_token_id`                                                                                                        |
| `payouts_proposed`    | `payouts`: each outcome's `token_id` and `payout_micro`                                                                   |
| `payouts_resolved`    | `payouts`, as for `payouts_proposed`                                                                                      |
| `fee_policy_updated`  | —; future fills can use the updated policy                                                                                |
| `cross_match_toggled` | `enabled`                                                                                                                 |

A market can resolve to a split instead of a single winner, where each outcome
pays its own price per share. A split always arrives as `payouts_proposed` and
`payouts_resolved`, never as `outcome_proposed` or `market_resolved` with an
empty token, so route on `kind`. Each `payout_micro` is what one share of that
outcome pays, as a decimal string of micro-collateral, and the two add up to
`"1000000"`. Match each entry to its outcome by `token_id`:

```json
{
  "op": "update",
  "channel": "market_status",
  "condition_id": "0x2174…",
  "sequence": 142,
  "data": {
    "kind": "payouts_resolved",
    "payouts": [
      { "token_id": "21742633…", "payout_micro": "370000" },
      { "token_id": "12345678…", "payout_micro": "630000" }
    ]
  }
}
```

Stop placing orders on a halted or resolved market and reconcile your open
orders through the [account stream](/docs/account-stream) and REST.
`market_resolved` and `payouts_resolved` are final and end trading. A proposal
(`outcome_proposed` or `payouts_proposed`) is not final: a later proposal can
replace it before the market resolves. Before resuming after `market_resumed`,
refresh the market and complete your [quote-readiness checks](/docs/api-traders/recovery#quote-readiness-state-machine).
`market_created` only describes the subscribed condition; it does not discover
other markets or announce every new active cycle.

#### Event-scoped lifecycle updates [#event-scoped-lifecycle-updates]

Subscribe with exactly one `event_id` instead of `condition_id` to follow an
event's current market. Multiple event subscriptions are allowed; neither an
unscoped subscription nor a subscription containing both IDs is accepted.
This subscription does not discover new events and does not include the
condition-scoped events above.

```json
{
  "op": "subscribe",
  "channels": [{ "name": "market_status", "event_id": "10000000-0000-4000-8000-000000000001" }]
}
```

Acknowledgements, subscription lists, updates, scoped errors and resets identify
this subscription with `event_id`. The first update is the next change after
subscription; no initial snapshot or replay is sent.

These notifications are best effort: changes can be missed or arrive out of order.
Refresh the event through REST when you need to confirm its current state.
`market_resolution_completed` does not confirm that onchain payout reporting has finished.

| `data.kind`                   | When received                                                     | Data                                                                                                                                                                                                     |
| ----------------------------- | ----------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `current_market_changed`      | A recurring cycle opens and becomes the event's current market    | `changed_at`, `main_market_id`, `market`                                                                                                                                                                 |
| `market_resolution_completed` | A market's resolved state and its winner or split have been saved | `resolved_at`, `resolved_market_id`, `resolved_condition_id`, `winning_token_id`, `main_market_id`, `replacement_status`, `replacement`; also `payouts` for a split and `expected_cycle` when applicable |

A newly active cycle arrives with its condition ID and outcome token IDs:

```json
{
  "op": "update",
  "channel": "market_status",
  "event_id": "10000000-0000-4000-8000-000000000001",
  "data": {
    "kind": "current_market_changed",
    "changed_at": "2026-09-14T05:00:00Z",
    "main_market_id": "20000000-0000-4000-8000-000000000001",
    "market": {
      "market_id": "20000000-0000-4000-8000-000000000001",
      "condition_id": "0x2174…",
      "state": "ACTIVE",
      "reference_price": "78221.016",
      "currency": "USD",
      "reference_at": "2026-09-14T05:00:00Z",
      "observe_at": "2026-09-14T05:05:00Z",
      "outcomes": [
        { "token_id": "21742633…", "kind": "yes", "label": "Up", "short_label": null },
        { "token_id": "12345678…", "kind": "no", "label": "Down", "short_label": null }
      ]
    }
  }
}
```

A resolution identifies the completed market and winning token. For example,
when the next cycle is expected but its market does not exist yet:

```json
{
  "op": "update",
  "channel": "market_status",
  "event_id": "10000000-0000-4000-8000-000000000001",
  "data": {
    "kind": "market_resolution_completed",
    "resolved_at": "2026-09-14T05:05:01Z",
    "resolved_market_id": "20000000-0000-4000-8000-000000000001",
    "resolved_condition_id": "0x2174…",
    "winning_token_id": "21742633…",
    "main_market_id": "20000000-0000-4000-8000-000000000001",
    "replacement_status": "expected",
    "replacement": null,
    "expected_cycle": {
      "reference_at": "2026-09-14T05:05:00Z",
      "observe_at": "2026-09-14T05:10:00Z"
    }
  }
}
```

A split resolution sets `winning_token_id` to an empty string and adds
`payouts`, the same list of `token_id` and `payout_micro` entries that
`payouts_resolved` carries. A single-winner resolution has no `payouts` field,
so check for it before reading `winning_token_id` as the winner.

`market` and a non-null `replacement` use exactly the same shape.
`main_market_id` stays outside this object: it is the event's saved pointer,
whereas `market_id` identifies the described market. On a completed resolution,
the pointer can still refer to the resolved market even when there is no replacement.
`reference_price` is null before the opening price is known. `currency`,
`short_label`, and cycle times can be null when not applicable.
An unclassified outcome has `kind: "unknown"`; use its label for display.

| `replacement_status` | Meaning                                                                                                                                              |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `active`             | `replacement` is the event's current open cycle                                                                                                      |
| `upcoming`           | `replacement` is a prepared cycle which has not opened yet                                                                                           |
| `expected`           | No replacement market exists yet, but another cycle is expected; `replacement` is null and `expected_cycle` contains `reference_at` and `observe_at` |
| `none`               | No eligible replacement or expected cycle is currently known; `replacement` is null                                                                  |

Use `current_market_changed.market` or an `active` replacement to identify the
new market to follow. Subscribe separately to its `condition_id` for status and
its outcome `token_id` values for orderbook or quote updates, then complete
[quote-readiness checks](/docs/api-traders/recovery#quote-readiness-state-machine)
before placing orders. An `upcoming` replacement is not open for trading, and
an `expected` cycle has no new market IDs to subscribe to yet.

Use full timestamps when comparing cycles across trading days. A next-day opening
is not an eight-second gap simply because its clock time matches today's opening.
These messages do not instruct a client to remove a card or change subscriptions.
Use the replacement's outcome token IDs for any later `best_quote` subscriptions.

On connect, reconnect, or an event-scoped `sequence_reset`, buffer new messages
and refresh the event through REST. These updates are live-only, so a gap cannot
be replayed. `sequence_reset` carries `event_id` and the usual `reason`, but no
sequence counter. Do not let an older market's resolution replace a newer
current market you've already received. If REST and buffered updates disagree,
refresh again before treating the state as current.

## Reconnect strategy [#reconnect-strategy]

Reconnect automatically if this socket closes unexpectedly. Trade service
connections may disconnect periodically and without prior notice, even if your
client has not requested a disconnect. Do not wait for an error frame before
reconnecting. See [Reconnecting streams](/docs/conventions#reconnecting-streams)
for guidance covering both the Trade and API services.

Recommended client loop:

1. Pause quote placement for every affected subject.
2. Reconnect with exponential backoff capped at a few seconds. A `503`
   on the handshake means the feed is temporarily unavailable — keep
   backing off and retrying; it'll accept once the feed is back.
3. Resend your `subscribe` set and begin buffering immediately.
4. Trust nothing locally until each channel has fresh state. Wait for the
   initial orderbook `snapshot` so the subject is active, then keep buffering while you
   fetch the REST orderbook. Parse REST `hash` as the baseline sequence,
   discard buffered frames at or below it, and route later frames on
   `data.kind`. The initial WebSocket snapshot alone is not a readiness fence.
   `best_quote` sends the current quote, with both sides `null` on an empty book.
   Public trades are not replayable.
   For `market_status`, keep buffering every lifecycle frame, read the market
   through REST, then apply the entire ordered buffer, including frames from
   before and during the read. If the latest lifecycle frame and REST
   disagree, stay paused and poll until REST catches up. Never let a stale
   REST response override a later halt, outcome proposal, or resolution.
   Because this live-only channel has no snapshot, replay, or subscription-time
   baseline marker, reconnecting cannot prove that no lifecycle event was
   missed. Keep your strategy paused until you review and accept the current
   REST state, or your documented risk policy permits resuming from it.
5. Reconcile the public state with the separately reseeded
   [account stream](/docs/account-stream#reconnect-strategy), then resume
   quoting only when both are current.
6. On `sequence_reset` mid-stream, return to the pause-and-rebuild path and
   follow the
   [`sequence_reset` recovery table](#sequence_reset). On an `error`
   with an `action`, follow [`error`](#error).

Run a staleness timer alongside this. Send a `ping` on an interval
(every \~25 s is plenty) and track when you last received *any* frame.
If nothing arrives within a slightly longer window (\~35 s), reconnect even
though you saw no error.

After the REST-watermark seed, the live WebSocket stream carries low-latency
book updates without continuous polling. Keep the
[orderbook REST endpoint](/docs/orderbook) in your startup, reconnect, and
reset recovery path; it is also suitable for one-shot snapshots and HTTP-only
clients.
