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.Beta: Perpetual order books use one canonical contract,
kairos.v2.BookSnapshot. Perps are not dual-published on v1 and v2.SubscribeRequest; the perpetual data frame it selects is v2.
Subscribe
Connect with the public market-data subprotocol:wss://staging-stream.kairos.trade.
Send the normal binary subscribe control frame:
perps_bookis never implied. It is not included whentopicsis empty. Request it by name.
The WebSocket takes the canonical id; the REST snapshot takes the venue-native one.hl-mainnet-btc-usdthere,BTCthere. Sending the wrong one to the WebSocket does not produce an error — see below.
The provider slug differs from the REST path segment.On success the gateway replays the cached frame before sending thehyperliquid_perpson the WebSocket,hyperliquidon REST. See Venue normalization.
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:
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 declaredbook_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: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.
Ordering algorithm
Keep state per(provider, instrument_id):
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.
Initial state and reconnect
At startup the gateway creates an ordered JetStream consumer with latest-per-subject delivery over thePERPETUAL_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.- Resubscribe with the same provider, canonical instrument ID, and
perps_booktopic. - Accept a cached snapshot only through the epoch/sequence algorithm above.
- Replace the local book atomically.
- Continue with strictly newer snapshots.
NATS subjects
The internal compacted recovery subject is:[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.
Next: Venue normalization.
