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/v1events in the round whereattacker_team = teamandvictim_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/v1offset.
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).
- 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).
- 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) orsynth/synth_complete/v1(team_a_win— Team A the perspective side, wire 0; event-catalog §6.18). A fallen nexus / ancient is state (nexus_fallenonlol/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). - 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. - Identity: epoch = the match's discovery ordinal in the run's game-state
stream — the forwarded
match_discovered, on the betty FRAME log itsroster_side/v1envelope, which carries the same recorded u64 id spelled the same way (event-catalog §6.11 rule 11), or on asynthlog the forwardedsynth/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 — onsynththe 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 (onsynthevery record after the completion is). - No void path: a match is never "restored";
void_valueis recorded (growth-path D5) and never exercised. - 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(asynthlog through the real scheduler:[0, 1000]onteam_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).
- 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 LoLlol/state/v2'sblue.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'sroster_side/v1resolves 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 thedota2/v1extractor drives too, plus the running objective tally), through the allocation-freeingest::dota2::frame::kills_from_record. - 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 ≥ Norred ≥ 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. - 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}), withmarket_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). - 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). - 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.8settlement_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'ssettlement_completeorvoid_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 itssettlement_completeretires it (flows.md Flow 3); the engine refuses a post-barrier claim on the epoch before it ever asks (itssettled_epochs). - 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'sstate/v2fold, 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:: LolFrameReaderon LoL,replay::dota2::Dota2FrameReaderon 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. - 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- blockmatch_discovered/v1envelope followed by frames with noroster_side/v1; never a positionally-assumed side — with no match live the roster is the envelope too, so a frame before it isFrameWithoutMatchas 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. - 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, haltsSidesUnbound/SidesRebound, releases at the completion, reports a frame with no match live asFrameWithoutMatchand a live match's missing binding asSidesUnbound, 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 haltsFrameWithoutMatchon a frame before the roster),crates/runtime/tests/race_serve.rs(the whole DAG through the scheduler over the committed race fixturedev_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 overdev_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_millsexactly 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).
- 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 (JudgedOverflowhalts; a settlement input is never wrapped). - The partition is strict and has no push: OVER is
total ≥ N + 1, UNDER istotal ≤ 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.) - 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 (MinutePassedis 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 haltsOrdinalMismatchrather 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). - 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). - 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 itssettlement_completeretires it (flows.md Flow 3); the engine refuses a post-barrier claim on the epoch before it ever asks (itssettled_epochs). - 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. - 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), andMinutePassed(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'sLineFamilybinding — the race's too. The per-slot title'sKillsOverflow/TickMismatch(SC-8 rule 7) pass through unchanged. - 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 andparse_csv'sm:nspelling; 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 overdev_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).
- 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'sKillsOverflow; the side family adds none, so SC-9'sJudgedOverflowis unreachable for it and stays typed). - The partition is strict and has no push: OVER is
count ≥ N + 1, UNDER iscount ≤ 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). - Finality is the frame that IS the minute: SC-9 rule 3 verbatim (the ordinal, the LoL identity check, the stream law — every snapshot).
- The unreached minute voids: SC-9 rule 4 verbatim, the reason text
"match completed before minute M". - 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. - 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. - Halts: SC-9 rule 7 verbatim (the shared fold's, the family's
EpochOverflow/CompletionWithoutLines/MinutePassed). - 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 them:n:tspelling),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.pytest_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.
- 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 (GoldOverflowon LoL,NetWorthOverflowon Dota2 — the title's own halts through the shared fold), so SC-9'sJudgedOverflowis unreachable for it and stays typed. - The partition is strict and has no push: OVER is
lead ≥ N + 1, UNDER islead ≤ 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. - Finality is the frame that IS the minute: SC-9 rule 3 verbatim.
- The unreached minute voids: SC-9 rule 4 verbatim, the reason text
"match completed before minute M". - 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). - 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>. - Halts: SC-9 rule 7 verbatim.
- 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 tentotal_goldfields summed; the roster-summednet_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.
- 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'sJudgedOverflowis 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. - The partition is strict and has no push: SC-9 rule 2 on the count:
OVER is
count ≥ N + 1, UNDER iscount ≤ N— a count EQUAL to the line is UNDER; the line N = 0 reads "s has taken / slain one". - 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.
- The unreached minute voids: SC-9 rule 4 verbatim, the reason text
"match completed before minute M". - 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 (MarketIdcovers 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). - 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. - Halts: SC-9 rule 7 verbatim — including the per-slot title's
binding halt: a Dota2 frame before its
roster_side/v1haltsSidesUnboundthrough 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 liveteam_countlane 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 runOutOfBoundsatvalidate_vectorrather than being priced. - 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).
- 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 —JudgedOverflowhalts); the handicapsat(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. - The partition is strict and has no push: OVER is
judged ≥ N + 1, UNDER isjudged ≤ 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. - 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-summeddota2/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'smatch_complete/v1scoreboard_deathsis a diagnostic cross-check ONLY (SC-1's posture on reported counts; LoL's completion carries no scoreboard); it is never a source. - 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 pathNoOutcome, and the corpus builders refuse it as theirNoMatchComplete— an opened market never settles on a guess. - 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 itsopen_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 genericPending → Closededge 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'sD_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'sQuoteSource::Modelon 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 — withHouseLine::nearest_eventaking the first minimum), so two runs of one manifest over one log write the same descriptor bytes and the sameMarketId; 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). - 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-v1extractors (model-plugin-layer §8; the same stream) and byclearing::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). - 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. - 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 theline_killslane 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.
- Unwind, per party: every party's
postedon 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. - 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
(
PotResiduein the ledger,PotNotDrainedin 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. - Emission: one
void_refund/v1per party with nonzero posted, accounts ascending then sleeves ascending, markets of the epoch in MarketId order, thenvoid_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. - 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 §3Settled → 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. - 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
postedon 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 clipspostedat zero per market — and the void itself still leaves its headroom unaffected. - 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 testposted_sums_to_the_pot_across_the_sign_crossing(Σ posted = pot through the §6 worked example's annihilation).