Settlement Contract — Decision Record #

The binding definition of what settles every Dust2 kill market (SC-1..SC-6), the match-winner claim family LoL, Dota2 and the synthetic synth title settle on (SC-7), the race-to-kills line family LoL and Dota2 settle on (SC-8), the kills-over-N-at-minute-M line family LoL and Dota2 settle on (SC-9), its per-side variant (SC-10), its gold-lead variant (SC-11), the tally families — towers, dragons, barons (SC-12) — the HOUSE-LINE families — the final total kills and the kill handicap over a line the HOUSE sets (SC-13) — and the void unwind every family shares (SC-V). Pinned before the parser is written; the preflight report certifies conformance; every rule carries a parser assertion. Companion to event-catalog.md (§6.1 match-log events) and flows.md Flow 0/3/5.

Decision authority: the product owner. Changes to this contract after data has been labeled invalidate the affected splits — retrain, re-evaluate.


SC-1 — Kill counting: enemy kills only #

final_kills(team) = count of kill/v1 events in the round where attacker_team = team and victim_team ≠ team.

  • Teamkills credit nothing (to either team).
  • Suicides credit nothing.
  • Deaths with no player attacker — C4, fall, world — credit no one, even when the victim is an enemy.

Rationale (the open-markets test): settlement must be a pure predicate over canonical events — that is what makes claim families declarative, provable, and portable across data providers. "Enemy kills" is computable from kill events alone. "Scoreboard-credited kills" depends on each provider's crediting rules, which vary by parser and patch — the opposite of oracle-grade. This rule also bounds the outcome space by construction: each of at most five enemies dies at most once, so 0 ≤ final_kills ≤ 5 is a theorem, not an assumption.

Data dependency: victim_team is a first-class field of kill/v1, populated once at ingest by joining victim_slot against the round_start/v1 roster (event-catalog.md §6.1) — the settlement predicate reads kill events alone, with no runtime join.

Assertions: 0 ≤ final_kills(team) ≤ enemies_alive_at_round_start(team); ingest asserts every kill's victim_team matches the roster join; any demo-provided scoreboard is a diagnostic cross-check only (mismatch flags the round for investigation; the kill-event count remains truth). A rostered player observed dying twice before the cutoff violates the theorem's premise ("each enemy dies at most once") and fails settlement as corrupt input — the round becomes ineligible per SC-3; the bound above is retained as a spec-named backstop even though duplicate-death enforcement makes it unreachable by construction.

SC-2 — Cutoff: the round_end event #

A kill counts iff its match-log offset ≤ the round's round_end/v1 offset.

Post-round kills (after the win condition — elimination, defuse, explosion, time — but before reset) are excluded. The round was decided; the market settles on what decided it.

The cutoff is the CANONICAL round_end (SC-5 composes with this rule): a provisional round_end reprices quotes but is not a cutoff, so a kill recorded between a provisional end and the canonical end counts toward final_kills. Dormant on the batch path (demo round ends are canonical by construction); operative on the live path.

Assertions: parser totals post-cutoff kills separately as a diagnostic; they never enter final_kills. round_end.reported_kills_t/ct are source-reported diagnostics (event-catalog §6.1), cross-checked against the count recomputed from kill events; a mismatch flags the round per SC-1 — the fields are never a source.

SC-3 — Eligibility: all real rounds #

Auto-excluded (never a market): warmup, knife/decider rounds, technical restarts (mp_restartgame), backup-restored rounds (round_restored/v1 — per the scope ledger, the demo never opens these), and rounds failed as corrupt input by settlement assertions (SC-1 duplicate-death and its siblings — SC-1's becomes-ineligible clause has its entry in this list).

Eligible: everything else, explicitly including shorthanded rounds (disconnects, 4v5) and overtime. The models see alive counts and economy state; excluding real conditions would shrink the dataset against the 1,500-round preflight gate and train models on a world cleaner than the one live operation faces.

Assertions: every round in the manifest carries eligible: bool plus a reason code; ineligible rounds are retained in the log (immutability), only unmarked for markets and training labels.

SC-4 — Disconnects mid-round #

No special rule, by construction: a kill already credited stays credited (history only grows); a player who disconnects while alive produces no kill event and therefore no count. The bound in SC-1 tightens naturally.

SC-5 — Finality #

Settlement consumes only round_end/v1 with finality: canonical. Provisional round ends may reprice quotes (per growth-path D4) but never settle. Batch-parsed demos are canonical by construction — the distinction becomes operative on the live path.

SC-6 — Worked examples (parser test fixtures) #

Scenario Count?
T player kills CT player before round_end +1 T
T player teamkills a T player +0 (both teams)
CT dies to the planted C4 explosion +0 (no attacker)
T player falls to death +0
CT kills T after the bomb exploded (post-round) +0 — diagnostic only
T player kills CT, then T player disconnects +1 T (stays)
4v5 round: the FULL side kills all 4 of the shorthanded side valid; assert ≤ 4 (the bound is the enemy roster size, not 5)

These seven fixtures ship as parser golden tests before any real demo is labeled.

SC-7 — Match-winner claim family (LoL / Dota2 / synth) #

The one binary family every match-outcome title settles on (ingest::match_winner; bound per title as lol/match_winner/v1, dota2/match_winner/v1 and synth/match_winner/v1 — the synthetic title of the market-dynamics simulation, event-catalog §6.18 — event-catalog §6.9 market_created row).

  1. Outcome space: two classes over the claim's side — class 0 the side lost, class 1 it won. The claim is always written on the model's fixed perspective side (blue / radiant / Team A, wire 0); the other side is the short side of the same book. Only the policy writes markets, and it writes side 0; the router and the outcome source accept either side of the vocabulary as defence in depth (a side-1 descriptor prices and settles correctly, it is simply never produced).
  2. Finality: the title's completion record and nothing else — lol/match_complete/v1 (blue_win), dota2/match_complete/v2 (radiant_win, GRID), dota2/match_complete/v1 (radiant_win, the betty archive) or synth/synth_complete/v1 (team_a_win — Team A the perspective side, wire 0; event-catalog §6.18). A fallen nexus / ancient is state (nexus_fallen on lol/state/v1), never a settlement signal: the feed's completion is the one recorded outcome, and a partial capture with no completion never settles (SC-5's posture). The match closes its market on the forwarded completion (event-catalog §6.11.1/§6.14.1/§6.15.1/§6.18 rule 4) and settles when both the outcome is final and the market is Closed (flows.md Flow 3, the same barrier as the kills family).
  3. Payout: a unit claim redeems 1000 mills if its side won, 0 otherwise ([0, 1000] — the §6.8 grid ends); the short side redeems the complement through the pot, which drains to exactly zero (matching-engine §6). No partial credit, no draw: no title's feed carries one.
  4. Identity: epoch = the match's discovery ordinal in the run's game-state stream — the forwarded match_discovered, on the betty FRAME log its roster_side/v1 envelope, which carries the same recorded u64 id spelled the same way (event-catalog §6.11 rule 11), or on a synth log the forwarded synth/synth_discovered/v1 (event-catalog §6.18). An epoch is FAMILY-LOCAL — this rule and every identity rule of SC-8 to SC-13 number epochs within THEIR family, so on a board (several families on one match) families share epoch numbers and the settlement barrier names its family (event-catalog §6.8); the descriptor carries the title's recorded match id, and settlement re-checks it against the epoch's recorded match (a mismatch halts — corrupted terms). A completion with no discovered match live, for a different match than the live one, or naming a side outside the two-side vocabulary halts; a different match discovered while one is live halts (the live match never completed — its market would stay open forever and the new one Pending), while a re-announcement of the live match is idempotent on LoL and Dota2 — on synth the title's state projection refuses it, the log having one producer (event-catalog §6.18 rule 7) —; a second completion is refused by the title's state projection (on synth every record after the completion is).
  5. No void path: a match is never "restored"; void_value is recorded (growth-path D5) and never exercised.
  6. Parser assertions: crates/ingest/src/match_winner.rs (the vector IS the price function), crates/replay/tests/match_winner_policy.rs (open at discovery, close at completion, the halts), crates/runtime/tests/synth_serve.rs (a synth log through the real scheduler: [0, 1000] on team_a_win, the cut log never settles), crates/clearing/tests/match_winner_settlement.rs (the Flow-3 analogue at both outcomes), crates/grid-edge/tests/match_winner_e2e.rs (the real GRID LoL game — red won — and the real Dota2 capture with a radiant win, through the whole chain).

SC-8 — Race-to-N-kills claim family (LoL + Dota2) #

The first intra-match LINE family: for each line N of the run's manifest (race_lines, ascending — the operator's {10, 15, 20} in the served runs), a market on whether the model's perspective side reaches N cumulative kills FIRST (replay::race_to_kills; bound as lol/race_to_kills/v1 and dota2/race_to_kills/v1 — event-catalog §6.9 market_created row — by both match-winner titles through the ONE RaceTitle seam).

  1. Outcome space: two classes over the claim's side, exactly SC-7's — class 1 the side reached N first, class 0 the other side did — priced through the SAME binary grid (ingest::match_winner::settlement_price, [0, 1000], one home) with the claim always written on the perspective side (blue / radiant, wire 0); the other side is the short side of the same book. The kill counts are the title's own per-side cumulative tallies, read off the snapshot, never re-derived by the family: on LoL lol/state/v2's blue.kills / red.kills (event-catalog §6.15.3, the §6.15.1 fold rule 1: a kill credits the KILLER's side); on Dota2 the betty frame (dota2/player_frame/v1, §6.11) carries per-SLOT cumulative kills and no side (§6.11 rule 3), so a side's count is the SUM over the slots the match's roster_side/v1 resolves to it (§6.11 rule 9) — the seam's per-match frame reader CARRIES that roster binding (replay::dota2::Dota2FrameReader — ingest::dota2::roster_side:: RosterBinding, the one binding the dota2/v1 extractor drives too, plus the running objective tally), through the allocation-free ingest::dota2::frame::kills_from_record.
  2. Finality is frame-granular: the line closes on the FIRST per-frame snapshot — a game minute on LoL, the sampled raw engine tick on Dota2 (§6.11 rule 6's stride; the frame IS the snapshot there) — whose counts satisfy blue ≥ N or red ≥ N (radiant / dire on Dota2), and the outcome is read off THAT snapshot. The LoL match log carries the kills as events inside the minute, but the game-state stream the market trades on — and the model prices on — is the per-minute snapshot; settling inside a minute would settle on a state no consumer of the run saw (on Dota2 the archive samples the counters at the stride, so the frame is the only state there is). Both sides may cross on one frame (a double cross): the HIGHER count reached N first in the only sense the stream can witness, and wins. In general the winner of every settled line is the side with the STRICTLY higher count on its crossing frame (a single crosser is necessarily higher; winner = red if red > blue else blue). Several lines may close on one frame (a kills jump past two lines); they close in line order, each read off the same snapshot.
  3. The tie voids: equal counts on the crossing frame (blue == red ≥ N) decide nothing — neither side reached N first at the stream's granularity — and the line VOIDS through SC-V's unwind (every party's posted returned, the pot drained to zero, void_complete{unwind}), with market_voided.reason = "tie at K-K on frame F"; a tie at or past several lines voids each of them, in line order, off the same frame. A tie at the crossing frame is a few per cent of races on either title (SC-V).
  4. The unreached line voids: if the match completes (the forwarded match_complete/v1, SC-7 rule 2's finality signal) with a line still unreached, that line and every higher one VOID the same way, reason "match completed before the line". A partial capture with no completion leaves them open (SC-5's posture — nothing settles or voids that the feed did not record). A small share of matches on either title never reach a line (SC-V).
  5. Identity — sequential lines as consecutive epochs: epoch = match ordinal × line count + line index (the match ordinal is SC-7 rule 4's discovery ordinal; positional, never allocated) — an epoch is FAMILY-LOCAL (SC-7 rule 4: this and every identity rule of SC-7 to SC-13 number epochs within THEIR family; the barrier names its family on a board — event-catalog §6.8 settlement_complete.family / void_complete.family); one market per line, all created Pending on the forwarded match envelope and opened one at a time by the lifecycle barrier (state-layers §3): the race to 15 opens on the race to 10's settlement_complete or void_complete, never before. A voided line is retired, never re-observed (there is no replay of a race). The descriptor carries {match id, side, threshold}; settlement re-checks the match id AND the line against the epoch's recorded crossing — a mismatch halts (corrupted terms); pricing a voided, a live or an already-RETIRED epoch halts (NoOutcome) — a settled epoch prices only until its settlement_complete retires it (flows.md Flow 3); the engine refuses a post-barrier claim on the epoch before it ever asks (its settled_epochs).
  6. One race fold, three drivers: the crossing is decided by replay::race_to_kills::RaceTracker — driven by the lifecycle policy over the game-state stream, by the feature extractor over the same stream (the line it prices is the line the policy has live — the fold steps FIRST, so the frame that crosses a line prices the NEXT line while that line's market is still Pending its barrier; that one quote finds no open market and is dropped, and a new line's first quote arrives on the following snapshot — state-layers §4), and by the clearing outcome source over the match log THROUGH THE TITLE'S OWN PROJECTOR (FrameTitle::Projector — the frame-kills seam's, shared with the minute family — the fold the run binds — LoL's state/v2 fold, Dota2's identity frame fold), so all three read the same snapshot and agree on the epoch, the crossing frame and the winner by construction — which rests on the projector being config-free (Default); a title whose projector needs run configuration must bind the SAME instance in both places. The seam's per-match FRAME READER (FrameTitle::FrameReader — replay::lol:: LolFrameReader on LoL, replay::dota2::Dota2FrameReader on Dota2: each the match's running objective tally, Dota2's also its roster binding) is owned by the shared frame fold (replay::frame_kills::FrameFold, which the race fold and the minute fold both drive) and fed every game-state record it folds, so each of the three drivers binds the match's sides from the same stream at the same record (the roster, which is also the frame log's match envelope), and the extractor reads the sides off its own fold's reader — one binding per driver, never a second copy.
  7. Halts: a per-frame snapshot with no discovered match live (FrameWithoutMatch), a frame index not strictly above the last within a match (FrameOrder — the crossing frame is the settlement unit, so a repeated or regressed frame is corrupt), a match ordinal whose lines leave the u64 epoch space (EpochOverflow), and — a per-slot title's binding halts — a frame inside a LIVE match before the record that binds its sides (SidesUnbound, naming the frame: on Dota2 a deaths- block match_discovered/v1 envelope followed by frames with no roster_side/v1; never a positionally-assumed side — with no match live the roster is the envelope too, so a frame before it is FrameWithoutMatch as on every title), a binding record contradicting the bound sides (SidesRebound), a binding record for another match while one is bound (the lifecycle fold's SC-7 rule 4 halt first; the binding refuses it on its own), a per-side kills sum leaving the u64 space (KillsOverflow — the counters are unbounded wire values; a settlement input is checked, never wrapped or clamped), a frame whose body tick disagrees with its envelope stamp (TickMismatch), and — on every family's fold, because the title's reader steps on every record — a forwarded objective event the reader refuses (a LoL tower without its owning side, Title(MissingField(SD_SIDE)); a torn body's own halt) or a side's tally leaving u64 (TallyOverflow) halt the driving fold; the match fold's own halts (SC-7 rule 4) pass through unchanged.
  8. Parser assertions: crates/replay/tests/race_to_kills_policy.rs (the fixture schedule through the real projector + fold: 11–11 on minute 6 voids the race to 10 with its reason text, the barrier opens the race to 15, 15–13 on minute 9 closes it, the completion voids the unreached race to 20; a 12–10 double cross settles for the higher count; a 17–21 jump closes two lines on one frame in line order, both for red (the higher count); the halts; the DOTA2 seam reads both sides' kills as the per-slot sums THROUGH THE ROSTER — a non-contiguous real partition — with the raw tick as the frame, halts SidesUnbound / SidesRebound, releases at the completion, reports a frame with no match live as FrameWithoutMatch and a live match's missing binding as SidesUnbound, halts on a torn frame body through the fold — a short players array (PlayersArity), a non-map slot, a slot missing its kills counter — never a swallowed frame, and the whole dota2 race fixture through the frame projector + policy + lifecycle fold yields the LoL fixture's lifecycle shape at tick stamps), crates/clearing/tests/race_to_kills_settlement.rs (the golden thread: the tie's unwind returns 64 000 / 36 000 and the balances to their deposits, the reached line pays the blue claim 100 × 1000, the pot zero; the descriptor halts name the field, the epoch and both ids; the red claim prices at 0; the dota2 arm: RaceOutcomeSource<Dota2> over the dota2 race fixture records radiant's race to 15 at frame 9's tick and nothing for the tie or the unreached line, prices the radiant claim at 1000 and the dire at 0 on the dota2 terms, refuses the lol rule and halts FrameWithoutMatch on a frame before the roster), crates/runtime/tests/race_serve.rs (the whole DAG through the scheduler over the committed race fixture dev_fixtures::lol_race_match_log: three markets created at discovery, opened by the barriers, refunds 70 000 / 30 000 on both voids, the payout, every pot zero, both taps, byte-identical twice), crates/runtime/tests/dota2_race_serve.rs (the same thread on Dota2 over dev_fixtures::dota2_race_match_log — the LoL schedule as betty frames over the real roster partition: the markets carry the dota2 family and the 8-byte match id, are created on the ROSTER and stamped at ticks, every frame quotes 700, the same 70 000 / 30 000 unwinds and 100 × 1000 payout, byte-identical twice), crates/bookmaker/tests/match_winner_router.rs (the race routes on the binary router; the families never cross — ten of them), crates/pricing/src/feature/lol_race.rs / dota2/race.rs (each extractor prices the line the fold has live AFTER the snapshot's crossing — never the one it just closed; the dota2 one reads the sides off its fold's reader and halts in the fold on a frame before the roster), crates/trainset/src/lol_race_dataset.rs / dota2_race_dataset.rs (rows only for lines that settle, labels agreeing with a raw-kill oracle over the real Riot match / the real archive match through its roster), sdk/python/tests/test_bridge.py (the app bridge on both titles: a lifted race to 10 refunded in full on the tie, pnl_mills exactly zero — the SC-V refund arithmetic pinned on a TRADED market).

SC-9 — Kills-over-N-at-minute-M claim family (LoL + Dota2) #

The second intra-match LINE family, and the first settled on a NAMED frame rather than a first crossing: for each line (M, N) of the run's manifest (minute_lines, by ascending minute — any m:n set the rule admits; the served house models are bound to lol {(10, 14), (15, 24), (20, 36)} and dota2 {(10, 13), (15, 23), (20, 34)}, the corpus medians), a market on whether the match's TOTAL kills at its M-th per-minute snapshot are OVER N (replay::kills_at_minute; bound as lol/kills_at_minute/v1 and dota2/kills_at_minute/v1 — event-catalog §6.9 market_created row — by both titles through the ONE frame-kills seam the race reads through, replay::frame_kills::FrameTitle — whose kills READ is a separate trait, FrameSource<Kills>, one impl per quantity a title exposes — with the family's terms on KillsAtMinuteTitle).

  1. Outcome space: two classes over the claim's side, SC-7's shape on a different vocabulary — side 0 is OVER (the side every market of this family is written on; the model's 2-class distribution prices it, class 1 = over), side 1 is UNDER, the short side of the same book (event-catalog §6.8) — priced through the SAME binary grid (ingest::match_winner::settlement_price, [0, 1000], one home). The total is the SUM of both sides' cumulative kills as the race reads them (SC-8 rule 1: lol/state/v2's per-side tallies on LoL; the per-slot counters summed through the roster on Dota2), summed with a checked add on the settling frame (JudgedOverflow halts; a settlement input is never wrapped).
  2. The partition is strict and has no push: OVER is total ≥ N + 1, UNDER is total ≤ N — an integer line N reads as the bookmaker's N + ½, so every settled line pays exactly one side and nothing ties. (A total equal to N is UNDER: 27 kills at minute 8 is not over 27.)
  3. Finality is the frame that IS the minute: the line settles on the snapshot whose ORDINAL within the match is M — the M-th per-frame snapshot the game-state stream carries after the match envelope, 0-based, counted by the frame fold (FrameFold) — and the outcome is read off THAT snapshot only; an earlier frame with a larger total decides nothing, and the fold never settles a line on a later frame (MinutePassed is a typed halt for a fold defect, unreachable by construction). On LoL the ordinal IS the game minute — state/v2. frame_index, the driving player-frame's 0-based minute, one frame per minute from minute 0 (§6.15.3 rule 1 / §6.15.2 rule 1) — and the shared frame fold CHECKS that identity on every LoL snapshot (FrameTitle::FRAME_INDEX_IS_ORDINAL; a gapped or late-starting log halts OrdinalMismatch rather than settling minute M on a later game minute); on Dota2 it is the M-th sampled frame at the archive's per-minute stride (§6.11 rule 6 — every 60th distinct tick, roughly one frame per in-game minute), the raw tick being the frame index the outcome records and no identity to check. Because the checkpoint is an ordinal, the fold must see EVERY snapshot of the match in stream order — the stream law; a driver that skipped one would misplace the checkpoint (the runtime feeds every record; the corpus builders and the clearing mirror fold the whole log).
  4. The unreached minute voids: if the match completes (the forwarded match_complete/v1) before its M-th snapshot, that line and every later one VOID through SC-V's unwind, market_voided.reason = "match completed before minute M" (each line names ITS minute). A partial capture with no completion leaves them open (SC-5's posture).
  5. Identity — sequential minutes as consecutive epochs: SC-8 rule 5's rule verbatim — epoch = match ordinal × line count + line index, one market per line, all created Pending on the forwarded match envelope and opened one at a time by the lifecycle barrier (state-layers §3); minutes must strictly ascend AND leave a QUOTE WINDOW: the first line's minute is at least 2 and each later line's at least the previous line's + 3 (MinuteLinesError::NoQuoteWindow). The tick order (pre-drain → ADV → LC → FX → MOD → DR → MG, BM at its merge slot — architecture.md quote fold step 8) makes a line at the previous minute + 1 open on the barrier and settle in the SAME tick — never quoted; at + 2 it is quoted on one frame but an order on that frame merges on the settling tick, after the close; only + 3 leaves a quoted frame whose order merges on the next frame, before the close (the first line the same way from the envelope: frame 0 quoted, the order merging on frame 1, the line settling on frame 2 at the earliest). The descriptor carries {match id, side, kills N (the race's threshold slot, 2), minute M (slot 3)}; settlement re-checks the match id AND both line fields against the epoch's recorded settlement — a mismatch halts (LineEpochMismatch, corrupted terms); pricing a voided, a live or an already-RETIRED epoch halts (NoOutcome) — a settled epoch prices only until its settlement_complete retires it (flows.md Flow 3); the engine refuses a post-barrier claim on the epoch before it ever asks (its settled_epochs).
  6. One minute fold, three drivers: the settlement is decided by replay::kills_at_minute::MinuteTracker — driven by the lifecycle policy over the game-state stream, by the feature extractors (<title>/minute-v1, model-plugin-layer §8) over the same stream (the fold steps FIRST, so the settling frame prices the NEXT line while its market is still Pending its barrier — that one quote is dropped, and the new line's first quote arrives on the following snapshot, state-layers §4), and by the clearing outcome source (clearing::kills_at_minute) over the match log THROUGH THE TITLE'S OWN PROJECTOR, so all three agree on the epoch, the settling frame and the winner by construction. The fold is the race's shared frame fold (FrameFold: the match fold, the title's per-match frame reader — the objective tally on both titles, the roster binding too on Dota2 — the frame order and the ordinal, and the last numbered frame's value, last_frame, the ONE value the extractor prices on) plus the one-line minute check; the race and the minute family differ only in what they decide per frame.
  7. Halts: the shared frame fold's — a snapshot with no discovered match live (FrameWithoutMatch), a frame index not strictly above the last (FrameOrder), a live match's frame before its binding record (SidesUnbound), a contradicting binding (SidesRebound), the match fold's own (SC-7 rule 4), the reader's own (a refused forwarded objective event, TallyOverflow — SC-8 rule 7), and on LoL a frame index that is not the ordinal (OrdinalMismatch, rule 3) — plus the family's: a match ordinal whose lines leave the u64 epoch space (EpochOverflow), a completion with no lines live (CompletionWithoutLines — reachable only past an ignored overflow halt), the settling frame's total leaving u64 (JudgedOverflow), and MinutePassed (rule 3). The lifecycle policy and the clearing source are the ONE line-family pair (replay::line_family::LinePolicy, clearing::line_family::LineOutcomeSource) over the family's LineFamily binding — the race's too. The per-slot title's KillsOverflow / TickMismatch (SC-8 rule 7) pass through unchanged.
  8. Parser assertions: crates/replay/tests/kills_at_minute_policy.rs (the race fixture's log under the lines {(5, 17), (8, 27), (20, 50)} through the real projector + fold: minute 5 reads 9–9, 18 > 17, and CLOSES the first line; the barrier opens the second; minute 8 reads 14–13, 27, NOT over 27, and closes it as UNDER — a settlement, never a void; the completion voids minute 20 with its reason text; the strict partition on hand-built frames — a total equal to the line is UNDER, one more is OVER, a line at minute 0 settles on the opening frame, an earlier larger total decides nothing; the halts; the lines rule and parse_csv's m:n spelling; the dota2 fixture through the frame projector yields the same lifecycle shape at tick stamps with the k-th frame as minute k), crates/clearing/tests/kills_at_minute_settlement.rs (the golden thread: minute 5 pays the over claim 100 × 1000 and the house 0, minute 8 pays the claim 0 and the house 100 × 1000 — account 100 000 + 36 000 − 64 000 = 72 000, house +28 000, every pot zero; the descriptor halts name the field, the epoch, both ids and both line fields; the voided epoch never prices; the dota2 arm through the mirrored frame projector records OVER at frame 5's tick and UNDER at frame 8's), crates/runtime/tests/minute_serve.rs / dota2_minute_serve.rs (the whole DAG through the scheduler over dev_fixtures::manifest_lol_minute / manifest_dota2_minute: three markets created at discovery and opened by the barriers, three lifts at 700, payouts 100 000 / 0 then 0 / 100 000, refunds 70 000 / 30 000 on the unreached line, ledger 960 000 / +40 000, both taps, byte- identical twice), crates/bookmaker/tests/match_winner_router.rs (the minute family routes on the binary router; the binary families (ten since SC-13) never cross), crates/pricing/src/feature/lol_minute.rs / dota2/minute.rs (each extractor prices the line the fold has live AFTER the settling frame — never the one it just closed — with the minutes to go off the ordinal), crates/trainset/src/lol_minute_dataset.rs / dota2_minute_dataset.rs (rows only for lines that settle, labels agreeing with a raw-total oracle over the real Riot match / the real archive match, over and under both present), sdk/python/tests/ test_bridge.py (the app bridge on both titles: the over claim paid, the under claim settled at price 0, the unreached line refunded in full).

SC-10 — Side-kills-over-N-at-minute-M claim family (LoL + Dota2) #

The per-side variant of SC-9 — the survey's literal "per-side kills over N at minute M": for each line (M, N, s) of the run's manifest (side_minute_lines, by ascending minute; s the team wire, 0 = blue / radiant, 1 = red / dire), a market on whether SIDE s's OWN cumulative kills at the match's M-th per-minute snapshot are OVER N (replay::side_kills_at_minute; bound as lol/side_kills_at_minute/v1 and dota2/side_kills_at_minute/v1 — event-catalog §6.9 market_created row — by both titles through the one frame-kills seam, with the family's terms on SideKillsAtMinuteTitle). Everything below the judged count IS SC-9: the minute families are ONE fold (six since SC-12 — replay::kills_at_minute::MinuteTracker, generic since this leg over what a line is judged on — MinuteJudged: the sum for SC-9's MinuteLine, the team's own counter for SideMinuteLine), one lines rule (LinesAtMinute), one policy and one clearing source (replay::line_family, clearing::line_family).

  1. Outcome space: SC-9 rule 1 verbatim — side 0 OVER (the claim, class 1), side 1 UNDER — over the TEAM's count: the line's team's cumulative kills as the race reads them (SC-8 rule 1: lol/state/v2's per-side tally on LoL; the team's slots' counters summed through the roster on Dota2), read off the frame with no arithmetic beyond the shared frame read (on Dota2 the per-slot sum is the title's own, guarded by SC-8 rule 7's KillsOverflow; the side family adds none, so SC-9's JudgedOverflow is unreachable for it and stays typed).
  2. The partition is strict and has no push: OVER is count ≥ N + 1, UNDER is count ≤ N — SC-9 rule 2 on the team's count (red on 13 at minute 8 under a line of 13 is UNDER while blue's 14 and the total 27 would both be over: the team decides, nothing else).
  3. Finality is the frame that IS the minute: SC-9 rule 3 verbatim (the ordinal, the LoL identity check, the stream law — every snapshot).
  4. The unreached minute voids: SC-9 rule 4 verbatim, the reason text "match completed before minute M".
  5. Identity — sequential lines as consecutive epochs, ONE line per minute: SC-9 rule 5 verbatim on the (minute, kills, team) line, with one consequence spelled out: a run may INTERLEAVE the two teams' lines (blue at 10, red at 13, blue at 16 …), but two teams at ONE minute are refused (NotAscending — the second market would open on the first's barrier and settle in the same tick, never quoted); a team wire outside {0, 1} is refused at construction (TeamOutOfRange). The descriptor carries {match id, side, kills N (slot 2), minute M (slot 3), TEAM s (slot 4, D_TEAM)}; settlement re-checks the match id AND all three line fields against the epoch's recorded settlement — the other team's line at the same minute and kills is another epoch's identity and halts (LineEpochMismatch); pricing a voided, a live or a RETIRED epoch halts (NoOutcome) as in SC-9.
  6. One minute fold, three drivers: SC-9 rule 6 with the side spelling — <title>/side-minute-v1 (model-plugin-layer §8: the six lanes carry the line's team and the team's own count in place of the total), clearing::side_kills_at_minute, SideKillsAtMinutePolicy<T> — all three the minute family's own types over side lines.
  7. Halts: SC-9 rule 7 verbatim (the shared fold's, the family's EpochOverflow / CompletionWithoutLines / MinutePassed).
  8. Parser assertions: crates/replay/tests/side_kills_at_minute_policy.rs (the race fixture's log under the lines {(5, 8, blue), (8, 13, red), (20, 30, blue)} through the real projector + fold: blue 9 > 8 closes the first line, red 13 not over 13 closes the second as UNDER where the total 27 would have been over, the completion voids minute 20; the judged count is the line's team on hand-built frames; the side fold and the total fold over the same frames agree on every epoch and frame and differ exactly in the judged count; the lines rule's team wire, the shared-minute refusal and the m:n:t spelling), crates/clearing/tests/side_kills_at_minute_settlement.rs (the source over both fixtures: each line's winner is its team's verdict, both claims priced on the side terms, the total family's rule foreign, the team cross-checked in the descriptor — the other team's line at the same minute and kills refused — the retire hook by epoch; the dota2 arm's roster-summed counts on the non-contiguous roster), crates/runtime/tests/side_minute_serve.rs / dota2_side_minute_serve.rs (the whole DAG through the scheduler: the minute serves' hand-derived economics — the same minutes and outcomes — on the side terms; byte- identical twice), crates/bookmaker/tests/match_winner_router.rs (the side family on the binary router, never crossing the total minute family even at the same minute and kills), the manifest's MF-9 wire and pairing (crates/runtime/src/manifest/mod.rs, tests/manifest_bin.rs), the extractors' lanes and pinned ids (pricing::feature::lol_side_minute / dota2::side_minute), the corpus builders' per-team oracle (trainset::lol_side_minute_dataset / dota2_side_minute_dataset), the SDK's decode of the five-field terms and the team's name (sdk/python/tests/test_events_and_book.py test_side_minute_market_created_carries_the_line_its_team_and_names_the_team).

SC-11 — Gold-lead-over-N-at-minute-M claim family (LoL + Dota2) #

The third minute-judged family — the first judged on a quantity other than kills: for each line (M, N, s) of the run's manifest (gold_lead_lines, by ascending minute; s the team wire, 0 = blue / radiant, 1 = red / dire; N a GOLD amount, 0..=100000), a market on whether SIDE s LEADS BY MORE THAN N gold at the match's M-th per-minute snapshot (replay::gold_lead_at_minute; bound as lol/gold_lead_at_minute/v1 and dota2/gold_lead_at_minute/v1 — event-catalog §6.9 market_created row — by both titles through the frame seam's GOLD read, replay::frame_kills::FrameSource<Gold>: on LoL the ten players' total_gold summed per side (slots 0–4 blue, 5–9 red — event-catalog §6.15.2 rule 1) off state/v2 (ingest::lol::state_v2::gold_from_record), on Dota2 the side's slots' net_worth summed through the roster (ingest::dota2::frame:: net_worth_from_record), with the family's terms on GoldLeadTitle). Everything below the judged value IS SC-9: the minute families (six since SC-12) are ONE fold (replay::kills_at_minute::MinuteTracker, generic over what a line is judged on and over WHAT THE FRAME YIELDS — MinuteJudged::Quantity: the kills pair for SC-9 / SC-10, the gold pair Gold here), one lines rule, one policy, one clearing source.

  1. Outcome space: SC-9 rule 1 verbatim — side 0 OVER (the claim, class 1), side 1 UNDER — over the TEAM's LEAD: gold[s] − gold[other], SATURATING at 0 (a side behind or level has a lead of 0; a settlement input is never wrapped), each side's sum itself checked in the read (GoldOverflow on LoL, NetWorthOverflow on Dota2 — the title's own halts through the shared fold), so SC-9's JudgedOverflow is unreachable for it and stays typed.
  2. The partition is strict and has no push: OVER is lead ≥ N + 1, UNDER is lead ≤ N — SC-9 rule 2 on the lead: a lead EQUAL to the line is UNDER (red leading by exactly 1,500 under a line of 1,500 is UNDER), and the line N = 0 reads "s is ahead" with a level game UNDER.
  3. Finality is the frame that IS the minute: SC-9 rule 3 verbatim.
  4. The unreached minute voids: SC-9 rule 4 verbatim, the reason text "match completed before minute M".
  5. Identity — sequential lines as consecutive epochs, ONE line per minute: SC-10 rule 5 verbatim on the (minute, gold, team) line — the teams may interleave across ascending minutes, two teams at one minute are refused (NotAscending), a team wire outside {0, 1} at construction (TeamOutOfRange), a gold line above the cap (GoldOutOfRange; the operator's item <minute>:<gold>:<team> admits up to six digits). The descriptor carries {match id, side, gold N (slot 2, the kills families' D_THRESHOLD), minute M (slot 3), TEAM s (slot 4, D_TEAM)}; settlement re-checks the match id AND all three line fields against the epoch's recorded settlement (LineEpochMismatch); pricing a voided, a live or a RETIRED epoch halts (NoOutcome).
  6. One minute fold, three drivers: SC-9 rule 6 with the gold spelling — <title>/gold-lead-v1 (model-plugin-layer §8: the six lanes carry the line's team, the team's SIGNED lead — negative behind, unlike the settlement's saturating lead: the model sees how far behind — and the margin), clearing::gold_lead_at_minute, GoldLeadAtMinutePolicy<T>.
  7. Halts: SC-9 rule 7 verbatim.
  8. Parser assertions: crates/replay/tests/gold_lead_at_minute_policy.rs (the gold fixture's log under the lines {(5, 800, blue), (8, 1500, red), (20, 0, blue)} through the real projector + fold: blue's lead of 1,000 closes the first line OVER, red's lead of EXACTLY 1,500 closes the second UNDER, the completion voids minute 20; the judged lead saturates at 0 on hand-built frames; the gold fold and the side fold over the same frames agree on every epoch and frame and differ exactly in what they judged; the lines rule's gold cap and six-digit spelling; the dota2 arm through the roster), crates/clearing/tests/gold_lead_at_minute_settlement.rs, crates/runtime/tests/gold_lead_serve.rs / dota2_gold_lead_serve.rs (the side serves' economics to the mill on the gold terms; byte-identical twice), crates/bookmaker/tests/match_winner_router.rs (the gold family never crossing the side family over identical descriptor numbers), the manifest's MF-10 wire and pairing, the extractors' lanes and pinned ids (pricing::feature::lol_gold_lead / dota2::gold_lead), the corpus builders' independent oracles (the ten total_gold fields summed; the roster-summed net_worth).

SC-12 — Tally-over-N-at-minute-M claim families (towers on LoL + Dota2; dragons and barons on LoL) #

The fourth, fifth and sixth minute-judged families in ONE contract — the first judged on a quantity NO snapshot field carries: for each line (M, N, s) of the run's manifest (tally_lines, by ascending minute; s the team wire, 0 = blue / radiant, 1 = red / dire; N a COUNT, 0..=20), a market on whether SIDE s's count of the family's KIND at the match's M-th per-minute snapshot is OVER N — the kind being the family text: towers_at_minute (towers TAKEN: the other side's towers destroyed), dragons_at_minute (dragons slain: the six drakes and the elder dragon by objective NAME — event-catalog §6.15.1's normative set), barons_at_minute (barons slain). replay::tally_at_minute — ONE generic family over the kind tag (TallyLine<K>, TallyAtMinute<T, K>); the claim families lol/<kind>_at_minute for every kind and dota2/towers_at_minute, each under its payout rule <family>/v1 (the betty archive records no objective kills, so Dota2 offers towers only — Family::offered_by, reading each title's TallyTitle::TALLY_KINDS_OFFERED, the ONE home of the offer; the manifest refuses the other two on dota2) — event-catalog §6.9 market_created row — through the frame seam's TALLY read, replay::frame_kills::FrameSource<Tally>: the title's frame READER (replay::lol::LolFrameReader, replay::dota2::Dota2FrameReader) folds the forwarded structure_destroyed/v1 / objective_slain/v1 events (§6.15.1 / §6.11.1 forwarding sets) into replay::tally::Tally and the read yields it AT each snapshot, with the family's terms on TallyTitle. Everything below the judged count IS SC-9: the minute families are ONE fold (replay::kills_at_minute::MinuteTracker — MinuteJudged::Quantity = Tally here), one lines rule, one policy, one clearing source.

  1. Outcome space: SC-9 rule 1 verbatim — side 0 OVER (the claim, class 1), side 1 UNDER — over the TEAM's COUNT of the kind: tally[kind][s], read off the tally with no arithmetic (the reader's credit is CHECKED, TallyOverflow, so SC-9's JudgedOverflow is unreachable and stays typed). A tower is credited to the side that TOOK it (the OTHER side than its recorded owner; the record's optional destroyer is not read), an objective to the slayer's side.
  2. The partition is strict and has no push: SC-9 rule 2 on the count: OVER is count ≥ N + 1, UNDER is count ≤ N — a count EQUAL to the line is UNDER; the line N = 0 reads "s has taken / slain one".
  3. Finality is the frame that IS the minute — SC-9 rule 3 verbatim, with the title's own event-ordering rule deciding WHICH events the minute's frame sees: on LoL a minute's events precede its frame (§6.15.2), so an event of minute M counts at minute M's snapshot; on Dota2 a frame precedes the structure events sharing its tick (§6.11 rule 8), so a tower destroyed AT frame T's tick first counts at frame T + 1 — a per-title settlement asymmetry stated here, never left to a fixture.
  4. The unreached minute voids: SC-9 rule 4 verbatim, the reason text "match completed before minute M".
  5. Identity — sequential lines as consecutive epochs, ONE line per minute: SC-10 rule 5 verbatim on the (minute, count, team) line — the teams may interleave across ascending minutes, two teams at one minute are refused (NotAscending), a team wire outside {0, 1} at construction (TeamOutOfRange), a count above the cap (CountOutOfRange; the operator's item <minute>:<count>:<team> admits up to two digits). The descriptor carries {match id, side, count N (slot 2, the kills families' D_THRESHOLD), minute M (slot 3), TEAM s (slot 4, D_TEAM)} — the KIND is the claim family, never a descriptor field, so a towers market and a dragons market over the same numbers are different markets by family (MarketId covers the family); settlement re-checks the match id AND all three line fields against the epoch's recorded settlement (LineEpochMismatch); pricing a voided, a live or a RETIRED epoch halts (NoOutcome).
  6. One minute fold, three drivers: SC-9 rule 6 with the tally spelling — <title>/<kind>-v1 (model-plugin-layer §8: the six lanes carry the line's team, the team's count and the margin; the grids of the three kinds differ only in their NAMES, so a towers model is a different artifact from a dragons model — the id pins the name and the bind refuses a model of another kind), clearing::tally_at_minute, TallyAtMinutePolicy<T, K>; the extractor prices the fold's OWN frame value (MinuteTracker::last_frame) — the count a model sees is the count the line settles on, one read.
  7. Halts: SC-9 rule 7 verbatim — including the per-slot title's binding halt: a Dota2 frame before its roster_side/v1 halts SidesUnbound through the TALLY read exactly as through the kills and gold reads (the tally needs no sides; the halt is the seam's invariant, so no tally line is ever settled on a pre-roster frame) — plus the reader's (SC-8 rule 7: a forwarded event the reader refuses, TallyOverflow), and the extractor's own: the live team_count lane shares the line cap (0..=20, model-plugin-layer §8), so a tally past 20 — impossible in a real match, a corrupted or foreign log — halts the run OutOfBounds at validate_vector rather than being priced.
  8. Parser assertions: crates/replay/tests/tally_at_minute_policy.rs (the tally fixture's log under the lines {(5, 0, blue), (8, 1, red), (20, 0, blue)} for each kind through the real projector + fold: blue's one tower / drake / baron closes the first line OVER, red's EXACTLY one closes the second UNDER, the completion voids minute 20; the judged count is the tally's cell; the three kinds' folds over one log agree on every epoch and frame; the lines rule's count cap and two-digit spelling; the dota2 towers arm with the same-tick rule), crates/clearing/tests/tally_at_minute_settlement.rs, crates/runtime/tests/tally_serve.rs / dota2_tally_serve.rs (the side serves' economics to the mill on the tally terms, per kind; byte-identical twice), crates/bookmaker/tests/match_winner_router.rs (a towers market never crossing a dragons market over identical descriptor numbers), the manifest's MF-11 wire and pairing (one field for three families, paired by FIELD), the extractors' lanes and pinned ids (pricing::feature::lol_tally / dota2::tally), the corpus builders' independent oracles (the events re-counted off the raw log).

SC-13 — House-line claim families: total kills and kill handicap over a HOUSE-SET line (LoL + Dota2) #

The first families whose line the OPERATOR does not pin. For each match of the run, ONE market per family: total_kills — the match's FINAL total kills (both sides' cumulative counts summed on the last per-frame snapshot before the completion) are OVER N; kill_handicap — team s's FINAL kill LEAD (its kills minus the other side's, saturating at 0) is OVER N. The line N (and, for the handicap, the team s) is chosen by the HOUSE on the match's OPENING snapshot — the open_minute-th, by ordinal — from the candidate range the run manifest records (house_line = [open_minute, lo, hi], replay::house_line::HouseTerms), by the run's house model: the pricing layer's setter evaluates the model once per candidate on that snapshot and takes the line whose OVER probability is nearest one half (rule 5). replay::house_line — ONE generic family over the kind tag (HouseLine<K>, HouseLineFamily<T, K>, HouseLinePolicy<T, K, S> over the LineSetter seam); the claim families <title>/total_kills and <title>/kill_handicap on both titles, each under its payout rule <family>/v1 — event-catalog §6.9 market_created row — through the frame seam's KILLS read (replay::frame_kills::FrameSource<Kills>, the race and minute families' read), the fold replay::house_line::HouseFold (the frame fold plus the opening flag and the LAST frame).

  1. Outcome space: SC-9 rule 1's two sides — side 0 OVER (the claim, class 1), side 1 UNDER — over the judged value: the total k0 + k1, CHECKED (a settlement input is never wrapped — JudgedOverflow halts); the handicap sat(k_s − k_other) (a tie or a deficit reads 0, so the line N = 0 reads "s leads the kill count"). The kills are the title's frame counters exactly as SC-8 reads them.
  2. The partition is strict and has no push: OVER is judged ≥ N + 1, UNDER is judged ≤ N — a value EQUAL to the line is UNDER. An integer line reads as N + ½. The handicap "s + N½" (the underdog covering) is UNDER of the (N, other) market — one uint threshold and a team spell every handicap, so no signed line exists on the wire.
  3. Finality is the LAST snapshot before the completion: the market closes on the forwarded match_complete (state-layers §3: Open → Closed), and the outcome is read off the last per-frame snapshot the fold read for the match (lol/state/v2; the roster-summed dota2/player_frame/v1) — frame-granular like every line family, SC-8 to SC-13: kills after the last snapshot are not in any frame and never count. Dota2's match_complete/v1 scoreboard_deaths is a diagnostic cross-check ONLY (SC-1's posture on reported counts; LoL's completion carries no scoreboard); it is never a source.
  4. No market before the opening snapshot; no void: a match that completes before its open_minute-th snapshot has NO market — nothing is created (there is no line to write), nothing is voided, nothing is unwound. Once created the market always settles (rule 2 has no tie), so SC-V's unwind has no producer in this family. A torn log (a match without its completion) never finalizes the epoch: a price asked for it halts the clearing path NoOutcome, and the corpus builders refuse it as their NoMatchComplete — an opened market never settles on a guess.
  5. Identity — one market per match, the line DERIVED: epoch = the match's discovery ordinal (the match winner's spelling; matches are sequential and the tick order gives the barrier headroom — the prior match's completion tick emits its settlement_complete, the next tick's pre-drain folds it into the lifecycle, and the next match's envelope and then its open_minute-th snapshot (open_minute ≥ 1) arrive at least two ticks later — so the barrier is already folded when the opening snapshot arrives and the market opens in the lifecycle step that creates it; the generic Pending → Closed edge stays the safety net). The descriptor carries {match id, side, threshold N (slot 2 — the kills families' D_THRESHOLD); the handicap adds the TEAM s (slot 4 — the side family's D_TEAM); slot 3, the minute families' minute, is unused — a house line has no minute} — the KIND is the claim family, never a descriptor field, so a total and a handicap market over the same numbers are different markets by family (a descriptor slot outside the family's set is ignored by every line-family source, SC-8 to SC-13 — the market id covers the bytes, so a foreign slot is a different market, never a different price). The line is a FOLD of declared inputs (core law 5): the opening snapshot, the frame fold's state after it (its last frame; a per-slot title's bound sides), the run's recorded house model (the manifest's first house sleeve's QuoteSource::Model on a single-family run; on a BOARD the sleeve TAGGED to the family — the setter bound per family column, model-plugin §12) and the recorded terms — the setter's total order is (|P(OVER) − ½| ascending, then the LOWER threshold, then the LOWER team), which IS the seam's candidate order (HouseLine::candidates — thresholds ascending, the handicap's two teams within each — with HouseLine::nearest_even taking the first minimum), so two runs of one manifest over one log write the same descriptor bytes and the same MarketId; a promotion that later re-binds the house sleeve's pricing model does NOT re-set a line (a line is set once per match, at its opening). The terms rule: 1 ≤ open_minute ≤ 200 (frame 0 carries no state to set a line on), lo ≤ hi ≤ 500 (the kills cap), at most 128 candidate thresholds (MAX_CANDIDATES — a bounded, declared amount of evaluation per match: one per threshold on the total, two on the handicap, one per team); the operator item <open_minute>:<lo>:<hi> at the three-digit numeral rule. Settlement re-checks the terms it can (the clearing source holds no setter and no model — it reads the line off the recorded descriptor): the match id is the epoch's (MatchEpochMismatch), the threshold is one the range admits (LineOutOfTerms — a line this run's setter could not have set), the team is a side of the kind; pricing a live epoch, a match that never opened (no market existed) or a RETIRED epoch halts (NoOutcome).
  6. One fold, three drivers — and ONE setter, two holders: HouseFold<T> is driven by the lifecycle policy (the game-state stream), by the <title>/total-v1 / <title>/handicap-v1 extractors (model-plugin-layer §8; the same stream) and by clearing::house_line::HouseLineOutcomeSource (the match log through the title's own projector), so all three agree on the opening frame, the epoch and the final frame by construction. The setter value the runtime bind constructs is handed to BOTH the policy and the extractor (Arc-shared) — one RULE, evaluated by each holder on the same opening frame with the same terms, so the line written into the descriptor and the line the extractor's lanes carry are equal by purity (the serve test pins the equality over a fixture run).
  7. Halts: SC-9 rule 7's frame-fold halts (decode, routing, a frame before the envelope or after the completion, the per-slot title's SidesUnbound / SidesRebound, FrameOrder, OrdinalMismatch); the setter refusing the opening snapshot (a model refusing the vector, a grid the snapshot cannot fill) halts the run — no market opens on a guessed line (HouseLinePolicyError::Setter); the terms and line rules above at construction.
  8. Parser assertions: crates/replay/tests/house_line_policy.rs (the race fixtures' logs through the real lol / dota2 projectors → policy → lifecycle fold: the envelope creates nothing, frame 3 creates AND opens the market with the setter's line — the descriptor's three / four fields, the ONE encoder, the id — frames 4–12 nothing, the completion closes; an opening past the match's length creates nothing for the whole match; the fold's steps and the last frame at the completion; the line rule's caps and the strict partition on the fixture's final (19, 18); the terms rule's bounds and item spelling; the kind tables), crates/clearing/tests/house_line_settlement.rs (37 over 36 pays OVER, 37 over 37 pays UNDER; blue's lead 1 over 0 / over 1; red over 0 by a deficit; every terms cross-check refuses; the dota2 arm through the frame projector), and the serve tests, the router test (a total market never crossing a handicap market), the manifest's MF-12 wire and pairing, the extractors' lanes and pinned ids, the corpus builders' independent oracles: crates/runtime/tests/house_line_serve.rs (6) / dota2_house_line_serve.rs (3) (the REAL pricing setter over the REAL house stump through the whole DAG: created AND opened on the opening snapshot, the features stream's line lane EQUAL to the created descriptor's line on every vector — rule 6 —, the claim paid on 37 over 30, the house on 37 over 37, blue's lead 1 over 0 / over 1, byte-identical twice; and — the served line CHOSEN, not the range's first candidate — a house model reading the line_kills lane whose pick falls strictly inside the terms (36 of 30..=40, the hand-derived nearest-one-half bucket) with a run whose house model is not benched refused at construction), crates/bookmaker/tests/ match_winner_router.rs (a total market foreign to the handicap router and to the race's over identical descriptor bytes), crates/runtime/tests/ manifest_bin.rs (--house-line, the terms' refusals, the model-less run refused), crates/pricing/tests/house_line.rs (the four grids' pinned ids; the setter's sweep over a real two-class artifact picking the first line nearest one half; the extractor emitting from the opening on), crates/trainset/src/house_dataset.rs (every candidate on every frame, labeled on the final frame against the fixture's kill table — a candidate EQUAL to that final total labeled UNDER, rule 2 in the corpus).

SC-V — Voiding a market with open interest (the unwind; every family) #

A market can un-happen before it settles: a cs2 round restored (round_restored, Flow 5), or — the intra-match line families, SC-8, SC-9, SC-10, SC-11 and SC-12 — a race that ends in a tie at the crossing frame, a line never reached before the match ends, or a checkpoint minute the match ends before (a few per cent of lines on either title). The house-line families of SC-13 never void: a match ending before its opening snapshot has no market, and an opened market always settles. The Lifecycle Manager's EpochVoided policy signal (event-catalog §6.13 rule 11) marks every market of the epoch Voided (state-layers §3); this contract says what the money does.

  1. Unwind, per party: every party's posted on the market — its cumulative at-risk cash over its own fills, payments − receipts (matching-engine §6) — returns from the pot. A negative posted (a realized profit on closes) is paid back. After the unwind every party's lifetime PnL on the market is exactly zero and its positions are cleared.
  2. The pot drains to exactly zero: Σ posted over the market's parties equals the pot at every offset (each fill's two cash legs sum to −1000·ΔOI and posted accumulates −cash — the §6 identity), so the refunds are exactly funded; a residue is corrupted history and halts the fold (PotResidue in the ledger, PotNotDrained in the exchange's mirrored view), the same runtime theorem check settlement makes. The golden thread, hand-derived from matching-engine §6's tranche table: the house asks 470, account 1 buys 100 — the long posted 100 × 470 = 47 000, the short 100 × (1000 − 470) = 53 000, pot 100 000; a void returns exactly those two rows. After a partial close of 40 @ 450 (the long receives 40 × 450 = 18 000, the short 40 × 550 = 22 000): posted 29 000 / 31 000, pot 60 000, the same identity. A round trip (sell all 100 @ 450) leaves the pot empty and posted +2 000 / −2 000: the void returns 2 000 to the account and takes 2 000 back from the sleeve.
  3. Emission: one void_refund/v1 per party with nonzero posted, accounts ascending then sleeves ascending, markets of the epoch in MarketId order, then void_complete/v1{mode: unwind} — the reopen barrier. A market nobody traded yields no rows; a market that round-tripped to flat still returns the realized cash both ways (Σ posted = 0 = its empty pot), so rule 1 holds for it too. The exchange stages the rows and applies them atomically at the barrier, exactly as it applies payouts; a market that arrives at the barrier with no rows must be flat.
  4. Not after settlement: a market whose payouts were emitted has an empty pot and no positions; voiding it means reversing payouts (reversal/v1, state-layers §3 Settled → Voided) — out of scope (live operation). Without that path a void that reaches a settled epoch halts with a typed error (VoidAfterSettlementUnimplemented) rather than improvising.
  5. Balances may go negative: a party that realized a profit on closes and posted that cash elsewhere pays it back on the unwind; its balance may go below zero. Conservation (matching-engine §9) holds throughout. A retail account's admission refuses new risk while its free balance is negative (matching-engine §5's funds check admits only cash-receiving closes); a sleeve's quote headroom is the §6 ceiling budget and is unaffected — its posted on the market returned to zero with it, which is the intended outcome. Under R1 v2 (risk-engine.md §3) a sleeve can no longer post a close's profit elsewhere — the headroom clips posted at zero per market — and the void itself still leaves its headroom unaffected.
  6. Parser assertions: crates/clearing/tests/void_unwind.rs (the flows.md Flow 3 worked example — $0.47 × 100, pot $100 — voided, its numbers hand-derived from the §6 tranche table: 47 000 / 53 000 refunded, balances back to exactly 100 000, pot zero, void_complete{unwind} last; a partial close unwinding 29 000 / 31 000; a round-tripped flat position returning 2 000 both ways; the exchange view folding the same rows to the same numbers; the settled-then-voided typed halt), crates/clearing/tests/engine_edges.rs (a never-traded market voids with no rows), crates/exchange/tests/settlement_feedback.rs (the refund rows staged and applied at the barrier, the residue halt, a stray refund), crates/bookmaker/tests/fold_matrix.rs (the sleeve's own refund stages and applies at the barrier; a bystander's never does), the ledger's in-module test posted_sums_to_the_pot_across_the_sign_crossing (Σ posted = pot through the §6 worked example's annihilation).