# 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/*`](/docs/portfolio), which remains the authoritative
source for your current account state and recovery.

This endpoint is the private counterpart to the public
[market stream](/docs/streaming). Application messages must be JSON text frames
and cannot exceed 64 KiB. Binary application frames are rejected.

## Authentication [#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](/docs/authentication)).
* 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 [#connect]

This endpoint allows **300 connection attempts per IP per 60 seconds**, shared
across accounts on that IP. See [stream connection limits](/docs/rate-limits#stream-connection-attempts).

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

| HTTP response                      | What to do                                                                                                   |
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `401` typed authentication failure | Correct an invalid supplied `Authorization` header; authentication still happens in the subscription payload |
| `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](/docs/errors), not WebSocket frames.

```bash
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:

```python
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())
```

```bash
wscat -c "$AGARA_WS_URL/trade/v1/account-stream"
> { "op": "subscribe", "channels": [{ "name": "account_events", "token": "agt_…" }] }
```

## Channels [#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 attempt is allowed per connection. The token pins the
channel to one user; rotate it by opening a new socket.

## Client → server [#client--server]

Same `op` vocabulary as the public stream:

```json
{ "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 [#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`             | A typed authentication, protocol, subscription, or connection failure                                                                                                  |
| `sequence_reset`    | You missed messages; reseed via REST, then keep listening. Carries a `reason` (`lagged` / `stream_reset`), same as the [public stream](/docs/streaming#sequence_reset) |
| `pong`              | Reply to your `ping`                                                                                                                                                   |
| `heartbeat`         | Server-pushed every \~10 s                                                                                                                                             |
| `subscription_list` | Reply to your `list` request; credentials are never included                                                                                                           |

`subscribed` is sent only after authentication succeeds:

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

A `list` response likewise omits the token:

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

Account-stream errors use the same nested envelope as the
[market stream](/docs/streaming#error):

```json
{
  "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.

| Code                            | Recovery                 | Action      |  Close | What to do                                                              |
| ------------------------------- | ------------------------ | ----------- | -----: | ----------------------------------------------------------------------- |
| `identity_token_expired`        | `refresh_identity_token` | `reconnect` | `4001` | Refresh once, open a new socket, subscribe, and reseed                  |
| `invalid_identity_token`        | `none`                   | `none`      | `1008` | Replace the identity token before opening a new socket                  |
| `invalid_personal_access_token` | `none`                   | `none`      | `1008` | Replace the PAT before opening a new socket                             |
| `forbidden_scope`               | `none`                   | `none`      | `1008` | Create a PAT with `account:stream`                                      |
| `wallet_setup_incomplete`       | `none`                   | `none`      | `1008` | Complete wallet setup before opening a new socket                       |
| `dependency_unavailable`        | `retry`                  | `reconnect` | `1013` | Back off, reconnect, subscribe, and reseed                              |
| `internal_error`                | `none`                   | `reconnect` | `1011` | Reconnect 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](/docs/streaming#error).
The connection can also close because of a transport failure without sending
an error frame; reconnect and reseed in that case.

### `update` [#update]

```json
{
  "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](/docs/streaming#sequence).

## Event payloads [#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](/docs/api-traders/pre-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`](#order_rejected) it's best-effort and
may be absent; correlate on `order_id` there.

### `fill` [#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.

```json
{
  "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"
}
```

| 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](/docs/api-traders/pre-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` [#order_accepted]

```json
{
  "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` [#order_cancelled]

```json
{
  "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"
}
```

| `reason`                  | Meaning                                                                                                                                                                                                                                                                                                                                         |
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `USER`                    | A 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_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                                                                                                                                                                                                                                                                   |
| `INSUFFICIENT_COLLATERAL` | Add 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](/docs/on-chain/positions#split) 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](/docs/api-traders/recovery).

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](/docs/orders#cancel-an-order).

### `order_rejected` [#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.

```json
{
  "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"
      }
    }
  }
}
```

| Field              | Notes                                                                                                                        |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
| `order_id`         | Your local order UUID. Always present; use it to correlate the notice and to deduplicate this `sequence: 0` rejection        |
| `order_hash`       | The order's EIP-712 hash when available; it is best-effort and may be `null`. Always fall back to `order_id` for correlation |
| `token_id`         | The outcome token the order traded. Best-effort; may be `null`                                                               |
| `failure.code`     | Stable terminal reason; branch on this value                                                                                 |
| `failure.title`    | Display text; do not parse it for control flow                                                                               |
| `failure.detail`   | Optional display text, omitted when no safe public detail exists; clients must not require it                                |
| `failure.recovery` | Typed 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`](/docs/orders#errors). This event covers failures after acceptance.
Reconcile against
[`GET /trade/v1/orders/{id}`](/docs/orders#look-up-one-order), including when
the event uses `sequence: 0`.

### `tokens_minted` and `tokens_merged` [#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](/docs/portfolio#position-operations) or an
[account batch](/docs/api-traders/account-batches). 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`](#fill) with
`settlement_mode` set to `MINT` or `MERGE`.

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

```json
{
  "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` [#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.

```json
{
  "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
}
```

| 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](/docs/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` [#payouts_redeemed]

The split counterpart of `tokens_redeemed`, emitted when a position in a market
resolved to a [split](/docs/automatic-redemption#split-resolutions) 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`.

```json
{
  "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"
    }
  ]
}
```

| Field                               | Notes                                                                                                                                   |
| ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `payout_micro`                      | Total collateral credited, as a decimal string of micro-collateral; the sum of the outcome payouts                                      |
| `outcomes`                          | Both outcomes, in the market's outcome order. An outcome you did not hold reports `"0"` shares and payout                               |
| `outcomes[].token_id`               | The outcome token                                                                                                                       |
| `outcomes[].payout_per_share_micro` | What one share of this outcome pays, as a decimal string of micro-collateral                                                            |
| `outcomes[].shares_redeemed`        | Shares of this outcome removed, as a decimal string in scaled units; divide by `size_scale` for shares                                  |
| `outcomes[].payout_micro`           | Collateral credited for this outcome: its shares times its price, rounded down to the micro unit, so a very small holding can pay `"0"` |
| `size_scale`                        | Scale 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` [#collateral_withdrawn-and-collateral_deposited]

These events report collateral balance changes:

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

```json
{
  "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}`](/docs/api-traders/account-batches#tracking-a-batch)
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`](/docs/portfolio#summary) for the authoritative balance.

### Batch provenance [#batch-provenance]

When a position or collateral event belongs to an
[account batch](/docs/api-traders/account-batches), it also carries
`batch_hash` and `batch_index`. Both fields are omitted otherwise:

```json
{
  "kind": "tokens_minted",
  "condition_id": "0x6e1c...",
  "size": 10000000,
  "size_scale": 1000000,
  "batch_hash": "0x51cda675...",
  "batch_index": 0
}
```

| 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}`](/docs/api-traders/account-batches#tracking-a-batch)
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-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](/docs/conventions#reconnecting-streams) 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](/docs/conventions#pagination): 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](/docs/api-traders/account-batches#what-you-observe-on-failure).

   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](/docs/streaming) also has an
   active-subscription + REST-watermark orderbook baseline and the full
   [quote-readiness lifecycle risk
   decision](/docs/api-traders/recovery#quote-readiness-state-machine) 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.
