# Stream account and position updates

Polling `/api/v1/accounts/{id}/state` gives you a number that is already stale by the time
you read it. `/ws/risk` pushes the same state as it changes — one socket carries balance,
positions, order lifecycle and fills for the account behind your credential.

1. ### Mint a handshake token

   `POST /api/auth/bootstrap` with the bearer you already hold and no body at all. The
   socket URL and its token are under `data.riskEngineWss`.

   ```bash
   curl -sX POST https://api.troncharts.xyz/api/auth/bootstrap \
     -H "authorization: Bearer $TOKEN" \
     -H "x-tenant-slug: $TC_TENANT_SLUG" | jq '.data.riskEngineWss'
   # { "endpoint": "wss://api.troncharts.xyz/ws/risk", "token": "rt_…", "expiresAt": 1785196800000 }
   ```
   ```ts
   const { data } = await sdk.auth.bootstrap()
   const { endpoint, token } = data.riskEngineWss
   ```
   Bootstrap is tenant-scoped like every `/api/v1` call — without `x-tenant-slug` it answers
   `404 unknown_tenant_origin`. The token lives 60 seconds, the socket that uses it burns it,
   and the mint is capped at 10 per minute per API key.

2. ### Authenticate as the very first frame

   `Authenticate` is the only frame accepted before authentication. On success the account
   is at `scope.accountId` — nested, not top-level. On `Result { ok: false, error: "token_invalid" }`
   the socket stays open, so retry with a fresh token instead of reconnecting.

   ```json
   { "type": "Authenticate", "token": "rt_9f3c…" }
   ```
   ```json
   { "type": "Authenticated", "ok": true,
     "scope": { "sessionId": "apiclient:…", "accountId": "8f1c…",
                "walletAddress": null, "isAdmin": false } }
   ```
   ```ts
   import { RiskEngineClient } from '@tronchartsxyz/api-client'

   const ws = new RiskEngineClient({
     url: endpoint,
     token,                                   // burned by this connect
     onFrame: (frame) => console.log(frame.type),
   })
   await ws.connect()                         // sends Authenticate, resolves on Authenticated
   ```
   :::caution[Mint a fresh token for every connect]
   `RiskEngineClient` reconnects with the same `token` it was constructed with, and bootstrap
   tokens are single-use — every automatic reconnect fails `token_invalid`. Bootstrap again
   and build a new client rather than relying on the built-in reconnect.
   :::

3. ### Subscribe, then ask for the snapshots

   `topics` is a fixed enum of topic names — never symbols — and must be non-empty.
   Subscribing snapshots `Account-State-Changed` only: positions and working orders that
   already exist arrive solely in reply to `Get-Position-Update` (a `Position-Snapshot` frame)
   and `Get-open-orders` (an `Open-Orders-Update`). Skip those two and the stream looks empty
   on an account holding positions.

   ```json
   { "type": "Subscribe", "topics": ["Account-State-Changed", "Position-Changed", "Balance-Changed"] }
   { "type": "Get-Position-Update" }
   { "type": "Get-open-orders" }
   ```
   ```ts
   ws.send({ type: 'Subscribe', topics: ['Account-State-Changed', 'Position-Changed', 'Balance-Changed'] })
   ws.send({ type: 'Get-Position-Update' })
   ws.send({ type: 'Get-open-orders' })
   ```
4. ### Hold the socket open

   Send `Ping` every 30 seconds for a `Pong { serverTime }`; `Ping` and `Alive` are the only
   frames exempt from the per-tier frame rate limit. Every other frame carries a per-socket
   monotonic `seq` — on a forward jump send `Resume { lastSeq }` to replay the ring buffer,
   and on `Resync-Required` re-send the two `Get-*` frames above. The SDK client does the
   heartbeat, gap detection and `Resume` for you. If your credential is `strict-single`, a
   second authenticated socket closes the first with code `1008 replaced_by_new_session`.

**Next:** [Follow an order from placement to fill](https://docs.troncharts.xyz/docs/recipes/track-an-order/) · [Risk channel](https://docs.troncharts.xyz/docs/realtime/risk/) · [Connecting](https://docs.troncharts.xyz/docs/realtime/connecting/)