# 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](https://docs.troncharts.xyz/docs/realtime/connecting/#subscribe-to-topics) you want.

## 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. |
| `Prop-Account-Update` | Challenge progress and status on a prop account. |
| `Blocking-Update`, `IP-Update`, `LoggedOff` | Session-level notices. |
| `Resync-Required` | Drop local state and re-snapshot. |

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.

## 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](https://docs.troncharts.xyz/docs/trading/orders/):
`Order-Sign-Result`, `Cancel-Order-Intent`, `Replace-Order-Intent`,
`Bracket-Insert`, `Bracket-Modify`, `Bracket-Cancel`, `Position-Close`. Each is
[tier-gated](https://docs.troncharts.xyz/docs/realtime/connecting/#tier-enforcement-per-frame).

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

Every pushed frame that leaves the per-socket outbox carries a monotonic
integer `seq`, starting at 1 for the life of the connection.

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

Control frames sent outside the outbox — `Pong` and the `Authenticated`
handshake — are deliberately unsequenced. Only assert contiguity over frames
that carry `seq`. A new connection restarts the lane at 1.

## Tracking an order to its outcome

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.