Skip to content

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.

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.

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.

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.

  1. Subscribe { topics: ["Order-Intent-Changed"] } before you place, so no transition happens while you are unsubscribed.
  2. POST /api/v1/oms/intents returns an intent id.
  3. Order-Intent-Update carries the venue order id, then the fills, then the terminal state. The stream is account-scoped, not per-intent — there is no intent.<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.