# Market data WebSocket — `/v1/market-data/ws`

Event-driven `bbo` and full `l2Book` updates for Hyperliquid venues and
Lighter. Hyperliquid BBO prefers Mosaiq's own node with sentry peering while
its book is fresh. Public Hyperliquid BBO subscriptions stay connected and
supply updates when the node is unavailable or its freshness is unproven.

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

## Authentication

The upgrade requires an API key. A missing or invalid key returns HTTP `401`;
Para JWTs are not accepted. Browsers cannot set the header and must not hold
the key.

```ts
import WebSocket from 'ws'

const ws = new WebSocket('wss://production.mosaiq.0d.finance/v1/market-data/ws', {
  headers: { Authorization: `Bearer ${process.env.MOSAIQ_API_KEY}` },
})
```

Rate limits and heartbeats are shared with the
[user-data WebSocket](/api/websocket#rate-limits). Extra market-data frames
beyond the message budget are dropped silently.

Venue maintenance announcements use
[`maintenance` on the execution WebSocket](/api/exchange-websocket#maintenance-announcements).
This market-data socket rejects that topic.

## Subscribe

```json
{ "method": "subscribe", "subscription": { "type": "bbo", "venue": "hyperliquid:xyz", "coins": ["TSLA", "SILVER"] } }
{ "method": "subscribe", "subscription": { "type": "l2Book", "venue": "hyperliquid", "coins": ["BTC", "ETH"] } }
{ "method": "subscribe", "subscription": { "type": "l2Book", "venue": "lighter", "coins": ["BTC", "ETH"] } }
```

`bbo` and `l2Book` are accepted. `coins` are canonical uppercase symbols;
`venue` is `lighter` or one exact Hyperliquid venue code. There is no
`unsubscribe`. Each topic keeps its own subscription set: a new subscription
for a topic and venue replaces that venue's coin set and leaves other venues
and the other topic untouched.

A request is rejected atomically, leaving the previous subscription active,
when it lists more than 100 coins, when the connection would exceed 100
distinct markets per topic, or when the venue or any coin is unknown,
inactive, or unmapped. Duplicates are removed after normalization. Validation
errors carry the codes `invalid_websocket_subscription`, `too_many_coins`,
`unknown_venue`, `unknown_asset`, or `symbol_mapping_missing` with the
`topic`; malformed JSON and user-data topics are answered with
`websocket_error` and no `topic`.

## Messages

```ts
interface WebSocketMessage<T> {
  status: 'ok'
  topic: 'bbo' | 'l2Book'
  data: T
  server_time_ms: number
}

interface Bbo {
  coin: string
  venue: string
  time: number
  bbo: [Level | null, Level | null] // [bid, ask]
}

interface L2Book {
  coin: string
  venue: string
  time: number
  levels: [Level[], Level[]] // [bids, asks], best price first
}

interface Level {
  px: string
  sz: string
  n: number
}
```

Topics are independent of each other and of user-data topics.
`server_time_ms` is an observation timestamp, not a cursor. `data.time` is the
venue frame timestamp; the two come from different clocks.

Hyperliquid BBO messages preserve one complete frame from the selected upstream.
Hyperliquid L2 snapshots are forwarded raw and unbucketed from `hl-node-server`.
Lighter BBO comes from its ticker feed; its book is reconstructed from a snapshot plus deltas, a nonce
gap drops that market's book and re-subscribes it upstream, and every public
message is a complete book. Lighter does not publish order counts, so its
levels use `n: 1`.

Every `l2Book` message is a full snapshot: a later message replaces the
earlier one, and levels from different messages must not be combined.
Subscriptions start live: the first message for a market is the next upstream
update, and nothing retained from before the subscription is sent. A slow
client skips to the latest frame per market; a connection that stays
backpressured for two seconds is closed.

## Availability and recovery

Lighter availability is tracked per market and topic. A stale TRX ticker
stops TRX BBO updates and clears its retained quote. SILVER and CL BBO updates
continue. A Lighter order-book nonce gap invalidates that market's L2 book;
it also affects BBO when BBO is derived from the order book. BBO uses the
ticker by default.

Only connections subscribed to an affected market receive an
`upstream_unavailable` error for that topic. Subscribing while any requested
market is unavailable triggers the error immediately, unless that connection
has already received an error for the ongoing topic outage. The subscription
remains active, and healthy markets in the same subscription continue to
publish. Recovery resumes updates for the recovered market without a
separate recovery message or a replay of pre-outage values.

Errors are deduplicated per connection and topic, across all subscribed
markets and venues. Further market outages do not produce additional errors
until the gateway observes all subscribed markets for that topic as available.
Replacing subscriptions can also reset this suppression if every remaining
market is available.

The error identifies the topic only. It does not contain `coin`, `venue`,
`market_id`, or a list of unavailable markets. An error reports that at least
one subscribed market became unavailable or was unavailable at subscription.
A brief outage can recover before the notification is sent; the gateway
still reports that outage. The error does not mean every market has stopped.
An update confirms data for its own market only. Neither an update nor the
absence of an error proves that other subscribed markets are ready.

```json
{
  "status": "error",
  "topic": "bbo",
  "data": { "code": "upstream_unavailable", "msg": "BBO feed temporarily unavailable" },
  "server_time_ms": 1780000000001
}
```

For `l2Book`, the same error uses `"topic": "l2Book"` and
`"msg": "L2 book feed temporarily unavailable"`.

A shared Lighter upstream connection failure can still affect every market
on that feed. Closing the gateway's Lighter ingest channel invalidates every
Lighter market for that topic. Hyperliquid availability remains shared across
its `hyperliquid*` venue codes. Hyperliquid and Lighter outages remain
independent, as do the `bbo` and `l2Book` topics.

### Compatibility change: Lighter market readiness

Per-market readiness changes the previous documented behavior, which treated
Lighter availability as a single source-wide state. This is a behavioral
compatibility change for clients that relied on that state. The endpoint,
authentication, subscription schema, update schema, and error schema are
unchanged; existing message parsers need no changes.

| Situation | Previous behavior | Per-market behavior |
| --- | --- | --- |
| TRX has no valid quote during startup | All Lighter BBO publication waits | SILVER and CL can publish as soon as their own quotes are ready |
| TRX becomes stale | All Lighter BBO publication stops | Only TRX BBO publication stops |
| A connection subscribes only to SILVER and CL | A TRX outage can trigger its error | A TRX outage does not trigger its error |
| A connection subscribes to TRX, SILVER, and CL | A TRX outage suppresses all three | It receives a topic error while SILVER and CL continue to publish |

To migrate a client that used source-wide health, track freshness separately
for each `(topic, venue, coin)`. Continue processing healthy-market messages
after a topic error. Do not reset every market's freshness when any one market
resumes. The error stream is not a complete market-status feed.

## Connection lifecycle

During a controlled restart the server closes with code `1012` and reason
`service restart; reconnect`; new upgrades may return HTTP `503` while it
drains. After any close, reconnect with jittered exponential backoff,
authenticate, and resend every subscription.

A live heartbeat does not prove a market is producing data. Track the arrival
time of the latest update per topic and market and stop trading from a value
once it is older than a threshold suited to that market's activity.
