Risk channel
/ws/risk is the primary programmatic surface. It is bidirectional: the server
pushes state, and you can act on the same socket instead of falling back to
REST.
Nothing is pushed until you Subscribe to the
topics you want.
Frames the server pushes
Section titled “Frames the server pushes”| Frame | Carries |
|---|---|
Account-State-Update |
The account’s aggregate state. |
Balance-Update / Margin-Update / Funding-Update / Risk-Update |
Balance, margin, funding, and risk as they change. |
Position-Update / Position-Removed / Position-Snapshot |
Live position state; the snapshot seeds your view. |
Open-Orders-Update |
The working-order book for the account. |
Order-Intent / Order-Intent-Update |
Order lifecycle, from composed to terminal. |
Order-Update / Trade-Update |
Order state and closed round trips, in the external vocabulary. |
Fill |
One frame per execution — never coalesced, so nothing is collapsed away. |
Account-Event |
Lifecycle events on the account. |
Account-Suspension-Update |
The account was blocked, and why — and again, with suspended: false, when the block is lifted. The block frame is sent before the forced flatten, so it arrives ahead of the Position-Removed frames that closing the positions produces. |
Prop-Account-Update |
Challenge progress and per-rule state on a prop account. |
Prop-Account-Status-Changed, Prop-Account-Promoted, Prop-Account-Payout, Prop-Account-Claim-Settled, Prop-Account-Collateral |
The challenge transitions, one frame per kind. Same topic as the update above. |
Companion-Event |
Derived risk / position / rule notes from the trading companion. |
Feature-Flags-Updated |
Your tenant’s feature flags changed. Carries no topic gate — it arrives on any authenticated socket. |
Market-Data-Policy-Updated |
Your tenant’s market-data policy — enabled venues and per-venue symbol filters — changed. Re-mint your market-feed token: the one you hold was minted under the old policy. Carries no topic gate. |
Trade-History-Result |
The reply to Request-Trade-History, not a push. |
Blocking-Update, IP-Update, LoggedOff |
Session-level notices. |
Resync-Required |
Invalidate local readiness and re-snapshot after a replay gap, outbox overflow or cross-instance bus reconnect. |
There is no server-pushed bracket frame. TP/SL leg state reaches you through
Open-Orders-Update and Order-Intent-Update like any other working order.
After Resync-Required, reconnect or issue fresh correlated Subscribe,
Get-Position-Update, Get-open-orders, Get-Balance and Get-Margin
requests. Only positive Result acknowledgements for every request establish
readiness: Subscribe includes the account-state snapshot. Keep unresolved
financial commands and their idempotency keys; a resync is not a rejection or
permission to submit them again with new keys. The cross-instance reconnect
notice repairs a detected Pub/Sub gap; it is not a continuous Redis health signal.
Frames you can send
Section titled “Frames you can send”Read requests — Get-Margin, Get-Balance, Get-Risk-Update,
Get-Position-Update, Get-open-orders, Request-Trade-History — plus
Subscribe / Unsubscribe to change topics and scope, and Resume to replay
after a gap.
Write frames mirror the REST OMS:
Order-Sign-Result, Cancel-Order-Intent, Replace-Order-Intent,
Bracket-Insert, Bracket-Modify, Bracket-Cancel, Position-Close. Each is
tier-gated.
There is no place-order frame. New orders start at
POST /api/v1/oms/intents; the socket carries the signature back
(Order-Sign-Result) and then everything that happens to the order afterwards.
Acting on a live order over the socket saves a round trip on a hot path; REST
is fine everywhere else and easier to retry safely.
Position-Close closes the whole position — passing qty is rejected with
partial_close_unsupported — and it accepts only hyperliquid, aster and
polymarket. Flatten anything else through POST /api/v1/oms/flatten.
Sequence numbers — detect a gap
Section titled “Sequence numbers — detect a gap”Every pushed frame that leaves the per-socket outbox carries a monotonic
integer seq, starting at 1 for the life of the connection.
if (frame.seq !== undefined) { if (lastSeq !== null && frame.seq !== lastSeq + 1) { await resnapshot() // a frame was dropped or coalesced away } lastSeq = frame.seq}A gap means one or more frames were dropped or coalesced — the server
merges rapid updates for the same key under load. Re-request the relevant
Get-* snapshot rather than trying to reconstruct the missing step.
Pong is the one frame deliberately left unsequenced: the heartbeat is
written straight to the socket, outside the outbox, so it never consumes a
number and never creates a gap.
Everything else that reaches you is sequenced, including Authenticated and
every Result — they go out through the same outbox as the push frames, just
without waiting for a flush. So a socket’s first frame is normally
Authenticated at seq: 1, and your first push frame is seq: 2. Seed
lastSeq from whatever arrives rather than special-casing the handshake, and
only assert contiguity over frames that actually carry seq. A new connection
restarts the lane at 1.
Tracking an order to its outcome
Section titled “Tracking an order to its outcome”Subscribe { topics: ["Order-Intent-Changed"] }before you place, so no transition happens while you are unsubscribed.POST /api/v1/oms/intentsreturns an intent id.Order-Intent-Updatecarries the venue order id, then the fills, then the terminal state. The stream is account-scoped, not per-intent — there is nointent.<id>topic — so match each frame on the intent id you were handed.
This is the intended pattern — polling an order endpoint in a loop will always lag the socket and costs you rate limit.