> ## Documentation Index
> Fetch the complete documentation index at: https://docs.0xarchive.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Lighter WebSocket channels

> Subscribe to live Lighter order books, trades, open interest, and funding on the 0xArchive WebSocket, and replay all six Lighter channels.

Lighter has six WebSocket channels. Four accept live subscriptions, and all six support historical replay. Live messages use the same `subscribe` command and `data` envelope as live Hyperliquid channels, with the payload shapes documented on this page.

| Channel                 | Live subscription | Historical replay | Live message                                                       |
| ----------------------- | ----------------- | ----------------- | ------------------------------------------------------------------ |
| `lighter_orderbook`     | Yes               | Yes               | Full top-20 L2 book, at most one per interval                      |
| `lighter_trades`        | Yes               | Yes               | Array of fills, two legs per trade                                 |
| `lighter_open_interest` | Yes               | Yes               | Market context: open interest, funding, prices, and 24-hour volume |
| `lighter_funding`       | Yes               | Yes               | The same market-context message as `lighter_open_interest`         |
| `lighter_candles`       | No                | Yes               | Replay only; use REST for current candles                          |
| `lighter_l3_orderbook`  | No                | Yes               | Replay only; use REST for current L3 depth                         |

## Endpoint and access

Connect to `wss://api.0xarchive.io/ws` with the same server-side authentication as any other channel. See [WebSocket connection](/websocket/connection). The `stream.0xarchive.io` endpoint does not carry Lighter channels: a Lighter subscribe there returns an `error` that names `wss://api.0xarchive.io/ws`.

Every plan, including Free, can subscribe to the live Lighter channels. Each delivered message is metered like other WebSocket data; see [Credits](/core-concepts/credits). For `lighter_orderbook`, each book sent is one metered message, so the interval you choose caps the order-book message rate. Your plan's subscription and connection limits apply, and each connection accepts at most 10 subscribe operations per second. See [WebSocket tier limits](/websocket/tier-limits).

## Subscribe

Use the Lighter symbols returned by `GET /v1/lighter/instruments`. Symbols are case-insensitive on subscribe, and the acknowledgment returns the symbol as that route lists it, which is uppercase for markets such as `BTC` and `ETH`. Match data messages to subscriptions without regard to case. Keep Lighter subscriptions in the Lighter family even when the same symbol text is also a Hyperliquid market.

<CodeGroup>
  ```javascript Order book theme={"theme":"github-dark"}
  ws.send(JSON.stringify({
    op: "subscribe",
    channel: "lighter_orderbook",
    symbol: "BTC"
  }));
  ```

  ```javascript Order book with interval theme={"theme":"github-dark"}
  ws.send(JSON.stringify({
    op: "subscribe",
    channel: "lighter_orderbook",
    symbol: "BTC",
    interval_ms: 250
  }));
  ```

  ```javascript Trades theme={"theme":"github-dark"}
  ws.send(JSON.stringify({
    op: "subscribe",
    channel: "lighter_trades",
    symbol: "BTC"
  }));
  ```

  ```javascript Open interest theme={"theme":"github-dark"}
  ws.send(JSON.stringify({
    op: "subscribe",
    channel: "lighter_open_interest",
    symbol: "BTC"
  }));
  ```

  ```javascript Funding theme={"theme":"github-dark"}
  ws.send(JSON.stringify({
    op: "subscribe",
    channel: "lighter_funding",
    symbol: "BTC"
  }));
  ```
</CodeGroup>

The server confirms each subscription before you should treat it as active:

```json theme={"theme":"github-dark"}
{"type":"subscribed","channel":"lighter_orderbook","coin":"BTC","symbol":"BTC"}
```

To stop a stream, send `unsubscribe` with the same channel and symbol:

```javascript theme={"theme":"github-dark"}
ws.send(JSON.stringify({
  op: "unsubscribe",
  channel: "lighter_trades",
  symbol: "BTC"
}));
```

```json theme={"theme":"github-dark"}
{"type":"unsubscribed","channel":"lighter_trades","coin":"BTC","symbol":"BTC"}
```

Sending `subscribe` again for a channel and symbol you already hold replaces that subscription. Use this to change `interval_ms` without unsubscribing first. The server sends a new `subscribed` acknowledgment. The subscription limit is checked before the replacement, so if the connection is already at its plan's [subscription limit](/websocket/tier-limits), unsubscribe first, then subscribe with the new `interval_ms`.

Live Lighter data arrives in the envelope `type`, `channel`, `coin`, `symbol`, and `data`, with `type` set to `data`. The `data` payload depends on the channel, as described below. Notices arrive as `error` messages; see [Errors and lag notices](#errors-and-lag-notices).

## `lighter_orderbook`

Each message is a full top-20 L2 book, not a diff. Replace your local book with every message instead of applying it as a change. This example is a real message shortened to three levels per side; live messages carry up to 20 levels per side.

```json theme={"theme":"github-dark"}
{
  "type": "data",
  "channel": "lighter_orderbook",
  "coin": "BTC",
  "symbol": "BTC",
  "data": {
    "coin": "BTC",
    "time": 1790294171459,
    "levels": [
      [
        { "px": "84368.7", "sz": "0.00020", "n": 1 },
        { "px": "84368.6", "sz": "0.00020", "n": 1 },
        { "px": "84368.3", "sz": "0.00010", "n": 1 }
      ],
      [
        { "px": "84368.8", "sz": "0.05720", "n": 1 },
        { "px": "84368.9", "sz": "0.14223", "n": 1 },
        { "px": "84369.1", "sz": "0.01198", "n": 1 }
      ]
    ]
  }
}
```

| Field            | Meaning                                                              |
| ---------------- | -------------------------------------------------------------------- |
| `data.coin`      | Lighter market symbol                                                |
| `data.time`      | Lighter's book update time, in milliseconds since the Unix epoch     |
| `data.levels[0]` | Bids, best (highest) price first, up to 20 levels                    |
| `data.levels[1]` | Asks, best (lowest) price first, up to 20 levels                     |
| `px`, `sz`       | Price and size as decimal strings, exactly as Lighter publishes them |
| `n`              | Always `1`, because Lighter does not publish per-level order counts  |

### Order-book rate and `interval_ms`

The server sends the newest book at most once per interval. The default interval is one book per second. To choose another rate, pass `interval_ms` on subscribe with a value from `100` to `5000`, inclusive. When the book changes several times within one interval, only the newest book is sent, and a newer book is never replaced by an older one.

The interval is a maximum rate, not a heartbeat. A book is sent only when Lighter publishes an update, so quiet markets send fewer books than the interval allows. On subscribe, the current book is sent immediately when one is available. An illiquid market can go minutes without a change, so the first book can stay current for a long time.

`interval_ms` is accepted only on `lighter_orderbook`. See [Errors and lag notices](#errors-and-lag-notices) for the messages returned by an invalid value.

## `lighter_trades`

Each message carries an array of fills. Every trade appears as two legs, one per side, with the same `tid`. This example is a real message with both legs of one trade:

```json theme={"theme":"github-dark"}
{
  "type": "data",
  "channel": "lighter_trades",
  "coin": "BTC",
  "symbol": "BTC",
  "data": [
    {
      "coin": "BTC",
      "side": "A",
      "px": "84367.9",
      "sz": "0.00003",
      "time": 1790294182211,
      "hash": "0000001dc8774b28000001a0d5d94943000000000000000000000000000000000000000000000000",
      "tid": 31944180930,
      "oid": 562953419896990,
      "crossed": false,
      "dir": null,
      "fee": null,
      "fee_token": null,
      "closed_pnl": null,
      "start_position": "109.79011",
      "users": ["281474976623827"]
    },
    {
      "coin": "BTC",
      "side": "B",
      "px": "84367.9",
      "sz": "0.00003",
      "time": 1790294182211,
      "hash": "0000001dc8774b28000001a0d5d94943000000000000000000000000000000000000000000000000",
      "tid": 31944180930,
      "oid": 844421425107071,
      "crossed": true,
      "dir": null,
      "fee": null,
      "fee_token": null,
      "closed_pnl": null,
      "start_position": "0.03940",
      "users": ["713845"]
    }
  ]
}
```

| Field                                   | Meaning                                                                           |
| --------------------------------------- | --------------------------------------------------------------------------------- |
| `coin`                                  | Lighter market symbol                                                             |
| `side`                                  | `A` for the ask side, `B` for the bid side                                        |
| `px`, `sz`                              | Price and size as decimal strings                                                 |
| `time`                                  | Trade time, in milliseconds since the Unix epoch                                  |
| `hash`                                  | Lighter transaction hash                                                          |
| `tid`                                   | Trade ID, shared by both legs of the trade                                        |
| `oid`                                   | Order ID for this leg's side                                                      |
| `crossed`                               | `true` for the taker leg, `false` for the maker leg                               |
| `users`                                 | The Lighter account index for this leg, as a one-element array of strings         |
| `start_position`                        | This account's signed position in the market before the trade                     |
| `dir`, `fee`, `fee_token`, `closed_pnl` | Always `null` in live messages, because Lighter's live stream does not carry them |

Count trades by distinct `tid`, not by array length. Compute volume from `sz` on one leg per `tid`; adding both legs doubles it. Keep a record of `tid` values you have processed so a trade is counted once across messages.

### Preliminary and finalized trades

Live trades are delivered as they happen and are preliminary. The finalized record, which adds fields the live stream does not carry, such as fees, is served by `GET /v1/lighter/trades/{symbol}`. That route returns only reconciled trades; `meta.finalized_through` reports how far reconciliation has reached. `GET /v1/lighter/trades/{symbol}/recent` serves the preliminary tier over REST. See [Trades](/rest-api/trades) for both tiers.

## `lighter_open_interest` and `lighter_funding`

Both channels carry the same market-context message. Subscribe to the channel name that fits your code. If you subscribe to both for one symbol, each update arrives on both channels and each message is metered. This example is a real message:

```json theme={"theme":"github-dark"}
{
  "type": "data",
  "channel": "lighter_open_interest",
  "coin": "BTC",
  "symbol": "BTC",
  "data": {
    "coin": "BTC",
    "ctx": {
      "openInterest": "172706178.266310",
      "funding": "0.000012",
      "premium": "-0.000327",
      "markPx": "84363.5",
      "oraclePx": "84397.0",
      "midPx": "84368.8",
      "dayNtlVlm": "908611371.550746",
      "dayBaseVlm": "10808.97087",
      "prevDayPx": "84285.9",
      "impactPxs": null
    }
  }
}
```

| Field          | Meaning                                                                                                                                                                                                                    |
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `openInterest` | Lighter's reported open interest, the same quantity as `open_interest` from `GET /v1/lighter/openinterest/{symbol}/current`                                                                                                |
| `funding`      | Lighter's current funding rate as a fraction, the same quantity and units as `funding_rate` from `GET /v1/lighter/funding/{symbol}/current`. Lighter publishes this rate in percent; the message carries it divided by 100 |
| `premium`      | Premium, also as a fraction                                                                                                                                                                                                |
| `markPx`       | Mark price                                                                                                                                                                                                                 |
| `oraclePx`     | Lighter's index price                                                                                                                                                                                                      |
| `midPx`        | Mid price                                                                                                                                                                                                                  |
| `dayNtlVlm`    | 24-hour volume in the quote currency                                                                                                                                                                                       |
| `dayBaseVlm`   | 24-hour volume in the base asset                                                                                                                                                                                           |
| `prevDayPx`    | Previous-day reference price, derived from the last trade price and Lighter's 24-hour percent change                                                                                                                       |
| `impactPxs`    | Always `null`, because Lighter has no impact prices                                                                                                                                                                        |

Numeric values are decimal strings. Updates arrive as Lighter publishes them, about once per second per market. On subscribe, the latest values are sent immediately when they are available. The message has no timestamp field, so record the receive time if your application needs one.

## Errors and lag notices

Rejected commands and stream notices arrive as `error` messages: `{"type":"error","message":"..."}`. Log the full message text.

| Situation                                                                                      | Message                                                                                                                                                    | What to do                                                                                                     |
| ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `interval_ms` on a channel other than `lighter_orderbook`                                      | `interval_ms is only supported on lighter_orderbook.`                                                                                                      | Remove `interval_ms` from the subscribe command                                                                |
| `interval_ms` outside `100` to `5000`                                                          | `interval_ms must be between 100 and 5000 for lighter_orderbook (got 50). Leave it out for one book a second.`                                             | Send a value in range, or leave it out for the default                                                         |
| Live subscribe to `lighter_candles` or `lighter_l3_orderbook`                                  | An `error` stating that the channel does not support real-time subscriptions                                                                               | Use REST for current reads and `op: "replay"` for history                                                      |
| Lighter subscribe on `stream.0xarchive.io`                                                     | `The 'lighter_trades' channel is not available on this endpoint. Subscribe to it on wss://api.0xarchive.io/ws.`                                            | Subscribe on `wss://api.0xarchive.io/ws`                                                                       |
| The connection falls behind `lighter_trades`                                                   | `Dropped ~N live lighter_trades messages for BTC: your connection fell behind the Lighter stream, and those trades were not delivered.`                    | The stream continues, but the dropped trades are not resent. Fill the gap from the REST trade routes           |
| The connection falls behind `lighter_open_interest` or `lighter_funding`                       | `Dropped ~N live lighter_funding messages for BTC: your connection fell behind the Lighter stream. Each message is a full state, so the next one resyncs.` | No repair is needed; the next message is complete                                                              |
| The connection stays behind on `lighter_trades`, `lighter_open_interest`, or `lighter_funding` | `Stopped the lighter_trades stream for BTC: your connection is too slow to keep up. Re-subscribe to resume.`                                               | That subscription sends nothing more. Reduce the subscription set or speed up processing, then subscribe again |

`lighter_orderbook` does not send drop notices for books skipped by its interval. Each book is a full state, so skipping an older book loses nothing, and a newer book is never replaced by an older one.

Move message handling off the socket callback when processing is slow, and watch for these notices in the same place you watch reconnects and gaps. See [WebSocket limits](/websocket/limits) for consumer sizing.

## Live and replay message shapes

Replay is available on all six Lighter channels, and `op: "replay"` returns `historical_data` rows in the stored Lighter shape, not the live shape above. Use separate parsers for live `data` messages and replay `historical_data` messages.

| Channel                 | Live `data` payload                                                                                                | Replay `historical_data` payload                                                                                                                    |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `lighter_orderbook`     | `coin`, `time`, and `levels` as `[bids, asks]`, each level with `px`, `sz`, and `n`; up to 20 levels per side      | `coin`, `time`, separate `bids` and `asks` arrays with `px`, `sz`, and `n`, plus `midPrice`, `spread`, and `spreadBps`                              |
| `lighter_trades`        | Array of fill legs with `px`, `sz`, `side`, `tid`, `oid`, `crossed`, `users`, `start_position`, `hash`, and `time` | Lighter trade records with fields such as `price`, `size`, `tradeId`, `orderId`, `accountIndex`, `isMaker`, `fee`, and `realizedPnl`                |
| `lighter_open_interest` | `coin` and the `ctx` object                                                                                        | `coin`, `openInterest`, `markPrice`, `oraclePrice`, `midPrice`, `dayNtlVolume`, `prevDayPrice`, `impactBidPrice`, `impactAskPrice`, and `timestamp` |
| `lighter_funding`       | `coin` and the same `ctx` object                                                                                   | `coin`, `fundingRate`, `premium`, and `timestamp`                                                                                                   |
| `lighter_candles`       | No live channel                                                                                                    | Candle rows                                                                                                                                         |
| `lighter_l3_orderbook`  | No live channel                                                                                                    | L3 order-level snapshots                                                                                                                            |

A bounded Lighter replay looks like this:

```javascript theme={"theme":"github-dark"}
ws.send(JSON.stringify({
  op: "replay",
  channel: "lighter_trades",
  symbol: "BTC",
  start: 1785193200000,
  end: 1785196800000,
  speed: 10
}));
```

A replay reads stored history and completes with `replay_completed`; it does not continue into the live stream. See [WebSocket replay](/websocket/replay) for replay controls, gap events, and run manifests.

## Server-side example

Keep the API key on the server. This Node.js example subscribes to BTC books at two per second and to BTC trades, and counts each trade once:

```javascript theme={"theme":"github-dark"}
import WebSocket from "ws";

const apiKey = process.env.OXARCHIVE_API_KEY;
if (!apiKey) throw new Error("Set OXARCHIVE_API_KEY");

const ws = new WebSocket("wss://api.0xarchive.io/ws", {
  headers: { Authorization: `Bearer ${apiKey}` }
});

const seenTrades = new Set(); // prune old tids in a long-running client

ws.onopen = () => {
  ws.send(JSON.stringify({ op: "subscribe", channel: "lighter_orderbook", symbol: "BTC", interval_ms: 500 }));
  ws.send(JSON.stringify({ op: "subscribe", channel: "lighter_trades", symbol: "BTC" }));
};

ws.onmessage = (event) => {
  const message = JSON.parse(event.data);
  if (message.type === "error") return console.error(message.message);
  if (message.type !== "data") return;

  if (message.channel === "lighter_orderbook") {
    const [bids, asks] = message.data.levels;
    console.log(message.symbol, "bid", bids[0]?.px, "ask", asks[0]?.px);
  }

  if (message.channel === "lighter_trades") {
    for (const leg of message.data) {
      if (seenTrades.has(leg.tid)) continue; // the other leg of a trade already counted
      seenTrades.add(leg.tid);
      console.log(message.symbol, "trade", leg.tid, leg.px, leg.sz);
    }
  }
};
```

## Related pages

Use [WebSocket channels](/websocket/channels) for the full channel matrix, [Real-time streams](/websocket/real-time) for live subscription design, [Lighter REST](/rest-api/lighter) for current and historical Lighter routes, and [WebSocket message schema](/websocket/schema) for the command and event contract.
