> ## Documentation Index
> Fetch the complete documentation index at: https://public-perps-docs.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Streaming

> Subscribe to live market context, orderbook updates, fills, and order status via WebSocket

The `PerpsWsClient` provides streaming data over WebSocket. Connections are made **directly to the DEX** for lowest latency — the SDK discovers the WebSocket URL from `GET /providers` automatically.

All incoming data is normalized to the same types used by REST responses, so your application logic works identically whether data comes from a REST call or a WebSocket event.

<Warning>
  WebSocket support is provider-specific. Not all channels may be supported by every provider — check the provider's documentation for available channels.
</Warning>

## Setup

```typescript theme={null}
import { createPerpsClient, PerpsWsClient } from '@lifi/perps-sdk';
import { hyperliquidWsProvider } from '@lifi/perps-sdk-provider-hyperliquid';

const client = createPerpsClient({ integrator: 'my-app', apiKey: 'your-api-key' });
const ws = new PerpsWsClient(client, {
  wsProviders: { hyperliquid: hyperliquidWsProvider() },
});
```

The `PerpsWsClient` lazily initializes connections — no WebSocket is opened until you call `subscribe()`.

## Subscribe

`subscribe()` is **async** and returns a `Promise<() => void>` — awaiting it yields the unsubscribe function. The descriptor's `dex` field selects the venue (use the `key` from `getProviders()`, e.g., `'hyperliquid'` or `'lighter'`).

```typescript theme={null}
const unsub = await ws.subscribe(
  { channel: 'orderbook', dex: 'hyperliquid', marketId: 'ETH' },
  (event) => {
    console.log('Best bid:', event.data.bids[0]?.price);
    console.log('Best ask:', event.data.asks[0]?.price);
  }
);

// Later: stop receiving updates
unsub();
```

`subscribe()` accepts an optional third argument, `onStatus`, a listener for the underlying connection's health. It fires `'reconnecting'` on a transient drop and the terminal `'disconnected'` once auto-reconnect is abandoned, so consumers can surface a reconnecting/disconnected state instead of silently showing stale data.

```typescript theme={null}
const unsub = await ws.subscribe(
  { channel: 'orderbook', dex: 'hyperliquid', marketId: 'ETH' },
  (event) => updateBook(event.data),
  (status) => {
    // status: 'connected' | 'reconnecting' | 'disconnected'
    setConnectionState(status);
  }
);
```

## getQuote

One-shot fill quote for `size` USD notional of `symbol` on a single venue: VWAP expected fill, price impact (bps), base-tier taker fee, and funding (perps only). This is a plain request/response service (not a WS subscription) — the streaming counterpart is [`subscribeQuote`](#subscribequote), and both share the `GetQuoteParams` shape. Cross-venue comparison is a consumer-side loop over `client.providers`.

```typescript theme={null}
import { getQuote } from '@lifi/perps-sdk';

const quote = await getQuote(client, {
  provider: 'hyperliquid',
  symbol: 'BTC',
  side: 'buy',
  size: 10_000,
  type: 'perps',
});
console.log(quote.expectedFillPrice, quote.priceImpactBps);
```

`GetQuoteParams`:

| Field      | Type        | Description                  |
| ---------- | ----------- | ---------------------------- |
| `provider` | `string`    | Provider identifier          |
| `symbol`   | `string`    | Display symbol, e.g. `"BTC"` |
| `side`     | `QuoteSide` | `'buy' \| 'sell'`            |
| `size`     | `number`    | USD notional to fill         |
| `type`     | `TradeType` | `'perps' \| 'spot'`          |

Throws a `PerpsError` when the provider plugin is not registered, no market matches the symbol and type, or on network / parsing errors.

## subscribeQuote

Stream live fill quotes for a market. The provider's WS plugin layers the quote on its orderbook channel, so a concurrent `orderbook` subscription on the same market shares a single wire subscription. Async; returns a `Promise<() => void>` unsubscribe function.

```typescript theme={null}
const unsub = await ws.subscribeQuote(
  { provider: 'hyperliquid', symbol: 'BTC', side: 'buy', size: 10_000, type: 'perps' },
  (quote) => {
    console.log('Expected fill price:', quote.expectedFillPrice);
    console.log('Price impact (bps):', quote.priceImpactBps);
  }
);
```

| Parameter | Type             | Description                                                                                                                                                                                    |
| --------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `params`  | `GetQuoteParams` | `{ provider, symbol, side, size, type }` — `symbol` is the display symbol (e.g. `"BTC"`), `side` is `'buy' \| 'sell'`, `size` is the USD notional as a `number`, `type` is `'perps' \| 'spot'` |
| `onQuote` | `QuoteListener`  | Called with each `Quote` update                                                                                                                                                                |

Throws a `PerpsError` when no WS provider factory is registered for `params.provider`, or no market matches the symbol and type.

## Available Channels

### marketsContext

Full context for every market on the DEX, keyed by `marketId`. Use this when a UI needs mark price, oracle price, 24h volume, open interest, or funding for the whole market list.

```typescript theme={null}
await ws.subscribe(
  { channel: 'marketsContext', dex: 'hyperliquid' },
  (event) => {
    // event.data: Record<string, MarketContext> — marketId -> context
    const btc = event.data['BTC'];
    console.log('BTC mark:', btc?.markPrice);
    console.log('BTC funding:', btc?.funding?.rate);
  }
);
```

### marketContext

Full context for a specific market. The event data is a `MarketContext` snapshot, so consumers should replace their cached context for that market when a new event arrives.

```typescript theme={null}
await ws.subscribe(
  { channel: 'marketContext', dex: 'hyperliquid', marketId: 'BTC' },
  (event) => {
    // event.data: MarketContext
    console.log('Mark:', event.data.markPrice);
    console.log('Oracle:', event.data.oraclePrice);
    console.log('24h volume:', event.data.volume24h);
  }
);
```

### orderbook

L2 orderbook for a specific market. Fires on every book update. Pass an optional `depth` to limit price levels (provider default if omitted), and an optional `priceStep` to request a price granularity — the desired bucket width in quote currency (e.g. `10` buckets a BTC book into \$10-wide levels). Providers that aggregate the book server-side honour `priceStep` best-effort; providers that stream the full book ignore it, and `undefined` requests full precision.

```typescript theme={null}
await ws.subscribe(
  { channel: 'orderbook', dex: 'hyperliquid', marketId: 'BTC', depth: 5, priceStep: 10 },
  (event) => {
    // event.data: OrderbookResponse — { bids, asks, timestamp, ... }
    const spread = Number(event.data.asks[0].price) - Number(event.data.bids[0].price);
    console.log('Spread:', spread);
  }
);
```

### candle

OHLCV candle updates for a specific market and interval.

```typescript theme={null}
await ws.subscribe(
  { channel: 'candle', dex: 'hyperliquid', marketId: 'BTC', interval: '1h' },
  (event) => {
    // event.data: Candle — { t, o, h, l, c, v }
    console.log('Close:', event.data.c);
  }
);
```

### trades

Public trade prints for a specific market. Fires as trades execute on the venue.

```typescript theme={null}
await ws.subscribe(
  { channel: 'trades', dex: 'hyperliquid', marketId: 'BTC' },
  (event) => {
    // event.data: Trade[] — { provider, marketId, price, size, side, timestamp, id? }
    // `side` is the taker (aggressor) side: 'buy' or 'sell'.
    for (const trade of event.data) {
      console.log(trade.side, trade.size, '@', trade.price);
    }
  }
);
```

### orderUpdates

Live order status changes for a user's orders. Requires the user's wallet address.

```typescript theme={null}
await ws.subscribe(
  { channel: 'orderUpdates', dex: 'hyperliquid', address: '0x...' },
  (event) => {
    // event.data: { openOrders: OpenOrder[]; triggerOrders: TriggerOrder[]; terminated: string[] }
    // openOrders / triggerOrders are upserts; terminated lists orderIds that
    // just reached a terminal status — evict them from both buckets.
    for (const order of event.data.openOrders) {
      console.log(order.orderId, order.filledSize);
    }
    for (const orderId of event.data.terminated) {
      console.log('terminated:', orderId);
    }
  }
);
```

### fills

Live fill notifications for a user's trades.

```typescript theme={null}
await ws.subscribe(
  { channel: 'fills', dex: 'hyperliquid', address: '0x...' },
  (event) => {
    // event.data: Fill[]
    for (const fill of event.data) {
      console.log(fill.market.baseAsset.displaySymbol, fill.side, fill.size, '@', fill.price);
    }
  }
);
```

### positions

Live position updates for a user.

```typescript theme={null}
await ws.subscribe(
  { channel: 'positions', dex: 'hyperliquid', address: '0x...' },
  (event) => {
    // event.data: Position[]
    for (const pos of event.data) {
      console.log(pos.market.baseAsset.displaySymbol, pos.side, pos.size, 'PnL:', pos.unrealizedPnl);
    }
  }
);
```

### spotBalances

Live spot balance updates for a user (e.g., USDC collateral held on the DEX).

```typescript theme={null}
await ws.subscribe(
  { channel: 'spotBalances', dex: 'hyperliquid', address: '0x...' },
  (event) => {
    // event.data: (Balance & { locked: string })[] — { categoryId, asset, units, valueUsd, locked }
    for (const bal of event.data) {
      console.log(bal.asset.displaySymbol, 'units:', bal.units, 'locked:', bal.locked);
    }
  }
);
```

### accountSummary

Live account roll-up for a user — portfolio value, available margin, margin used, and unrealized PnL. Requires the user's wallet address. Field coverage matches the venue's own stream (e.g. Hyperliquid's `portfolioValue` covers perps equity only; spot balances have their own [`spotBalances`](#spotbalances) channel).

```typescript theme={null}
await ws.subscribe(
  { channel: 'accountSummary', dex: 'hyperliquid', address: '0x...' },
  (event) => {
    // event.data: AccountSummary
    //   { portfolioValue, availableMargin, marginUsed, unrealizedPnl } — decimal strings
    console.log('Portfolio value:', event.data.portfolioValue);
    console.log('Available margin:', event.data.availableMargin);
  }
);
```

## Multiple Subscriptions

You can subscribe to multiple channels simultaneously. Each returns an independent unsubscribe function.

```typescript theme={null}
const unsubMarketContext = await ws.subscribe(
  { channel: 'marketsContext', dex: 'hyperliquid' },
  (event) => updateMarketContextDisplay(event.data)
);

const unsubBook = await ws.subscribe(
  { channel: 'orderbook', dex: 'hyperliquid', marketId: 'BTC' },
  (event) => updateOrderbookDisplay(event.data)
);

const unsubOrders = await ws.subscribe(
  { channel: 'orderUpdates', dex: 'hyperliquid', address: userAddress },
  (event) => updateOrdersDisplay(event.data)
);

// Unsubscribe individually
unsubBook();

// Or close everything at once
ws.close();
```

## Connection Lifecycle

* **Lazy connection** — The WebSocket connects on the first `subscribe()` call for each DEX
* **Automatic reconnection** — Jittered exponential backoff on disconnect. The first-retry delay is drawn once per socket from `[500, 1500)`ms (to de-synchronize reconnect storms) and grows exponentially, capped at 10s. After 10 attempts the socket is declared `disconnected`.
* **Resubscription** — All active subscriptions are automatically re-sent after reconnection
* **Keepalive** — 30-second ping/pong heartbeat to detect stale connections
* **Refcounted** — Duplicate subscriptions to the same channel are deduplicated; the upstream subscription is removed only when the last listener unsubscribes
* **Manual reconnect** — `ws.reconnect(provider)` forces an already-created provider whose socket reached terminal `disconnected` to reconnect with a fresh retry budget. It is a safe no-op when the provider is unknown or not terminal.

## Cleanup

Always close the client when you're done to release WebSocket connections:

```typescript theme={null}
// Close all connections and subscriptions
ws.close();
```

Individual subscriptions can be cleaned up by calling the returned unsubscribe function. The underlying WebSocket connection stays open as long as at least one subscription is active on that DEX.

## Type Safety

The `subscribe` method is fully typed — the callback receives the correct event type based on the subscription channel:

```typescript theme={null}
// TypeScript knows event.data is Record<string, MarketContext>
await ws.subscribe({ channel: 'marketsContext', dex: 'hyperliquid' }, (event) => {
  event.data; // Record<string, MarketContext> — no cast needed
});

// TypeScript knows event.data is OrderbookResponse
await ws.subscribe({ channel: 'orderbook', dex: 'hyperliquid', marketId: 'BTC' }, (event) => {
  event.data.bids; // OrderbookLevel[] — no cast needed
});
```
