What a market is
A market is identified by: fixture · odd (SuperOddsType + MarketParameters + outcome) · side (HOLD / BREAK) · levelL · window [t₀ … t₀+W]
The odd identity keys on SuperOddsType and MarketParameters, so different Over/Under lines are distinct on-chain markets — market_params is part of the market PDA.
L is snapped from the anchored StablePrice at t₀. L and the settling value share one scale: demargined probability × 1000, read from Pct[].
The two sides:
- HOLD wins if the odd’s implied probability stays ≥ L for the whole window.
- BREAK wins if it reaches ≥ L at any point in the window.
Single-proof settlement
You never prove “the odd stayed aboveL for the entire window.” Proving a negative over a continuous interval would require submitting every datapoint in it. Instead, each side resolves from one anchored odds update.
BREAK — proven
Resolves the moment anyone submits the update where prob ≥ L. One Merkle proof, one CPI into the validator. If no such update arrives by
t₀+W, BREAK loses — window close is the timeout.HOLD — optimistic
The mirror image. Anyone may submit the single update where prob dipped below
L to defeat it. If none is submitted by challenge close (window end), HOLD wins.Market instructions
1
create_market
create_market(fixture_id, odd_key, market_params, side, level, window_start, window_end, fee_bps) opens the market and initializes its USDC vault PDA. The signer is the market authority, and the fee washes back to them.create_market_v2(…, fee_recipient) is identical but takes an explicit fee recipient, so the protocol fee routes to a dedicated house wallet. Both instructions coexist on the live program with identical account layouts.2
deposit
deposit(side, amount) stakes USDC into the vault on YES or NO and records a Position.3
resolve_market
resolve_market(value, proof) is permissionless single-proof settlement, as described above. Anyone can call it.4
claim
claim() pays winners their pro-rata share of the vault, minus fee_bps on winnings.Payout math
Computed inclaim, entirely in u128 with checked operations:
The real TxLINE CPI
resolve_market CPIs into a validator program to check the submitted (value, proof). The program pins exactly two acceptable addresses as Rust constants in signal_markets/src/lib.rs and branches on which one the caller passes as the txline_program account:
Any other address is rejected by an account constraint.
Selecting a path
Two independent settings must agree, because they control different things:TXLINE_PROGRAM_ID(keeper env) selects which program is CPI’d into.resolver.tspasses it as thetxline_programaccount unconditionally.MOCK_VALIDATOR(keeper env, defaulttrue) selects what proof payload is built — a stub proof rather than a real one fetched from TxLINE.
MOCK_VALIDATOR=true with TXLINE_PROGRAM_ID left at its devnet default sends a stub proof to the real validator, which rejects it.
Verified on devnet
The real path is deployed and verified. A genuine two-stage TxLINE Merkle proof passed through the CPI in:keeper/scripts/verify-odds-proof-onchain.ts, which borsh-encodes OddsProofPayload to mirror the Rust struct field for field.
Two operational findings from that run:
- The roots account is the
txoracle insert_batch_roottarget. - Verification costs ~234k compute units, so a resolve on this path must raise its compute budget above the default.
Epoch timing: detection is instant, proof is not
TxLINE publishes odds proofs in wall-clock 5-minute batches. A datapoint becomes provable only after its interval closes and the batch publishes. The keeper’sfetchPublishedOddsProof computes the boundary and retries across the publication buffer.
That splits settlement into two clocks:
Detection
Real-time. The keeper sees a crossing on the stream the instant it happens, and settles on it.
Trustless verification
Lags ~5 minutes plus a publication buffer — the time until the datapoint’s batch is anchored.
mock_validator — a sub-minute prediction window closes long before its proof batch exists.
Settlement receipts
Every keeper settlement records the exact datapoint that decided it:messageIdts- the settling value
- the
resolve_markettransaction signature
GET /receipt?market=<pubkey>. The result modal renders that TxLINE datapoint beside a Solana Explorer link to the resolve transaction, so you can verify a settlement against the feed and the chain rather than trusting the app.
Replay mode
The World Cup fixtures the protocol was built against have finished, so the keeper can replay a capture of real recorded TxLINE events — realmessageIds, real timestamps, real demargined Pct values, recorded off the live stream into keeper/replay/odds-capture.json.
With REPLAY_FILE set, replayFeed.ts emits those events through the same onEvent handler the live stream feeds, with gaps preserved and timestamps rebased onto the replay clock. Signal buffering, board seeding, the in-window settlement backstop, and the post-window sweeper all run exactly as they do live. Everything on-chain — markets, deposits, resolve, claim — stays real. Only the feed source changes.
replay/fanout.json fans one capture across several fixture ids, each entering the recording at a different offset with its own level shift, so the board shows a full slate rather than a single card.
Replayed events carry their original
messageIds, so TxLINE has no current proof batch for them. The real-validator path cannot be exercised from a replay; it is verified separately, as shown above.