Skip to main content
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 /dexes 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.

Setup

import { createPerpsClient, PerpsWsClient } from '@lifi/perps-sdk';

const client = createPerpsClient({ integrator: 'my-app' });
const ws = new PerpsWsClient(client);
The PerpsWsClient lazily initializes connections — no WebSocket is opened until you call subscribe().

Subscribe

All channels use the same pattern: pass a subscription descriptor and a callback. The returned function unsubscribes when called.
const unsub = await ws.subscribe(
  { channel: 'orderbook', dex: 'hyperliquid', symbol: '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();

Available Channels

prices

All mid prices for every market on the DEX. Fires on every price change.
await ws.subscribe(
  { channel: 'prices', dex: 'hyperliquid' },
  (event) => {
    // event.data: PricesResponse — { prices: Record<string, string> }
    console.log('BTC:', event.data.prices['BTC']);
  }
);

orderbook

L2 orderbook for a specific market. Fires on every book update.
await ws.subscribe(
  { channel: 'orderbook', dex: 'hyperliquid', symbol: 'BTC' },
  (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);
  }
);

trades

Public trade feed for a specific market.
await ws.subscribe(
  { channel: 'trades', dex: 'hyperliquid', symbol: 'BTC' },
  (event) => {
    // event.data: HistoryItem[]
    for (const trade of event.data) {
      console.log(trade.side, trade.size, '@', trade.price);
    }
  }
);

candle

OHLCV candle updates for a specific market and interval.
await ws.subscribe(
  { channel: 'candle', dex: 'hyperliquid', symbol: 'BTC', interval: '1h' },
  (event) => {
    // event.data: Candle — { t, o, h, l, c, v }
    console.log('Close:', event.data.c);
  }
);

orderUpdates

Live order status changes for a user’s orders. Requires the user’s wallet address.
await ws.subscribe(
  { channel: 'orderUpdates', dex: 'hyperliquid', address: '0x...' },
  (event) => {
    // event.data: Order[]
    for (const order of event.data) {
      console.log(order.orderId, order.status, order.filledSize);
    }
  }
);

fills

Live fill notifications for a user’s trades.
await ws.subscribe(
  { channel: 'fills', dex: 'hyperliquid', address: '0x...' },
  (event) => {
    // event.data: HistoryItem[]
    for (const fill of event.data) {
      console.log(fill.symbol, fill.side, fill.size, '@', fill.price);
    }
  }
);

positions

Live position updates for a user.
await ws.subscribe(
  { channel: 'positions', dex: 'hyperliquid', address: '0x...' },
  (event) => {
    // event.data: Position[]
    for (const pos of event.data) {
      console.log(pos.symbol, pos.side, pos.size, 'PnL:', pos.unrealizedPnl);
    }
  }
);

Multiple Subscriptions

You can subscribe to multiple channels simultaneously. Each returns an independent unsubscribe function.
const unsubPrices = await ws.subscribe(
  { channel: 'prices', dex: 'hyperliquid' },
  (event) => updatePriceDisplay(event.data.prices)
);

const unsubBook = await ws.subscribe(
  { channel: 'orderbook', dex: 'hyperliquid', symbol: '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 — Exponential backoff reconnection on disconnect (150ms, 300ms, 600ms, up to 10s)
  • 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

Cleanup

Always close the client when you’re done to release WebSocket connections:
// 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 knows event.data is PricesResponse
await ws.subscribe({ channel: 'prices', dex: 'hyperliquid' }, (event) => {
  event.data.prices; // Record<string, string> — no cast needed
});

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