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

Event-driven `bbo` and full `l2Book` updates for Hyperliquid venues and
Lighter.

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` must be a non-empty list of canonical
asset symbols. Use an exact `venue` code from an active entry in
`assetData.universe[].venues[]`. 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
source frame timestamp; the two come from different clocks. BBO timestamps can
repeat or decrease. Apply BBO updates in arrival order for each `(venue, coin)`;
do not use `data.time` as a sequence number or a unique update identifier.

Each `bbo` message carries a complete bid/ask update.
A `null` bid or ask means that side is empty. Clear its previous value;
an empty side does not itself mean the feed is unavailable.

Hyperliquid L2 snapshots preserve exact price and size strings, without
bucketing, up to 500 levels per side. Lighter does not publish order counts,
so its levels use `n: 1`.

Every `l2Book` message is a full snapshot. Replace the previous book; do not
combine levels from different messages. Each new subscription starts with the
next update, without replaying an earlier value. Replacing a subscription
preserves pending L2 updates for markets that remain subscribed.

A slow client may skip intermediate frames and receive only the latest one
per market. If it cannot keep up for two seconds, the connection closes.

## Availability and recovery

Track availability separately for each `(topic, venue, coin)`. A Lighter market
that has not received a valid BBO update is unavailable without stopping
healthy markets.
A quiet Lighter market can retain a valid BBO; elapsed time since its last
message alone does not make the quote invalid.

A new Hyperliquid L2 subscription waits for that market's next snapshot.
A healthy quiet market can remain pending without an outage error, and there
is no guaranteed maximum wait. After a feed interruption, a new valid snapshot
is required before the affected market publishes again.
Hyperliquid L2 books are served only while Mosaiq's node is proven fresh, by
the same check that selects the BBO source. When the node falls more than two
seconds behind, L2 subscribers receive `upstream_unavailable` instead of an old
book, including subscribers that join during the outage. After the node
recovers, each market resumes on its next snapshot; a quiet market can stay
unavailable until its book changes.

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 feed outage can affect several markets at once. Hyperliquid BBO availability
is shared across its `hyperliquid*` venue codes; Hyperliquid L2 tracks each
requested market separately. Hyperliquid and Lighter outages are independent,
as are the `bbo` and `l2Book` topics.

Continue processing healthy-market messages after a topic error. An update
for one market does not restore availability for the others.

## 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. When its API key is revoked, rotated or expires, the server closes with
code `1008` and reason `api key revoked`. After any close, reconnect with
jittered exponential backoff, authenticate, and resend every subscription.

A live heartbeat does not prove that every market is producing data. Treat an
`upstream_unavailable` error or connection close as an invalidation signal. Do
not expire a Lighter BBO only because that market is quiet. Updates are
event-driven, and the last quote can remain current. Monitor message age, but
do not treat it as an expiry deadline unless the venue specifies one.
