Agara

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 — same protocol, authed.

To follow halts, resumptions, and resolution for a market, use market_status by condition. To receive newly active markets and resolved-market data for a recurring event, use market_status by event on this same socket.

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.

The HTTP upgrade can fail before a socket exists:

HTTP responseWhat to do
401 typed authentication failureRemove or correct an invalid Authorization header; the public stream itself needs no token
426 websocket_upgrade_requiredSend a valid WebSocket upgrade request
429 rate_limitedWait for Retry-After, then reconnect
503 dependency_unavailableBack off with jitter and reconnect

These are HTTP Problem Details, not WebSocket frames.

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:

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())
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

Use this public socket for book, quote, trade, and market-state updates. Open a second connection to the 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.

Channels

ChannelSubjectWhat you get
orderbooktoken_idFull-depth book updates. Current v1 sends a replacement snapshot for every book change; clients must still route on data.kind
best_quotetoken_idTop-of-book bid/ask, emitted only on real moves
tradescondition_idOne frame per executed trade across all outcomes of the market
market_statusEither condition_id or event_idMarket 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

All client frames have an op. A protocol error can close the connection; use the action field and close code in the error contract.

subscribe

{ "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:

{
  "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

Mirror the exact subscription shape:

{
  "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

{ "op": "ping" }
{ "op": "list" }

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

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

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.

opWhen
subscribedAck for a subscribe entry
unsubscribedAck for an unsubscribe entry
updateOne event for a channel you subscribed to
errorA request, subscription, or connection failed. Carries an action telling you what to do; reconnect errors close the connection (see error)
sequence_resetYou missed messages on this channel — discard local state and follow the recovery table
pongReply to your ping
heartbeatServer-pushed every ~10 s
subscription_listReply to your list

update

{
  "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 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

{
  "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:

ChannelWhat the next update will beWhat you should do
orderbookCurrent v1 sends a replacement snapshot; clients must still route on data.kindDiscard 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_quoteThe current top-of-book, with both sides null when the book is emptyDiscard 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
tradesThe next trade whenever it happens — public trades during the gap are lostResume from the next trade; the API does not currently replay the public tape
market_status by conditionThe next lifecycle event whenever it happens — events during the gap are lostKeep 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 eventThe next current-market change or completed resolution — no replayBuffer 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_eventsThe next account event whenever it happens — events during the gap are lostBuffer 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.

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

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

{
  "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.

CodeStreamRecoveryActionCloseWhat to do
dependency_unavailablebothretryreconnect1013Back off, reconnect, resubscribe, and reseed
forbidden_scopeaccountnonenone1008Create a PAT with account:stream
identity_token_expiredaccountrefresh_identity_tokenreconnect4001Refresh once, then reconnect and reseed
internal_errorbothnonereconnect1011Reconnect and reseed; retain diagnostics from surrounding HTTP requests
invalid_identity_tokenaccountnonenone1008Replace the identity token before opening a new socket
invalid_messagebothnonenone1008Correct the operation, channel, or field values before reconnecting
invalid_personal_access_tokenaccountnonenone1008Replace the PAT before opening a new socket
malformed_websocket_messagebothnonenone1007Send valid JSON text on a new socket
market_closedmarketnonenone—Stop quoting that market; other subscriptions remain available
server_restartingbothretryreconnect1012Reconnect after a short backoff, then reseed
slow_consumerbothnonereconnect4008Make the read loop drain frames promptly before reconnecting
stream_subject_not_foundmarketnonenone—Correct the outcome token; the socket stays open
stream_subject_unavailablemarketnoneresubscribe—Resubscribe that subject, then follow its channel-specific recovery above; not every channel sends a baseline
subscription_limit_exceededmarketnonenone—Remove subscriptions or open another connection
unsupported_websocket_databothnonenone1003Send JSON text, not a binary application frame
wallet_setup_incompleteaccountnonenone1008Complete AGARA wallet setup before opening a new socket
websocket_message_too_largebothnonenone1009Keep each frame and message at or below 64 KiB
websocket_protocol_errorbothnonenone1002Correct the WebSocket protocol violation before reconnecting

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 receiveScopeSubscription still live?What you do
sequence_resetone subjectyesdiscard local state and follow the channel recovery table above
error · action: "resubscribe"one subjectnoresubscribe that subject, then follow its channel-specific recovery above; do not assume the next update is a baseline
error · failure.code: "market_closed"one subjectnostop subscribing to that subject; the market has finished
error · action: "reconnect"whole connectionnoreopen 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.

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

orderbook

The first update after subscribe is a snapshot:

{ "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:

{ "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

{ "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

{ "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:

FieldMeaning
fill_idStable identifier for the trade
taker_token_id / maker_token_idOutcome 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
priceThe 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
sizeTrade size in engine units. Divide by size_scale to get shares
price_scale / size_scaleSelf-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.

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 for worked examples.

This matching is enabled when the market's cross_match_enabled flag is set (see 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

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

What you needSubscribe withUpdates
Status of a market you already knowcondition_idCreation, halt, resume, proposal, resolution, and policy changes
Newly active cycles and resolved-market data for an eventevent_idcurrent_market_changed and market_resolution_completed, including market and replacement details

Get these IDs from 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

Subscribe with condition_id to follow a specific market:

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

For example, a resolution arrives in this envelope:

{
  "op": "update",
  "channel": "market_status",
  "condition_id": "0x2174…",
  "sequence": 142,
  "data": { "kind": "market_resolved", "winning_token_id": "21742633…" }
}
kindCarries
market_creatednum_outcomes (currently 2), tick_size, price_scale, size_scale, min_price, max_price, cross_match_enabled
market_halted—
market_resumed—
outcome_proposedproposed_token_id
market_resolvedwinning_token_id
payouts_proposedpayouts: each outcome's token_id and payout_micro
payouts_resolvedpayouts, as for payouts_proposed
fee_policy_updated—; future fills can use the updated policy
cross_match_toggledenabled

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:

{
  "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 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. market_created only describes the subscribed condition; it does not discover other markets or announce every new active cycle.

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.

{
  "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.kindWhen receivedData
current_market_changedA recurring cycle opens and becomes the event's current marketchanged_at, main_market_id, market
market_resolution_completedA market's resolved state and its winner or split have been savedresolved_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:

{
  "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:

{
  "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_statusMeaning
activereplacement is the event's current open cycle
upcomingreplacement is a prepared cycle which has not opened yet
expectedNo replacement market exists yet, but another cycle is expected; replacement is null and expected_cycle contains reference_at and observe_at
noneNo 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 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 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 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, 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. On an error with an action, follow 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 in your startup, reconnect, and reset recovery path; it is also suitable for one-shot snapshots and HTTP-only clients.