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.
WebSocket support is provider-specific. Not all channels may be supported by every provider — check the provider’s documentation for available channels.
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', provider: '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', provider: '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. Pass an optional depth to limit price levels (provider default if omitted).
await ws.subscribe(
{ channel: 'orderbook', provider: 'hyperliquid', symbol: 'BTC', depth: 5 },
(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.
await ws.subscribe(
{ channel: 'candle', provider: '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', provider: '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', provider: 'hyperliquid', address: '0x...' },
(event) => {
// event.data: Fill[]
for (const fill of event.data) {
console.log(fill.asset.displaySymbol, fill.side, fill.size, '@', fill.price);
}
}
);
positions
Live position updates for a user.
await ws.subscribe(
{ channel: 'positions', provider: 'hyperliquid', address: '0x...' },
(event) => {
// event.data: Position[]
for (const pos of event.data) {
console.log(pos.asset.displaySymbol, pos.side, pos.size, 'PnL:', pos.unrealizedPnl);
}
}
);
spotBalances
Live spot balance updates for a user (e.g., USDC collateral held on the DEX).
await ws.subscribe(
{ channel: 'spotBalances', provider: 'hyperliquid', address: '0x...' },
(event) => {
// event.data: SpotBalance[] — { coin, total, hold }
for (const bal of event.data) {
console.log(bal.coin, 'total:', bal.total, 'hold:', bal.hold);
}
}
);
Multiple Subscriptions
You can subscribe to multiple channels simultaneously. Each returns an independent unsubscribe function.
const unsubPrices = await ws.subscribe(
{ channel: 'prices', provider: 'hyperliquid' },
(event) => updatePriceDisplay(event.data.prices)
);
const unsubBook = await ws.subscribe(
{ channel: 'orderbook', provider: 'hyperliquid', symbol: 'BTC' },
(event) => updateOrderbookDisplay(event.data)
);
const unsubOrders = await ws.subscribe(
{ channel: 'orderUpdates', provider: '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', provider: 'hyperliquid' }, (event) => {
event.data.prices; // Record<string, string> — no cast needed
});
// TypeScript knows event.data is OrderbookResponse
await ws.subscribe({ channel: 'orderbook', provider: 'hyperliquid', symbol: 'BTC' }, (event) => {
event.data.bids; // OrderbookLevel[] — no cast needed
});