Account stream
Real-time per-user events over WebSocket. Receive order, fill, position, redemption, and collateral changes as they happen.
WebSocket origin: wss://app.agara.xyz — connect to
/trade/v1/account-stream.
Authed: bearer token required. Subscribe once per connection; the
server pushes typed JSON order, fill, position, redemption, and collateral
events. Use it as a low-latency supplement to
/trade/v1/portfolio/*, which remains the authoritative
source for your current account state and recovery.
This endpoint is the private counterpart to the public market stream. The protocol shape is identical (subscribe / update / heartbeat / sequence_reset); the differences are the URL, the auth requirement, and the event payloads.
Authentication
Browsers can't set Authorization on a WebSocket handshake, so the
bearer travels inside the subscribe payload. Both token types work:
- A personal access token carrying the
account:streamscope (create one). - A Privy session JWT (browser app surface; identity tokens granted by the in-app Privy login flow).
If the token is missing, invalid, lacks the scope, or the caller has
no agara wallet, the server replies with one error frame describing
the failure and the subscribe attempt does not register.
Connect
The endpoint speaks JSON text frames. No subprotocol negotiation.
Connect with any WebSocket library and send the JSON subscription frame:
Channels
| Channel | Subject | What you get |
|---|---|---|
account_events | The token's owner | Order and fill updates, position changes, redemptions, and collateral balance changes |
Only one subscription is meaningful per connection: the token already pins the channel to one user. Re-subscribing with a new token (JWT rotation) registers an additional subscriber under the new identity; close and reconnect on rotation if you want a clean swap.
Client → server
Same op vocabulary as the public stream:
The token replays verbatim on unsubscribe so the server can match
the entry. ping, list behave as on the public stream.
Server → client
Same frame envelope as the public stream:
op | When |
|---|---|
subscribed | Auth succeeded; events will start arriving |
unsubscribed | Ack for an unsubscribe entry |
update | One account event |
error | Auth failure, scope failure, or a stopped feed. Carries an action when there's a recovery (see below) |
sequence_reset | You missed messages; reseed via REST, then keep listening. Carries a reason (lagged / stream_reset), same as the public stream |
pong | Reply to your ping |
heartbeat | Server-pushed every ~10 s |
subscription_list | Reply to your list request |
Like the public stream, error frames carry an action telling you
how to recover (the public stream docs cover
the full set). The cases specific to this stream:
code | action | Meaning |
|---|---|---|
unauthorized | reconnect | Token invalid, expired, or revoked. Reopen the socket with a fresh credential; the token is part of the subscription |
unauthorized | (none) | You have no agara wallet yet. Complete onboarding before subscribing; reconnecting won't help |
forbidden | (none) | PAT does not carry account:stream. Terminal; fix the token's scopes |
internal_error | resubscribe | Transient internal fault during auth. Resubscribe to retry |
subject_unavailable | resubscribe | Your event feed stopped mid-stream. Resubscribe and buffer with the same token, then reseed via REST |
feed_unavailable | reconnect | The feed is temporarily unavailable; the connection closes and the handshake returns 503 until it recovers. Reconnect on a backoff; it accepts once the feed is back |
update
Events with a positive sequence share a globally monotonic counter and arrive
in sequence order. An order_rejected notice can use sequence: 0; dedupe it
by order_id and do not compare its ordering with positive-sequence events.
Sequence values can have gaps within a channel; see the
public stream's sequence rules.
Event payloads
Every event under data carries a kind discriminator. Route on it.
Order events (fill, order_accepted, order_cancelled) also carry
order_hash, the EIP-712 hash of the order. For
signed orders you compute this
hash yourself before you submit, so you can match every event back to
the originating order without waiting on (or ever seeing) the placement
response, which is what makes correlating acks and fills across many
simultaneous submissions reliable. It's always present on fill,
order_accepted, and order_cancelled, alongside the server-assigned
order_id. On order_rejected it's best-effort and
may be absent; correlate on order_id there.
fill
Emitted once per fill leg the caller participates in. A single trade produces two frames in total (one to the taker, one to the maker), delivered to whichever side(s) the caller owns.
| Field | Notes |
|---|---|
role | "TAKER" or "MAKER", your side of the fill |
order_id | Your local order UUID (echoed back by the engine on placement) |
order_hash | The order's EIP-712 hash, always present on order events. For signed orders you know it before submitting, so it's how you correlate the fill back to your submission |
token_id | The outcome token you traded. On NORMAL fills both roles trade the same token; on MINT / MERGE the roles touch different tokens |
side | Your side. On NORMAL fills, maker is the opposite of taker. On MINT both sides are BUY; on MERGE both are SELL |
price | Your leg's execution price in engine units. Divide by price_scale to get collateral units per share (60 / 100 = 0.60) |
size | Filled 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", "MINT", or "MERGE" |
fee_micro | Platform fee charged to your side, as a decimal string of micro-collateral. Taker legs can be charged under an active fee policy in any settlement mode; maker legs report "0" |
order_accepted
Confirms that a new order rested on the book (in whole or in part)
after any immediate fills. original_size is the size you placed and
remaining_size is what's still open after any immediate fills (here
3M of the 8M filled on entry, 5M resting); convert each with
/ size_scale.
order_accepted is emitted only when an order rests on the book, so its wire
tif is "GTC". A REST GTD order also reports "GTC" here because the
engine rests it as GTC while the service tracks and enforces its expiration;
read the REST order row to distinguish them.
order_cancelled
reason | Meaning |
|---|---|
USER | You called DELETE /trade/v1/orders/{id} or cancel-all |
FAK_REMAINDER | FAK order's unfilled remainder swept off the book |
SELF_TRADE_PREVENTION | The maker side was cancelled to prevent self-trade |
MARKET_RESOLVED | The market resolved while the order was resting, so it was swept off the book |
order_rejected
Emitted when an order we accepted (202 / PENDING) is later rejected by the
engine: not enough balance or shares, a post-only order that would cross, or a
FOK that could not fully fill. Other submission failures become FAILED and
must be discovered from the REST order row.
| Field | Notes |
|---|---|
order_id | Your local order UUID. Always present, the universal correlation / dedupe key |
order_hash | The order's EIP-712 hash. Present for any order that reached submission (every balance / shares / post-only rejection); null only when the order failed before it was signed. If you key your local state on the hash, dedupe on it and fall back to order_id on the rare null |
token_id | The outcome token the order traded. Best-effort; may be null |
reason | Free-text explanation of why the order couldn't be placed |
Most balance / shares / post-only rejections are caught synchronously as a
422 on the placement request; this event covers the ones
that slip past that check because your balance or the book moved. Reconcile
against the order row from
GET /trade/v1/orders/{id}, which is the durable record.
tokens_minted and tokens_merged
Emitted when you split collateral into a YES/NO pair (mint) or combine
a pair back into collateral (merge), either through the
positions endpoints or an
account batch. Per-market (touches both
outcomes by construction), so the wire carries condition_id rather than a
single token_id. A cross-match is reported as a fill with
settlement_mode set to MINT or MERGE.
size / size_scale is the per-outcome quantity (mint credits this
much of each outcome; merge burns this much of each).
tokens_redeemed
Emitted when a resolved position clears and any payout is credited to your
collateral balance. A position that holds only losing shares can report
payout_micro: "0". Refetch positions for condition_id and your portfolio
summary when you receive the event. This reports the account change; it does
not confirm that any related on-chain operation has finished.
| Field | Notes |
|---|---|
winning_token_id | The outcome that paid one collateral unit per share |
losing_token_id | The outcome that paid zero |
winning_shares_redeemed | Winning shares removed, as a decimal string in scaled units; divide by size_scale for shares |
losing_shares_zeroed | Losing shares removed, as a decimal string in scaled units; divide by size_scale for shares |
payout_micro | Collateral credited, as a decimal string of micro-collateral |
size_scale | Scale for both share fields |
This event reports the account change, not completion of the process that
initiated it. For automatic redemption, a positive
payout produces a REDEEM portfolio activity. A zero-payout clearance may not,
so the cleared position state is authoritative.
collateral_withdrawn and collateral_deposited
These events report collateral balance changes:
Both monetary fields are decimal strings. amount_micro is the change and
cash_balance_micro is your collateral balance immediately after that change.
collateral_withdrawn means the amount has left your available AGARA
collateral. It does not mean the withdrawal has settled on-chain. For a
batch withdrawal, keep reading
GET /trade/v1/batches/{batch_hash}
until it reaches a final state.
collateral_deposited can be a new collateral credit or compensation for a
withdrawal that did not complete. Refetch your
portfolio summary for the authoritative balance.
Batch provenance
When a position or collateral event belongs to an
account batch, it also carries
batch_hash and batch_index. Both fields are omitted otherwise:
| Field | Notes |
|---|---|
batch_hash | The batch's hash: the digest you signed and the value POST /trade/v1/batches returned |
batch_index | The operation's zero-based position in the batch |
Together they identify the related operation, but they are a correlation pair,
not an event ID. Process every event in sequence order even when you have seen
the pair before. For example, a failed split can produce tokens_minted
followed by tokens_merged with the same pair. A failed withdrawal can produce
collateral_withdrawn followed by collateral_deposited, and recovery can
later produce another effect with that pair. Do not discard a repeated pair as
a duplicate.
Use
GET /trade/v1/batches/{batch_hash}
for the final batch status. The stream reports account changes, not the final
on-chain verdict.
These fields are best-effort delivery on a live stream: if the exchange restarts while your batch is in flight, the events may arrive without them. The batch's status endpoint is always the durable record.
Reconnect strategy
The stream is live-only, with no replay. On reconnect you may have missed events. Recommended client loop:
-
Subscribe to
account_eventsand begin buffering events after thesubscribedacknowledgement. -
Seed via REST while buffering.
POST /trade/v1/portfolio/open-orders/listfor active orders.POST /trade/v1/portfolio/positions/list,GET /trade/v1/portfolio/summary, andGET /trade/v1/portfolio/tradesfor the rest of your current account state.GET /trade/v1/portfolio/activitiesfor durable account history.
Open orders, trades, and activities page with keyset cursors: follow
next_cursorto the end for a complete seed. Positions come back whole in one response. -
Reconcile the buffered events against stable IDs such as
order_idandfill_id, then process new events live. This closes the gap between the REST snapshot and the subscription without applying duplicates. -
On
sequence_reset, buffer new events and redo the seed, then reconcile the buffer before trusting local state. Events that fired during the gap are gone; REST is the authoritative recovery. If you seesequence_resetrepeatedly, your client may be reading too slowly; profile the read loop.
If the feed is temporarily unavailable the handshake returns 503 (and
a live connection is closed with error, action: "reconnect"). Keep
reconnecting on your backoff; the handshake accepts once the feed is
back, and then subscribe, buffer, and seed as above.
The order matters: subscribing and buffering first prevents an event from landing unseen between the REST snapshot and the live feed.