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.

Authenticated: bearer token required in the first subscription. 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. Application messages must be JSON text frames and cannot exceed 64 KiB. Binary application frames are rejected.

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

An HTTP Authorization header does not replace the token in the subscription payload. If the token is invalid, expired, lacks the scope, or belongs to an account without a ready AGARA wallet, the server sends one typed error frame and does not acknowledge the subscription.

Only one account subscription attempt is allowed per socket, even when the first attempt fails or is later unsubscribed. Open a new connection to correct a credential, rotate a token, or subscribe again.

Connect

This endpoint allows 300 connection attempts per IP per 60 seconds, shared across accounts on that IP. See stream connection limits.

No subprotocol negotiation is required. The HTTP upgrade can fail before a socket exists:

HTTP responseWhat to do
401 typed authentication failureCorrect an invalid supplied Authorization header; authentication still happens in the subscription payload
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 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 attempt is allowed per connection. The token pins the channel to one user; rotate it by opening a new socket.

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 protocol shape requires a token on unsubscribe, but the server never echoes credentials in acknowledgements or subscription_list. ping and list otherwise behave as on the public stream. After unsubscribe, open a new socket if you need to subscribe again.

Server → client

Same frame envelope as the public stream:

opWhen
subscribedAuth succeeded; events will start arriving
unsubscribedAck for an unsubscribe entry
updateOne account event
errorA typed authentication, protocol, subscription, or connection failure
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; credentials are never included

subscribed is sent only after authentication succeeds:

{
  "op": "subscribed",
  "channel": "account_events"
}

A list response likewise omits the token:

{
  "op": "subscription_list",
  "channels": [{ "channel": "account_events" }]
}

Account-stream errors use the same nested envelope as the market stream:

{
  "op": "error",
  "failure": {
    "code": "identity_token_expired",
    "title": "Your session has expired",
    "detail": "Please log in again to continue.",
    "recovery": {
      "strategy": "refresh_identity_token"
    }
  },
  "action": "reconnect",
  "channel": "account_events"
}

For a code your client recognizes, switch on failure.code and act on action. For an unknown code, do not honor either automatic instruction; pause account mutations, log the frame, and update your client. An actual socket close remains authoritative.

CodeRecoveryActionCloseWhat to do
identity_token_expiredrefresh_identity_tokenreconnect4001Refresh once, open a new socket, subscribe, and reseed
invalid_identity_tokennonenone1008Replace the identity token before opening a new socket
invalid_personal_access_tokennonenone1008Replace the PAT before opening a new socket
forbidden_scopenonenone1008Create a PAT with account:stream
wallet_setup_incompletenonenone1008Complete wallet setup before opening a new socket
dependency_unavailableretryreconnect1013Back off, reconnect, subscribe, and reseed
internal_errornonereconnect1011Reconnect and reseed; do not retry a second subscription on this socket

Malformed frames, unsupported binary data, oversized messages, slow clients, and restarts use the common WebSocket error matrix. The connection can also close because of a transport failure without sending an error frame; reconnect and reseed in that case.

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. Process every positive-sequence transition: an accepted order, its fills, and its cancellation can all repeat the same order_id and order_hash. Deduplicate fill records only by fill_id. An order_rejected notice can use sequence: 0; deduplicate that notice 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...",
  "market_title": "Will Brent crude close above $100 a barrel in 2026?",
  "outcome_name": "Yes",
  "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.

Use market_title and outcome_name to display a new order immediately, including when its token was absent from your initial REST response. The outcome label can be Yes, No, Up, Down, or another market label; it is independent of side. These fields do not supply an event link or logo. Keep those from REST when available.

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
USERA cancel request: you called DELETE /trade/v1/orders/{id} or cancel-all, your GTD order expired, or we cancelled your orders, for example because your account was frozen
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
INSUFFICIENT_COLLATERALAdd collateral or reduce your other BUY orders in this market before placing this BUY again. It was cancelled because your resting BUY orders in this market (both outcomes) added up to more than your cash balance, which can follow a fill on one of your BUY orders, splitting collateral or a withdrawal

Don't wait for order_cancelled for an order cancelled while it was still PENDING: none is sent, and no order_accepted either. GET /trade/v1/orders/{order_id} reports the order as CANCELLED. That happens when:

  • you cancel it while it is still PENDING (the cancel returns 200)
  • cancel-all reaches it while it is still PENDING
  • it is a GTD order that expires while still PENDING

Keep reconciling an order that was already SUBMITTING when the cancel arrived. It is cancelled only if it rests on the book, so you can receive order_accepted and fills before its order_cancelled. If it filled completely first, no order_cancelled is sent and it ends MATCHED. If it ends REJECTED or FAILED, an order_rejected event is sent instead. Treat its status as provisional until every fill_id you received appears in GET /trade/v1/orders/{order_id}/trades and GET /trade/v1/orders/{order_id} reflects those fills and any order_cancelled or order_rejected you received. A fill can still arrive after that, so if a late fill is plausible, follow the API trading recovery procedure.

A cancel that returns 200 sends nothing to the venue and produces no event of its own. If an earlier cancel removed the order from the book, the order's earlier events, including fills and that cancel's order_cancelled, can still arrive after the 200. If an earlier cancel reached the order after it had already filled completely, that cancel sends no order_cancelled, the 200 can still say CANCELLED, and the order changes to MATCHED once its fills appear on it. A FAK order that filled only in part changes from CANCELLED to PARTIALLY_FILLED instead, and the change can come after its fills appear. Its FAK_REMAINDER order_cancelled was sent when the order executed, before the earlier cancel reached it, so you have it only if you were connected then. Whether or not you received it, wait for PARTIALLY_FILLED for a FAK order that filled only in part, and for MATCHED after a complete fill. See Cancel an order.

order_rejected

Emitted whenever an accepted order later reaches a persisted FAILED or REJECTED state. This includes engine rejections such as insufficient shares and failures elsewhere in submission. REST remains the durable record.

{
  "op": "update",
  "channel": "account_events",
  "sequence": 0,
  "data": {
    "kind": "order_rejected",
    "order_id": "8c63c5e2-9f6d-4a3b-a8de-1bd0f4d9c14e",
    "order_hash": "0x9f2c...",
    "token_id": "21742633...",
    "failure": {
      "code": "insufficient_shares",
      "title": "Insufficient shares",
      "detail": "Available shares are lower than the requested amount.",
      "recovery": {
        "strategy": "none"
      }
    }
  }
}
FieldNotes
order_idYour local order UUID. Always present; use it to correlate the notice and to deduplicate this sequence: 0 rejection
order_hashThe order's EIP-712 hash when available; it is best-effort and may be null. Always fall back to order_id for correlation
token_idThe outcome token the order traded. Best-effort; may be null
failure.codeStable terminal reason; branch on this value
failure.titleDisplay text; do not parse it for control flow
failure.detailOptional display text, omitted when no safe public detail exists; clients must not require it
failure.recoveryTyped recovery instruction for the terminal failure

The possible terminal codes are chain_execution_failed, insufficient_balance, insufficient_shares, internal_error, market_closed, order_funding_failed, order_not_fillable, order_submission_failed, post_only_would_cross, and sequence_conflict. There is no free-text reason field.

Most balance, shares, and post-only rejections are caught synchronously as a 422. This event covers failures after acceptance. Reconcile against GET /trade/v1/orders/{id}, including when the event uses sequence: 0.

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 position in a market resolved to a single winner 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.

payouts_redeemed

The split counterpart of tokens_redeemed, emitted when a position in a market resolved to a split clears. Each outcome pays its own price per share, so the event lists both outcomes instead of a winner and a loser. A split never arrives as tokens_redeemed; route on kind.

{
  "kind": "payouts_redeemed",
  "condition_id": "0x6e1c...",
  "size_scale": 1000000,
  "payout_micro": "8110000",
  "outcomes": [
    {
      "token_id": "21742633...",
      "payout_per_share_micro": "370000",
      "shares_redeemed": "10000000",
      "payout_micro": "3700000"
    },
    {
      "token_id": "92018471...",
      "payout_per_share_micro": "630000",
      "shares_redeemed": "7000000",
      "payout_micro": "4410000"
    }
  ]
}
FieldNotes
payout_microTotal collateral credited, as a decimal string of micro-collateral; the sum of the outcome payouts
outcomesBoth outcomes, in the market's outcome order. An outcome you did not hold reports "0" shares and payout
outcomes[].token_idThe outcome token
outcomes[].payout_per_share_microWhat one share of this outcome pays, as a decimal string of micro-collateral
outcomes[].shares_redeemedShares of this outcome removed, as a decimal string in scaled units; divide by size_scale for shares
outcomes[].payout_microCollateral credited for this outcome: its shares times its price, rounded down to the micro unit, so a very small holding can pay "0"
size_scaleScale for every share field

As with tokens_redeemed, this reports the account change, not completion of the process that initiated it; 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

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. Use backoff when opening a new socket, then restore your subscription and account state below.

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.

    • Fully page POST /trade/v1/orders/list for the complete order lifecycle, including orders that have not reached the book. Use POST /trade/v1/portfolio/open-orders/list for the resting and active portfolio view.
    • 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.

    Orders, portfolio open orders, trades, and activities page with keyset cursors: follow next_cursor to the end for a complete seed. Positions are unpaginated. 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. When seeding multiple exchanges, require the array to be empty on every page.

  3. Reconcile buffered order and fill records. Use order_id and order_hash to correlate an order, but do not discard a positive-sequence transition merely because either value repeats. Process those transitions in sequence order, deduplicate fills only by fill_id, and deduplicate a sequence: 0 order_rejected notice by order_id.

    A live frame can arrive before REST reflects it. For a fill, poll GET /trade/v1/orders/{order_id}/trades until its fill_id appears, then poll GET /trade/v1/orders/{order_id} until the order state reflects all listed fills or a later terminal transition. A fully filled LIMIT or MARKET order is MATCHED. A partially filled GTC or GTD LIMIT order is PARTIALLY_FILLED with is_terminal: false. After an order_cancelled frame with reason: "FAK_REMAINDER" for a partially filled FAK LIMIT or MARKET order, wait for PARTIALLY_FILLED with is_terminal: true and retain its fills. The frame cancels the unfilled remainder. A FOK order never partially fills. For order_accepted, order_cancelled, or order_rejected, poll the order until it reflects that event or a later state. Apply the same order-versus-fill check to orders in the REST seed. Only then redo the fully paginated orders read and refresh portfolio open orders. If the order state never catches up, stay paused and investigate.

    Do not arithmetically apply a buffered fill or token or collateral effect to the REST inventory: REST responses do not carry a WebSocket sequence that tells you whether that change is already included, and token and collateral effects have no unique event ID. Every fill, tokens_minted, tokens_merged, tokens_redeemed, payouts_redeemed, collateral_deposited, and collateral_withdrawn event is a signal to refetch positions and summary. Wait for an observable result, not merely two identical reads:

    Every positions response used as recovery evidence must exclude AGARA from unavailable_exchanges; require that array to be empty when rebuilding a complete multi-exchange view.

    • For a collateral event carrying batch_hash, wait for the batch's final status and, on failure, its unwound_at. If no later fill, mint, merge, redemption, deposit, or withdrawal can also have changed cash, require summary cash_balance_micro to match the newest collateral frame. The same restriction applies to a collateral event without a batch hash. When later cash-changing events exist, do not compare summary with one frame in isolation; reconcile the complete ordered set of effects or stay paused.
    • For a mint or merge carrying batch_hash, wait for its final batch status and, on failure, its unwound_at; require positions to reflect the settled operation or completed unwind.
    • For redemption, require the resolved market's position to be cleared.

    Before using unwound_at as proof, check the batch queue. If a lower batch is FAILED_DIVERGENT, a higher failed row's marker does not prove its inventory was restored. Keep mutations paused and follow the stranded-sequence recovery rule.

    If an effect has no independently verifiable final condition, or REST never reaches it, stay paused and investigate.

  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, but the subscription remains active; REST is the authoritative recovery. If you see sequence_reset repeatedly, your client may be reading too slowly; profile the read loop.

  5. Resume quoting only after the market stream also has an active-subscription + REST-watermark orderbook baseline and the full quote-readiness lifecycle risk decision is satisfied.

If the feed is temporarily unavailable the handshake returns 503. A live connection receives failure.code: "dependency_unavailable", action: "reconnect", then closes with 1013. 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.

A disconnect does not cancel resting orders. If account state is uncertain, pause new placements and use the REST cancel and order endpoints to reconcile before resuming.