# Read WebSocket — `/v1/ws`

The beta WebSocket is topic-based. It polls `orderUpdates` and `userFills` every
**100 ms**, while `clearinghouseState` remains at **500 ms** because it pushes a
larger full account snapshot. A single connection can subscribe to multiple
topics. `orderUpdates` and `userFills` send
`type: 'snapshot'` for the first message after subscribe, then `type: 'update'`
only when updates are observed after the connection-local snapshot.

Production URL: `wss://production.mosaiq.0d.finance/v1/ws`.

WebSocket connections are not authenticated. Pass `?user=<evm_address>` or
include `user` in the subscription. If omitted, the server uses its configured
default user.

This read stream is separate from the authenticated
[`/v1/exchange/ws`](/api/exchange-websocket) write transport.
See the [low-latency integration guidelines](/api/optimizing-latency)
for the recommended two-socket client state machine.

For Hyperliquid-backed data, Mosaiq runs its own Hyperliquid node with
low-latency sentry peering instead of depending on the public API path.

```ts
const apiBaseUrl = 'https://production.mosaiq.0d.finance'
const user = '0x0000000000000000000000000000000000000001'

const wsUrl = new URL('/v1/ws', apiBaseUrl)
wsUrl.protocol = wsUrl.protocol === 'https:' ? 'wss:' : 'ws:'
wsUrl.searchParams.set('user', user)

const ws = new WebSocket(wsUrl)
```

## Rate Limits

The `/v1/ws` endpoint is limited per client IP:

| Limit                  | Value        | Behavior                                                  |
| ---------------------- | ------------ | --------------------------------------------------------- |
| Concurrent connections | `10`         | Extra upgrades are rejected with HTTP `429`.              |
| New connections        | `30`/minute  | Extra upgrades are rejected with HTTP `429`.              |
| Incoming messages      | `2000`/minute | Extra WebSocket frames are silently dropped, with no error response. |

Rate budgets refill continuously rather than resetting on wall-clock minute
boundaries. Prefer one connection with multiple subscriptions, and back off
before reconnecting after a `429`.

## Subscribe

```jsonc
{ "method": "subscribe", "subscription": { "type": "clearinghouseState", "user": "0x..." } }
{ "method": "subscribe", "subscription": { "type": "orderUpdates", "user": "0x..." } }
{ "method": "subscribe", "subscription": { "type": "userFills", "user": "0x..." } }
```

There is no unsubscribe message yet. Sending a new valid subscription for an
already active topic replaces that topic's parameters, for example `user`, but
does not remove other active topics on the same connection.

## Response Envelope

Every pushed message is wrapped in:

```ts
interface WebSocketMessage<T> {
  status: 'ok'
  topic: 'clearinghouseState' | 'orderUpdates' | 'userFills'
  data: T
  server_time_ms: number
}
```

Use `topic` to route the message; do not infer the stream from keys inside
`data`. `server_time_ms` is when Mosaiq produced the message. WebSocket errors
use `status: 'error'` with an error object in `data`.

## `clearinghouseState`

Same payload as the `data` field returned by `GET /v1/clearinghouseState`.
It is a live RAM snapshot, so a just-accepted place may briefly be absent until
Mosaiq applies the venue acknowledgement into live state.

```ts
interface ClearinghouseState {
  user: string
  clearinghouseState: InnerClearinghouseState
}

interface InnerClearinghouseState {
  positions: Array<Position>
  marginSummary: MarginSummary
  userCaps: UserCaps
  universe: Array<UniverseAsset>
  marketCtxs: Array<MarketContext>
  meta: ClearinghouseMeta
}

interface MarginSummary {
  accountValue: number
  totalCashUsdc: number
  availableFunds: number
  excessLiquidity: number
  initialMarginUsed: number
  maintenanceMarginUsed: number
  grossNotional: number
  cushion: number
  leverage: number
}

interface UserCaps {
  tier: string
  openOrders: number
  maxOpenOrders: number
  maxSingleOrderNotionalUsdc: number
  openOrderNotionalUsdc: number
  maxOpenOrderNotionalUsdc: number
  grossNotionalUsdc: number
  maxGrossNotionalUsdc: number
  initialMarginUsageRate: number
  maxInitialMarginUsageRate: number
  maxAssetVenueNotionalUsdc: number
  assetVenueNotionals: Array<{
    asset: string
    venue: string
    notionalUsdc: number
  }>
}

interface Position {
  coin: string
  venue: string
  entryPx: number
  szi: number
  positionValue: number
  unrealizedPnl: number
  unrealizedFunding: number
}

interface UniverseAsset {
  name: string
  szDecimals: number
  maxLeverage: number
  marginTableId: number
  onlyIsolated?: boolean
  isDelisted?: boolean
  constraints?: MarketConstraints
  venues: Array<UniverseVenue>
}

interface UniverseVenue {
  venue: string
  venueSymbol: string
  instrumentType: 'perp'
  status: 'active' | 'post_only' | 'reduce_only' | 'prelisted' | 'delisted' | 'inactive'
  constraints: MarketConstraints
}

interface MarketContext {
  name: string
  venue: string
  funding?: string
  oraclePx?: string
}

interface MarketConstraints {
  sizeDecimals: number
  sizeIncrement?: string
  priceDecimals?: number
  tickSize?: string
  minBaseAmount?: string
  minNotional?: string
  maxBaseAmount?: string
  maxMarketNotional?: string
  maxLimitNotional?: string
  maxPositionNotional?: string
  openInterestCap?: string
  maxOpenOrders?: number
  supportsReduceOnly?: boolean
  supportsPostOnly?: boolean
  requiresIsolatedMargin?: boolean
}

interface ClearinghouseMeta {
  venues: Array<VenueRuntimeStatus>
  recovery?: Array<VenueRecoveryStatus>
}

interface VenueRuntimeStatus {
  venue: string
  status: 'unknown' | 'online' | 'offline'
  lastCheckedAtMs?: number
  lastOkAtMs?: number
  message?: string
}

interface VenueRecoveryStatus {
  venue: string
  phase: 'recovering' | 'active' | 'halted'
  message?: string
}
```

:::note\[Canonical asset symbols]
`universe[*].name`, `positions[*].coin`, `marketCtxs[*].name`, and
`/v1/exchange.orders[*].a` all use Mosaiq canonical asset symbols. Venue
symbols are internal mappings. Positions are venue-scoped: `(coin, venue)`
identifies one settled leg. Missing mappings are explicit order errors, never
identity fallbacks.
:::

:::note\[Venue constraints and status]
`venues[*].constraints` is what `/v1/exchange` validates for the selected venue.
`meta.venues` is runtime venue reachability and is separate from each market's
trading status in `universe[*].venues[*].status`.
:::

## `orderUpdates`

Bounded stream of the user's latest order lifecycle rows, returned
oldest-to-newest within each message. The first message after subscribe is a
bounded `snapshot`; later messages are `update` payloads containing only orders
whose public state changed after that snapshot.

```ts
interface OrderUpdates {
  user: string
  type: 'snapshot' | 'update'
  orderUpdates: Array<OrderUpdate>
}

interface OrderUpdate {
  orderId: string
  cloid: string
  a: string
  venue: string
  b: 'buy' | 'sell'
  status:
    | 'accepted'
    | 'open'
    | 'partially_filled'
    | 'filled'
    | 'cancelled'
    | 'failed'
    | 'uncertain'
  filledSize: string
  avgFillPrice?: string
  createdAtMs: number
  updatedAtMs: number
}
```

`open` means venue open-order evidence proves the order is live/resting. `failed`
means the order reached a definitive unsuccessful lifecycle state. `uncertain`
means Mosaiq cannot yet assert the final lifecycle state; wait for later
`orderUpdates` or reconcile through `openOrders` and `userFills` before acting. A
REST cancel ack does not immediately become `cancelled`; finality comes from the
venue.

## `userFills`

Bounded stream of confirmed venue fills, returned oldest-to-newest within each
message. Fill ids are stable and idempotent. The first message after subscribe
is a bounded `snapshot`; later messages are `update` payloads containing only
fills observed after that snapshot.

```ts
interface UserFills {
  user: string
  type: 'snapshot' | 'update'
  userFills: Array<UserFill>
}

interface UserFill {
  fillId: string
  orderId: string
  cloid: string
  a: string
  venue: string
  b: 'buy' | 'sell'
  size: string
  price: string
  liquidityRole: 'maker' | 'taker'
  fees?: {
    exchange: string
    mosaiq: string
  }
  createdAtMs: number
}
```

Venue codes include `hyperliquid`, `hyperliquid:xyz`, `hyperliquid:mkts`, and
`lighter`. Treat them as opaque strings so newly enabled venues do not require a
client release.

`fees.exchange` and `fees.mosaiq` are the separate USDC amounts charged for
that confirmed fill. They may be negative when the corresponding maker rate is
a rebate. Fees belong to fills rather than order lifecycle updates: an unfilled
order pays no fee, and a partially filled order receives one fee object per fill.

:::note\[Snapshot/update batches]
For every topic that emits `type: 'snapshot' | 'update'`, initial snapshots are
capped at 20 rows and later update batches are capped at 100 rows. Larger
backlogs are drained over subsequent update messages.
:::

## Margin Fields

* `availableFunds`: equity minus initial margin; room to open more risk.
* `excessLiquidity`: equity minus maintenance margin; account health buffer.
* `leverage`: `grossNotional / accountValue`; a metric, not a setting.

See [Cross Margin](/concepts/portfolio-margin) for the beta margin model.
