Skip to content

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

    Terminal window
    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 }

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

    { "type": "Authenticate", "token": "rt_9f3c…" }
    { "type": "Authenticated", "ok": true,
    "scope": { "sessionId": "apiclient:…", "accountId": "8f1c…",
    "walletAddress": null, "isAdmin": false } }
  3. 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.

    { "type": "Subscribe", "topics": ["Account-State-Changed", "Position-Changed", "Balance-Changed"] }
    { "type": "Get-Position-Update" }
    { "type": "Get-open-orders" }
  4. 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 · Risk channel · Connecting