Skip to main content
Settlement is the part of Volmarket that has to be trustless, because it is the part that moves money. This page covers how a market is defined, how it resolves from a single proof, how that proof is verified on-chain, and why verification lags detection.

What a market is

A market is identified by: fixture · odd (SuperOddsType + MarketParameters + outcome) · side (HOLD / BREAK) · level L · 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.
The program is odds-only. Score and stat markets are not implemented.

Single-proof settlement

You never prove “the odd stayed above L 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.
Both sides therefore need at most one proof, and the burden always falls on the party asserting that a crossing happened. Everything settles on the anchored line. Internal stake never decides an outcome.

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 in claim, entirely in u128 with checked operations:
The fee applies to winnings only, never to returned stake.

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.
The name TXLINE_PROGRAM_ID means two different things.As a Rust constant in the program, it is the mock_validator address above.As a keeper environment variable, it is the address the keeper passes as the txline_program account — and its devnet default is the real txoracle (6pW64gN1…), not the mock. Set it explicitly to the mock address to use the mock path.

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.ts passes it as the txline_program account unconditionally.
  • MOCK_VALIDATOR (keeper env, default true) selects what proof payload is built — a stub proof rather than a real one fetched from TxLINE.
Setting only one is a misconfiguration. 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:
Reproduce it with 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_root target.
  • Verification costs ~234k compute units, so a resolve on this path must raise its compute budget above the default.
The running demo uses the mock — not because the integration is incomplete, but because of epoch timing.

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’s fetchPublishedOddsProof 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.
This is inherent to proof-anchored settlement, not lag that can be engineered away. It interacts with the HOLD/BREAK asymmetry in a specific way. A winning HOLD is optimistic, so it finalizes the moment its window closes with no proof required. A winning BREAK must be proven, so it cannot finalize until its batch publishes. The interface states this honestly: a result shows provisional at window close and upgrades to verified once the proof lands. It is also why a demo runs on 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:
  • messageId
  • ts
  • the settling value
  • the resolve_market transaction signature
These are served over 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 — real messageIds, 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.