# Write WebSocket — `/v1/exchange/ws`

`/v1/exchange/ws` is the authenticated low-latency write transport. It accepts
the same `orders[]` batch shape as [`POST /v1/exchange`](/api/exchange) for
place, modify, and cancel requests, but it is intentionally narrower so each
frame has predictable ordering and risk sequencing.

This is separate from the unauthenticated read WebSocket at
[`/v1/ws`](/api/websocket).

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

For connection management, pipelining, lifecycle ordering, recovery, and private
networking recommendations, see [Optimizing latency](/api/optimizing-latency).

## Authentication

Open the WebSocket with the same API-key auth header used by private HTTP
endpoints:

```http
Authorization: Bearer <your-api-key>
```

## Cancel orders on disconnect

Opt in to automatic cancellation when opening the connection:

```text
wss://production.mosaiq.0d.finance/v1/exchange/ws?cancelAllOnDisconnect=true&reconnectGraceMs=2000
```

| Query parameter | Default | Description |
| --- | --- | --- |
| `cancelAllOnDisconnect` | `false` | Cancel the authenticated user's open orders after their last opted-in write WebSocket disconnects. |
| `reconnectGraceMs` | `2000` | Wait this many milliseconds before cancelling so an opted-in reconnect can suppress the trigger. Requires `cancelAllOnDisconnect=true`; maximum `30000`. |

Only connections with `cancelAllOnDisconnect=true` participate. If several
opted-in connections exist for the same user, cancellation starts only after
the last one disconnects. A new opted-in connection during `reconnectGraceMs`
cancels the pending trigger. The normal 30-second inbound idle timeout also
counts as a disconnect.

:::warning\[Best effort only]
Do not rely on disconnect cancellation as a guaranteed risk control. A gateway
restart, network or venue failure, or partial cancellation can leave orders
open. After reconnecting, always reconcile with
[`GET /v1/openOrders`](/api/open-orders) and explicitly cancel any remaining
orders.
:::

## Rate Limits

Exchange requests share the authenticated user's REST exchange budget, including
across API-key rotations and concurrent connections. All incoming frames also
share the per-IP `WS_MESSAGES_PER_MINUTE_PER_IP` abuse limit; exceeding it closes
the connection.

## Frame

Each client frame carries a numeric `id` and one exchange `request`. Place
orders use the same fields as REST: a limit carries `p` and `t`, while a market
order carries `slippage` and no price or TIF. The write WebSocket requires `c`
on every placement so its asynchronous result can be correlated unambiguously.

```jsonc
{
  "id": 1,
  "request": {
    "type": "place",
    "orders": [
      {
        "a": "BTC",
        "b": "buy",
        "p": "64000",
        "s": "1",
        "venue": "hyperliquid",
        "t": "Alo",
        "c": "0x00000000000000000000000000000005"
      }
    ]
  }
}
```

Every server text frame uses the same post envelope as Hyperliquid:

```jsonc
{
  "channel": "post",
  "data": {
    "id": 1,
    "response": {
      "type": "action" | "error",
      "payload": {
        // Hyperliquid-style post payload for order/cancel actions.
      }
    }
  }
}
```

For order actions, successful frames mirror Hyperliquid's post response shape.
`statuses[]` is aligned by index with the request `orders[]`.

```jsonc
{
  "channel": "post",
  "data": {
    "id": 1,
    "response": {
      "type": "action",
      "payload": {
        "status": "ok",
        "response": {
          "type": "order",
          "data": {
            "statuses": [
              { "resting": { "cloid": "0x00000000000000000000000000000005" } }
            ]
          }
        }
      }
    }
  }
}
```

Mosaiq applies its risk checks before forwarding to the selected venue. A
`place` or `modify` frame gets no success response until the venue response is
observed. Use the public `cloid` returned on placement for later modify/cancel
requests.

`cancel` sends a single `response.type: "action"` frame with one status per
requested cancel.

## Scope

The write WebSocket deliberately supports less than REST:

* homogeneous batches of place, modify, or cancel actions using `orders[]`;
* Hyperliquid or Lighter only;
* limit placements with explicit `p`, `t`, and `c`, where the supported limit
  TIFs are `Alo` and `Ioc`;
* market orders with `slippage`, `c`, and optional `reduceOnly`; they use the
  same venue-side bounds as REST;
* native `modify` supports Hyperliquid and Lighter, targets the public `cloid`,
  and accepts a new price and size without `t`; the existing TIF is preserved;
* no funding actions or `mass_cancel`.

Malformed or ambiguous batches are rejected at frame level with
`response.type: "error"`. This includes unsupported request types, empty or
over-cap batches, and duplicate `cloid` values inside one `modify` batch.

Item-level failures are returned in aligned `statuses[]` slots and do not submit
that item. Other valid items in the same frame can still be submitted.

Use [`POST /v1/exchange`](/api/exchange) for the full REST surface.
