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 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, 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
| 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
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.
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) |
sequence_reset | You missed messages on this channel — discard local state and follow the recovery table |
pong | Reply to your ping |
heartbeat | Server-pushed every ~10 s |
subscription_list | Reply 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:
| 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.
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.strategytells you how to correct the underlying failure.actiontells 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
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.
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:
| 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, sopriceis 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 at0.40, the taker's YESpriceis0.60. They sum to one collateral unit, the value of a YES+NO pair. Sopricefor these trades is1 − 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 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. 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…" }
}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:
{
"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.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:
{
"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_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
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:
- Pause quote placement for every affected subject.
- Reconnect with exponential backoff capped at a few seconds. A
503on the handshake means the feed is temporarily unavailable — keep backing off and retrying; it'll accept once the feed is back. - Resend your
subscribeset and begin buffering immediately. - Trust nothing locally until each channel has fresh state. Wait for the
initial orderbook
snapshotso the subject is active, then keep buffering while you fetch the REST orderbook. Parse RESThashas the baseline sequence, discard buffered frames at or below it, and route later frames ondata.kind. The initial WebSocket snapshot alone is not a readiness fence.best_quotesends the current quote, with both sidesnullon an empty book. Public trades are not replayable. Formarket_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. - Reconcile the public state with the separately reseeded account stream, then resume quoting only when both are current.
- On
sequence_resetmid-stream, return to the pause-and-rebuild path and follow thesequence_resetrecovery table. On anerrorwith anaction, followerror.
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.