Skip to main content
This tutorial connects a client to a live Synthetic Book stream, subscribes to one canonical definition, decodes its initial snapshot, applies deltas, and recovers safely after a sequence gap. Reach for it when you want a weighted package of prediction-market legs — a spread, a basket, a relative-value pair — priced as one book instead of assembling the legs yourself. For formula semantics and worked examples, read the Synthetic Books guide first.

What you need

  • A canonical ID such as syn_0123456789abcdef from the Synthetic Books definition workflow.
  • Kairos API credentials. The same credential triple creates definitions and authenticates the stream; no second Synthetic Books secret is issued. Reading an existing ID also works on the bounded anonymous tier in an environment where it is enabled.
  • The public WebSocket control schema and synthetic book schema.
The stream endpoint is:
Definitions are created through the public Order Execution API:
The WebSocket itself remains subscription-only: it does not accept raw formulas and accepts the canonical ID returned by that API. Definition lifecycle details are managed by Kairos; store and reuse only the opaque IDs returned by the public control plane.

1. Craft the formula

A definition is a weighted sum:
Submit this payload to POST /v1/synthetics. This request creates a 50-level decomposed book for A - B:
Use market and outcome identifiers returned by Kairos market metadata. A positive weight buys that outcome when buying the synthetic; a negative weight sells it. Always encode weight as a decimal string so values such as "0.333333333" are represented exactly. Choose an output mode based on what your client needs: For example:
The response includes the canonical ID and an opaque, account-owned lease:
The values above are illustrative. Use the ID returned by the service; do not hash the definition or construct the ID yourself. Refresh the lease before expires_at_ms with POST /v1/synthetics/subscriptions/{subscription_id}/refresh. When the caller no longer needs the definition, release its handle with DELETE /v1/synthetics/subscriptions/{subscription_id}. Send the same three API-key headers on both calls. A lease lasts 10 minutes; refreshing every five minutes leaves room for a retry. The account may hold at most 25 active leases, and definition creation is limited to 30 requests per minute by default. Only the account that created a lease can refresh, release, or inspect it. An expired lease cannot be revived: create the definition again and use the newly returned subscription UUID.

2. Compile the protobuf schemas

Download the control schemas and the synthetic schema, preserving their directory paths. For Python:
Add generated to PYTHONPATH, or package the generated modules with your application.

3. Connect and authenticate

Programmatic clients should send all three API-key headers during the WebSocket upgrade:
Browser clients use a session JWT in Sec-WebSocket-Protocol. For details and the anonymous public protocol, see Market Data Stream authentication. For example, a Python client using an enabled anonymous tier can open the same endpoint with:
Never put credentials in query parameters, source control, or logs.

4. Subscribe to the synthetic ID

Send a binary SubscribeRequest using the ordinary control-message envelope:
Set the fields exactly as follows:
The ID must match syn_ followed by 16 lowercase hexadecimal characters. The synthetic topic must be named explicitly; an empty topic list does not select it. An accepted request produces SubscribedResponse (0x14). A cached snapshot may arrive immediately, so dispatch every frame by tag instead of waiting for the acknowledgment before reading data.

Choose how much of the book you need

One synthetic ID can be consumed at three weights. Name one or more of these topics in topics; a request may list several, and a later request for the same ID adds to what the connection already holds. synthetic.snapshot and synthetic.quote require provider: "synthetic". A connection that holds both synthetic and synthetic.snapshot for one ID receives each snapshot once. Keep the snapshot with the highest ownership_epoch, then the highest synthetic_sequence: the one sent on subscribe and a live one can arrive in either order. Between snapshots a synthetic.snapshot subscriber holds the last full state, which can be behind the live book by the deltas published since. A snapshot is published on the first materialization, on every status change, and then periodically.

5. Decode snapshots and deltas

Synthetic data frames have a two-byte header:
0x51 is reserved for synthetic status notices and is not sent on this stream. Control frames such as SubscribedResponse still use their one-byte header. Ignore unknown tags and unsupported versions so future additions do not break the connection. This complete Python client subscribes with API-key credentials and maintains a price-keyed local book:
The example keeps nano-unit integers as dictionary keys. Convert only for display using the exponent carried by the frame; both exponents are currently 9. Integer keys avoid floating-point collisions between price levels.

6. Apply deltas safely

A snapshot replaces the entire local book. A delta applies only when:
  1. Its ownership_epoch equals the snapshot epoch you currently hold.
  2. Its previous_sequence equals your current synthetic_sequence.
  3. Its synthetic_sequence is greater than the current sequence.
Ignore a delta from the current epoch when its synthetic_sequence is less than or equal to the sequence already applied. A replay can briefly overlap with live delivery, so receiving the same delta twice is not a sequence gap. Recover only when a newer delta does not connect to the current sequence, or when a higher ownership epoch arrives without its snapshot. Each SyntheticLevelChange is addressed by side and price_nano. A zero qty_nano removes that price; a positive quantity replaces the complete level. In decomposed mode, replace the recipes with the recipes on the change as well. InputVersionChange.leg_index addresses the corresponding leg in the canonical definition echoed by the snapshot. Apply those sparse changes if your client displays or validates source lineage.
Gotcha: a failed check has no tolerance — resync fully. If any sequence or epoch check fails, discard the local book and send the same SubscribeRequest again. That requests a cached snapshot and its contiguous deltas without consuming another subscription slot. If no complete replay is available, wait for the next snapshot. Never apply a delta to an old baseline.
The gateway also fences the stream on your behalf. A delta whose previous_sequence does not chain onto the frame the gateway holds is dropped rather than forwarded, as is a delta from a lower ownership_epoch and any duplicate redelivery. So a gap upstream shows up as silence, not as a broken chain, and it ends at the next snapshot — the producer snapshots every 64 deltas, and the gateway’s replay buffer holds at most 256. While the chain is broken, a re-subscribe replays nothing at all until that snapshot re-anchors it; a snapshot plus a chain with a hole would leave you confidently wrong. The gateway drops frames that fail validation before they ever reach you: a mismatched synthetic_id, a zero synthetic_sequence, a price_unit_exponent / qty_unit_exponent other than 9, an empty publisher_instance_id, a non-monotonic or crossed ladder, a non-positive quantity on a snapshot level, a negative qty_nano on a change, or a change whose side is neither BOOK_SIDE_BID nor BOOK_SIDE_ASK.

7. Decide whether the quote is usable

Only treat levels as executable when status is MATERIALIZATION_STATUS_EXECUTABLE. Empty ladders with NO_LIQUIDITY, and all non-live statuses, are not zero-priced opportunities. For execution-aware clients:
  • Reject or revalidate recipes after valid_until_ms.
  • Check max_input_age_ms and cross_venue_skew_ms against your own policy.
  • Read the source action on each recipe leg; do not infer it from the sign of the formula at execution time.
  • Include fees, balances, allowances, and partial-fill risk before submitting source orders.

8. Read the top of the book only

Subscribe with topics: ["synthetic.quote"] to receive one small frame instead of the ladder: Each level carries price_nano, qty_nano, and one recipes entry per venue resting at that level, with the venue name, its token, the action, and the venue’s own price_nano and qty_nano. On a union_l2 book the level price already includes that venue’s taker fee, and the recipe price is the raw price the venue shows. Recipe weight_nano, source_seq, and source_epoch are not set on a quote. A quote is sent when you subscribe, when the best bid or best ask changes in price, quantity, or venue, when status changes, and with every snapshot of the book. Changes deeper in the book send nothing. Apply these rules:
  • Keep the quote with the highest ownership_epoch, then the highest synthetic_sequence, and drop anything older. The quote sent on subscribe and a live quote can arrive in either order.
  • A quote with MATERIALIZATION_STATUS_UNSPECIFIED and no sides is a withdrawal: the server lost track of the book, because of a sequence gap or a rejected frame. It carries the sequence of the quote it withdraws. Clear the quote you hold. Nothing is sent on subscribe during that time.
  • Only treat the prices as executable when status is MATERIALIZATION_STATUS_EXECUTABLE.
  • On a union_l2 book the best bid can be above the best ask. The two sides rest on different venues, so the cross is a real cross-venue quote.
  • The next snapshot produces a fresh quote.

9. Unsubscribe and reconnect

To stop the stream, send:
with the same contract_id and provider: "synthetic". The server responds with UnsubscribedResponse (0x15). This drops every topic the connection holds for that ID. To drop some synthetic topics and keep the rest, add the topic names as repeated string field number 4 of the same request:
Only the three synthetic topics can be named. Naming the last topic the connection holds for that ID releases the subscription as a whole. After any disconnect, clear the local book, reconnect with exponential backoff and jitter, and send a fresh subscription. Do not carry a snapshot or sequence across connections.

Errors and close codes

Subscribe rejections arrive as ErrorResponse (0x17) with action: "subscribe". The ones you are most likely to hit here are synthetic requires a canonical synthetic id (syn_ followed by 16 hex digits), synthetic.snapshot and synthetic.quote require provider synthetic, Subscription limit exceeded: max N subscriptions per connection, and Subscription limit exceeded: too many active subscriptions for this account. The connection stays open — only the offending request is dropped. The close codes you will see on this stream: The full catalogue of ErrorResponse messages, upgrade-time rejections, and every close code is on Market Data Stream → Errors & Disconnection.

Stream limits

Formula limits are listed in the Synthetic Books guide. The WebSocket limits apply independently: One synthetic ID counts as one subscription however many of the three topics the connection holds for it. Re-subscribing to the same provider and ID, or adding another topic to it, does not consume another subscription slot. For all connection limits, error frames, close codes, and reconnect behavior, see Market Data Stream.

Troubleshooting