Gateway Wire Protocol #

The byte-level contract between the gateway and every external client — the retail interface consumer and the Python SDK's quoting clients alike. This document is to the WebSocket boundary what event-catalog.md is to recorded streams: the frame registry of record.

Position in the system: the gateway is one of exactly three sanctioned impure edges (architecture.md Level 3, Edge Layer). Its transport is WebSocket (architecture.md Level 2 container: "WebSocket/HTTP"); its thread model is the dev-kickoff §4 deployment view — one runtime thread (T1, every fold, sole stream writer) and the gateway IO-thread family (T2 — acceptor + per-connection shuttles, dev-kickoff §4; all socket I/O, no member touches a fold), one inbox queue between them, drained at the §1 loop's DR point (and its pinned DR-equivalent points, §4). Session semantics are state-layers.md §8.1 (session state = cursors only). Input stamping is matching-engine.md §2 (the gateway stamping rule). Nothing in this document overrides those specs; it pins the bytes that flow across them.

1. Frame encoding law #

Every protocol frame is one WebSocket binary message containing one canonical-CBOR value under the exact encoding law of event-catalog.md §3: definite lengths, shortest-form integers, integer map keys sorted ascending, no tags, no floats, one valid encoding per value. The stream-core codec is the one encoder/decoder home — the gateway adds no second CBOR implementation.

Floats never cross the wire, in either direction. The SDK is a thin skin (core law 4): it presents USDC floats and probability floats to client code and quantizes at its own boundary — prices to integer mills on the 1..=999 grid, probabilities to Q0.16 (Σ = 65 536, event-catalog §6.4) — before encoding a frame. The gateway validates integers and rejects; it never converts.

Frame envelope (CBOR map, integer keys):

(0) frame : u64  — frame-type id from the registry below
(1) body  : map  — frame-specific fields, integer keys

Frame bodies are closed maps: a body carrying a field id not listed in this registry is malformed (parse-hardening: unknown fields are not forward-compatibility here — frame versions are; see §7 caps).

Text fields — a directory entry's stream name, a quoting entry's family text, a reject's detail — are NFC-normalized UTF-8, verified by the codec on both encode and decode; their length caps are §7's, checked on the decoding half — the reject detail additionally passes the server's encode seam (§7).

2. Frame registry #

Inbound (client → gateway):

id frame body fields
1 hello/v1 (0) protocol_version: u64 = 1 · (1) account_id: u64 — bind an existing account · (2) initial_balance_mills: u64 — create an account with this opening balance. Exactly one of (1)/(2) present.
2 subscribe/v1 (0) stream_id: bytes32 — from the hello_ack directory · (1) from_offset: u64 — 0 = genesis, K = resume cursor
3 order_submit/v1 (0) client_ref: u64 — client correlation token, echoed in the ack, never recorded · (1) market_id: bytes32 · (2) side: u64 — 0 higher / 1 lower · (3) action: u64 — 0 buy / 1 sell · (4) size: u64 wire integer — claim units, nonzero, range-checked down to u32 and the run's max_order_size (§7; matching-engine §1) · (5) quoted_version: u64 — the book version the client saw
4 forecast_submit/v1 (0) client_ref: u64 — echoed in the ack, never recorded · (1) sleeve: u64 — the external quoting sleeve this account drives (u32 wire range; §5 codes 9/10) · (2) market_id: bytes32 · (3) seen_version: u64 — the game-state version the forecast derives from (recorded client intent; the quote fold prices at the version the record's STAMP names — architecture.md external drive rule 1) · (4) p: array of 1..=8 u64 wire integers (§7 decode-level shape: empty or over-cap closes the connection), each u32-checked, summing to exactly 65 536 AND exactly as wide as the target market family's outcome-class count (6 on a CS2 kills market, 2 on a binary market — a match winner or a race to kills — Q0.16, event-catalog §6.4; both laws are §5 code 11) — the recorded forecast the quote fold prices through steps 1–8 against the family's payout vector
5 ladder_submit/v1 (0) client_ref · (1) sleeve · (2) market_id · (3) seen_version — as frame 4 · (4) levels: array of at most MAX_LADDER_LEVELS maps {(0) side: u64 — 0 bid / 1 ask · (1) price: u64 — mills, 1..=999 · (2) size: u64 — claim units, nonzero, u32-checked} (§5 code 12) — an explicit whole-ladder replace entering the quote fold at steps 8+; an empty array is the pull (cancel: what you don't post is gone)
6 fetch/v1 (0) stream_id: bytes32 · (1) from_offset: u64 · (2) max_count: u64 — 0 = up to the server page cap. The classic pull API: a bounded request/response read with no standing subscription and no retained cursor — the client paginates with the returned next_offset.
7 model_register/v1 (0) client_ref: u64 · (1) model_id: bytes32 — the operator control edge (model-plugin §9). The operator names the model by content id ONLY: every recorded registration field (format, schema, size, meta hash) is derived server-side from the verified artifact store, so a declared-vs-stored contradiction is unrepresentable on this wire. Acceptance appends model_registered to the control ROOT log and auto-runs admission gates 1–3 (model-plugin §10), each result appended by the same writer. For an already-SUSPENDED model the frame is the §9 RE-ADMISSION trigger: no second model_registered record (ids are content-derived — a duplicate registration is corruption), gates 1–3 simply re-run onto the log and the ack carries the first gate record's seq; re-registration in any other state rejects code 14. A stored artifact whose recorded feature_schema_id is the grid of no column of this run (or of two) is refused code 16 unknown_feature_schema — checked after the store lookup (code 13) and BEFORE the §9 state, on the fresh and the re-admission path alike, since such a model has no §9 state at all; nothing is appended and no gate runs
8 model_shadow/v1 (0) client_ref · (1) model_id — appends model_shadowed (effective at next market_open, §9)
9 model_promote/v1 (0) client_ref · (1) model_id · (2) bookmaker: u64 (u32-checked) — appends model_promoted; the sleeve must be a manifest HOUSE sleeve (the runtime halts on anything else, so the gateway is the enforcement point — the codes-4/7/10 posture)
10 model_suspend/v1 (0) client_ref · (1) model_id · (2) reason: u64 (0 operator / 1 invalid_rate / 2 coverage — the recorded SuspendReason domain, decode-checked) — appends model_suspended (immediate, §9)
11 backtest_submit/v1 (0) client_ref · (1) model_id · (2) document: bytes — the gate-4 report-card DOCUMENT (d2-report-card/1 canonical CBOR). The gateway decodes it with the hardened card reader (fuzzed under its own bijection, §7), refuses a card whose candidate contradicts (1), derives the verdict from the card's own coverage fields through the one-home outcome_from_card (never a claimed flag — model-plugin §10), stores the card and the details string content-addressed under the run's cards/ and details/ (fsynced before the record), and appends model_gate_result{backtest}
12 sleeve_suspend/v1 (0) client_ref: u64 · (1) sleeve: u64 (u32-checked) · (2) reason: u64 (0 operator / 2 liquidated — the recorded SleeveReason domain, decode-checked: 1 retired and any other value refused, connection-fatal) — appends sleeve_suspended{sleeve, reason, effective 0} (immediate; risk-engine.md §5, §11). The sleeve is any bookmaker sleeve the run manifest declares, house or external; the gateway's SLEEVE MIRROR — a sleeve fold beside the drain-time mirror registry, over the manifest's full declared bookmaker-sleeve set — refuses an undeclared sleeve and a suspension the fold would halt on with code 14, so a recorded one is never refused by the fold. Reason 0 is the operator's own pause (risk-engine.md §8 E3); reason 2 is the cross-run stop's, sent by the desk watch (risk-engine.md §8 E7)
13 sleeve_resume/v1 (0) client_ref · (1) sleeve — appends sleeve_resumed{sleeve}; the sleeve mirror refuses a resume of a sleeve not Suspended(operator) with code 14 — a quoting sleeve, or a sleeve suspended with reason 2, which no resume lifts inside the epoch (risk-engine.md §5, §12 C105)
14 operator_hello/v1 (0) protocol_version: u64 = 1 · (1) account_id: u64 — bind an existing account as the session's operator · (2) initial_balance_mills: u64 — create an account with this opening balance (account_created/v1) and bind it as the operator. Exactly one of (1)/(2) present — hello/v1's body under hello/v1's rules (§3 rules 1–2, codes 2 and 6), acked by hello_ack/v1; refused with code 22 on a run whose manifest pins no retail_fee_rule (MF-16) or no max_resting_orders (MF-17, model-plugin-layer §12 — no subaccount instruction is admitted without both); the session it opens is an OPERATOR session — the operator of customer subaccounts, a role apart from the control edge's operator of rows 7–13 — the only session rows 15 and 16 are valid on (§3 rule 3e). An operator is an account: nothing records the role — the subaccounts a session opens name its account (event-catalog §6.6), and that record is the operator's authority over them
15 open_subaccount/v1 (0) client_ref: u64 — echoed in the ack, never recorded · (1) customer_key: bytes — exactly 32 (§7), the customer's Ed25519 public key · (2) opening_balance_mills: u64 — within §5 code 21's two bounds, a signed mills balance and the run's total opening balance — appends subaccount_opened/v1{operator = the session's account, customer_key, opening_balance} (event-catalog §6.6), acked by order_ack/v1, whose seq is the subaccount id. The key's strict parse is the fold's (instruction_rejected{bad_key}, event-catalog §6.7), never the gateway's, so an acked opening is history whatever its key
16 relay_instruction/v1 (0) client_ref: u64 — echoed in the ack, never recorded · (1) instruction: bytes — 1..=MAX_INSTRUCTION_BYTES (§7), the customer's canonical instruction (event-catalog §6.6.1) VERBATIM · (2) signature: bytes — exactly 64 (§7), the customer's Ed25519 signature over (1) — appends instruction_relayed/v1{operator = the session's account, instruction, signature} (event-catalog §6.6), acked by order_ack/v1, whose seq is the record's offset — a place instruction's order id. The gateway neither decodes the instruction nor verifies the signature: both are the exchange fold's, from the recorded bytes (matching-engine §5), so an altered, unsigned, replayed or foreign instruction is a recorded refusal, never a wire reject

Outbound (gateway → client):

id frame body fields
129 hello_ack/v1 (0) run_id: bytes32 · (1) account_id: u64 — the bound account (for creation: the account_created record's order-log offset, per the dev-kickoff §2 id table) · (2) max_order_size: u64 — the run-manifest cap the gateway validates against · (3) directory: array of {(0) name: text — non-empty, at most MAX_STREAM_NAME_TEXT bytes and unique, a rule the client half enforces (§7), (1) stream_id: bytes32} — every stream of the run, root logs included under the reserved names match_log, orders, and control (the gateway crate's protocol module homes the MATCH_LOG_NAME/ORDER_LOG_NAME/CONTROL_LOG_NAME consts beside the cap they fit, the registry of record) · (4) quoting: array of {(0) sleeve: u64, (1) ceiling_mills: u64, (2) family: text — OPTIONAL} — the run manifest's external quoting sleeves in ASCENDING sleeve order, a rule the client half enforces (§7; a duplicate sleeve would be an ambiguous scope) (static run facts, like (2); which account holds a sleeve is learned by claiming — §5 code 10). (2) is the SCOPE of a sleeve the manifest tags to one family of a board (model-plugin §12, BK-4 on an external sleeve): the claim family text of that family — the exact market_created.claim_series.family string every market of the family carries (event-catalog §6.9), so a client matches entry to market by text, never by deriving <title>/<name>; ABSENT when the sleeve quotes every family of the run (an unscoped sleeve's entry is the two-field map it always was). A scoped sleeve's quoting frames (rows 4/5) are refused outside its family — §5 code 17; a quoting entry is a closed map like every frame body (§1)
130 subscribe_ack/v1 (0) stream_id: bytes32 · (1) head_offset: u64 — the offset the next appended record will take, at ack time
131 order_ack/v1 (0) client_ref: u64 · (1) seq: u64 — the stamped sequence = the record's order-log offset · (2) replay_ts: u64 — the stamped replay timestamp (§4 Stamping: the latest published game-state ts at arrival; for an order, forecast or ladder never the PRE-STATE genesis class — every order, forecast or ladder this frame acks is gated on a market that exists, and the first market is created in the tick that publishes the first game-state record (a snapshot on CS2, the forwarded match envelope on a match-winner family — state-layers §3), so no order, forecast or ladder is stamped before any state, and the one record of the other client frames stamped before any state outside the operator rows, an account_created from a pre-run hello, is acked by hello_ack, which carries no ts; an operator's opening or relay (rows 15/16) names no market at the wire, so one accepted before the first published state is acked with the genesis class's 0 (§4's genesis rule), its instruction meeting its market downstream (matching-engine §5). The VALUE 0 is still reachable: on a feed whose first published state is itself stamped 0 — Riot's frame 0, every real LoL match — a later append borrows that 0 and nothing recorded distinguishes it from a pre-state stamp, §4's genesis rule). Acks every accepted client ingress append — order, forecast, ladder, subaccount opening, relayed instruction (account creation acks via hello_ack); one ack frame, because they are one log with one counter
132 reject/v1 (0) client_ref: u64 — present iff the rejected frame carried one · (1) code: u64 (§5) · (2) detail: text — at most MAX_REJECT_DETAIL_BYTES and at most MAX_REJECT_DETAIL_CHARS characters on the client half (§7; every reject/v1 is produced by Driver::reject, whose detail passes the ONE seam protocol::bounded_detail — 512 characters, a non-NFC result replaced by the fixed marker DETAIL_WITHHELD — so the client admits exactly what the seam leaves unchanged); the server writes a non-empty detail (a literal, a decoded text, or — were a non-NFC text ever to reach the seam — the marker), the client admits an empty one
133 record/v1 (0) stream_id: bytes32 · (1) offset: u64 · (2) record: bytes — the canonical record bytes verbatim as appended (event-catalog §4 envelope; the client decodes with the same catalog)
134 fetch_end/v1 (0) stream_id: bytes32 · (1) next_offset: u64 — where the next fetch page starts · (2) head_offset: u64 — the stream head at reply time (next_offset = head_offset ⇔ the client is caught up). Terminates one fetch's record/v1 run.
135 control_ack/v1 (0) client_ref: u64 · (1) seq: u64 — the stamped seq = the record's CONTROL-log offset (the control root has its own counter, §4) · (2) replay_ts: u64 — the stamped replay timestamp (§4 Stamping: the latest published game-state ts at arrival; 0 = a control append that arrived before the first published state — or, on a feed whose first published state is itself stamped 0 (Riot's frame 0), one that arrived after it: nothing recorded distinguishes the two, §4's genesis rule, as for row 131). Acks every accepted control append, sent only after the control-root fsync; a register's auto-run gate results ride the control stream as records, not extra acks

There is no frame for market data distinct from records: clients read the same immutable streams every fold reads. The wire format of a pushed event IS its recorded form — replayability crosses the boundary for free.

3. Session lifecycle on the wire #

The state machine is state-layers.md §8.1 (an order may be submitted from Authenticated — a classic pull-only client never subscribes; and fetch/v1 is a pure query with no machine transition at all). This section maps the transitions to frames.

sequenceDiagram
    participant C as client (SDK / consumer)
    participant T2 as gateway IO thread
    participant T1 as runtime thread (§1 loop)
    C->>T2: hello/v1 {initial_balance 10_000_000}
    T2->>T1: inbox: session action
    Note over T1: DR step — stamp account_created<br/>(replay_ts = latest published state ts, seq = order-log offset)<br/>append, fsync
    T1-->>T2: outbox: hello_ack {run_id, account_id 3, max_order_size, directory, quoting[{sleeve, ceiling_mills, family?}]}
    T2-->>C: hello_ack/v1
    C->>T2: subscribe/v1 {game_state, from 0}
    T2->>T1: inbox
    T1-->>T2: outbox: subscribe_ack + record/v1 backfill from offset 0
    T2-->>C: records, ascending, gapless
    C->>T2: order_submit/v1 {client_ref 9, market, higher, buy, 100, v482}
    T2->>T1: inbox
    Note over T1: DR — validate, stamp, append, fsync
    T1-->>T2: outbox: order_ack {client_ref 9, seq, replay_ts}
    Note over T1: MG→ME at the next state-emitting tick (§4) — fill/reject/unfilled appended to market-events
    T1-->>T2: outbox (PU): record/v1 (the terminal event, if subscribed)
    T2-->>C: order_ack, then the terminal event
Diagram source
sequenceDiagram
    participant C as client (SDK / consumer)
    participant T2 as gateway IO thread
    participant T1 as runtime thread (§1 loop)
    C->>T2: hello/v1 {initial_balance 10_000_000}
    T2->>T1: inbox: session action
    Note over T1: DR step — stamp account_created<br/>(replay_ts = latest published state ts, seq = order-log offset)<br/>append, fsync
    T1-->>T2: outbox: hello_ack {run_id, account_id 3, max_order_size, directory, quoting[{sleeve, ceiling_mills, family?}]}
    T2-->>C: hello_ack/v1
    C->>T2: subscribe/v1 {game_state, from 0}
    T2->>T1: inbox
    T1-->>T2: outbox: subscribe_ack + record/v1 backfill from offset 0
    T2-->>C: records, ascending, gapless
    C->>T2: order_submit/v1 {client_ref 9, market, higher, buy, 100, v482}
    T2->>T1: inbox
    Note over T1: DR — validate, stamp, append, fsync
    T1-->>T2: outbox: order_ack {client_ref 9, seq, replay_ts}
    Note over T1: MG→ME at the next state-emitting tick (§4) — fill/reject/unfilled appended to market-events
    T1-->>T2: outbox (PU): record/v1 (the terminal event, if subscribed)
    T2-->>C: order_ack, then the terminal event

Rules:

  1. hello/v1 or operator_hello/v1 (§2 row 14) is the first frame on a connection, and the only one — a second hello of either kind, or any other frame before it, is a protocol violation (close, §6). A successful hello is §8.1's Connected → Authenticated.
  2. With field (2), the gateway creates the account: it stamps and appends account_created/v1 (event-catalog §6.6) whose account_id is its own order-log offset — recorded, never a counter (dev-kickoff §2). The hello_ack is sent only after the fsync, like every root-log ack. With field (1), the gateway binds iff that account_id is a recorded account_created offset in the order log; unknown ids reject (code 2) and the session stays Connected. That binding is BY RECORDED EXISTENCE, not by signature (§9), and it is not authentication: an account id is an order-log offset readable by any subscriber of the order root, so any client naming it is bound to it — state-layers.md §8.1's Authenticated state names only that a hello succeeded. Transport-level access control (the deployment's TLS + credential) is outside this protocol. The record attributes an append to the session's bound account, never to a key holder; "verifiable from the root logs" (architecture.md, opening paragraph) therefore means recomputable, not authored — an authenticated ingress beyond the customer's signed instructions is the verifiable-order-book design's, out of scope. An operator session (row 14) binds the same way, by recorded existence. What is SIGNED is the customer instruction an operator relays (row 16): Ed25519 over the recorded bytes, verified in the exchange fold under the subaccount's recorded key (matching-engine §5), so a relayed instruction's record proves authorship by the customer key, while the operator's own appends stay attributed by recorded existence.
  3. subscribe/v1 per stream, any time after hello. Delivery per stream is ascending, gapless, from from_offset — backfill and live push are one path with one cursor. A from_offset beyond the current head is a valid forward cursor: delivery begins when the stream reaches it (the §8.1 "0, head, or a resume cursor" enumeration is not exhaustive). Cross-stream interleaving carries no promise; per-stream offsets are the client's clock. 3b. Two read styles, one data. fetch/v1 (classic request/response) and subscribe/v1 (push) read the same immutable streams and may be mixed freely on one session: a classic client polls with fetch and never subscribes; a streaming client subscribes; a hybrid backfills with fetch pages, then subscribes from next_offset. A fetch's reply — its record/v1 frames and the closing fetch_end/v1 — is contiguous on the wire (no subscription push interleaves inside one page). Fetch retains no session state: from_offset beyond head returns zero records and next_offset = from_offset. 3d. Control frames (7–13) are valid from any authenticated state, exactly like the quoting frames of rule 3c below — the operator is any authenticated session (the model-plugin §9 approver parameter; signed control belongs to the alignment note's phase 1, out of scope). Their appends ride the control ROOT log with its own counter (§4) and ack with control_ack/v1 after the fsync. 3c. Quoting frames (4/5) are valid from any authenticated state, exactly like order_submit/v1 — a quoting client typically subscribes to game_state, lifecycle, and market_events to build its view, but nothing requires it. The first accepted quoting frame naming an external sleeve binds that sleeve to the session's account for the run (first-claim; §5 codes 9/10) — the binding is derived from the recorded order log, never allocated, so a headless re-run derives it identically. A sleeve the hello_ack directory SCOPES to one family (§2 frame 129 field 4 (2)) accepts quoting frames for that family's markets alone: a frame naming another family's market is refused wire-side (§5 code 17), nothing is appended, and a refused frame binds nothing — the sleeve stays claimable, as after any reject. An unscoped sleeve quotes every market of the run, as every external sleeve did before. Engine outcomes for a quoting sleeve (quote_accepted / quote_rejected / quote_expired / quote_cancelled, fills as maker) arrive as recorded market events, not wire acks — the same recorded-fate rule as orders (§5 preamble). 3e. Operator frames (15/16) are valid only on an operator session (§2 row 14; code 20 otherwise), from any authenticated state, exactly like order_submit/v1. An operator session may name any subaccount in a relayed instruction, and the fold refuses one whose subaccount_opened names another operator (foreign_account, matching-engine §5) — the authority is the recorded opening, checked where the bytes are judged, so a headless re-run judges it identically. One customer key may own subaccounts under several operators; each subaccount's balance, reservations and positions are its own (matching-engine §6).
  4. Reconnection is re-subscription: nothing authoritative lives in the session, so hello (existing account) + subscribes at the client's last delivered offsets restore it byte-identically (§8.1).

4. Stamping, durability, ordering #

  • Stamping is matching-engine §2 verbatim: replay_ts = the latest published game-state event's ts at arrival (during a halt: the last published state's ts); seq = the per-gateway monotonic counter. The counter is the order log's next offset — every accepted append (order, account_created, forecast, ladder, subaccount_opened, instruction_relayed) increments both in step, so stamped seq equals positional offset: one counter home, the same convention as every derived stream. The CONTROL root carries its own counter under the same rule — one counter home per root ingress log; control records join the tick loop under the leg-1 STRICT control join (model-plugin §11 pin 1), which is exactly when a live drain surfaces them.
  • Durability: the order log is a root log — fsync before ack (event-catalog §5). Group commit within a tick is sanctioned; an acked frame's record survives any crash.
  • Inbox drain points: T2 appends inbound frames to the inbox as they arrive; T1 drains it at the DR point of each tick-GROUP (a group is a strictly-increasing-ts boundary — with distinct-ts ticks that is every tick; within an equal-ts group only its first tick drains), and at three DR-EQUIVALENT points pinned here: between pacing slices — while the next record is still ahead of its deadline; a record already past it is served at once with no slice and so no inter-record drain, and the tick-group DR point is then the only drain (§8) —, during the pre-run await phase, and once after the final tick (reads only — §5 code 8). Every inter-tick drain stamps with the same latest-published-state ts the next DR would use (nothing publishes between ticks) and the inbox channel preserves arrival order for seq, so the recorded history is byte-identical to a single-point drain — the extra points buy only ack/fetch responsiveness while paced, and only while the schedule has slack to sleep (ack latency on a serve running behind moves to the next tick's DR point, never the recorded order). Orders that arrive while a tick is in flight are stamped at the next drain with the then-latest published state ts — there is no sub-tick clock (§2). The merge is then deferred to the next STATE-emitting tick and joined by a STRICT >. A stamp names the latest published state, which does not advance across a non-state tick (a kill that emits no game-state version), so two consecutive ticks can share one stamp and a live drain's arrival timing alone would decide which tick merges the order. To keep that deterministic: the live driver DRAINS at each group's DR point (appends + acks, for latency) but MERGES an order only at a tick that emits a state, holding it across non-state ticks; the headless loop rolls its cursor back on any non-state tick. Both then merge the order at the first state-emitting tick whose ts strictly exceeds the stamp — the same tick — and because a non-state tick re-quotes nothing and opens no market, the resting book is unchanged by the wait (the fill is economically identical). Equal-ts states are legal (real logs carry several versions at one ts): a stamp resolves to the LAST version at its ts, live drains fall only on tick-group boundaries where ts strictly increases, and the strict join places records at those boundaries — so equality is unambiguous. Only a ts REGRESSION halts (StateTsNotIncreasing), as corrupt input; the earlier equal-ts halt was narrowed to this on real data.
  • Before the first published state there is no state ts to borrow: records accepted then (an account_created from a pre-run hello, a subaccount_opened or instruction_relayed from an operator session (§2 rows 15/16), or a control append — registering a model before the run starts is the expected operator flow; no market exists yet for an order) are stamped replay_ts = 0, the genesis value. Order-log genesis records join the first STATE-emitting tick's merge (the state gate applies to genesis too — the first match record is often a non-state roster); control-log genesis records join the first tick whose ts strictly exceeds 0 (the strict control join, no state gate). This is the one sanctioned stamp class that names no game-state event, pinned here (the matching-engine §2 rule's edge case). Genesis records join under the strict rule, nothing more: a stamp of 0 is strictly below every tick stamped above 0, so a pre-state record merges at the first state-emitting tick with ts > 0 with no special case. When the feed's first game-state event is itself stamped 0 (Riot's frame 0 — every real LoL match), a record accepted after it is stamped 0 too (rule 1: the latest published state's ts), and nothing recorded distinguishes it from a pre-state record — so a stamp-0 record never joins a tick stamped 0: live, the driver presents only records stamped strictly below the tick's ts (its drain-group gate would otherwise present them at a tick they are not below); headless, the join is the same strict comparison — both merge it at the first state-emitting tick whose ts strictly exceeds 0. Found by the live LoL gate: an earlier "genesis widening" (stamp-0 records eligible until a state exists) had merged a post-state order at the ts-0 first tick headless (no quote rested yet) while live it filled a frame later — one fill apart, the §8 equality broken. The widening is gone.
  • Quoting-record consumption: a recorded forecast or ladder is stamped, fsynced, and MERGED exactly like an order (it is an order-log append with a class-3 recorded key). When the merge reaches it, it drives the external sleeve's quote fold inline: the batch folds into the engine at that exact slot, rides the sleeve's quote stream stamped with the record's replay_ts, and is tagged with the version of the state the stamp names — every batch input a pure function of the recorded data, which is what keeps the §8 equality independent of which tick a live drain assigned the record to (architecture.md external drive rule 1 pins the argument and why the record's seen_version is never the batch tag).
  • Outbox fill: acks are enqueued at DR (post-fsync); stream pushes are enqueued at the §1 PU step, after the taps. Each session's outbox is FIFO, so an order's ack always precedes the pushed record of its terminal event. T2 performs every socket write — the fold thread never blocks on a slow client (dev-kickoff §4).
  • Outbound I/O fairness: each T2 connection attempts an inbound read after at most 32 outbound frames (OUTBOUND_BURST_FRAMES). A continuously replenished subscription must not prevent later subscriptions, orders, or quoting frames from reaching the inbox. Bursts preserve the outbox's FIFO order, including a queued close. This is a work bound, not a wall-time guarantee: a blocked socket write still stalls only that connection's shuttle, and the bounded-outbox slow-consumer rule (§7) is unchanged.
  • The merge quadruple (matching-engine §2) already guarantees an order can never trade against a book that hasn't reflected the state the order saw; the wire adds nothing and takes nothing from that.
  • Terminal-event correlation: market events carry account ids, not seqs. For one (account, market), terminal events are emitted in merge order = the account's seq order, so correlation is positional. A per-order id on market events is a recorded event-catalog extension, deliberately not minted here (wire frames must not force recorded schemas) — except for a relayed instruction, whose order id IS its seq: retail_rested, retail_cancelled and instruction_rejected name it (event-catalog §6.7).

5. Gateway validation — reject codes #

Matching-engine §8: "malformed input is not history" — what fails validation is never appended. What passes is appended and meets its recorded fate downstream (stale, halted, unfunded, unfilled — and, for a relayed instruction, undecodable, foreign, unsigned, replayed, self-crossing, oversold or over the resting cap — those arrive as market events, not wire rejects).

code meaning trigger
1 protocol_state frame out of order for the session state (e.g. order before hello) — followed by close
2 unknown_account hello or operator_hello (1) names an offset that is not an account_created record
3 size_zero order size 0 (matching-engine §8 gateway-validation row)
4 size_over_max order size above the run-manifest max_order_size — the engine trusts gateway validation; an oversize order reaching it is a halt, so the gateway is the enforcement point
5 unknown_stream subscribe or fetch names a stream_id not in the run's directory
6 bad_hello hello or operator_hello with both/neither of account_id and initial_balance, an unsupported protocol_version, an initial_balance_mills that does not fit a signed mills balance, or one that would lift the run's TOTAL OPENING BALANCE past the signed-mills maximum — the sum of the manifest's sleeve ceilings and every account_created and subaccount_opened balance the run's logs hold, checked; money moves between parties and pots, a sleeve's free balance never below the negative of its ceiling, so every balance, pot and sum the folds form stays inside the signed-mills range; the exchange fold holds the same bound and halts on a record breaking it (matching-engine §5 item 8), this refusal its mirror; nothing is appended
7 unknown_market an order or quoting frame names a market_id with no market_created record in this run, OR (quoting frames) a market this run prices no claim family for — "market is not priced by this run": with no family there is no payout vector and no class count, so there is no width to check and nothing the fold could price — the engine halts on unknown market ids (corrupted history, matching-engine §8 posture), so the gateway is the enforcement point, exactly as for code 4. The mechanism (one process): a read-only same-process query of the scheduler's created-market set; nothing recorded depends on it (rejects are wire-only, the headless re-run never re-validates), and the same fact set is derivable from the lifecycle records the gateway already serves — which is the physical split's implementation (out of scope).
8 run_over an order, quoting frame, control frame (a sleeve control frame to a sealed run among them — nothing left to stop, risk-engine.md §8 E7), operator frame (rows 15/16), or account-creating hello of either kind after the final tick. The run's last merge has already happened: a record appended now could never be processed live, while the headless re-run WOULD consume it (its stamp is the final state's ts) — appending it would break the §8 equality. So once the run ends, the gateway answers reads (fetch, subscribe, existing-account hello) but appends nothing; write frames reject with this code.
9 unknown_sleeve a quoting frame names a sleeve the run manifest does not declare as an external quoting sleeve (house sleeves are model-driven and can never be claimed); the runtime HALTS on such a recorded record (RuntimeError::UnknownQuotingSleeve, at the ingress partition and, defensively, at the drive's sleeve-index read — unreachable after the partition, which validated the same bookmakers vector), so the gateway is the enforcement point exactly as for codes 4/7/10. The quote-stream append carries a DIFFERENT typed halt on a different invariant — MissingQuoteStream at its ONE lookup: the stream set and the bookmaker list derive from one manifest — reached from the house arm too, where no wire frame exists and this code's posture does not apply
10 sleeve_taken a quoting frame names an external sleeve already bound to a different account. The first accepted quoting append naming a sleeve binds it (§3 rule 3c); the gateway enforces the binding at drain so a contradicting record is never appended — the runtime derives the same binding from the recorded log and HALTS on a contradiction (corrupted history; the gateway-is-the-enforcement-point posture of codes 4/7)
11 bad_forecast forecast_submit whose p does not sum to exactly 65 536, or whose width is not the target market family's outcome-class count (checked after the market is known — an unknown market is code 7; a known market with no priced family is also code 7, "market is not priced by this run"). Array shape (1..=8) and per-element u32 range are §7 decode-level — this code is the readable semantic failure
12 bad_ladder ladder_submit failing the matching-engine §4 step-1 structural admission the gateway pre-validates: duplicate (side, price) level, own bid not strictly below own ask (self-cross), size 0, or price off the 1..=999 mill range. One implementation — the gateway calls the exchange crate's own batch_is_valid plus the range checks; a recorded ladder can therefore never be structurally rejected by the engine (the fold re-checks and halts on contradiction, exactly as for code 10)
13 unknown_model a control frame names a ModelId the run's artifact store does not hold (register cannot honor "artifact stored, hash verified" — model-plugin §9), or an action names a model the §9 fold has never seen
14 bad_control_state a control action the §9 state machine refuses at drain (promote before shadow, re-registration outside Suspended, suspend a non-running model, promote to a non-house sleeve), OR a sleeve action (rows 12/13) the gateway's drain-time SLEEVE MIRROR refuses — a sleeve the manifest does not declare as a bookmaker sleeve (code 14, not code 9), a reason-2 suspension of a Suspended(liquidated) sleeve or a reason-0 suspension of any Suspended sleeve, a resume of a Quoting sleeve or of one suspended with reason 2, a reason-2 suspension where reason 2 is not admitted (reason 2 is admitted only on a run whose inclusion's document names R6 v1; elsewhere SleeveReasonUndeclared, code 14 at the wire) — the five fold halts of risk-engine.md §5 it pre-empts (SleeveUnknown, SleeveAlreadySuspended, SleeveNotSuspended, ResumeAfterLiquidated, SleeveReasonUndeclared), a re-sent reason-2 frame among them, refused and harmless; OR an action naming a model with a PENDING queued effect ("action pending until the next market_open — retry after it"; promotion after a pending shadow stays legal — its acceptance is order-invariant, the §9 shadow-then-promote window). Pre-checked against the gateway's drain-time MIRROR registry so an invalid transition is never appended — the fold halts on a recorded bad transition, making the gateway the enforcement point (the codes-4/7/10 posture). The mirror can only LAG the true fold (the barrier-lift race), and the pending-effect refusal is what makes every ACCEPTED record's fold verdict invariant to open interleaving: mirror-accept ⇒ fold-accept, and a refusal in the race window is conservative — the operator retries after the open
15 bad_gate_document backtest_submit whose document fails the hardened card decode, whose candidate ModelId contradicts the frame's, which names a model not awaiting gate 4, or whose card fails a server-checkable identity claim — split_name other than validation (§10 split governance: only a validation card is a valid admission gate-4 result; the sealed test split is graded offline by d2-trainset test-eval), wrapper_version not this build's, or feature_schema_id not the registered artifact's binding
16 unknown_feature_schema model_register naming a STORED artifact whose recorded feature_schema_id is the grid of NO column of this run — or of TWO (a column's grid is its tagged model sleeve's recorded schema, or the family's v1; Scheduler::schema_for is unique-or-none, the fold's own column_for_schema rule). The §9 registration fold HALTS on such a recorded registration (RuntimeError::UnknownFeatureSchema / AmbiguousSchema, the corrupted-history posture — runtime control_edge.rs), so the gateway is the enforcement point, exactly as for codes 4/7/10: nothing is appended and gate 1 never runs for it (the startup gate pre-computation, model-plugin §10, holds an entry for exactly the artifacts a column serves, and the MISSING entry is what the register handler refuses — one predicate, one site). Named for the fold halt it pre-empts, as every code here is (unknown_market ↔ UnknownMarket); the bench's BenchError::ForeignGrid is a different fact (a features record of another grid reaching a bound model, model-plugin §12). The mechanism (one process): a read-only same-process query of the scheduler's per-column schemas; nothing recorded depends on it (rejects are wire-only), and the fact set is derivable from the run manifest and the artifact store the gateway already holds — so the physical split (out of scope) keeps the check at the edge without a stream read (a stronger position than code 7's, which reads a lifecycle fold). The check sits at the edge, before any ack, because a wrong-grid registration that was acked and recorded would halt the live serve at the register drain for every session; it holds for a grid of no column or of two.
17 sleeve_scope a quoting frame (rows 4/5) names a market outside its sleeve's SCOPE — the sleeve is one the run manifest tags to one family of a board (model-plugin §12, BK-4 on an external sleeve; the hello_ack directory carries the scope as the entry's field (2), §2 frame 129) and the market belongs to another family — "market is not of the sleeve's family". Checked AFTER the market is known and priced (code 7) and, on a forecast, after the width law (code 11), BEFORE any append, on both quoting rows through the scheduler's ONE scope rule (Scheduler::sleeve_covers: an unscoped sleeve covers every market; a scoped sleeve covers exactly the markets its family's column claimed); a refused frame binds nothing (§3 rule 3c). The runtime HALTS on such a recorded record (RuntimeError::QuotingScope, architecture.md external drive rule 3 — the scope clause: ahead of the retired-market skip, behind the market being PRICED — a created market no column claimed halts UnpricedMarket on either row, a never-created one keeps its own halt), so the gateway is the enforcement point, exactly as for codes 4/7/10: nothing recorded depends on the reject, the headless re-run never re-validates it, and the predicate is pure in the manifest and the recorded market_created records — monotone over the run, so a frame the gate admits is never a record the halt fires on
18 sleeve_suspended a quoting frame (rows 4/5) WITH LEVELS of a sleeve the gateway's sleeve mirror holds Suspended — from the append of the frame that suspended it (row 12, either reason, or the gateway's own start-time reason-2 record, risk-engine.md §8 E7) until a sleeve_resumed of a reason-0 suspension; a reason-2 suspension lasts to the run's seal (risk-engine.md §5). The EMPTY ladder (the pull) is exempt, appended and acked. Checked after code 19 — a frame both would refuse is refused with 19. The producer is the gateway's sleeve mirror, fed by the sleeve frames' appends; wire-only, as every reject code is: the runtime's sleeve fold silences every quoting record of a suspended sleeve, so no recorded fact depends on the refusal
19 feed_stalled a quoting frame (rows 4/5) of a run whose feed is stalled — from the stall record's tick until the first state above the stall record's ts after the recovery (risk-engine.md §3 R7 (f)); the EMPTY ladder (the pull) is exempt, appended, acked and silenced. An operator frame (rows 15/16) is no quoting frame and is never refused with this code: an open_subaccount/v1 and a relay_instruction/v1 are appended and acked through the stall, and a relayed instruction meets the feed gate in the fold (risk-engine.md §3 R7 v2 (e): a place stamped at or below the stall record's ts fills nothing on the book the stall emptied and never rests). Wire-only, as every reject code is: the runtime silences, by the pause or by the record's stamp, every frame the gateway would refuse, so no recorded fact depends on the refusal
20 not_operator an operator frame (rows 15/16) on a session opened by hello/v1 — only an operator_hello/v1 session opens subaccounts and relays instructions (§3 rule 3e); nothing is appended
21 bad_opening_balance an open_subaccount/v1 whose opening_balance_mills does not fit a signed mills balance, or would lift the run's total opening balance past the signed-mills maximum (code 6's two bounds on a hello's initial balance); nothing is appended
22 no_fee_rule an operator_hello/v1 on a run whose manifest pins no retail_fee_rule (MF-16) or no max_resting_orders (MF-17, model-plugin-layer §12) — the run declares no fee rule or no resting cap, so it serves no operator session; nothing is appended and the session stays Connected

6. Connection-fatal conditions #

These close the connection (after a best-effort reject/v1 where a frame was decodable enough to answer): undecodable or non-canonical CBOR, a frame exceeding the size cap, an unknown frame-type id, an inbound frame id from the outbound range, a closed-map violation, a decode-checked enum value outside its domain (rows 10 and 12 — the reasons of model_suspend/v1 and sleeve_suspend/v1), a byte field outside its fixed length (rows 15/16: a customer key other than 32 bytes, a signature other than 64, an instruction empty or over MAX_INSTRUCTION_BYTES), or rule-1 hello violations. A close loses nothing durable — §8.1's Dropped state; the client reconnects and resumes from its cursors.

7. Parse-hardening caps #

The gateway parses external-format bytes; the same posture as model-plugin-layer.md §16 applies at this edge:

  • MAX_FRAME_BYTES = 65 536 — inbound WebSocket message size cap, enforced INSIDE the WebSocket library's reassembly loop (max_message_size/max_frame_size config — a hostile client can never make the server buffer beyond the cap) with a defense-in-depth re-check before the bytes cross threads.
  • OUTBOX_FRAMES = 4096 — per-session outbox capacity; a session whose outbox fills (a client requesting faster than it reads) is dropped as a slow consumer — per-session queued memory is capped, never attacker-rate-unbounded. A drop loses nothing durable (§6).
  • MAX_LADDER_LEVELS = 32 — levels per ladder_submit/v1; a longer array is malformed at decode (connection-fatal, §6 — a cap violation is hostile input, the MAX_FRAME_BYTES posture, not a semantic reject). The house fold posts at most 2 levels; 32 bounds the per-record book work an external sleeve can demand. The constant is homed with the recorded schema it governs (bookmaker events — an over-cap RECORDED ladder must refuse to decode as corrupted history) and re-exported here as the protocol constant.
  • MAX_FETCH_PAGE = 512 — records per fetch/v1 reply page; larger requests truncate to the cap (the client paginates via next_offset), max_count = 0 requests exactly one full page.
  • MAX_INSTRUCTION_BYTES = 128 — the relayed instruction's length (§2 row 16 (1)), above the widest well-formed instruction (a place, 119 bytes — event-catalog §6.6.1's derivation; a cancel at most 68). Empty or over the cap is malformed at decode (connection-fatal, §6 — the MAX_LADDER_LEVELS posture), checked before the frame's own copy of the bytes (the decode buffer itself bounded by MAX_FRAME_BYTES); a well-sized but non-canonical instruction is no wire fault — it is appended and meets its recorded bad_instruction (matching-engine §5). Homed with the recorded schema it governs (the exchange's order-log events — an over-cap RECORDED instruction must refuse to decode as corrupted history) and re-exported here, as MAX_LADDER_LEVELS is. The customer key (row 15 (1)) is exactly 32 bytes and the signature (row 16 (2)) exactly 64 — RFC 8032's lengths, any other length malformed at decode on the wire and in the record alike.
  • Forecast payload width: stream-core MAX_CLASSES (= 8) — weights per forecast_submit/v1 (§2 row 4). An empty array or one over the cap is malformed at decode (connection-fatal, §6 — the MAX_LADDER_LEVELS posture), and the bound is applied BEFORE the length sizes any buffer. Inherited via pricing::contract, not duplicated; the semantic law — width == the target market family's outcome-class count — is §5 code 11.
  • Codec depth cap: stream-core MAX_DEPTH (= 32) — inherited, not duplicated.
  • The quoting entry's family text on the client half (§2 frame 129 field 4 (2)) is a NON-EMPTY text of at most MAX_FAMILY_TEXT bytes — the run manifest's title cap, the / and its family cap (16 + 1 + 32), one vocabulary, one cap for every text drawn from it, the longest the runtime offers being 26 — checked before the text is copied; the entries ascend by sleeve id (the server's own order) and a duplicate or descending pair is malformed, never resolved by position.
  • A directory entry's stream name on the client half (§2 frame 129 field 3 (0)) is a NON-EMPTY text of at most MAX_STREAM_NAME_TEXT bytes — the runtime's bound on its derived names, homed with that vocabulary (runtime::streams::MAX_DERIVED_NAME_TEXT): the longest, probabilities/<model id>, is the 14-byte prefix and a model id's 64 lowercase-hex bytes = 78; the other name derived from a recorded id, quotes/<sleeve id>, is at most 17; every remaining name is a fixed word; the directory's other vocabulary — the gateway's three reserved root-log names (§2 frame 129) — is asserted under the cap at compile time. Checked before the text is copied, counted in UTF-8 bytes on both halves; and the names are UNIQUE — a duplicate is malformed, never resolved by position (the quoting entries' rule above).
  • The family text the SDK reads off a recorded settlement_complete/v1 barrier (SC_FAMILY, event-catalog §6.8 — a RECORD text, not a frame field: it reaches the client inside record/v1 (§2 frame 133 (2)) and the SDK applies this section's one optional-text rule to it) is held to MAX_FAMILY_TEXT, the <title>/<family> vocabulary it is drawn from (ClaimSeries::family); so are the claim series' title and family a client reads off market_created/v1 (the series catalogued at event-catalog §6.9; THIS section sets the caps — the title to MAX_TITLE_NAME_LEN, the family text to MAX_FAMILY_TEXT) — both sides of the SDK's family comparison under one cap. A void_complete/v1's family (VC_FAMILY) is read by no client of ours — a void names family-exact market ids — and any client that comes to read it is held to the same cap. The recorded half applies NO decode-time length bound: replay's barrier decoder reads an absent family as None (the single-family run, §6.8) and refuses an empty or non-text one; its claim-series decoder requires a present text; a recorded text's only bound is the run manifest its producer drew it from — and, on corrupted history, the segment layout's per-record ceiling (event-catalog §4, MAX_RECORD_LEN = 16 MiB), the backstop that keeps the allocation bounded.
  • A reject/v1 detail (§2 frame 132 (2)) on the client half is a text of at most MAX_REJECT_DETAIL_BYTES = 4 × MAX_REJECT_DETAIL_CHARS (= 2 048) — checked first, on the borrowed text, the allocation bound — AND of at most MAX_REJECT_DETAIL_CHARS (= 512) characters, counted over those bytes: the client admits exactly the details the server's seam leaves unchanged, so a detail within the byte cap but over the character cap — a frame no server of ours emits, which the encoder's own arm would cut — is malformed like an over-byte one, and every admitted reject/v1 re-encodes to its own bytes (the §1 identity the frame codec's fuzz target asserts, below). Every reject/v1 is produced by Driver::reject, whose detail passes the ONE seam protocol::bounded_detail — applied again inside the encoder's own arm, so the invariant holds for any caller: the detail is cut by CHARACTER at 512 (at most 2 048 UTF-8 bytes), and a result the codec would refuse (non-NFC — a detail may quote a client's decoded text, which the inbound codec already held to NFC, so this is the by-construction guarantee, not a live path) is replaced by the fixed marker DETAIL_WITHHELD ("detail withheld: non-NFC text"), never silently emptied. Repair is a SERVER act at that seam; the decoding half never repairs — the client refuses a detail over the cap (never truncates it) and refuses non-NFC text as §1 says — and admits an empty detail, which no server of ours writes (every detail is a literal, a decoded text or the marker; a code is its own reason). The code itself is one of this table's (§5) on both halves — an unknown code is malformed, never named by number on a decoded frame.
  • The frame codec's cargo-fuzz targets (the gateway entry of model-plugin §15's parser-fuzzing list): crates/gateway/fuzz/fuzz_targets/decode_inbound.rs and decode_outbound.rs, a standalone cargo-fuzz workspace that is no root-workspace member and runs by an operator's hand, never in CI (the posture crates/runtime/fuzz/ and crates/grid-edge/fuzz/ set); the same workspace's third target, decode_report_card.rs, is the submitted-card reader's — every input going to ReportCard::from_canonical_bytes unchanged, as backtest_submit/v1 field (2)'s bytes do in production (the only bounds the frame cap and the reader's own MAX_CARD_BYTES, which it checks itself — the two caps equal, so a card inside an accepted frame is never over the reader's and that arm is defence in depth, reached by the seeder's probe alone), under the same three arms as the frame targets — no panic (a typed CardError per refusal), no overflow, and the reader's own bijection on every Ok: an accepted card re-encodes to its input (the law the gate-4 details_hash chain rests on; pricing/tests/backtest_gate.rs's byte-flip sweep asserts it on its flips); its seeds are the real gate-4 card over two distinct models, its boundary derivatives through the public fields and the real writer, and one refused card per CardError arm but the size cap's (the probe's), each naming its arm, an exhaustive arm map making an arm without a seed a build or run error; beside the Q.16 domain's range arm, the reader refuses fifteen facts of aggregate's construction (the first three, six more, the calibration tables' five, and the bucket MEANS tied to the paired snapshot count), each a CardError::Inconsistent; model-plugin §10. Every libFuzzer input reaches the frame decoder UNCHANGED, as a WebSocket Binary message's bytes do in production (the only pre-filter is this section's size cap, which envelope repeats as FrameTooLarge), under three arms: no panic (a typed ProtocolError per refusal), no overflow (cargo-fuzz's default build keeps overflow checks on) and the §1 identity on every Ok — the accepted bytes re-encode, through encode_inbound / encode_outbound, to exactly themselves (the oracle the bit-flip sweep asserts on its sweep set; a lenient read, a normalised value or a second accepted spelling is the finding). The corpora are seeded by examples/seed_corpus.rs from every §2 row through the public encoders over real fixture material — the scoped LoL board's run manifest, its real root-log ids and the derived stream directory the runtime's own stream-set constructor writes for it, its external sleeves as the quoting entries, the real gate-4 card, a real match-log record — plus the accepted side of each cap the two decoders check on a frame's fields (the forecast width, the ladder's levels, the 78-byte probabilities/<model id> name, the worst-case and the empty reject detail), the scoped sleeve's claim text (the fixture's lol/race_to_kills, 17 bytes — the vocabulary's claim texts are at most 26, under MAX_FAMILY_TEXT, so no server writes a longer one, and the longest is in the dictionary only), a fetch/v1 at the full page the DRIVER truncates to (MAX_FETCH_PAGE is no decode cap — larger requests truncate, above — so it is accepted by construction), and refused seeds for the refusal arms (the codec depth cap among them: no legal frame approaches it, so it has a refused side only; the frame-size cap is reached by a separate over-cap probe); a bounded local run per target is the deliverable its README records. The deterministic sweeps in crates/gateway/src/protocol/tests.rs — the single-bit-flip identity sweep (event-catalog §3 rule 5, adopted by §1) and truncation over every registry row of both halves, and the seeded-garbage sweep through both decoders — and the typed malformed-input tests stay the merge gate (model-plugin §15).
  • Closed maps (§1): unknown field ids in any frame body are malformed.
  • Every integer field range-checked at decode (side/action discriminants, u32 sizes, protocol_version).
  • Caps are constants in the gateway crate (MAX_LADDER_LEVELS re-exported from bookmaker events and MAX_INSTRUCTION_BYTES from the exchange's order-log events per the bullets above; the depth cap inherited from stream-core; the two text caps DERIVED here from bounds homed where the text is written — MAX_FAMILY_TEXT composed from the run manifest's MAX_TITLE_NAME_LEN / MAX_FAMILY_NAME_LEN, MAX_STREAM_NAME_TEXT the runtime's MAX_DERIVED_NAME_TEXT itself, with this crate's reserved root-log names asserted under it) — the crate is the registry of record for the rest, as ingest is for field ids (event-catalog §6.1).

8. The demo replay host #

The demo serving mode (architecture.md LIVESIM, headless variant): the gateway binary re-serves a recorded match log through the full §1 loop, live clients attached. The runtime thread runs the tick loop with the DR step draining the live inbox (the headless variant's recorded-order-log source is replaced, its ordering rules unchanged — dev-kickoff §1); the order log is a fresh root segment the gateway writes. Pacing — mapping replay-ts units to wall time so humans and SDK clients experience a live match — is a command-line parameter (replay-ts units per wall second, in the served title's own clock unit: event-catalog §6.12 rule 3 for cs2, §6.11 rule 4 for dota2, §6.15.2 for lol — the archive-fed logs; a GRID-fed log's replay_ts is the envelope sequenceNumber, event-catalog §6.13–§6.15 rule 6, with no wall-time unit, so pacing it is sequence units per wall second, not game time; 0 = unpaced). The implementation paces against an ABSOLUTE DEADLINE on the driver edge: the run's origin is the wall instant of the first paced record and its replay ts, and every later record is due at the origin plus its replay-ts offset at the pace (driver/pace.rs — Pacer::deadline_for, exact integer microseconds), so per-record overhead eats into the next sleep instead of adding to it; a record already past its deadline is served at once and the schedule catches up. WHILE the serve keeps up (per-tick cost below the nominal gap) the error is bounded by one record's overhead and is never cumulative; at a pace faster than the machine can serve (the fast-forward researcher paces — FH_TPS 2000 at stride 1 runs the tick loop flat out, ops/sandbox/lib.sh) every deadline is already passed and the serve runs as fast as it can: the pace is a CEILING, not a guarantee. A log whose stamps restart (a second match from zero) re-anchors the origin at the restart record — due now, the remainder paced from it. (A driver that sleeps the nominal gap per record with no clock read accumulates its per-record overhead and under-serves the pace; the absolute deadline is what keeps a 64 Hz master at 1× at 64 records per wall second while per-record work fits the gap.) The wall clock is read for pacing ONLY, at one allow-listed Instant::now (the gateway is the sanctioned impure edge — architecture.md determinism rule 2's edge clause, clippy.toml's call-site allow; the gateway's tests/driver_pacing.rs counts the sites): nothing read from it enters a record, a stamp or an id. It is one of the two sites rule 2 names; the other is the live capture's stall detector, which decides WHETHER a feed-health record is appended (event-catalog §6.13 rule 12 (d)), counted by its own crate's gate. A served log carrying a feed stall replays it structurally — the pull at the stall record, the pause to the recovery record — compressed to its sequence gap at the pace, the records carrying no wall time. The one number derived from that clock that leaves the edge is the run's pacing TALLY: for every record whose stamp exceeds its predecessor's, whether the loop slept for it (HELD — it went out at its deadline, late by the final inbox drain and the sleep's slop) or found its deadline already reached (LATE — this section's catch-up, which at a pace the machine cannot serve is every record); a record whose stamp does not exceed its predecessor's — an equal-stamp record, a restart's re-anchor record — is due at once by the rules above and is not counted. The driver returns the two counts beside the runtime's counters; the host prints them on a line of their own after the summary line (an unpaced run prints that it is unpaced), so the summary line stays the headless run's six counts. The tally enters no record, stamp or id and is written to no stream; tests/driver_pacing.rs reads it where a tight wall-clock ceiling stood (a ceiling measures the runner, not the driver — the one wall bound the gate keeps is a 20 s completion timeout against a schedule that parks a record minutes out). Pacing affects WHEN ticks run, never what they compute; it does move which tick a live order falls in, the order log being a root the gateway writes.

The demo host holds the run until one session has authenticated (await_session — the serve exists to be traded against; a run that started before any client connected would replay to an empty room). Operators can disable it for headless-with-sockets serving; the CLI defaults it on.

The determinism equation crosses the wire (normative): a live session produces its root artifacts — the match log it served, the order log it recorded, and the control log it recorded (empty when no operator acted, but a root input all the same — derived stream ids bind it as a lineage parent). Re-running those through the headless scheduler reproduces every derived stream byte-identically (verify::diff_runs empty). The live run's recorded root logs (the order and control logs) and its derived streams are sealed when the run completes, so every derived-stream file of the live run directory is byte-identical to the headless re-run's as a whole file, footers included. Live serving is a pacing skin over the same fold; this equality is the gateway's golden gate — gated live for the control thread by the lifecycle e2e (register → gates → card → shadow → promote → re-run).

9. What this document does not cover #

  • The operator control edge is covered: frames 7–13 and control_ack/v1 above are its registry rows; the control ROOT log is the run directory's control.d2s, served in the hello_ack directory under the reserved name control. A stream created MID-RUN (a registered model's probability stream at its shadow's open) joins later hellos' directories; already-connected clients learn it by reconnecting (§3 rule 4).
  • Signed-quote ingress (bounded quote keys) — the real-world alignment phase (the alignment note §3), out of scope; account and operator binding is by recorded existence, not signature — the one signed ingress is the customer instruction an operator relays (§2 row 16), verified in the exchange fold (matching-engine §5).
  • Broadcast/live ingestion (Flow 4) — upstream of the match log, invisible here.