WebSocket API
Real-time crypto derivatives data streaming
Connection
Two protocols are supported. Choose based on your needs:
wss://apiv2.laevitas.ch/ws
Native WebSocket (W3C)
apikey: header (or `auth` RPC)
Industry standard - same protocol used by Deribit, Binance, OKX, Bybit, Hyperliquid
wss://apiv2.laevitas.ch/stream
Socket.IO v4
auth: { apiKey: 'your-key' }
Auto-reconnect, acknowledgements - requires Socket.IO client library
Limits & Close Codes
| Subscriptions | 200 |
| Inbound messages / sec | 20 |
| Max lifetime | 24 h |
| Slow-consumer threshold | 2 MB queued |
| Concurrent connections per API key | 5 |
| Concurrent connections per IP | 20 |
Need higher caps? Contact [email protected] with your use case.
| 1001 | Server going away (graceful shutdown) |
| 4001 | Auth failed — surface to user, no auto-retry |
| 4002 | Idle timeout (pong missed) — reconnect with backoff |
| 4003 | Slow consumer — drain faster or reduce subscriptions |
| 4004 | 24 h lifetime cap reached — reconnect |
| 4005 | Too many concurrent connections (per key or IP) |
| 4006 | No active streaming pass — buy a pass and reconnect (x402 only) |
| 4008 | Inbound message rate exceeded |
Best practices
The gateway behaves like industry-standard exchange feeds (Deribit, Binance, OKX, Bybit). The patterns below are the same ones a hardened exchange client uses — calibrated to the exact server-side timers and limits documented above.
Don't blindly reconnect on every disconnect. The close code tells you exactly what happened — treat each one differently:
| 1006 / 1011 | Network hiccup, abnormal close. Reconnect with exponential backoff (1s → 2s → 4s → ... cap at 30s, jitter ±20%). Resubscribe to your channels. |
| 1001 | Server graceful shutdown (deploy / restart). Reconnect after 1–5s — the new pod is usually up by then. No backoff needed. |
| 4001 | Auth failed. Don't auto-retry. Surface to the user — key is missing, invalid, revoked, or your credit-token is bad. Reconnecting with the same credentials will fail identically. |
| 4002 | Idle timeout (you missed a pong). Your client wasn't replying to pings. Reconnect immediately and check that your event loop isn't blocked. Most native WS clients auto-pong; if you see this, suspect a stalled handler. |
| 4003 | Slow consumer. You couldn't drain incoming messages fast enough (server queued >2 MB to your socket). Don't blindly reconnect — you'll hit it again. Reduce subscriptions, batch processing, or move to a faster network. Then reconnect. |
| 4004 | 24h lifetime cap. Industry-standard hygiene. Reconnect immediately and resubscribe. Build this into your client — it's not an error, it's an expected event roughly once per day. |
| 4005 | Too many concurrent connections (5 per API key, 20 per IP). Don't reconnect — close another connection first or contact support to raise your cap. |
| 4006 | x402 pass expired or absent. Buy a new pass via POST /api/v1/x402/ws-pass/{hour|day}, then reconnect with the returned credit token. |
| 4008 | Inbound rate limit (>20 messages/sec). Throttle your subscribe calls or batch them. Reconnect after a short pause; don't loop. |
- Persist your subscription list locally — on every reconnect, resubscribe to everything (the server has no memory of prior sessions).
- Implement exponential backoff with jitter for codes 1006/1011. Without jitter, a fleet of clients all reconnecting at the same offset will dogpile the server after every restart.
- Plan for the 24h disconnect (4004). If you stagger your connect time across replicas, your fleet won't all rotate at the same minute.
- Pong promptly. Standard
ws/socket.ioclients do this automatically. If you write your own client, sendpongwithin 75 s of everyping(server pings every 25 s). - Bundle your subscribes — one
subscribecall with N channels rather than N calls with one channel each. Stays well under the 20/sec inbound rate limit. - Log the close code on every disconnect. Without it you can't tell a 4003 from a 4004 from a network blip.
- Don't reconnect on 4001, 4003, or 4005 without first fixing the underlying problem. Auto-retry will produce the same error and burn your connection cap.
- Don't tight-loop reconnect. No backoff = you trigger 4005 within seconds and then can't reconnect at all until the cap window clears.
- Don't open one connection per channel. One connection holds up to 200 subscriptions. Five connections per API key cap.
- Don't subscribe to firehoses you can't drain.
book.*,trades.*, or broad reference-data wildcards can deliver thousands of events per second — if your handler blocks, you'll hit 4003 in under a minute. - Don't send the API key in the URL query string. It ends up in access logs and proxy traces. Use the
apikeyheader instead. - Don't poll
pingas a heartbeat. The server pings you; just respond with pong. The JSON-RPC{"method":"ping"}exists for round-trip latency probes, not liveness.
ws)import WebSocket from 'ws';
const URL = 'wss://apiv2.laevitas.ch/ws';
const API_KEY = process.env.LAEVITAS_API_KEY;
const SUBSCRIPTIONS = [
'trades.spot.binance.BTCUSDT',
'trades.perpetuals.binance.BTCUSDT',
'open-interest.perpetuals.bybit.BTCUSDT',
'funding-rate.perpetuals.bybit.BTCUSDT',
];
// Codes that mean "the same retry will fail the same way" — surface and stop.
const FATAL = new Set([4001, 4003, 4005, 4006]);
let attempt = 0;
function connect() {
const ws = new WebSocket(URL, { headers: { apikey: API_KEY } });
ws.on('open', () => {
attempt = 0; // reset backoff on successful connect
ws.send(JSON.stringify({ id: 1, method: 'subscribe', params: { channels: SUBSCRIPTIONS } }));
});
ws.on('message', (raw) => {
const msg = JSON.parse(raw.toString());
if (msg.channel) handleEvent(msg.channel, msg.data);
});
ws.on('close', (code, reason) => {
console.log(`closed code=${code} reason=${reason}`);
if (FATAL.has(code)) {
console.error('Fatal close — not reconnecting. Fix the underlying issue.');
return;
}
// Exponential backoff with jitter; cap at 30s.
const base = Math.min(1000 * 2 ** attempt++, 30_000);
const jitter = base * (0.8 + Math.random() * 0.4);
setTimeout(connect, jitter);
});
ws.on('error', (err) => console.error('ws error:', err.message));
}
connect();
~30 lines. Handles all the failure modes documented above. Substitute SUBSCRIPTIONS with whatever you need; pass an X-Credit-Token header instead of apikey for x402 ws-pass auth.
A 4006 close means your pass expired (or was never active). Don't add 4006 to your fatal set blindly — instead, on 4006: call POST /api/v1/x402/ws-pass/hour (or /day) to buy a new pass, store the new x-credit-token, then reconnect. Treat it as a "refill required" signal, not a fatal error.
Streaming passes (x402)
Pay-as-you-go WebSocket access for x402 wallet customers. API-key customers are unaffected — your subscription continues to include unlimited streaming. The pass model is a time-slot reservation: you pay once for hour or day access, the server records your wallet's expiry in Redis, and you can connect / subscribe / disconnect / reconnect freely until the slot ends. There is no per-event metering, no refund machinery, and no automated SLA.
POST /api/v1/x402/ws-pass/hour
POST /api/v1/x402/ws-pass/day
- POST to one of the pass routes — receive a 402 with USDC payment requirements (Base mainnet).
- Sign and retry with the
PAYMENT-SIGNATUREheader. The server settles on-chain, grants your slot, and returns anx-credit-tokenJWT. - Connect to
wss://apiv2.laevitas.ch/wswith that JWT in theX-Credit-Tokenheader. - The server validates your wallet has an active slot. Disconnect and reconnect freely until expiry.
- When the slot expires, your next connection attempt returns close code
4006. Buy another pass to resume. - If your connection is still open at the expiry timestamp, the server closes it with the same
4006code — a long-lived connection cannot outlive its paid window.
Buying a second pass while one is active extends your expiry by the new pass's duration. A day pass on top of a 30-min-remaining hour pass leaves you with 24h 30min remaining. Passes accumulate; nothing is lost.
- Authentication failures (401, 4001, 4006)
- Connection cap or rate-limit rejections (4005, 4008)
- Server-initiated lifetime closes (4004) — passes survive across reconnects
- Server restarts — passes survive
| Tier | Price | Streaming included | Monthly equivalent (24/7) |
| WS hour pass | $0.50 | 1 hour | $360 |
| WS day pass | $10.00 | 24 hours | $300 |
| API subscription | $500/month | unlimited (REST + WS) | $500 |
For sustained heavy usage, the API subscription remains the most cost-effective option (rate-limit-free, full historical access, support included). Day passes are convenience pricing for short bursts — not a subscription replacement.
Passes are non-refundable in USDC. If a service issue affects your pass, contact [email protected] — we may extend your pass or grant REST credits at our discretion. There is no automated SLA or compensation threshold.
Channels
Real-time executed trades with strategy detection. Use the dedicated reference-data channels for OI and funding.
| {market} | perpetuals futures spot predictions |
| {exchange} | binance, deribit, okx, bybit, hyperliquid, coinbase, kraken, polymarket |
| {instrument} | Instrument name |
Quote-based OHLC bars. Futures/options include mark/index prices, OI, and IV (options). Spot includes last_price, bid/ask spread, sizes, and 24h rolling stats. Predictions include probability OHLC plus bid/ask price, size, and spread OHLC.
| {market} | perpetuals futures spot predictions |
| {tf} | 1m, 5m, 15m, 30m, 1h, 4h, 12h, 1d |
Trade-based OHLC with volume breakdown and VWAP.
| {market} | perpetuals futures spot predictions |
| {tf} | 1m, 5m, 15m, 30m, 1h, 4h, 12h, 1d |
Real-time OI updates from raw venue ticker/open-interest streams without the full ticker candle payload.
| {market} | perpetuals futures |
| {exchange} | Venue-dependent; use the same exchange names as the corresponding futures/options catalog. |
| {instrument} | Instrument name |
Dedicated perpetual funding updates with current rate, optional 8h-normalized rate, next funding fields, and mark/index price when supplied by the venue.
| {market} | perpetuals |
| {exchange} | bybit, okx, binance, deribit, hyperliquid, kraken, nado where funding is available |
| {instrument} | Instrument name |
Forced-liquidation events (margin calls, ADL) with price, side, USD-denominated size, mark + index price. The venue's raw payload is intentionally omitted — available via the corresponding REST endpoint.
| {market} | perpetuals futures |
| {exchange} | binance, bybit, okx, kraken, nado |
| {instrument} | Instrument name |
Full L2 snapshots (bids + asks, up to 100 levels per side). Each event is a complete snapshot — no deltas, no sequence reconstruction. Includes pre-computed cumulative liquidity at depth tiers (10/20/50/100), imbalance ratios, and microprice for cascade / imbalance detection.
| {market} | perpetuals futures spot predictions (no options — venues don't expose L2) |
| {exchange} | binance, bybit, okx, hyperliquid, coinbase, kraken, polymarket |
| {instrument} | Instrument name |
Perpetual swaps live under their own market namespace:
trades.perpetuals.{exchange}.{instrument}
and ohlc.{ticker,vt}.perpetuals.….
The pre-1.27 alias that also surfaced perp events on
trades.futures.… /
ohlc.…futures.…
has been removed — those channels are
now dated-futures only. If your client was relying on the
alias, switch perp subscriptions to the
perpetuals market.
Polymarket prediction markets resolve and expire fast —
sports / event-driven instruments can disappear within
hours. Don't hard-code instrument slugs in client
integrations. Discover live instruments via
/api/v1/predictions/catalog
(filter by category /
currency) and browse
available categories at
/api/v1/predictions/categories.
Equity perps (NVDA, TSLA, MSTR, AAPL, …), commodity
perps (GOLD, OIL/CL, SILVER, NATGAS, …), FX perps
(EURUSD, GBPUSD, JPYUSD) and the small index segment all
publish on
trades.perpetuals.…,
ohlc.{ticker,vt}.perpetuals.…,
liquidations.perpetuals.…,
book.perpetuals.…,
open-interest.perpetuals.…,
and funding-rate.perpetuals.…
— same shape as crypto perps. Discover instruments
and venues via
/api/v1/macro/catalog.
Examples:
trades.perpetuals.kraken.PF_NVDAXUSD,
ohlc.ticker.perpetuals.hyperliquid.xyz:NVDA-USD.1m,
trades.perpetuals.binance.XAUUSDT,
book.perpetuals.hyperliquid.flx:GOLD-USD,
open-interest.perpetuals.bybit.XAUTUSDT.
Hyperliquid HIP-3 sub-venues (xyz,
flx,
km,
cash,
vntl,
hyna) are colon-prefixed
in the instrument name. Equity / FX perps only tick during
the underlying market's hours on some venues
(Kraken PF_*, Hyperliquid
cash:*); 24/7 venues like
Hyperliquid xyz:* and
Binance *USDT stream
continuously.
Use * as a wildcard in
the market,
exchange, or
instrument position to
subscribe to multiple channels with a single request.
Trailing * auto-pads
remaining segments — trades.*
is shorthand for trades.*.*.*.
Examples:
trades.*,
liquidations.perpetuals.*,
book.perpetuals.*.BTCUSDT,
open-interest.*,
funding-rate.perpetuals.*,
trades.predictions.polymarket.*,
ohlc.ticker.*.binance.BTCUSDT.1m.
Wire format: each event arrives with the
channel field set to
the resolved concrete path
(e.g. trades.spot.binance.BTCUSDT),
not the wildcard pattern you subscribed to. Client dispatch
keys off the concrete path the same way it does for
non-wildcard subs.
Constraints:
* is rejected in the
{channel} position (pick a
channel type), in OHLC {dataType}
(pick ticker or
vt), and in OHLC
{timeframe} (pick a
timeframe). Wildcards count as 1 against the 200-sub
per-connection cap. High-volume firehose subs (e.g.
book.*) need a fast
consumer — if the outbound buffer fills you'll be
closed with code 4003.
Payload Examples
Perpetuals Trade Event
{
"trade_id": "401318266",
"instrument_name": "BTC-PERPETUAL",
"exchange": "deribit",
"direction": "sell",
"price": 93230,
"amount": 9000,
"contracts": 900,
"mark_price": 93220.37,
"index_price": 91523.43,
"open_interest": 328996550,
"oi_change": 1390
}
Options Trade Event
— example expiry; see /options/catalog for current
{
"trade_id": "401321563",
"instrument_name": "BTC-26MAR27-100000-C",
"strike": 100000,
"option_type": "C",
"direction": "sell",
"price": 0.0426,
"premium_usd": 48894.91,
"iv": 43.98,
"delta": -4.437,
"strategy": "BULL_DIAGONAL_SPREAD"
}
OHLC Ticker Event
{
"state": "live",
"instrument_name": "BTC-PERPETUAL",
"timeframe": "5m",
"mark_price_open": 93322.9,
"mark_price_high": 93326.78,
"mark_price_low": 93284.12,
"mark_price_close": 93294.52,
"oi_close": 3525.95
}
Open Interest Event
{
"timestamp": 1777464961535,
"exchange": "bybit",
"instrument_name": "BTCUSDT",
"instrument_type": "perpetual",
"currency": "BTC",
"open_interest": 91234.5,
"oi_before": 91102.1,
"oi_change": 132.4,
"mark_price": 89990,
"index_price": 89980,
"source": "ticker"
}
Funding Rate Event
{
"timestamp": 1777464961535,
"exchange": "bybit",
"instrument_name": "BTCUSDT",
"instrument_type": "perpetual",
"currency": "BTC",
"funding_rate": 0.0001,
"funding_8h": 0.0001,
"next_funding_rate": 0.00012,
"next_funding_time": 1777478400000,
"mark_price": 89990,
"index_price": 89980,
"source": "ticker"
}
OHLC VT Event
{
"state": "live",
"instrument_name": "BTC-PERPETUAL",
"timeframe": "5m",
"open": 93302.5,
"high": 93317.5,
"low": 93302.5,
"close": 93317.5,
"vwap": 93306.59,
"buy_volume": 2030,
"sell_volume": 760
}
Liquidation Event
{
"timestamp": 1777461297250,
"exchange": "binance",
"instrument_name": "BTCUSDT",
"instrument_type": "perpetual",
"direction": "buy",
"position_side": "short",
"category": "forced",
"price": 90000,
"amount": 0.5,
"amount_usd": 45000,
"mark_price": 89990,
"index_price": 89980
}
Order Book Snapshot
{
"timestamp": 1777464961535,
"exchange": "bybit",
"instrument_name": "BTCUSDT",
"instrument_type": "perpetual",
"depth": 100,
"bids": [[89990, 1.5], [89989, 0.8]],
"asks": [[89991, 0.6], [89992, 2.1]],
"bid_liquidity_10": 1512918.4,
"ask_liquidity_10": 1212726.78,
"imbalance_10": 0.11,
"microprice": 89990.78
}
Operations
Subscribe
{
"id": 1,
"method": "subscribe",
"params": {
"channels": [
"trades.perpetuals.binance.BTCUSDT",
"open-interest.perpetuals.bybit.BTCUSDT",
"funding-rate.perpetuals.bybit.BTCUSDT"
]
}
}
Response
{
"id": 1,
"result": {
"subscriptionIds": ["sub_123", "sub_124", "sub_125"],
"channels": [
"trades.perpetuals.binance.BTCUSDT",
"open-interest.perpetuals.bybit.BTCUSDT",
"funding-rate.perpetuals.bybit.BTCUSDT"
]
}
}
Data Event
{
"channel": "open-interest.perpetuals.bybit.BTCUSDT",
"data": { "open_interest": 91234.5, "oi_change": 132.4, ... }
}
Unsubscribe
{
"id": 2,
"method": "unsubscribe",
"params": { "subscriptionId": "sub_123" }
}
Subscribe
socket.emit('subscribe', {
channel: 'open-interest.perpetuals.bybit.BTCUSDT'
}, (response) => {
console.log('ID:', response.subscriptionId);
});
Unsubscribe
socket.emit('unsubscribe', {
subscriptionId: 'sub-123'
});
Connection
Subscribe
Validation runs server-side. Errors come back in the subscribe response. See the Wildcards callout above for syntax.
—
Active Subscriptions
No active subscriptions
Events
Connect and subscribe to see events
Native WebSocket Examples
JavaScript / Node.js
const WebSocket = require('ws');
// Server-side: pass the key as a header. In browsers (where headers aren't
// allowed on WebSocket), send an `auth` RPC as the first message instead:
// ws.send(JSON.stringify({ id: 'auth-1', method: 'auth', params: { apiKey } }))
const ws = new WebSocket('wss://apiv2.laevitas.ch/ws', {
headers: { apikey: 'your-api-key' },
});
ws.on('open', () => {
console.log('Connected to WebSocket');
// Subscribe to a concrete channel + a wildcard pattern in one request.
// Wildcards (`*`) work in market / exchange / instrument positions.
ws.send(JSON.stringify({
id: 1,
method: 'subscribe',
params: {
channels: [
'trades.perpetuals.binance.BTCUSDT', // one concrete channel
'liquidations.perpetuals.*', // every perp liquidation across exchanges
'open-interest.perpetuals.bybit.BTCUSDT',
'funding-rate.perpetuals.bybit.BTCUSDT',
],
},
}));
});
ws.on('message', (data) => {
const msg = JSON.parse(data);
// Handle subscription response
if (msg.id === 1 && msg.result) {
console.log('Subscribed:', msg.result.subscriptionIds);
}
// Handle data events
if (msg.channel && msg.data) {
if (msg.channel.startsWith('open-interest.')) {
const oi = msg.data;
console.log(`OI: ${oi.exchange} ${oi.instrument_name} ${oi.open_interest}`);
} else if (msg.channel.startsWith('funding-rate.')) {
const funding = msg.data;
console.log(`Funding: ${funding.exchange} ${funding.instrument_name} ${funding.funding_rate}`);
} else {
console.log(msg.channel, msg.data);
}
}
});
ws.on('close', () => console.log('Disconnected'));
Python
import websockets
import asyncio
import json
async def main():
uri = 'wss://apiv2.laevitas.ch/ws'
headers = {'apikey': 'your-api-key'}
async with websockets.connect(uri, additional_headers=headers) as ws:
print('Connected to WebSocket')
# Subscribe to a concrete channel + a wildcard pattern in one request.
# Wildcards (`*`) work in market / exchange / instrument positions.
await ws.send(json.dumps({
'id': 1,
'method': 'subscribe',
'params': {'channels': [
'trades.perpetuals.binance.BTCUSDT', # one concrete channel
'liquidations.perpetuals.*', # every perp liquidation across exchanges
'open-interest.perpetuals.bybit.BTCUSDT',
'funding-rate.perpetuals.bybit.BTCUSDT',
]},
}))
async for message in ws:
msg = json.loads(message)
# Handle subscription response
if msg.get('id') == 1 and msg.get('result'):
print(f"Subscribed: {msg['result']['subscriptionIds']}")
# Handle data events
if 'channel' in msg and 'data' in msg:
channel = msg['channel']
payload = msg['data']
if channel.startswith('open-interest.'):
print(f"OI: {payload['exchange']} {payload['instrument_name']} {payload['open_interest']}")
elif channel.startswith('funding-rate.'):
print(f"Funding: {payload['exchange']} {payload['instrument_name']} {payload['funding_rate']}")
else:
print(channel, payload)
asyncio.run(main())
Socket.IO Examples
JavaScript / Node.js
const io = require('socket.io-client');
const socket = io('wss://apiv2.laevitas.ch/stream', {
transports: ['websocket'],
auth: { apiKey: 'your-api-key' }
});
socket.on('connect', () => {
console.log('Connected to WebSocket');
// Subscribe to BTC open interest without the full ticker payload.
const channel = 'open-interest.perpetuals.bybit.BTCUSDT';
socket.emit('subscribe', { channel }, (response) => {
console.log('Subscribed:', response.subscriptionId);
});
socket.on(channel, (data) => {
console.log(`OI: ${data.open_interest} source=${data.source}`);
});
});
socket.on('disconnect', () => console.log('Disconnected'));
Python
import socketio
sio = socketio.Client()
@sio.event
def connect():
print('Connected to WebSocket')
# Subscribe to BTC open interest without the full ticker payload.
channel = 'open-interest.perpetuals.bybit.BTCUSDT'
sio.emit('subscribe', {'channel': channel},
callback=lambda r: print(f"Subscribed: {r['subscriptionId']}"))
@sio.on('open-interest.perpetuals.bybit.BTCUSDT')
def on_open_interest(data):
print(f"OI: {data['open_interest']} source={data['source']}")
@sio.event
def disconnect():
print('Disconnected')
sio.connect('wss://apiv2.laevitas.ch/stream',
auth={'apiKey': 'your-api-key'},
transports=['websocket'])
sio.wait()