Skip to content

Repository files navigation

FlashXI

Don't just watch the World Cup, play it! Predict the next 5 minutes of a match as it happens and get paid for how close you get, settled on Solana with TxLINE Merkle proofs.

FlashXI (built with a new primitive called Express Micro Markets, or EMM) is a Solana-native prediction market for live football. Every five minutes during a match, a new market opens with one standardized question: what will the cumulative match stats look like at the end of this window? Users submit a six-dimensional guess vector, stake USDC into a shared pool, and earn a proportional share of that pool based on how close their guess was to the Merkle-proven actual outcome — not on whether they picked the right side of a binary bet.


Important Links

Link URL
Live App View Here
Demo Video View Here
Pitch Deck View Here

Solana Programs

All EMM programs are deployed on Solana devnet. TxLINE's on-chain oracle program is an external dependency used for Merkle proof verification at settlement.

Contract Address Explorer
Market EvsWMNLoaGTPNxAC2p887LvfNPkyR355CUqiUe2qbYku View on Explorer
Vault AuiAvhPm3Uc7to4DnJ6CrV2exJB3rKcpyFXqG3WXZJtJ View on Explorer
Oracle Adapter CQPChBxEt5Lwe25BVCMFmMUEySG38HSQDCQEG7ycEimM View on Explorer
Treasury En9BChd5H1gaDfvpXig7cfczAvd5utzsrksVVDNAvJa2 View on Explorer
TxLINE txoracle 6pW64gN1s2uqjHkn1unFeEjAwJkPGHoppGvS715wyP2J View on Explorer

Important Transactions

Representative on-chain transactions from a recent devnet market lifecycle (fixture 18187298, Brazil vs Norway replay). Sourced from recent activity on the Market program — no deploy or upgrade transactions.

Name Transaction Hash
Create Market 4EzzXhDxd3DwCXtHxEqfmDeCBunv4mttjDEpvhXoxdQvB6zrJ8nDjd2i
DTUxDRKVAK2QDuL82Gn34qAPeBeA9kPF
Place Guess
stake + guess vector
5jPP9wPGTSyM7agipxyZgq5Be7EwxDbdYLBSEkCsUzhH1fsDMmBKrZgZG37ViyUcXw3G
K59p3BstFHwjM6E72uWx
Lock Market 4DEhhtFMiGZeLSdifxWjpeMhLbmdExPVAHe27dCXBwEmbDy9u46XEuSPwnwrbHs1MreNjd
PYGuK8iaZ6tmFDyU7a
Settle Market Batch
Merkle proof + TxLINE CPI
2Ftm25yntSF9n6RuiMbtGNmAh46GXPqUuSmsFZPQfHzKSCXZjA64joe3G3jYdE6FHJ2tHAr6
XESUbZi8ApyyJCEe
Claim Payout
proximity-weighted pool share
VpoUTeBLXqH76Y2cAxvEWECFgZsCX24jtMbWpXjsNpc4WXHvteoFMb6Z8d89n5msZXtwA98
cbcpTeJXWA8REGwD

Major Modules

Source links point at marshal-AM/flash11 on main. Each module has its own README with architecture notes and line-level references.

Module Path Docs
Solana Anchor programs programs/ programs/README.md
Market program programs/market (covered in programs README)
Vault program programs/vault (covered in programs README)
Oracle Adapter programs/oracle_adapter (covered in programs README)
Treasury program programs/treasury (covered in programs README)
TxLINE client & types backend/txline backend/txline/README.md
Data layer (auth, SSE, buckets, historical) backend/data-layer backend/data-layer/README.md
Oracle fetch & proof mapping backend/oracle-fetch backend/oracle-fetch/README.md
Demo session / round runner / UI API backend/demo backend/demo/README.md
On-chain TS clients & math backend/programs backend/programs/README.md
Bot simulation backend/bots backend/bots/README.md
Frontend app app/ app/README.md
HTTP server entry backend/server.ts

List of Contents


Introduction

Football is the world's most watched sport, and in-match prediction is one of the oldest forms of engagement fans have with a live game. Yet the dominant prediction market platforms — Kalshi, Polymarket, and their peers — were built for a different shape of question: Will this team win? Will this candidate be elected? Will this metric exceed a threshold? Those are valuable products, but they leave an entire category of in-match engagement unaddressed: predicting how the next stretch of play will unfold in concrete, measurable terms.

FlashXI fills that gap. Instead of asking users to pick one side of a discrete outcome, FlashXI asks a single standardized question every five minutes for the entire duration of a match:

At the end of this five-minute window, what will the cumulative match stats be?

Where typical prediction markets move slowly — capital locked for hours or days, markets tied to full-match or season-long outcomes — FlashXI is built for speed. A new window opens every five minutes, settles within minutes of the window closing, and lets you claim and redeploy stake before the next phase of play. The product is for anyone who finds traditional prediction markets too slow to match the pace of live football.

The answer is a stat vector — six integers representing goals, yellow cards, and corners for each team. Users stake Circle devnet USDC (production will use mainnet USDC) into a shared pool, submit their guess vector, and after the window closes the actual vector is fetched from TxLINE with a Merkle proof and settled on Solana. Payout is distributed by weighted closeness: the closer your guess was to the proven actual values, the larger your share of the pool. There is no win/lose binary. A user who was off by one corner still earns something meaningful if their overall vector was closer than most.

The codebase is organized as — four cooperating Anchor programs on Solana devnet, a Node/TypeScript backend that orchestrates market lifecycle and TxLINE integration, and a vanilla JavaScript frontend served statically from the same server.

What Users Predict

The locked stat set for v1 is defined in backend/calibration/stat-set-definition.ts and consists of six TxLINE stat keys:

Index Stat ID TxLINE Key Meaning
0 p1_goals 1 Participant 1 (home) goals
1 p2_goals 2 Participant 2 (away) goals
2 p1_yellow 3 P1 yellow cards
3 p2_yellow 4 P2 yellow cards
4 p1_corners 7 P1 corners
5 p2_corners 8 P2 corners

Each guess component is an integer from 0 to 20. The settlement actual is the cumulative total at the window checkpoint — not the delta within the five-minute window. Deltas are used only for calibration (computing per-stat scale factors that normalize distance math across stats with different natural variance).

Technology Stack

  • Blockchain: Solana devnet, Anchor 0.32.1, @solana/web3.js 1.98
  • Stake token: Circle devnet USDC (4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU)
  • Oracle: TxLINE devnet API + on-chain txoracle program (6pW64gN1s2uqjHkn1unFeEjAwJkPGHoppGvS715wyP2J)
  • Backend: Node.js, TypeScript, tsx runtime
  • Frontend: Vanilla HTML/CSS/JS (no React bundler), Phantom wallet integration

Current State of the Product

The live app today runs replays historical TxLINE data from finished matches through the same market lifecycle code that production will use. This is intentional: historical replay makes demos deterministic, and lets judges and testers walk through complete settle-and-claim cycles without waiting for a real match.

The production path is already implemented in code: LiveScoreStream consumes TxLINE's live SSE feed at /api/scores/stream, and WindowScheduler in live mode creates, locks, and settles markets against real-time snapshots.

Who FlashXI Is For

FlashXI is designed for football fans who want engagement beyond passive viewing — people who form opinions about how a match phase will unfold and want those opinions to have measurable, financial expression. It is also for users who find typical prediction markets too slow: Kalshi and Polymarket are built around longer horizons and slower resolution, while FlashXI runs on five-minute windows that open, lock, settle, and pay out in rapid cycles aligned to live match tempo. The crypto-native prediction market audience that already uses those platforms but wants faster cadence and finer-grained in-match questions — not just a single fixed outcome pick — is a core target user.

For developers and auditors, FlashXI is a reference implementation of a novel use of TxLINE's stat-validation primitive: not as a boolean predicate ("did team X score?") but as a source of exact multi-stat vectors for continuous on-chain scoring. The four-program Solana architecture, phased verification scripts, and TypeScript/Rust math parity make the repo a starting point for anyone building proximity-weighted oracle-settled markets.

The Core Pitch

Predict the next five minutes of the match, in numbers. Get paid by how close you were. Trust nothing but the Merkle proof.

This is not a tagline alone — it is an architectural constraint. Every design decision flows from it:

  • "Next five minutes" → fixed TxLINE bucket windows, not event-triggered markets
  • "In numbers" → six-stat vector guess, not YES/NO
  • "How close you were" → weighted-distance payout, not win/lose
  • "Merkle proof" → TxLINE stat-validation CPI, not operator resolution

The Problem

Kalshi and Polymarket work well for elections, macro events, and season-long sports futures. Applied to in-match, high-frequency prediction, the same mechanics break down in four ways.

Capital Lock-Up and Long Resolution Horizons

Championship contracts sit open for months; in-play markets often resolve only at full-time or on a major event. Capital stays locked until resolution — you cannot redeploy stake five minutes later when the pressing spell you care about starts.

Over 90 minutes of micro-phases, users either skip most of the match or hold many positions they cannot exit. FlashXI collapses that horizon: independent five-minute windows, each with its own pool, settlement, and claim.

Discrete Outcomes and Winner-Take-All Risk

Kalshi and Polymarket are not Yes/No-only — both run multi-option and categorical markets (election menus, score buckets, grouped sports props). The mechanic is still the same: pick one discrete option; if it does not resolve, the position pays zero (or you exit at a loss when liquidity allows). No partial credit for being almost right.

That fits enumerable questions ("Which candidate wins?") but not rich in-match forecasting. You might read a pressing spell correctly — shots, corners, a card, no goal — yet lose everything if you picked the wrong bucket. FlashXI replaces categorical selection with a numeric stat vector; payout scales with proximity. One corner off still earns a strong pool weight.

Liquidity and Repricing in Short Windows

Order books and AMMs need time to build depth and reprice. Five-minute windows are too short for either without latency arbitrage. FlashXI uses a shared pool split by proximity — no order book, no AMM, no mid-window repricing.

Resolution Trust Models

Off-chain resolution (oracle, multisig, UMA-style dispute) adds trust assumptions and latency. FlashXI settles from TxLINE Merkle proofs: stat-validation API → on-chain validateStatV2 CPI → immutable actual_vector on the market account. Anyone can verify in the proof viewer or re-derive payout math from chain state.

A Concrete Scenario on Traditional Platforms

67th minute, Team A pressing hard — corners, a yellow, shots, no goal.

Polymarket might offer a goal Yes/No, a score-at-70 menu (0-0, 1-0, …), and corner O/U as separate discrete bets. You buy 0-0 and read the phase well; you only profit on the contracts you actually entered. Miss the right menu item and the shape of your read does not pay.

Kalshi lists similar enumerated in-play bands — same winner-take-all settlement per contract, just more labels to choose from.

FlashXI asks for the full stat vector. You predict [0, 0, 0, 1, 3, 1]; actual is [0, 0, 0, 1, 2, 1] — one corner off. You earn a high proximity weight without needing the resolver to match your bucket.

The gap is structural: traditional platforms pay on which contract wins; FlashXI pays on how close your numbers were across six stats, every five minutes.

Why Existing Sportsbooks Do Not Solve This Either

Live sportsbooks (goalscorer, corners, match winner) are still discrete markets with bookmaker margins — no reward for partial accuracy on compound questions. FlashXI shows TxLINE odds for reference (backend/demo/odds-view.ts) but settles entirely via on-chain proximity math against Merkle-proven stats.


The Solution

FlashXI reframes in-match prediction from "pick one outcome and hope" to "predict the stat line and get paid by accuracy." The product is built on three pillars: short fixed windows, proximity-weighted pool payouts, and Merkle-proven oracle settlement.

How FlashXI Addresses Each Pain Point

Long lock-up → Five-minute independent windows. Each TxLINE-aligned bucket is its own market. Stake, predict, settle, claim — then move to the next window. Capital is not trapped for the full match duration unless the user chooses to participate in multiple consecutive windows.

Winner-take-all categorical loss → Proximity-weighted pool split. Every participant with a valid guess receives a weight computed from the Euclidean distance between their guess vector and the proven actual vector, normalized per stat. Payout is (your_weight / total_weight) × pool_total, minus a protocol fee. Closer guesses earn more; distant guesses earn less; nobody is automatically wiped out for picking the wrong discrete bucket.

Single categorical pick → Six-dimensional numeric vector. Users express a full stat line — goals, cards, corners for both teams — rather than selecting one option from a fixed menu of outcomes. This captures richer information about how they expect the match phase to unfold.

Opaque resolution → TxLINE Merkle proofs on Solana. Settlement calls TxLINE's stat-validation endpoint with the fixture ID, sequence number, and stat keys. The response includes Merkle proofs that the Oracle Adapter verifies on-chain. The Market program stores the verified actual vector and runs payout math deterministically.

TxLINE at a Glance

TxLINE is not a decorative data feed bolted onto a standalone betting app. It is the load-bearing infrastructure without which FlashXI cannot operate:

  • Fixture catalog — match metadata, kickoff times, participant names, competition grouping
  • Live score stream — SSE feed of cumulative stat snapshots as matches progress
  • Historical score archives — full progression logs for finished matches, used in demo replay
  • Five-minute bucket timingepochDay, hourOfDay, interval fields that define market windows
  • Stat validation proofs — Merkle-verified exact stat values at a given sequence number for on-chain settlement

If TxLINE auth fails, snapshots stop. If sequence numbers are non-monotonic, stat-validation fails. If stat keys 1, 2, 3, 4, 7, 8 are missing, markets cannot settle truthfully. The entire product is designed around TxLINE's data model and proof primitive.

flowchart LR
  User -->|"guess + USDC"| Market
  TxLINE -->|"Merkle proof vector"| OracleAdapter
  OracleAdapter --> Market
  Market --> Vault
  Market --> Treasury
  Vault -->|"proportional payout"| User
Loading

TxLINE

This section explains exactly how FlashXI consumes TxLINE data — every endpoint, every load-bearing field, every script involved — and why the integration is architectural rather than optional.

Why TxLINE Is Load-Bearing

FlashXI does not maintain its own score database. It does not scrape third-party sites for live stats. It does not let operators manually enter results. Every number that determines whether you get paid flows from TxLINE through a verified pipeline:

  1. Ingest — live SSE or historical bulk fetch produces FixtureSnapshot objects
  2. Schedule — bucket tracker detects five-minute boundaries and triggers market lifecycle
  3. Settle — stat-validation API returns Merkle proofs for the checkpoint sequence
  4. Verify — Oracle Adapter CPIs into TxLINE txoracle to confirm proofs on-chain
  5. Pay — Market program computes weights from the verified actual vector

Remove any step and the product stops. Without auth, you cannot call the API. Without snapshots, you cannot know when windows open or close. Without seq, stat-validation has no anchor. Without Merkle proofs, on-chain settlement has no trustless ground truth.

The demo app uses historical data not because TxLINE is optional in demo mode, but because replaying a finished match gives deterministic, repeatable demos while exercising the identical settlement path production uses also helping out people to test the app easily.

Authentication and Activation

TxLINE access requires a two-step credential chain implemented in backend/data-layer/auth.ts and bootstrapped via npm run activate-txline (scripts/activate-txline.ts):

Step 1 — Guest JWT. The client calls TXLINE_GUEST_AUTH (https://txline-dev.txodds.com/auth/guest/start on devnet) to obtain a short-lived JWT.

Step 2 — On-chain subscription. Using the wallet keypair, the client calls TxLINE's on-chain subscribe instruction with a service level ID and subscription duration. This burns/transfers TXL tokens per TxLINE's subscription model.

Step 3 — API token activation. The subscription transaction signature is submitted to POST /api/token/activate along with a wallet signature and selected leagues. TxLINE returns an API token.

Both TXLINE_JWT and TXLINE_API_TOKEN are persisted to .env. Every authenticated request sends:

Authorization: Bearer {TXLINE_JWT}
X-Api-Token: {TXLINE_API_TOKEN}

On 401 Unauthorized, both TxLineClient and LiveScoreStream automatically renew the guest JWT and retry. Production deployments should monitor token expiry and subscription renewal.

API Endpoints and Data Ingestion

The TxLINE HTTP client lives in backend/txline/client.ts and wraps these endpoints:

Endpoint Purpose in FlashXI
GET /api/fixtures/snapshot Match catalog for /matches page
GET /api/scores/stream Live SSE feed — production mode
GET /api/scores/historical/{fixtureId} Full SSE log — demo replay (preferred)
GET /api/scores/updates/{fixtureId} Incremental updates — historical fallback
GET /api/scores/snapshot/{fixtureId} Point-in-time snapshot — last-resort fallback
GET /api/scores/stat-validation Merkle proofs for settlement
POST /api/token/activate API token after on-chain subscribe

Historical and updates responses arrive as SSE-formatted text, parsed by parseSseJsonRecords in backend/data-layer/sse.ts. The live stream uses readSseMessages with Last-Event-ID for reconnect resume.

The LiveScoreStream class (backend/data-layer/live-stream.ts) implements the SnapshotSource interface: it maintains the latest snapshot per fixture ID, emits snapshot events, and handles JWT renewal on auth failure. Payload shapes are normalized — raw arrays, { scores: [...] }, { data: [...] }, or single records — all filtered through isScoreRecord().

Load-Bearing Fields and Stat Keys

Score records are parsed in backend/data-layer/parse-score.ts. A record is considered valid only when it contains:

Field Aliases Why it matters
fixtureId FixtureId Primary key for markets and oracle calls
ts Ts Wall-clock timestamp → originalTs → bucket derivation
seq Seq Monotonic sequence — stat-validation anchor at settlement
stats Stats Map of TxLINE key → cumulative value
statusSoccerId GameState Match phase; abandonment (IDs 14–19) voids markets

The six stat keys used for market settlement are locked in LOCKED_STAT_SET:

Keys 1, 2 → goals (P1, P2)
Keys 3, 4 → yellow cards (P1, P2)
Keys 7, 8 → corners (P1, P2)

Stat-validation responses (backend/txline/types/stat-validation.ts) carry the proof payload: eventStatRoot, statProofs, subTreeProof, mainTreeProof, and per-stat { key, value, period } entries. These map directly to the Anchor validateStatV2 instruction via backend/oracle-fetch/payload-mapper.ts.

Five-Minute Bucket Alignment

Market windows are not arbitrary app timers. They are derived from TxLINE's own bucketing scheme in backend/data-layer/bucket.ts:

epochDay  = floor(timestamp_ms / 86_400_000)
hourOfDay = UTC hour component
interval  = floor(UTC minutes / 5)

window_start = epochDay × 86400000 + hour × 3600000 + interval × 300000
window_end   = window_start + 300_000 ms

This alignment serves two purposes. First, it ensures FlashXI windows correspond to the same temporal buckets TxLINE uses internally for stat aggregation. Second, it powers anti-frontrun protection: before creating a market, the backend checks whether stats for that bucket have already landed in the feed (backend/programs/anti-frontrun.ts). If they have, market creation is rejected — you cannot bet on a window whose outcome is already observable.

Settlement via Stat Validation

When a window locks, settlement begins. The function fetchWindowVector in backend/oracle-fetch/fetch-window-vector.ts:

  1. Calls GET /api/scores/stat-validation?fixtureId={id}&seq={seq}&statKeys=1,2,3,4,7,8
  2. Receives Merkle proofs and exact stat values for the checkpoint
  3. Maps the response to an Anchor-compatible payload
  4. Submits settle_market_batch (two batches: stats 0–3, then 4–5) via the Market program
  5. Market CPIs into Oracle Adapter → TxLINE validateStatV2
  6. Verified values are written to market.actual_vector

Demo Mode vs Production Live Mode

Both modes converge on the same WindowScheduler — only the snapshot source differs.

Demo / Replay Mode

  1. User starts a session via POST /api/demo/sessions
  2. DemoSessionManager bulk-fetches historical data via fetchFixtureFeed(fixtureId) — a three-tier fallback chain (historical → updates → snapshot)
  3. ReplaySession re-emits snapshots through the SnapshotSource interface with a virtual replay clock
  4. WindowScheduler(mode: "replay") creates, locks, and settles markets against replayed checkpoints
  5. Raw Action records power the story timeline UI; markets trigger on bucket boundaries, not individual events

Production Live Mode

  1. startLiveScheduler() starts LiveScoreStream + WindowScheduler(mode: "live")
  2. SSE stream pushes real-time score records
  3. Each record becomes a FixtureSnapshot with sessionId: "live"
  4. Bucket tracker detects new five-minute boundaries from originalTs
  5. Markets are created, locked after wall-clock WINDOW_MS, settled via the same oracle path

Two timestamps are preserved during replay:

  • originalTs — real TxLINE wall time; used for bucket derivation and oracle proofs
  • replayVirtualTs — UX pacing only; controls how fast the demo replays without affecting settlement
sequenceDiagram
  participant Auth as TxLINE Auth
  participant API as TxLINE API
  participant DL as Data Layer
  participant WS as WindowScheduler
  participant OF as Oracle Fetch
  participant Chain as Solana

  Auth->>API: JWT + API token
  API->>DL: SSE scores / historical feed
  DL->>WS: FixtureSnapshot
  WS->>WS: new bucket → createMarket
  WS->>WS: window end → lockMarket
  WS->>OF: settleMarket(seq)
  OF->>API: stat-validation
  API-->>OF: Merkle proofs + values
  OF->>Chain: settle_market_batch CPI
  Chain-->>WS: actual_vector stored
Loading
flowchart TB
  subgraph demo [Demo Mode]
    HistFetch["fetchFixtureFeed()"]
    Replay["ReplaySession"]
    HistFetch --> Replay
  end

  subgraph prod [Production Mode]
    SSE["LiveScoreStream"]
  end

  Replay --> Scheduler["WindowScheduler"]
  SSE --> Scheduler
  Scheduler --> MarketLifecycle["create → lock → settle → claim"]
Loading

Historical Feed Fallback Chain

When bulk-loading a fixture for demo replay, fetchFixtureFeed in backend/data-layer/historical.ts tries three sources in order:

  1. GET /api/scores/historical/{fixtureId} — full SSE log, preferred for replay and calibration because it contains the complete progression with Action records for the story feed
  2. GET /api/scores/updates/{fixtureId} — incremental updates stream, used when historical archive is empty
  3. GET /api/scores/snapshot/{fixtureId} — point-in-time snapshot, last resort when only a single checkpoint exists

After fetch, snapshots are deduplicated and validated for monotonic seq ordering via assertMonotonicSeq. Non-monotonic sequences break stat-validation alignment and are rejected before entering the replay engine.

Finished fixtures eligible for historical discovery must have statusSoccerId in {5, 10, 13} (full time variants) and fall within the age window of 6 hours to 14 days after kickoff. This window ensures TxLINE historical archives are populated while excluding stale fixtures with degraded feed quality.

Session Isolation and Concurrency

Live snapshots carry sessionId: "live". Demo sessions carry unique UUIDs hashed to 32 bytes for PDA derivation. This isolation prevents snapshot collisions when multiple demo sessions run concurrently against different fixtures on the same server. The SnapshotSource interface abstracts the source — scheduler code never branches on demo vs live except for lock timing (liveLockDelayMs vs replay manual lock).


Architecture

This section explains the full FlashXI system — frontend, backend, scheduler, on-chain programs, and external oracle — through a concrete user story, then drills into market mechanics, payout math, and account structure.

User Story: Alex Predicts Brazil vs Norway

Alex opens FlashXI in a browser connected to Phantom wallet with devnet USDC from the Circle faucet.

Act 1 — Discovery. Alex navigates to /matches and sees a catalog of available fixtures grouped by competition. Each card shows team names, kickoff time, and data readiness indicators. Alex selects Brazil vs Norway (fixture 18187298) — the calibration reference match with 25 actionable five-minute windows.

Act 2 — Session start. On /match/18187298, Alex clicks to start a demo session. The frontend calls POST /api/demo/sessions with the fixture ID. DemoSessionManager bulk-fetches the historical TxLINE feed, resolves actionable checkpoints (windows where stats are non-zero), and returns a session ID. Alex is redirected to /demo/{sessionId}.

Act 3 — Prediction phase. The demo UI enters phase: "prediction". A modal presents six stat inputs — goals, yellow cards, corners for each team — plus a stake amount in USDC. Alex enters [1, 0, 0, 1, 3, 2] and stakes 10 USDC. The frontend calls POST /api/tx/place-guess; the backend builds an unsigned transaction via tx-builder.ts; Alex signs with Phantom and submits. The Market program CPIs into Vault deposit — Alex's USDC moves into the market escrow; pool_total increments.

Meanwhile, five bot wallets (backend/bots/bot-runner.ts) submit randomized guess vectors with staggered timing to simulate market depth. Alex sees competitor activity on the leaderboard panel.

Act 4 — Lock. When the prediction timer expires, DemoRoundRunner calls lock_market on-chain. Status transitions from Open to Locked. No further guesses are accepted for this window.

Act 5 — Observation. The UI enters phase: "observation". The replay engine advances through TxLINE Action records — goals, cards, corners — displayed in a story feed (backend/demo/story-feed.ts). This phase is narrative; it does not affect settlement mechanics. The actual settlement target was already determined by the checkpoint snapshot at window end.

Act 6 — Settlement. The backend calls fetchWindowVector(fixtureId, seq) to obtain Merkle proofs, then submits two settle_market_batch transactions (stats 0–3, then 4–5). The Oracle Adapter verifies proofs via TxLINE CPI. The Market stores actual_vector. finalize_weights iterates all position accounts and computes each user's weight from the on-chain distance formula.

Act 7 — Results and claim. SSE events announce round_settled. The leaderboard ranks participants by weight — Alex sees their position relative to bots. Alex clicks Claim; the frontend calls POST /api/tx/claim. The Market computes Alex's gross share, deducts the treasury fee, and CPIs Vault payout plus Treasury collect_fee. USDC returns to Alex's wallet proportional to accuracy.

The cycle repeats for the next five-minute window in the session until all actionable checkpoints are exhausted.

flowchart TB
  subgraph frontend [Frontend app/]
    Matches["/matches"]
    MatchPage["/match/:id"]
    DemoUI["/demo/:sessionId"]
    Leaderboard["/leaderboard"]
    ProofViewer["/proof-viewer"]
    Client["shared/demo-client.js"]
  end

  subgraph backend [Backend backend/]
    Server["server.ts"]
    DemoRoutes["demo/demo-routes.ts"]
    SessionMgr["demo/demo-session-manager.ts"]
    RoundRunner["demo/demo-round-runner.ts"]
    Scheduler["scheduler/window-scheduler.ts"]
    TxBuilder["demo/tx-builder.ts"]
    DataLayer["data-layer/"]
    OracleFetch["oracle-fetch/"]
    Bots["bots/bot-runner.ts"]
  end

  subgraph chain [Solana Programs]
    MarketProg["market"]
    VaultProg["vault"]
    OracleProg["oracle_adapter"]
    TreasuryProg["treasury"]
  end

  subgraph external [External]
    TxLINE["TxLINE API + txoracle"]
    Phantom["Phantom wallet"]
  end

  Matches --> MatchPage --> DemoUI
  DemoUI --> Client
  Client --> DemoRoutes
  DemoRoutes --> TxBuilder --> MarketProg
  MarketProg --> VaultProg
  MarketProg --> OracleProg
  MarketProg --> TreasuryProg
  OracleProg --> TxLINE
  Client --> Phantom
  Scheduler --> MarketProg
  RoundRunner --> MarketProg
  DataLayer --> Scheduler
  OracleFetch --> OracleProg
  DemoRoutes --> Leaderboard
  Server --> DemoRoutes
Loading

System Components

Frontend (app/) — Static HTML pages with shared theming (shared/theme.css) and the FlashXI app shell (shared/app-shell.js). No build step; the server serves files directly. Key pages:

  • /matches — fixture catalog
  • /match/:fixtureId — pre-session match hub
  • /demo/:sessionId — main prediction UX with guess modal, phase indicator, story feed
  • /leaderboard — per-window rankings and claim button
  • /window/:marketPubkey — raw on-chain positions viewer
  • /proof-viewer — settlement proof display

shared/demo-client.js centralizes Phantom connection, transaction signing, and API calls for place-guess and claim flows.

Backend (backend/) — Node/TypeScript services:

  • server.ts — HTTP server, static routing, health check at /health
  • demo/demo-session-manager.ts — multi-round session orchestration
  • demo/demo-round-runner.ts — single-window lifecycle: predict → lock → observe → settle
  • demo/demo-routes.ts — REST + SSE API surface
  • demo/tx-builder.ts — unsigned transaction construction for wallet signing
  • demo/leaderboard.ts — off-chain ranking with tie-breaking (weight → distance → raw error)
  • scheduler/window-scheduler.ts — mode-agnostic market lifecycle driver
  • programs/market.ts — TypeScript client for all Market instructions
  • bots/bot-runner.ts — simulated competitors and auto-claim

On-chain programs (programs/) — Four Anchor programs deployed on devnet. See Solana Contracts for instruction-level detail.

External services — TxLINE API for data and proofs; Circle devnet faucet for USDC; Solana devnet RPC for transaction submission.

Market Mechanics

Fixed cadence. Every window is exactly five minutes, aligned to TxLINE buckets. There is no variable-duration or event-triggered market creation — the product runs on a steady clock, not reactive match drama.

Shared pool. All stakes for a window go into one escrow account (Vault PDA per market). There are no individual bet accounts with binary payoffs — only positions with guess vectors and cumulative stake.

Stake size vs weight. Stake amount does not affect weight computation. Weight is purely a function of guess accuracy. Stake size only scales the payout numerator: a user with 20 USDC staked and weight W earns twice the gross payout of a user with 10 USDC staked and the same weight W, because both draw from the same pool proportionally.

Market states.

Open → Locked → Settled
         ↘ Voided (match abandoned → full refund)
  • Open: guesses accepted until window_end_ts (live) or manual lock (replay)
  • Locked: no new guesses; settlement begins
  • Settled: actual vector stored, weights finalized, claims enabled
  • Voided: match abandoned (statusSoccerId 14–19); void_refund returns full stakes

Anti-frontrun. Before create_market, the backend queries whether TxLINE has already published stats for the target bucket. If yes, creation fails. This prevents betting on windows whose outcomes are already observable in the feed.

Batch settlement. Six stats are settled in two on-chain batches ([4, 2]) because Merkle proof payloads exceed transaction size limits in a single call. Each batch CPIs into the Oracle Adapter independently.

Bot simulation. Five bot wallets submit random guess vectors during the prediction phase and auto-claim after settlement. This ensures demo windows have competitive depth and that leaderboard/payout UX is testable without manual multi-wallet setup.

sequenceDiagram
  participant User
  participant Backend
  participant Market
  participant Vault
  participant OracleAdapter
  participant TxLINE

  Backend->>Market: create_market
  User->>Backend: place_guess (via Phantom)
  Backend->>Market: place_guess
  Market->>Vault: deposit (CPI)
  Backend->>Market: lock_market
  Backend->>TxLINE: stat-validation (seq)
  TxLINE-->>Backend: proofs + values
  Backend->>Market: settle_market_batch (×2)
  Market->>OracleAdapter: fetch_verified_vector (CPI)
  OracleAdapter->>TxLINE: validate_stat_v2 (CPI)
  Backend->>Market: finalize_weights
  User->>Backend: claim (via Phantom)
  Backend->>Market: claim
  Market->>Vault: payout (CPI)
  Market->>Treasury: collect_fee (CPI)
Loading

The Math Behind Proximity-Weighted Payouts

All payout math runs on-chain in the Market program (programs/market/src/math/fixed_point.rs), with a TypeScript mirror in backend/programs/market-math.ts for off-chain verification and leaderboard display. Constants:

N_STATS = 6
FIXED_POINT_SCALE = 1_000_000
EPSILON = 1
MAX_GUESS = 20
WEIGHT_DISTANCE_SCALE = 100

Scale factors. Before market creation, per-stat scale factors are loaded from backend/calibration/artifacts/normalization.json. Each factor is computed from historical five-minute deltas on the calibration fixture:

scaleFactor_i = max(p95_i, stddev_i, 0.1)

For Brazil vs Norway, the encoded scale factors are approximately [0.196, 2.0, 1.0, 0.1, 4.0, 2.0] for the six stats respectively. These are written to the market account at creation as fixed-point integers (round(factor × 1_000_000)) and never change for the lifetime of that market.

Step 1 — Per-stat normalized error.

For each stat i, given guess g_i, actual a_i, and scale factor s_i:

If g_i == a_i:  norm_i = 0
Else:           norm_i = ceil(|g_i - a_i| × FIXED_POINT_SCALE / s_i)
                norm_i = max(norm_i, 1)

Ceiling division ensures small non-zero differences never truncate to zero. The scale factor normalizes across stats — being off by one goal is penalized differently than being off by one corner, reflecting each stat's natural variance.

Step 2 — Weighted Euclidean distance.

distance = floor_sqrt( Σ(norm_i²) × WEIGHT_DISTANCE_SCALE )

Integer square root via Newton's method (max 64 iterations) keeps computation deterministic on-chain.

Step 3 — Weight.

weight = FIXED_POINT_SCALE² / (distance + EPSILON)

Closer guesses produce lower distance → higher weight. The epsilon prevents division by zero for perfect guesses.

Step 4 — Payout.

userGross = (weight × poolTotal) / totalWeight
fee       = (userGross × feeRateBps) / 10_000
userNet   = userGross - fee

Treasury fee rate is configured in the Treasury program state. Claims are only valid after weights_finalized = true.

Worked Example

Actual vector: [1, 0, 0, 1, 4, 2] (goals, yellows, corners) Pool total: 100 USDC (10 from Alex, 90 from bots combined) Fee rate: 100 bps (1%)

Three participants:

User Guess Vector Stake
Alex [1, 0, 0, 1, 3, 2] 10 USDC
Bot A [1, 0, 0, 1, 4, 2] 50 USDC
Bot B [0, 0, 2, 0, 1, 0] 40 USDC

Bot A's guess exactly matches the actual → all norms zero → distance zero → weight = 1_000_000² / (0 + 1) = maximum.

Alex is off by one corner (stat index 4: guess 3, actual 4). With scale factor 4.0 for corners:

norm_corners = ceil(1 × 1_000_000 / 4_000_000) = ceil(0.25) = 1

All other stats match (norm = 0). Distance = floor_sqrt(1² × 100) = 10. Weight = 1_000_000² / (10 + 1) ≈ 90,909,090,909.

Bot B is far off on multiple stats — large norms, large distance, low weight.

After finalize_weights, suppose totalWeight is dominated by Bot A (perfect guess). Bot A receives the vast majority of the 100 USDC pool; Alex receives a meaningful but smaller share for being close; Bot B receives little. Exact amounts depend on the integer fixed-point arithmetic on-chain, but the ordering is: Bot A > Alex > Bot B.

Important: Alex's 10 USDC stake and Bot A's 50 USDC stake do not affect weights — only the pool share calculation. Bot A earns more total USDC both because their weight is highest and because their stake contributes more to the pool they are drawing from.

6D Weight Landscape (Projection)

The full guess space is six-dimensional

image

Account Model and On-Chain State

FlashXI uses Program Derived Addresses (PDAs) extensively. Seed definitions live in backend/programs/constants.ts:

Market PDA

seeds = ["market", fixture_id (u32 LE), session_id (32 bytes), epoch_day (u32 LE), hour (u8), interval (u8)]

Each five-minute window for a fixture/session combination gets a unique market account storing status, window timestamps, scale factors, actual vector, total weight, and pool reference.

Position PDA

seeds = ["position", market_pubkey, user_pubkey]

Stores the user's guess vector, cumulative stake, computed weight, and claimed flag.

Escrow PDA (Vault)

seeds = ["escrow", market_pubkey]

Holds the SPL token account for the market pool. Only the Market program can CPI deposit/payout/withdraw.

Adapter Config PDA

seeds = ["adapter_config"]

Oracle Adapter global configuration.

Provider Config PDA

seeds = ["provider", provider_id (u8)]

Per-provider settings for TxLINE (provider ID 0).

Treasury State PDA

seeds = ["treasury_state"]

Protocol fee rate and accumulated revenue.

HTTP API Surface

The demo backend exposes REST and SSE endpoints from backend/demo/demo-routes.ts and backend/server.ts:

Session management

  • POST /api/demo/sessions — start a new demo session for a fixture; returns sessionId, label, round count
  • GET /api/demo/sessions/:id — session state snapshot (phase, current round, fixture metadata)
  • GET /api/demo/sessions/:id/events — Server-Sent Events stream for phase transitions, transaction signatures, settlement results

Transaction building

  • POST /api/tx/place-guess — build unsigned place_guess transaction for Phantom signing; body includes market pubkey, guess vector, stake amount
  • POST /api/tx/claim — build unsigned claim transaction for a settled market position

Read APIs

  • GET /api/window/:marketPubkey — all positions for a market, including bot isSimulated labels
  • GET /api/demo/sessions/:id/leaderboard — ranked positions for current or specified round
  • GET /api/fixtures/catalog — match catalog with readiness tiers
  • GET /health — server health check

Static routes

  • / redirects to /matches
  • /match/:fixtureId — pre-session match page
  • /demo/:sessionId — main prediction UX
  • /leaderboard, /proof-viewer, /window/:marketPubkey, /predict

Production will extend this surface with live session endpoints and webhook integrations for stream health — the transaction building and read APIs remain unchanged because on-chain instruction layout is stable.

Frontend Phase Machine

The demo UI in app/demo/index.html tracks session phase as a state machine:

idle → prediction → locking → observation → settling → results → (next round or complete)

During prediction, the guess modal is enabled and the countdown shows remaining time in the window. During observation, the story feed replays TxLINE Action records while settlement executes in the background. During results, the leaderboard highlights weights and enables the claim button for positions with non-zero expected payout.

SSE events drive phase transitions client-side without polling. Key event types include round_started, tx_confirmed, round_locked, round_settled, and session_complete. Transaction signatures in events link directly to Solana Explorer for transparency.

Tracking Window and First Actionable Bucket

Not every five-minute bucket in a match has meaningful stat activity. The pre-match bucket often has all-zero cumulative stats and is skipped. backend/fixtures/tracking-window.ts aligns demo UX to start from the first actionable checkpoint — the first bucket where at least one stat in the locked set is non-zero, minus one window for prediction context.


Solana Contracts

FlashXI deploys four custom Anchor programs on Solana devnet, plus relies on TxLINE's external txoracle program. Each program has a narrow responsibility; together they implement the full market lifecycle.

flowchart TB
  Market -->|"deposit / payout / withdraw"| Vault
  Market -->|"fetch_verified_vector"| OracleAdapter
  OracleAdapter -->|"validate_stat_v2"| TxOracle["TxLINE txoracle"]
  Market -->|"collect_fee"| Treasury
Loading

Market Program

Address: EvsWMNLoaGTPNxAC2p887LvfNPkyR355CUqiUe2qbYku Source: programs/market/src/lib.rs

The Market program is the orchestrator. It owns the market lifecycle and payout math.

create_market — Initializes a Market PDA for a specific fixture, session, and TxLINE bucket. Writes scale factors from calibration, sets status to Open, and CPIs into Vault init_escrow to create the per-market token escrow. Off-chain, createMarket() in backend/programs/market.ts includes anti-frontrun checks before submission.

place_guess — Accepts a six-element guess vector and stake amount while the market is Open. Creates or updates the user's Position PDA. CPIs into Vault deposit to move tokens from the user's ATA into the escrow. Increments pool_total. In live mode, rejects guesses after window_end_ts.

lock_market — Requires now >= window_end_ts. Sets status to Locked. No further guesses permitted.

settle_market_batch — Called twice per market (stats 0–3, then 4–5). CPIs into Oracle Adapter fetch_verified_vector with Merkle proof accounts. Writes verified stat values into actual_vector. When all six stats are settled, status becomes Settled.

finalize_weights — Iterates all Position accounts for the market. Computes each weight via compute_weight using the stored actual vector and per-market scale factors. Sets market.total_weight and weights_finalized = true.

claim — Computes userGross = weight × poolTotal / totalWeight, deducts treasury fee, CPIs Vault payout to the user and Treasury collect_fee to the protocol. Marks position.claimed = true.

void_market — Sets status to Voided when the match is abandoned. Enables void_refund per position, which CPIs Vault withdraw to return full stakes.

Vault Program

Address: AuiAvhPm3Uc7to4DnJ6CrV2exJB3rKcpyFXqG3WXZJtJ Source: programs/vault/src/lib.rs

The Vault program is the custodian. It holds all staked tokens in per-market escrows and executes transfers only via CPI from the Market program.

init_escrow — Creates the escrow PDA and associated token account for a new market.

deposit — Moves tokens from user ATA to escrow. Increments pool_total. Callable only by Market CPI.

payout — Transfers computed payout amount from escrow to user ATA. Decrements pool_total, increments total_paid_out. Callable only by Market CPI.

withdraw — Returns full stake to user (void refund path). Callable only by Market CPI.

Security is enforced by assert_market_cpi() in programs/vault/src/cpi_auth.rs — direct user calls to Vault instructions are rejected.

Oracle Adapter Program

Address: CQPChBxEt5Lwe25BVCMFmMUEySG38HSQDCQEG7ycEimM Source: programs/oracle_adapter/src/lib.rs

The Oracle Adapter is the bridge between FlashXI and TxLINE's on-chain oracle.

register_provider — Registers TxLINE as provider ID 0 with schema version and configuration.

fetch_verified_vector — Accepts Merkle proof accounts and stat metadata. CPIs into TxLINE txoracle validate_stat_v2. Returns verified stat values to the Market program. This is the most compute- and size-intensive instruction due to proof account payload.

The TypeScript client in backend/programs/oracle-adapter.ts and the proof fetch pipeline in backend/oracle-fetch/ construct the account metas and instruction data required for this CPI.

Treasury Program

Address: En9BChd5H1gaDfvpXig7cfczAvd5utzsrksVVDNAvJa2 Source: programs/treasury/src/lib.rs

The Treasury program manages protocol economics separately from market logic.

initialize — Creates treasury state PDA with initial fee rate.

collect_fee — Receives fee portion during claim CPI. Callable only by Market.

update_fee_rate — Governance instruction to adjust fee bps (within configured cap).

migrate_stake_mint — Supports migration from test token mint to Circle devnet USDC (executed via npm run treasury:migrate-usdc).

withdraw_treasury — Allows authority to withdraw accumulated protocol revenue.

Fee collection is atomic with claim — users receive net payout after fee deduction in a single transaction flow.

Transaction Size and Compute Considerations

Settlement transactions are the largest in the system. Merkle proof accounts for six stats across two batches require versioned transactions with address lookup tables (ALTs) and elevated compute unit budgets. The script npm run measure:adapter-tx exists to profile transaction sizes during development.


Roadmap

FlashXI v0.1 demonstrates the full lifecycle on devnet with historical replay. The following four milestones chart the path to production.

1. Mainnet Deployment

Deploy all four EMM programs to Solana mainnet with audited, immutable or upgrade-governed binaries. Switch TxLINE integration from devnet to mainnet. Replace devnet USDC with mainnet USDC as the stake token.

2. Expanded Stat Vectors

The original product vision included eight stats (adding shots and fouls per team). v1 locks six stats (goals, yellow cards, corners) for proof reliability. v2 will recalibrate normalization artifacts for an expanded stat set, update N_STATS in the Market program, and adjust batch sizes for settlement transactions. Each added stat increases proof payload size — batching strategy and lookup table design must be revisited.

3. Social Features

Add cross-fixture leaderboards, shareable proof links (verify any settlement via /proof-viewer), push notifications when new five-minute windows open, and optional social sharing of prediction results. Explore integration with fan communities and watch-along experiences where FlashXI windows align with broadcast pacing.


Conclusion

FlashXI transforms in-match football prediction from slow, discrete, winner-take-all categorical markets into a short-cycle, continuous, proof-backed vector market. Every five minutes, users answer one standardized question — what will the stat line look like? — stake into a shared pool, and earn proportional payouts based on accuracy. No picking one bucket from a fixed menu. No total wipeout for being close but wrong on the underlying stats. No opaque resolution.

TxLINE makes this possible. Their live and historical feeds supply the stat snapshots that drive market timing. Their five-minute bucket model aligns window boundaries. Their stat-validation API and on-chain txoracle program deliver Merkle-proven ground truth that Solana programs verify without trusting FlashXI operators. Without TxLINE, there is no feed, no proofs, and no product.

Solana makes it practical. Four cooperating programs — Market, Vault, Oracle Adapter, Treasury — split concerns cleanly. Proximity-weighted payout math runs deterministically on-chain. Users connect Phantom, sign transactions, and receive USDC payouts in seconds after settlement.


License

MIT License Copyright (c) 2026

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages