Skip to main content
Instead of polling for fills, subscribe to the real-time stream and react the instant a quote executes. Each fill arrives as a trade plus a position change, which feeds directly into your next quoting cycle. The stream is Server-Sent Events (Content-Type: text/event-stream). Connect with any SSE / EventSource client and reconnect on drop.

Endpoints

There are two user streams. For market making, prefer the all-markets stream — one connection covers your whole book.
Both streams are scoped to your authenticated account for the trades and positions they carry. Use /perps/stream/user to cover an N-market book with one connection; use /perps/stream/events when you specifically need a market’s order book alongside your fills.

What it carries

The stream delivers a canonical envelope and, scoped to your authenticated account for the fills and positions parts, three things:

Orderbook

The public CLOB book (single-market stream only; levels with size:"0" mean the level was removed).

Your trades

Your fills: side, price, quantity, liquidity (maker/taker), createdAt.

Your positions

Your positions: side, quantity, entryPrice, realizedPnl, status (upserts + removedIds).

Event model

Every message is an envelope:
{ "eventId": "…", "eventType": "PerpStreamDelta", "eventSeq": 10432, "engineTs": "…", "sourceTs": "…", "payload": { } }
EventeventTypeMeaning
snapshotPerpStreamSnapshotFull state on connect: book + recent trades + your positions.
deltaPerpStreamDeltaIncremental changes: changed book levels, newly-seen trades, position upserts/removals.
error{ code, message, retryable } on upstream failure.
eventSeq increases monotonically (once per emitted event) so you can detect gaps. Apply the initial snapshot, then fold in each delta. If you see a sequence jump or the connection drops, reconnect — you will receive a fresh snapshot to re-sync from.

Consume it

// One connection covers your whole book. Each trade / position carries its marketId.
const es = new EventSource(
  `${API_BASE}/perps/stream/user?openOnly=true`,
  { withCredentials: true } // sends the session cookie; or front with an API-key proxy
);

let lastSeq = null;

es.addEventListener("snapshot", (e) => {
  const { payload, eventSeq } = JSON.parse(e.data);
  lastSeq = eventSeq;
  initPositions(payload.positions);            // baseline across all markets
});

es.addEventListener("delta", (e) => {
  const { payload, eventSeq } = JSON.parse(e.data);
  if (lastSeq != null && eventSeq > lastSeq + 1) {
    es.close();      // gap detected — reconnect and re-snapshot
    reconnect();
    return;
  }
  lastSeq = eventSeq;

  for (const t of payload.trades ?? []) onFill(t);          // your execution, any market
  for (const p of payload.positions?.upserts ?? []) onPosition(p);
});

es.addEventListener("error", (e) => {
  // network drop or { code, message, retryable } — back off and reconnect
});

function onFill(trade) {
  // trade.liquidity === "maker" | "taker"; adjust inventory, then re-quote that market
  updateInventory(trade);
  scheduleRequote(trade.marketId);
}
A fill shows up as a trade and a position delta. Use the position delta as the source of truth for inventory, then skew or shrink your next quotes/bulk ladder accordingly. See inventory management.

Reconcile with pull endpoints

The stream is push-first, but you can reconcile at any time:
  • GET {API_BASE}/perps/positions — your open positions.
  • GET {API_BASE}/perps/orders/open — your currently resting orders.
  • GET {API_BASE}/perps/trades — your recent fills.
  • GET {API_BASE}/perps/balances — your perp balance / margin view.
Idle connections receive a comment heartbeat (:heartbeat) every few seconds so proxies keep the stream open. If you stop seeing heartbeats, treat the connection as dead and reconnect.