Streaming
Real-time market data over WebSocket. Subscribe to orderbook depth, top-of-book quotes, the public trades tape, and market lifecycle events.
WebSocket origin: wss://app.agara.xyz — connect to
/trade/v1/market-stream.
Public — no token required. Subscribe to one or more channels per connection; you receive JSON frames as the market moves. A single connection can carry many subscriptions across many markets.
For per-user order, fill, position, redemption, and collateral events, see the account stream — same protocol, authed.
Connect
Any WebSocket client works. The endpoint speaks JSON text frames; no subprotocol negotiation is required.
Connect with any WebSocket library and send JSON frames:
Channels
| Channel | Subject | What you get |
|---|---|---|
orderbook | token_id | Full book snapshot, then deltas |
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 | condition_id | Halt / resume / resolve / fee-policy / cross-match events |
All channels require a specific subject id. There is no firehose.
Client → server
All client frames have an op. Unknown ops return an error frame —
the connection stays open.
subscribe
Each entry gets its own subscribed ack. Re-subscribing to a channel
you already have is a no-op (just a fresh ack).
unsubscribe
Mirror shape — the server stops sending and replies with unsubscribed.
ping and list
ping returns { "op": "pong", "server_time": "..." }. list
returns { "op": "subscription_list", "channels": [...] } so you
can audit what the connection has open.
Server → client
Every server frame has an op. Subject-bearing frames carry exactly
one of token_id or condition_id, never both — 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
For orderbook, the first update carries a snapshot. best_quote sends the
current quote when at least one side exists; an entirely empty book stays
quiet until a quote appears. For trades and market_status, the first
update is the next event after subscribe — there is no replay.
sequence
sequence on every update is a globally monotonic event counter —
it tells you the absolute order in which events happened across the
whole exchange.
Within a single (channel, subject) pair, frames you receive are
strictly increasing: next.sequence > last.sequence, except when a
sequence_reset interrupts. After a reset the counter resumes from
whatever the next event carries, still increasing from there. A
backwards step or equality inside one subject is a bug — please file
an issue.
Across channels or subjects, sequence still tells you the true
ordering, but the order frames arrive on your WebSocket is not
guaranteed to match it. If you need a strict cross-channel timeline,
buffer locally and sort by sequence.
The counter is gappy within a channel: an orderbook subscriber sees
1000, 1003, 1007, … because the gaps are events on other channels.
sequence_reset
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 restarted. 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 | The next frame may be a delta after client lag | Discard your local book; fetch the REST snapshot or unsubscribe and resubscribe, then ignore deltas until a snapshot arrives |
best_quote | The current top-of-book when either side is non-empty; otherwise no frame until a quote appears | Discard your local quote; fetch the REST orderbook if you must distinguish an empty book from a delayed update |
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 | The next lifecycle event whenever it happens — events during the gap are lost | Refetch market state via REST if it matters |
account_events | The next account event whenever it happens — events during the gap are lost | Buffer new events, then reseed with POST /trade/v1/portfolio/open-orders/list and POST /trade/v1/portfolio/positions/list; reconcile by stable IDs before resuming live processing |
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
Something went wrong. The connection normally stays open, but an error with
action: "reconnect" is followed by closure. Every error
carries a code and a human-readable message, and — when there's a
recovery you can act on — an action.
action tells you what to do:
"resubscribe"— one subscription stopped, but the connection is healthy and your other subscriptions keep flowing. Discard any local state for that subject and send a freshsubscribefor the same channel + subject; the nextupdateis a new baseline. You'll see this assubject_unavailableif a feed ends mid-stream."reconnect"— this connection can't serve you any further. Close the socket and open a new one, then resubscribe. Two cases: the credential on an account stream was rejected (the token is tied to the subscription, so a fresh one means a fresh connection — refresh it before reconnecting); or the market-data feed is temporarily unavailable (code: "feed_unavailable"), in which case the handshake will return503until it's back, so reconnect on a backoff and let it retry.- no
actionfield — retrying the same request won't help. Fix the request or stop that subscription. These carry acodeyou can switch on:unknown_token(no such market),terminal_market(stop subscribing because the market has finished),too_many_subscriptions(you hit the per-connection cap),invalid_message(malformed frame), orwrong_endpoint(that channel belongs on the other stream).
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, rebuild from the next update |
error · code: "terminal_market" | 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" (code: "feed_unavailable") and is
closed, and 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. (In rarer cases we may close
the connection with no frame at all — treat it the same.)
Channel payloads
orderbook
First update after subscribe is a snapshot:
Subsequent frames are deltas (no tick_size):
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. kind distinguishes the two shapes — route on it.
best_quote
price/size are integers in engine units — apply price_scale /
size_scale the same way as orderbook levels. Either side can be
null if that side of the book is empty. Frames are emitted only when
the top-of-book actually moves.
trades
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").
The engine always fills against the same-outcome book first and only
crosses to the complementary book if size remains. This cross-book
matching is on only when the market's cross_match_enabled flag is
set (see market_status); when it's off, the two
books trade independently and you'll only ever see "NORMAL" trades.
market_status
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 |
fee_policy_updated | —; future fills can use the updated policy |
cross_match_toggled | enabled |
Reconnect strategy
Connections drop — handle them. Recommended client loop:
- 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. - Trust nothing locally until each channel has fresh state. The first
orderbookupdate is a snapshot.best_quotesends the current quote when at least one side is non-empty. Fortradesandmarket_status, the first update is the next event. Public trades are not replayable; refetch the event detail if you need current market status. - On
sequence_resetmid-stream, 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.
A live WebSocket update stream replaces the polling pattern from the orderbook REST endpoint for any latency-sensitive strategy. The REST endpoint still works for one-shot snapshots and HTTP-only clients.