Agara

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:stream scope (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.

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 the JSON subscription frame:

import asyncio, json, os, websockets
 
URL   = f"{os.environ['AGARA_WS_URL']}/trade/v1/account-stream"
TOKEN = "agt_…"
 
async def main():
    async with websockets.connect(URL) as ws:
        await ws.send(json.dumps({
            "op": "subscribe",
            "channels": [{ "name": "account_events", "token": TOKEN }],
        }))
        async for raw in ws:
            print(json.loads(raw))
 
asyncio.run(main())
wscat -c "$AGARA_WS_URL/trade/v1/account-stream"
> { "op": "subscribe", "channels": [{ "name": "account_events", "token": "agt_…" }] }

Channels

ChannelSubjectWhat you get
account_eventsThe token's ownerOrder 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:

{ "op": "subscribe", "channels": [
    { "name": "account_events", "token": "agt_…" }
] }
 
{ "op": "unsubscribe", "channels": [
    { "name": "account_events", "token": "agt_…" }
] }
 
{ "op": "ping" }
{ "op": "list" }

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:

opWhen
subscribedAuth succeeded; events will start arriving
unsubscribedAck for an unsubscribe entry
updateOne account event
errorAuth failure, scope failure, or a stopped feed. Carries an action when there's a recovery (see below)
sequence_resetYou missed messages; reseed via REST, then keep listening. Carries a reason (lagged / stream_reset), same as the public stream
pongReply to your ping
heartbeatServer-pushed every ~10 s
subscription_listReply 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:

codeactionMeaning
unauthorizedreconnectToken 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_errorresubscribeTransient internal fault during auth. Resubscribe to retry
subject_unavailableresubscribeYour event feed stopped mid-stream. Resubscribe and buffer with the same token, then reseed via REST
feed_unavailablereconnectThe 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

{
  "op": "update",
  "channel": "account_events",
  "sequence": 18243,
  "data": {
    "kind": "fill",
    ...
  }
}

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.

{
  "kind": "fill",
  "role": "TAKER",
  "fill_id": "8421",
  "order_id": "8c63c5e2-9f6d-4a3b-a8de-1bd0f4d9c14e",
  "order_hash": "0x9f2c...",
  "token_id": "21742633...",
  "side": "BUY",
  "price": 60,
  "size": 10000000,
  "price_scale": 100,
  "size_scale": 1000000,
  "settlement_mode": "NORMAL",
  "fee_micro": "1200"
}
FieldNotes
role"TAKER" or "MAKER", your side of the fill
order_idYour local order UUID (echoed back by the engine on placement)
order_hashThe 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_idThe outcome token you traded. On NORMAL fills both roles trade the same token; on MINT / MERGE the roles touch different tokens
sideYour side. On NORMAL fills, maker is the opposite of taker. On MINT both sides are BUY; on MERGE both are SELL
priceYour leg's execution price in engine units. Divide by price_scale to get collateral units per share (60 / 100 = 0.60)
sizeFilled 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", "MINT", or "MERGE"
fee_microPlatform 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

{
  "kind": "order_accepted",
  "order_id": "8c63c5e2-9f6d-4a3b-a8de-1bd0f4d9c14e",
  "order_hash": "0x9f2c...",
  "token_id": "21742633...",
  "side": "BUY",
  "price": 60,
  "remaining_size": 5000000,
  "original_size": 8000000,
  "price_scale": 100,
  "size_scale": 1000000,
  "tif": "GTC"
}

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

{
  "kind": "order_cancelled",
  "order_id": "8c63c5e2-9f6d-4a3b-a8de-1bd0f4d9c14e",
  "order_hash": "0x9f2c...",
  "token_id": "21742633...",
  "side": "BUY",
  "price": 60,
  "remaining_size": 5000000,
  "price_scale": 100,
  "size_scale": 1000000,
  "reason": "USER"
}
reasonMeaning
USERYou called DELETE /trade/v1/orders/{id} or cancel-all
FAK_REMAINDERFAK order's unfilled remainder swept off the book
SELF_TRADE_PREVENTIONThe maker side was cancelled to prevent self-trade
MARKET_RESOLVEDThe 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.

{
  "kind": "order_rejected",
  "order_id": "8c63c5e2-9f6d-4a3b-a8de-1bd0f4d9c14e",
  "order_hash": "0x9f2c...",
  "token_id": "21742633...",
  "reason": "insufficient balance"
}
FieldNotes
order_idYour local order UUID. Always present, the universal correlation / dedupe key
order_hashThe 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_idThe outcome token the order traded. Best-effort; may be null
reasonFree-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.

{
  "kind": "tokens_minted",
  "condition_id": "0x6e1c...",
  "size": 10000000,
  "size_scale": 1000000
}
{
  "kind": "tokens_merged",
  "condition_id": "0x6e1c...",
  "size": 10000000,
  "size_scale": 1000000
}

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.

{
  "kind": "tokens_redeemed",
  "condition_id": "0x6e1c...",
  "winning_token_id": "21742633...",
  "losing_token_id": "92018471...",
  "winning_shares_redeemed": "10000000",
  "losing_shares_zeroed": "2500000",
  "payout_micro": "10000000",
  "size_scale": 1000000
}
FieldNotes
winning_token_idThe outcome that paid one collateral unit per share
losing_token_idThe outcome that paid zero
winning_shares_redeemedWinning shares removed, as a decimal string in scaled units; divide by size_scale for shares
losing_shares_zeroedLosing shares removed, as a decimal string in scaled units; divide by size_scale for shares
payout_microCollateral credited, as a decimal string of micro-collateral
size_scaleScale 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:

{
  "kind": "collateral_withdrawn",
  "amount_micro": "1000000",
  "cash_balance_micro": "9000000",
  "batch_hash": "0x51cda675...",
  "batch_index": 2
}
{
  "kind": "collateral_deposited",
  "amount_micro": "1000000",
  "cash_balance_micro": "10000000",
  "batch_hash": "0x51cda675...",
  "batch_index": 2
}

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:

{
  "kind": "tokens_minted",
  "condition_id": "0x6e1c...",
  "size": 10000000,
  "size_scale": 1000000,
  "batch_hash": "0x51cda675...",
  "batch_index": 0
}
FieldNotes
batch_hashThe batch's hash: the digest you signed and the value POST /trade/v1/batches returned
batch_indexThe 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:

  1. Subscribe to account_events and begin buffering events after the subscribed acknowledgement.

  2. Seed via REST while buffering.

    • POST /trade/v1/portfolio/open-orders/list for active orders.
    • POST /trade/v1/portfolio/positions/list, GET /trade/v1/portfolio/summary, and GET /trade/v1/portfolio/trades for the rest of your current account state.
    • GET /trade/v1/portfolio/activities for durable account history.

    Open orders, trades, and activities page with keyset cursors: follow next_cursor to the end for a complete seed. Positions come back whole in one response.

  3. Reconcile the buffered events against stable IDs such as order_id and fill_id, then process new events live. This closes the gap between the REST snapshot and the subscription without applying duplicates.

  4. 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 see sequence_reset repeatedly, 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.

On this page