Skip to main content
Prerequisites: Complete the Quickstart guide to set up authentication and subscriptions
API Endpoints: Use https://txline.txodds.com/api/ for mainnet or https://txline-dev.txodds.com/api/ for devnet

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 * as config from '../common/config';
import * as users from '../common/users';

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

Fetching Scores Data

Retrieve a snapshot of scores for a specific fixture:
const fixtureId = 17952170;
const response = await users.apiClient.get(`/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 updateUrl = `${config.API_BASE_URL}/scores/updates/${epochDay}/${hourOfDay}/${interval}`;
const updates = await users.apiClient.get(updateUrl);
console.log(`Updates found:`, updates.data);

Single-Stat Validation

Validate a single statistic against on-chain Merkle roots:
// Fetch validation data from API
const url = `/scores/stat-validation?fixtureId=17952170&seq=941&statKey=1002`;
const response = await users.apiClient.get(url);
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: validation.summary.eventStatsSubTreeRoot,
};

// Prepare Merkle proofs
const fixtureProof = validation.subTreeProof.map((node: any) => ({
  hash: node.hash,
  isRightSibling: node.isRightSibling,
}));

const mainTreeProof = validation.mainTreeProof.map((node: any) => ({
  hash: node.hash,
  isRightSibling: node.isRightSibling,
}));

// Prepare stat to validate
const stat1 = {
  statToProve: validation.statToProve,
  eventStatRoot: validation.eventStatRoot,
  statProof: validation.statProof.map((node: any) => ({
    hash: node.hash,
    isRightSibling: node.isRightSibling,
  })),
};

// 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).toBuffer("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 url2 = `/scores/stat-validation?fixtureId=17952170&seq=941&statKey=1002&statKey2=1003`;
const response2 = await users.apiClient.get(url2);
const validation2 = response2.data;

// Prepare second stat (stat1 is already defined above)
const stat2 = {
  statToProve: validation2.statToProve2,
  eventStatRoot: validation2.eventStatRoot,
  statProof: validation2.statProof2.map((node: any) => ({
    hash: node.hash,
    isRightSibling: node.isRightSibling,
  })),
};

// 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 with automatic JWT renewal:
import { EventSource } from 'eventsource';

const streamUrl = `${config.API_BASE_URL}/scores/stream`;

const eventSource = new EventSource(streamUrl, {
  fetch: async (input, init) => {
    let response = await fetch(input, {
      ...init,
      headers: {
        ...init?.headers,
        'Accept-Encoding': 'deflate',
        'Authorization': `Bearer ${users.authState.jwt}`,
        'X-Api-Token': users.authState.apiToken,
      },
    });

    // Renew JWT if expired
    if (response.status === 403 || response.status === 401) {
      const newJwt = await users.renewJwt();
      response = await fetch(input, {
        ...init,
        headers: {
          ...init?.headers,
          'Authorization': `Bearer ${newJwt}`,
          'X-Api-Token': users.authState.apiToken,
        },
      });
    }

    return response;
  },
});

eventSource.onmessage = (event) => {
  console.log('Received scores update:', event.data);
};

eventSource.onopen = () => {
  console.log('Stream connected');
};

eventSource.onerror = (err) => {
  console.error('Stream error:', err);
};

JWT Token Renewal

The API client automatically handles JWT expiration and renewal. After a JWT expires (typically 1 month), the next request will intercept the 403 response, automatically renew the JWT, and retry the original request.
// Any API call will automatically renew the JWT if expired
const response = await users.apiClient.get(`/scores/snapshot/${fixtureId}?asOf=${Date.now()}`);

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