Skip to main content
Live in production, still flag-gated. Perpetuals are gated by PERPETUALS_PUBLIC_API_ENABLED, and the WebSocket half by PERPETUALS_V2_ENABLED, which is derived from the same rollout flag. Both are on in production and staging today; a gateway with the flag off carries no perps_book topic at all. Read Overview before writing code.
The complete wire contract for consuming live perpetual books: how to subscribe, how to decode the binary frame, and the exact ordering rule that keeps your local book correct across reconnects.
Beta: Perpetual order books use one canonical contract, kairos.v2.BookSnapshot. Perps are not dual-published on v1 and v2.
Prediction-market channels continue to use their existing v1 messages. The control request used to subscribe is also still the existing v1 SubscribeRequest; the perpetual data frame it selects is v2.

Subscribe

Connect with the public market-data subprotocol:
Staging exposes the same contract at wss://staging-stream.kairos.trade. Send the normal binary subscribe control frame:
Example logical request:
perps_book is never implied. It is not included when topics is empty. Request it by name.
The WebSocket takes the canonical id; the REST snapshot takes the venue-native one. hl-mainnet-btc-usdt here, BTC there. Sending the wrong one to the WebSocket does not produce an error — see below.
The provider slug differs from the REST path segment. hyperliquid_perps on the WebSocket, hyperliquid on REST. See Venue normalization.
On success the gateway replays the cached frame before sending the Subscribed acknowledgment (0x14), so a data frame arriving ahead of the ack is expected, not a protocol violation.

Subscribe errors

A rejected control request comes back as a binary control frame:
ErrorResponse carries a message and an action (subscribe or unsubscribe).
⚠ There is no “unknown instrument” error The gateway validates the provider against the perpetual set and the id as a transport token. It does not check the id against the catalog. A misspelled instrument is acknowledged like any other subscription and then simply never delivers a frame. Identity is enforced one layer down, on the data path — the NATS subject, the provider enum, and InstrumentRef.instrument_id must all agree, and a mismatch is dropped silently rather than reported to you. If a subscription goes quiet, verify the instrument id before suspecting the network.

Data frame

Each perpetual book is a binary WebSocket frame:
Reject a frame if either prefix byte is unknown, and do not pass the version byte to protobuf decoding. Both bytes are envelope, not payload.
The source schema is kairos/v2/market_data.proto in the kairos-proto repository, vendored at proto/ in the monorepo. Generate bindings from that file; do not reproduce the message by hand.

Snapshot semantics

Every frame is a complete replacement for the declared book_view_id. feed_mode is BOOK_FEED_MODE_SNAPSHOT_ONLY, including Kalshi: Kairos applies venue updates internally and emits the resulting full book. Important fields: source is fully populated and fully checked: source_cell_id, latency_domain_id, ingest_region, producer_id (orderbook_streamer:<provider>), and connection_id must all be non-empty, source_mode must be SOURCE_MODE_DIRECT, and a live frame must carry DELIVERY_PHASE_REALTIME. A frame that fails any of these is dropped before fan-out. InstrumentRef.provider uses the perpetual-specific enum. For example, a request uses the slug hyperliquid_perps, the internal subject venue is hyperliquid, and the protobuf provider is PROVIDER_HYPERLIQUID_PERPS. These are deliberate namespaces, not aliases to guess at runtime.

Exact decimals

Each price and quantity is:
For example, coefficient: "600001", scale: 1 means 60000.1. Canonical encoding has no leading zeroes, removes fractional trailing zeroes, and encodes zero only as ("0", 0). Book prices and quantities are positive.
Use a decimal or integer-plus-scale type, and never route 64-bit timestamps or sequences through a JavaScript number. See Exact values and time.

Ordering algorithm

Keep state per (provider, instrument_id):
The gateway applies the same fence before fan-out. Clients should still apply it, because reconnects can cross gateway replicas and buffered application work can complete out of order. An upstream reconnect changes source.connection_id and advances source_epoch; sequence then restarts at 1 inside that new lineage. A streamer ownership transfer advances the owner portion of the epoch, so a process restart cannot resume beneath a gateway’s cached cursor. source_epoch is composed, not a counter:
owner_epoch is the cross-process ownership fence; session_ordinal counts upstream socket generations beneath one owner, starting at 1. That reserved range is why a reconnect can restart sequence at 1 without ever looking stale to a long-lived gateway.
Treat source_epoch as opaque and compare it only for ordering. Do not decompose it, and do not assume a sequence reset means data loss — check the epoch first.

Initial state and reconnect

At startup the gateway creates an ordered JetStream consumer with latest-per-subject delivery over the PERPETUAL_BOOK_CHECKPOINTS stream. Each recovered snapshot is validated as a live frame, then rewritten to DELIVERY_PHASE_RECOVERY_REPLAY and re-serialized, and placed in the same epoch/sequence-fenced cache as live data. When a client subscribes, it receives that frame immediately if one is cached, then continues with live replacements.
source.delivery_phase is the only field that distinguishes a replayed frame from a realtime one. DELIVERY_PHASE_RECOVERY_REPLAY versus DELIVERY_PHASE_REALTIME.
On reconnect:
  1. Resubscribe with the same provider, canonical instrument ID, and perps_book topic.
  2. Accept a cached snapshot only through the epoch/sequence algorithm above.
  3. Replace the local book atomically.
  4. Continue with strictly newer snapshots.

NATS subjects

The internal compacted recovery subject is:
The live subject is:
Subject tokens use canonical uppercase percent encoding for characters outside [A-Za-z0-9_-]. Application clients normally do not construct NATS subjects; they subscribe through the WebSocket provider/instrument fields.

Backpressure

Snapshots replace state, so a client may coalesce queued snapshots only by keeping the greatest (source_epoch, sequence) for the same instrument.
Never compare or coalesce sequences across instruments. Bound your queues and reconnect when processing lag violates your freshness requirement.
Next: Venue normalization.