Skip to main content
Prerequisites: Complete the Quickstart guide to set up authentication and subscriptions. 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: The Txoracle type used below comes from the generated types in IDL & Types (Mainnet) or IDL & Types (Devnet). Use Program Addresses for program IDs and PDA derivation.

Overview

This guide demonstrates how to validate scores data against on-chain Merkle roots using cryptographic proofs. You’ll learn how to fetch validation data and perform both single-stat and two-stat validations.

Setup

import * as anchor from "@coral-xyz/anchor";
import { PublicKey, ComputeBudgetProgram } from "@solana/web3.js";
import { BN } from "@coral-xyz/anchor";
import axios from "axios";

const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const program = anchor.workspace.Txoracle as anchor.Program<Txoracle>;

// Create HTTP client with authentication
const httpClient = axios.create({
  timeout: 30000,
  headers: {
    "Content-Type": "application/json",
    "Authorization": `Bearer ${jwt}`,
    "X-Api-Token": apiToken
  },
  baseURL: "https://txline.txodds.com",
});

function toBytes32(value: string | number[] | Uint8Array): number[] {
  const bytes = Array.isArray(value)
    ? Uint8Array.from(value)
    : value instanceof Uint8Array
      ? value
      : value.startsWith("0x")
        ? Buffer.from(value.slice(2), "hex")
        : Buffer.from(value, "base64");

  if (bytes.length !== 32) {
    throw new Error(`Expected 32 bytes, received ${bytes.length}`);
  }

  return Array.from(bytes);
}

function toProofNodes(nodes: Array<{ hash: string | number[] | Uint8Array; isRightSibling: boolean }>) {
  return nodes.map((node) => ({
    hash: toBytes32(node.hash),
    isRightSibling: node.isRightSibling,
  }));
}

Fetching Scores Data

Retrieve a snapshot of scores for a specific fixture:
const fixtureId = 17952170;
const response = await httpClient.get(`/api/scores/snapshot/${fixtureId}?asOf=${Date.now()}`);
console.log(`Snapshot for fixture ${fixtureId}:`, response.data);
Search for recent score updates:
const now = new Date();
const targetTime = new Date(now.getTime() - (5 * 300000)); // 25 minutes ago
const epochDay = Math.floor(targetTime.getTime() / 86400000);
const hourOfDay = targetTime.getUTCHours();
const interval = Math.floor(targetTime.getUTCMinutes() / 5);

const updates = await httpClient.get(`/api/scores/updates/${epochDay}/${hourOfDay}/${interval}`);
console.log(`Updates found:`, updates.data);

Single-Stat Validation

Validate a single statistic against on-chain Merkle roots:
// Fetch validation data from API
const response = await httpClient.get("/api/scores/stat-validation", {
  params: {
    fixtureId: 17952170,
    seq: 941,
    statKey: 1002
  }
});
const validation = response.data;

// Prepare fixture summary
const fixtureSummary = {
  fixtureId: new BN(validation.summary.fixtureId),
  updateStats: {
    updateCount: validation.summary.updateStats.updateCount,
    minTimestamp: new BN(validation.summary.updateStats.minTimestamp),
    maxTimestamp: new BN(validation.summary.updateStats.maxTimestamp),
  },
  eventsSubTreeRoot: toBytes32(validation.summary.eventStatsSubTreeRoot),
};

// Prepare Merkle proofs
const fixtureProof = toProofNodes(validation.subTreeProof);
const mainTreeProof = toProofNodes(validation.mainTreeProof);

// Prepare stat to validate
const stat1 = {
  statToProve: validation.statToProve,
  eventStatRoot: toBytes32(validation.eventStatRoot),
  statProof: toProofNodes(validation.statProof),
};

// Define validation predicate
const predicate = {
  threshold: 0,
  comparison: { greaterThan: {} },
};

// Find the daily scores PDA
const targetTs = validation.summary.updateStats.minTimestamp;
const epochDay = Math.floor(targetTs / (24 * 60 * 60 * 1000));

const [dailyScoresPda] = PublicKey.findProgramAddressSync(
  [
    Buffer.from("daily_scores_roots"),
    new BN(epochDay).toArrayLike(Buffer, "le", 2),
  ],
  program.programId
);

// Execute validation using view (read-only simulation)
const computeBudgetIx = ComputeBudgetProgram.setComputeUnitLimit({
  units: 1_400_000
});

try {
  const isValid = await program.methods
    .validateStat(
      new BN(targetTs),
      fixtureSummary,
      fixtureProof,
      mainTreeProof,
      predicate,
      stat1,
      null,  // No second stat
      null   // No operator
    )
    .accounts({
      dailyScoresMerkleRoots: dailyScoresPda
    })
    .preInstructions([computeBudgetIx])
    .view();

  if (isValid) {
    console.log("On-chain stat validation passed");
  } else {
    console.log("On-chain stat validation rejected the predicate");
  }
} catch (err) {
  console.error("Validation simulation failed:", err);
}

Two-Stat Validation

Validate a comparison between two stats (e.g., score difference). This example builds on the single-stat validation above:
// Fetch validation data including a second stat
const response2 = await httpClient.get("/api/scores/stat-validation", {
  params: {
    fixtureId: 17952170,
    seq: 941,
    statKey: 1002,
    statKey2: 1003
  }
});
const validation2 = response2.data;

// Prepare second stat (stat1 is already defined above)
const stat2 = {
  statToProve: validation2.statToProve2,
  eventStatRoot: toBytes32(validation2.eventStatRoot),
  statProof: toProofNodes(validation2.statProof2),
};

// Define operation and predicate
const op = { subtract: {} };
const predicate2 = {
  threshold: 5,
  comparison: { lessThan: {} },
};

// Execute two-stat validation (reuses variables from single-stat example)
const isValid2 = await program.methods
  .validateStat(
    new BN(targetTs),
    fixtureSummary,
    fixtureProof,
    mainTreeProof,
    predicate2,
    stat1,
    stat2,
    op
  )
  .accounts({
    dailyScoresMerkleRoots: dailyScoresPda,
  })
  .preInstructions([computeBudgetIx])
  .view();

console.log("Two-stat validation result:", isValid2);

Real-Time Scores Streaming

Subscribe to real-time scores updates:
const streamUrl = "https://txline.txodds.com/api/scores/stream";
const streamResponse = await fetch(streamUrl, {
  headers: {
    Authorization: `Bearer ${jwt}`,
    "X-Api-Token": apiToken,
    Accept: "text/event-stream",
    "Cache-Control": "no-cache",
  },
});

if (!streamResponse.ok) {
  throw new Error(`Stream failed: ${streamResponse.status}`);
}

// Reuse the readSseMessages and parseSseData helpers from the Streaming Data guide.
for await (const message of readSseMessages(streamResponse)) {
  console.log(message.event ?? "message", parseSseData(message.data));
}

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