Skip to main content
Prerequisites: Complete the Quickstart guide to set up authentication and subscriptions, and book the fixture you want to validate. The snippets assume jwt is the guest JWT from /auth/guest/start and apiToken is the value returned by /api/token/activate.
API Endpoints: Use https://txline.txodds.com/api/ for mainnet or https://txline-dev.txodds.com/api/ for devnet
Program setup: Use generated types that match your selected network, and use Program Addresses, Program Reference (Mainnet), or Program Reference (Devnet) for program IDs and PDA derivation.

Overview

On-chain validation proves, inside a Solana transaction, that TxODDS published a particular score record or odds update. For stats, it also checks a condition you choose, such as “Participant 1 scored more goals than Participant 2”. The instruction returns true only when the proof matches the published root and the condition holds. Otherwise it returns false; it does not fail the transaction. Use the same network for the API request, IDL/types, program ID, and on-chain root PDA.

Before You Validate: Book the Fixture

Validation is tied to booking. Before you validate a fixture’s stats or odds, the validating wallet must book that fixture with book_fixture. Booking creates a fixture_ticket for your wallet and that fixture. validateStatV4 and validateOddsV4 check for it and fail without it. This applies even when your subscription bundle already covers the fixture.
  • Book once per fixture, with the wallet that will sign the validations.
  • One booking covers every later validation of that fixture, stats and odds alike, with no per-call fee.
  • The proof must be for the booked fixture. A ticket for one fixture cannot validate another.
See How a Booking Connects to Validation for the details.
The legacy validateStat instruction and its /api/scores/stat-validation endpoint are not part of the current program interface. Single-stat and two-stat checks are V4 strategies; see Single-Stat and Two-Stat Checks. Fixture proof validation with validateFixture is a read-only simulation and does not need a booking; see Runnable Devnet Examples.

Worked Example: Prove a Match Result

This example proves on-chain that Participant 1 beat Participant 2 in a finished soccer match. In the Soccer Feed, stat key 1 is Participant 1’s total goals and key 2 is Participant 2’s.
1

Book the fixture

Pick the match from GET /api/fixtures-schedule, request a quote, and submit book_fixture, as described in Booking Fixtures. Do this once. If your wallet already has the fixture’s fixture_ticket, skip this step.
2

Find the final score record

The match result is in the record whose Action is game_finalised. Take its Seq.
The historical endpoint covers fixtures that started between two weeks and six hours ago. For a more recent match, take the record from the scores stream or updates instead. See Choosing the Score Sequence.
3

Fetch the proof

Request both goal totals. The order of statKeys sets the order of payload.leaves, so leaves[0] is Participant 1’s goals and leaves[1] is Participant 2’s.
4

Stage a worker ticket

Send validateFixtureForTree. It checks the proof’s fixture summary against the day’s on-chain scores root and keeps the verified root in a short-lived worker_ticket. This step does not check the booking.
5

Validate the result

Describe the condition as a strategy. Here it is “goals 1 minus goals 2 is greater than 0”.
Send an Ed25519 verify instruction for the backend signature, followed by validateStatV4 with the payload, the strategy, and your fixture_ticket. The program checks the booking, then the proof, then evaluates the strategy. The result is true if the proof matches and Participant 1 won. It is false if Participant 1 did not win or if the proof does not match.
6

Close the worker ticket

Send closeWorkerTicket to reclaim the worker ticket’s rent. The fixture_ticket stays, so later validations of this fixture skip step 1.
The same booking also lets this wallet validate the fixture’s odds updates with validateOddsV4. The rest of this page is the reference for each step.
The repository includes runnable devnet examples for both paths in examples/devnet/scripts. See Runnable Devnet Examples for setup and script mapping.

Validation Checklist

Before sending a V4 validation, confirm:
  • The wallet holds a fixture_ticket for the fixture being validated.
  • The proof came from the same API host as your activated subscription.
  • The program ID matches the network used for the proof.
  • The root PDA (daily_scores_roots or daily_batch_roots) uses the epoch day of payload.ts: Math.floor(payload.ts / 86_400_000).
  • The epoch day is encoded as u16 little-endian (2 bytes).
  • Every proof hash is decoded to exactly 32 bytes.
  • The fixture ID and sequence number refer to the score update you intend to validate.
  • Each strategy index refers to the same position in the requested statKeys array.

Setup

Fetching Scores Data

Retrieve a snapshot of scores for a booked fixture:
Search for recent score updates:

Choosing the Score Sequence

The seq parameter is not a placeholder. Use the sequence value from a real score record observed through one of the scores data endpoints:
  • /api/scores/snapshot/{fixtureId}
  • /api/scores/updates/{epochDay}/{hourOfDay}/{interval}
  • /api/scores/historical/{fixtureId}
  • /api/scores/stream
Do not call /api/scores/stat-validation-v4 with seq=0. Score sequences start at 1 for a fixture and increment as new score records are produced. Depending on your client and response mapper, the payload field may appear as Seq or seq; use that observed value when requesting the validation proof.

Phase and Status Semantics

Pick the score record whose game phase matches the condition you want to prove. For example, “final score of the first half” should use the record for the completed first-half phase or halftime/rest state, not an arbitrary in-running first-half update. If you validate an in-running first-half record, the predicate means the condition was true at that observed moment. That is different from proving the final first-half result. Check the sport-specific phase tables, such as Soccer Feed, and use the score record’s statusId, StatusId, gameState, or equivalent phase field when deciding whether a record is suitable for settlement. For final match outcome settlement on the current devnet and mainnet releases, use a scores record with action=game_finalised. These finalisation records set statusId and period to 100, so the same final-outcome validation path covers regulation-time wins, extra-time wins, penalty wins, and abandoned matches.

V4 Validation

validateStatV4 and validateOddsV4 take their proof material from backend-signed payloads and require the wallet to hold a fixture_ticket for the fixture, created by book_fixture. A fixture_ticket places no limit on the number of V4 validations for its fixture and no per-call payment applies. The V4 instructions are not .view() calls: they are sent in a transaction.

Instruction Sequence

Each validation is three instructions, sent as separate transactions: domain and workerId are chosen by the client. Use one domain per validation type so scores and odds tickets do not share an address; the runnable examples use 0 for odds and 1 for scores. workerId is a u16. The same workerId must be passed to validateFixtureForTree, to validate*V4 as its leading argument, and to closeWorkerTicket. After closeWorkerTicket, validate*V4 and a second closeWorkerTicket fail for that (domain, workerId) until validateFixtureForTree stages it again. The worker_ticket and fixture_ticket PDAs are derived as described in Program Addresses.

Backend Co-Signing

The co-signature protects you as the client. The on-chain validation only runs on a payload signed by TxODDS’ backend admin key, so a successful validation guarantees that TxODDS, not a third party, produced the data that was verified on-chain. validateStatV4 and validateOddsV4 (and book_fixture) must be preceded, in the same transaction, by an Ed25519 signature-verification instruction. The program reads instructions_sysvar and checks that the preceding instruction verified:
  • the public key of the network’s backend admin, and
  • a message equal to the SHA-256 hash of the Borsh-encoded payload passed to the validation instruction (StatValidationPayload or OddsValidationInputV4).
The signature itself is the signature field of the API response (base64). A missing instruction, a different signing key, or a payload that does not hash to the signed message makes the instruction fail. The backend admin public key differs per network; see Program Addresses. Check that the key you build the instruction with is the one published there. If you use a different key, the instruction fails.
payload must be the exact object passed to the validation instruction. Place the returned instruction immediately before the validateStatV4 or validateOddsV4 instruction.

V4 Stat Validation

GET /api/scores/stat-validation-v4 takes fixtureId, seq, and a comma-separated statKeys. Use a real seq from an observed score record. The response is { payload, signature }:
Every leaf must be covered by exactly one strategy predicate. A V2 payload that carries a rawMask is 512 bytes larger and does not fit a legacy transaction alongside the Ed25519 instruction; send it as a v1 transaction (@solana/web3.js cannot build these; @solana/kit can). Each leaves[i] corresponds to the i-th key in statKeys.

Single-Stat and Two-Stat Checks

Request one stat key and cover it with a single predicate to check one stat. Request two keys and use a binary predicate to compare them, for example a score difference. Both run through the same three-step sequence as above.
A transaction that confirms does not mean the proof matched or the predicate held. A missing booking or co-signature fails the transaction, but a proof or predicate that does not hold returns false. The program returns the boolean result as the instruction’s return data. Read it from the confirmed transaction’s meta.returnData, as the runnable scripts do.

V4 Odds Validation

GET /api/odds/validation-v4 takes messageId and ts of a finalized odds update. The response is { payload, signature }: validateOddsV4 takes OddsValidationInputV4 (ts, oddsLeafHash, subTreeProof). oddsLeafHash is the SHA-256 of the Borsh-encoded Odds struct built from payload.oddsSnapshot, with fields in the order of the IDL’s Odds type. Use domain 0, pass daily_batch_roots as dailyMerkleRoots in step 1, and map summary.oddsSubTreeRoot to subTreeRoot:
The fixture_ticket must be for the odds update’s fixture. When you choose an odds update to validate, check that its FixtureId is bookable, for example by matching it against GET /api/fixtures-schedule. An update for a fixture that cannot be booked can never be validated.

Runnable Scripts

Runnable versions of the sequences above:

Common Validation Errors

See Troubleshooting for the full activation, streaming, and validation checklist.

Real-Time Scores Streaming

Subscribe to real-time scores updates:

Validation Use Cases

On-chain validation enables trustless verification of:
  • Trading Settlement - Prove score outcomes for bet settlement
  • Conditional Logic - Execute smart contract logic based on verified game stats
  • Dispute Resolution - Provide cryptographic proof of game data
  • Automated Markets - Settle prediction markets with on-chain verification
  • Score Differentials - Validate margins and score differences for complex betting scenarios