Trustless, formally-verified settlement for FIFA World Cup 2026 1X2 (match-result) prediction
markets on Solana. Built for the Superteam World Cup Hackathon, Track 1: Prediction Markets &
Settlement (submitted 2026-07-19). Data source: TxODDS Tx LINE (Txoracle program).
Proof it's real — open these first:
- A real World Cup fixture, settled on-chain against Tx LINE's live Merkle feed. Market
GaiXEuSBb3spjoptxHCoyScycN4sCy164jCF3jT9v8T3— fixture 18192996, authenticated scoreline 2–3, outcome Away, winner paid pro-rata.- Re-derive it yourself, from any wallet (permissionless, read-only, no signer, no fee):
cd tests-devnet && bun install bun run reverify.ts GaiXEuSBb3spjoptxHCoyScycN4sCy164jCF3jT9v8T3 # -> reverify -> true- 4 Kani formal proofs, all PASS — re-run in one command:
cd crates/rulebook && cargo kani(committed transcript:docs/KANI_PROOF_TRANSCRIPT.txt).- Program live on devnet:
AepSNpDzMUdBgjxA9irxxL7NTQHxXtDVq6rnqq17Lxk· every transaction signature:DEPLOYMENTS.mdSubmission text:
SUBMISSION.md· on-chain evidence:DEPLOYMENTS.md
Prediction markets don't die from bad odds. They die from bad resolution. Polymarket/UMA's 2026 crisis settled a disputed $60M+ market against a filed 8-K by token vote — the side with more tokens decided what "happened," not the facts. The other common failure mode is just as bad: a closed oracle pipe where you trust that Chainlink (or whoever) pushed the right number, with no way to check it yourself.
VAR does neither. It settles a World Cup match with no token vote, no dispute bond, no trusted
arbiter. At resolution, the program re-derives the outcome and payout straight from Tx LINE's own
on-chain Merkle-rooted feed via CPI, runs the authenticated scoreline through a Kani-proven
deterministic football rulebook, writes a proof-carrying receipt, and exposes a permissionless
reverify instruction so any wallet can re-run the whole proof in one transaction and watch it
come back green. It's the on-chain VAR that Tx LINE published daily Merkle roots for but never
shipped.
flowchart LR
subgraph SDK["packages/sdk (TypeScript)"]
ACT["TxLineClient.activate()<br/>4-step free World Cup access"]
WIT["statWitness()<br/>fetches stat-validation Merkle proofs"]
end
subgraph PROG["programs/var-settlement (Anchor)"]
CM["create_market → Market PDA + USDC vault"]
DEP["deposit → escrow, pool totals"]
AH["attest_home(home witness)"]
RES["resolve(away witness, status)"]
CLM["claim → pro-rata payout"]
REV["reverify() → bool<br/>permissionless, read-only replay"]
end
subgraph RB["crates/rulebook (pure Rust — Kani-proven INV-1..4)"]
CORE["resolve_outcome + settle<br/>checked arithmetic, fail-closed"]
end
ORACLE["Tx LINE / Txoracle<br/>daily_scores_roots PDA<br/>(TxODDS on-chain Merkle feed)"]
WIT --> AH
WIT --> RES
AH -- "CPI validate_stat(EqualTo,<br/>threshold = home_goals)" --> ORACLE
RES -- "CPI validate_stat(EqualTo,<br/>threshold = away_goals)" --> ORACLE
RES --> CORE
CORE --> RCT["ResolutionReceipt<br/>(source root, ruleset hash,<br/>scoreline, outcome, split)"]
REV --> CORE
Settlement is two transactions (attest_home, then resolve) because the two Merkle proofs
together exceed Solana's 1232-byte transaction limit.
crates/rulebook is the moat: a pure, dependency-free Rust crate with no Solana imports. It
compiles unchanged into the Anchor program and is independently checked with cargo kani. If the
CPI wiring or account layout ever changes, the proven core doesn't move.
create_market(fixture_id, kind=1X2, home_stat_key, away_stat_key, period, fee_bps, resolve_deadline)
-> opens a Market PDA + USDC vault, three empty pools (Home/Draw/Away)
deposit(outcome, amount)
-> USDC transfers into the vault, updates the Position PDA + the market's pool totals
attest_home(home: StatWitness) // permissionless — step 1 of 2
-> binds the witness to the market's fixture_id / home_stat_key / period
(FixtureMismatch / StatKeyMismatch / StatPeriodMismatch — no cross-fixture or
cross-stat spoofing)
-> CPIs Txoracle::validate_stat(EqualTo, threshold=home_goals) against daily_scores_merkle_roots
-> must return true, or it fails closed (StatNotAuthenticated); records the attested count
resolve(away: StatWitness, status_code) // permissionless — step 2 of 2
-> same binding checks + CPI for away_goals
-> runs rulebook::resolve(MatchState, Pools, fee_bps) -> Outcome + Settlement
-> writes ResolutionReceipt (source_root, ruleset_hash, scoreline, outcome, payout split)
-> flips Market.status = Settled, emits MarketResolved
(two steps because both Merkle proofs together exceed the 1232-byte tx limit)
claim()
-> per Position: pays floor(stake * net / winning_pool) for winners, or the full stake back
on Refund; guarded by a claimed flag so a Position can only be paid once
reverify() -> bool // permissionless, read-only
-> re-runs rulebook::resolve from the stored receipt's authenticated scoreline
-> asserts it still matches the recorded outcome and settlement, bit for bit
resolve and reverify both call the exact same pure function,
rulebook::resolve(&MatchState, Pools, fee_bps) -> Resolution. There's no separate "resolution
logic" to audit for reverify — it's the identical code path, so a green reverify means the
receipt is reproducible from scratch, not just internally consistent.
crates/rulebook ships four Kani proof harnesses (crates/rulebook/src/lib.rs,
#[cfg(kani)] mod proofs), run with cargo kani — committed transcript at
docs/KANI_PROOF_TRANSCRIPT.txt: Complete - 4 successfully verified harnesses, 0 failures:
- INV-1 — totality / fail-closed (
inv1_resolve_outcome_total_and_correct):resolve_outcomeis total over the full input range — it always returns exactly oneOutcome, never panics, and degenerate goal counts or a non-Completed*status always resolve toRefund. - INV-2 — conservation (
inv2_settlement_conserves):pot == home + draw + awayalways; on a paying settlementfee + net == potandnet <= pot; on a refundfee == 0andnet == 0. The program cannot mint value at the pool level — every unit paid out came from the pot. - INV-3 — settlement fail-closed (
inv3_settle_fail_closed):Outcome::Refundand anyfee_bps > MAX_FEE_BPSalways settle as a full refund withfee == 0. - INV-4 — determinism (
inv4_resolve_outcome_deterministic): identical inputs always yield an identical resolution — no hidden state, no clock or randomness dependence (settleis pure, so settlement determinism follows).
The per-winner payout bound — no payout, or split of payouts, ever exceeds net — divides by a
symbolic divisor at u128 width, which is intractable for Kani's bit-blasting backend. It is
covered instead by 3 property-test invariants × 4,000 cases each (12,000 cases at full USDC
magnitude) in crates/rulebook/tests/payout_props.rs.
A further property, no double-claim, is enforced by the claimed guard in
programs/var-settlement/src/lib.rs::claim() (require!(!p.claimed, ...)); it's a program-level
account-state property, not a pure-function one, so it's outside Kani's scope — the claim path is
exercised end-to-end on devnet by tests-devnet/smoke.ts (see DEPLOYMENTS.md).
Verified by hand against txline.txodds.com/documentation and the on-chain IDL saved at
docs/idl/txoracle_mainnet.json (2026-07-05). Re-confirm on first live connect; TxODDS may revise.
Txoracleprogram: mainnet9ExbZjAapQww1vfcisDmrngPinHTEfpjYRWMunJgcKaA, devnet6pW64gN1s2uqjHkn1unFeEjAwJkPGHoppGvS715wyP2J.- API host: mainnet
https://txline.txodds.com/api, devnethttps://txline-dev.txodds.com/api. - Free World Cup activation, no payment, no TxL token charged: on-chain
subscribe(serviceLevel, durationWeeks)→POST /auth/guest/start(guest JWT) → wallet-sign${txSig}:${leagues}:${jwt}with Ed25519/tweetnacl →POST /api/token/activatereturns anX-Api-Token. Data calls send bothAuthorization: Bearer <jwt>andX-Api-Token: <token>. The implementation that has actually been run live end-to-end istests-devnet/txline-activate.ts;packages/sdk/src/txline.ts::TxLineClient.activateis the library-shaped version of the same flow (itssubscribe()is still a stub — the script builds thesubscribeinstruction directly). - Service levels: L1 = World Cup/Friendlies, 60s-delayed, works on devnet. L12 = real-time sub-second, mainnet only.
validate_stat— the settlement primitive, called fromprograms/var-settlement/src/lib.rs::txoracle_cpi::validate_stat_equal:VAR calls it twice per resolution — once invalidate_stat( ts: i64, fixture_summary: ScoresBatchSummary, // { fixture_id, update_stats, events_sub_tree_root } fixture_proof: Vec<ProofNode>, // proves the fixture summary in the day's main tree main_tree_proof: Vec<ProofNode>, // nested proof up to the daily root predicate: TraderPredicate, // { threshold, comparison: GreaterThan|LessThan|EqualTo } stat_a: StatTerm, // { stat_to_prove: { key, value, period }, event_stat_root, stat_proof } stat_b: Option<StatTerm>, op: Option<BinaryExpression> // Add | Subtract ) -> boolattest_homewithcomparison = EqualTo, threshold = claimed home_goals, once inresolvefor away — atruereturn means the claimed integer is the Merkle-authentic one. Discriminator[107, 197, 232, 90, 191, 136, 105, 185](from the IDL), invoked raw (not through a generated Anchor client) sinceTxoracleisn't a workspace dependency.daily_scores_rootsPDA: seeds[b"daily_scores_roots", (epoch_day as u16).to_le_bytes()],epoch_day = floor(ts_millis / 86_400_000). Derivation lives in bothpackages/sdk/src/txline.ts::dailyScoresRootsPda(TS) and is documented inspec.md§2.- Merkle model: roots publish per epoch-day, nested (stat →
event_stat_root→events_sub_tree_root→ daily main root). Verification is against the day's committed root — i.e. post-update, not per-tick.resolveaccounts for this with a 7-day grace window pastresolve_deadline(RESOLVE_GRACE_SECSinprograms/var-settlement/src/lib.rs) so settlement can land after the root covering the final whistle actually publishes. validate_oddsalso exists onTxoracle(odds are on-chain-verifiable too) — noted for a future Over/Under market, unused in V1.
spec.md full design spec — architecture, invariants, threat model
SUBMISSION.md the hackathon submission text (pitch, evidence, links)
DEPLOYMENTS.md every on-chain artifact: program IDs, deploy txs, settlement
signatures, re-run commands
crates/rulebook/src/lib.rs the verified core: types, resolve_outcome, settle,
winner_payout, resolve, + Kani proofs (#[cfg(kani)])
crates/rulebook/tests/ 22 unit tests + payout_props.rs (12,000 proptest cases)
scenarios/*.json 12 golden real-World-Cup vectors (golden.rs runs all of them)
programs/var-settlement/src/lib.rs the Anchor program: create_market/deposit/attest_home/resolve/
claim/reverify, the txoracle_cpi module, account/PDA layout
packages/sdk/src/txline.ts TS client: TxLINE activation, stat-validation fetch, PDA derivation
tests-devnet/ devnet integration suite: smoke.ts (full lifecycle),
txline-activate.ts + txline-settle.ts (live real-feed settlement),
reverify.ts (stranger-wallet re-verification)
idl/var_settlement.json the program's own IDL
docs/idl/txoracle_mainnet.json the Txoracle IDL this integration was built against
docs/KANI_PROOF_TRANSCRIPT.txt committed cargo-kani transcript (4 harnesses, 0 failures)
docs/TRUST_SURFACE.md trusted vs. untrusted, threat model, fail-closed behavior
docs/AUDIT.md self-audit: proven invariants, arithmetic discipline, limitations
docs/DEMO_VIDEO_SCRIPT.md 3-minute demo shot list
docs/TXLINE_API_FEEDBACK.md builder feedback on the Tx LINE API/docs
SUBMISSION_CHECKLIST.md gate list toward the 2026-07-19 Earn submission
No trust required — clone and run the proven core directly.
# 22 unit tests + the 12-scenario golden suite (real World Cup matches, hand-checked expected
# outcomes) + 12,000 property-test payout cases
cargo test -p rulebook
# formal proofs: totality, conservation, fail-closed, determinism (crates/rulebook/src/lib.rs)
cargo kani -p rulebook
# the on-chain program compiles clean against the same rulebook crate
cargo check -p var-settlementAnd the highest-signal 30 seconds: re-run the permissionless reverify against the real-fixture market already settled on devnet — from any wallet, even one that never touched the market:
cd tests-devnet && bun install
bun run reverify.ts GaiXEuSBb3spjoptxHCoyScycN4sCy164jCF3jT9v8T3 # -> reverify -> truecargo test -p rulebook is fast and deterministic. cargo kani -p rulebook runs Kani's bounded
model checker over all four proof harnesses — expect it to take real time (bounded exhaustive
search, not a fuzz run) since it explores the full input range per kani::assume bound, not a
sample.
Verified and passing today (every claim backed by a signature in DEPLOYMENTS.md — clone and
re-run):
- 4 Kani proofs PASS —
cargo kani -p rulebook, transcript committed atdocs/KANI_PROOF_TRANSCRIPT.txt(Complete - 4 successfully verified harnesses, 0 failures). cargo test -p rulebook— 22 unit tests + 12 golden real-WC vectors + 12,000 proptest payout cases, all green.cargo check -p var-settlement— clean, exit 0.- Deployed on devnet:
AepSNpDzMUdBgjxA9irxxL7NTQHxXtDVq6rnqq17Lxk. - End-to-end devnet smoke test PASSED (
tests-devnet/smoke.ts): fullcreate → deposit → attest_home → resolve → reverify → claimlifecycle with real SPL transfers; final balances 158 / 40 / 2 (the 2% fee) match the rulebook math exactly. (This one runs against the mock oracle — it exercises the escrow/claim machinery; real-feed authentication is the next bullet.) - LIVE settlement of a real fixture against the real feed: fixture 18192996 (home 2 – 3
away) authenticated via live Tx LINE
stat-validationMerkle proofs and settled by two-step CPI into the real devnetTxoracleover the on-chain daily root — settled marketGaiXEuSBb3spjoptxHCoyScycN4sCy164jCF3jT9v8T3, outcome Away, winner paid. A stranger wallet re-derives it:reverify() → true. - Tx LINE's free World Cup tier (4-step activation) run live end-to-end — activation tx in
DEPLOYMENTS.md.
Open, flagged not hidden:
- Match status is a resolver input, not oracle-authenticated. The two goal counts are
Merkle-authenticated on-chain; the status code is caller-supplied, and the proven rulebook
fail-closes every non-
Completedstatus toRefund(INV-1) — the worst a caller can force is a refund, never a fabricated decisive result. Binding status to an authenticated feed field is the gap to close before mainnet (docs/AUDIT.md). - Demo video — script ready at
docs/DEMO_VIDEO_SCRIPT.md, recording pending. - Mainnet L12 (real-time, sub-second) — descoped for the hackathon: the free World Cup access grant is devnet L1, and mainnet is a wiring follow-up (same activation + CPI path), not a core change.
Nothing here claims more than what's been run. See SUBMISSION_CHECKLIST.md for the full gate list.
VAR is positioned strictly as settlement infrastructure / verification tooling, not an operated
sportsbook. Pools settle in USDC only — the settlement path never touches, requires, or
references the TxL token, and zero TxL is ever paid (the free World Cup tier's subscribe call
passes the TxL mint/treasury accounts the Txoracle interface requires, at a price of 0). Entry is
free-to-enter parimutuel pools (stake USDC on an outcome, winners split the net pot pro-rata). No
order book, no market-making, no custody beyond escrow-until-settlement.