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.
-
Mint a handshake token
Section titled “Mint a handshake token”POST /api/auth/bootstrapwith the bearer you already hold and no body at all. The socket URL and its token are underdata.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 }const { data } = await sdk.auth.bootstrap()const { endpoint, token } = data.riskEngineWssBootstrap is tenant-scoped like every
/api/v1call — withoutx-tenant-slugit answers404 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. -
Authenticate as the very first frame
Section titled “Authenticate as the very first frame”Authenticateis the only frame accepted before authentication. On success the account is atscope.accountId— nested, not top-level. OnResult { 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 } }import { RiskEngineClient } from '@tronchartsxyz/api-client'const ws = new RiskEngineClient({url: endpoint,token, // burned by this connectonFrame: (frame) => console.log(frame.type),})await ws.connect() // sends Authenticate, resolves on Authenticated -
Subscribe, then ask for the snapshots
Section titled “Subscribe, then ask for the snapshots”topicsis a fixed enum of topic names — never symbols — and must be non-empty. Subscribing snapshotsAccount-State-Changedonly: positions and working orders that already exist arrive solely in reply toGet-Position-Update(aPosition-Snapshotframe) andGet-open-orders(anOpen-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" }ws.send({ type: 'Subscribe', topics: ['Account-State-Changed', 'Position-Changed', 'Balance-Changed'] })ws.send({ type: 'Get-Position-Update' })ws.send({ type: 'Get-open-orders' }) -
Hold the socket open
Section titled “Hold the socket open”Send
Pingevery 30 seconds for aPong { serverTime };PingandAliveare the only frames exempt from the per-tier frame rate limit. Every other frame carries a per-socket monotonicseq— on a forward jump sendResume { lastSeq }to replay the ring buffer, and onResync-Requiredre-send the twoGet-*frames above. The SDK client does the heartbeat, gap detection andResumefor you. If your credential isstrict-single, a second authenticated socket closes the first with code1008 replaced_by_new_session.
Next: Follow an order from placement to fill · Risk channel · Connecting