Skip to content

Repository files navigation

Onside logo: the flame check

Onside

Read the game. Make the call.

onside.today · technical documentation


Onside is a live World Cup prediction game you open during the match: a continuous feed of rapid "what happens next" micro-predictions generated from the live match state, priced off the live consensus odds, and settled by the feed minutes later. Streaks multiply your points, friends battle you head to head with coins in escrow, divisions promote and relegate you every knockout round, and every eligible result is provable against TxLINE's on-chain Merkle roots on Solana mainnet. When Onside says you called it, the receipt links to the proof.

Built solo for the TxODDS World Cup Hackathon, Consumer & Fan Experiences track, on TxLINE's real-time data.

A live call in the match room A call landing The post-match card with an on-chain verified receipt

Contents

What it is

During a live match, Onside serves one call at a time: next goal, a corner in the next five minutes, will the trailer draw level, a VAR verdict as the review happens. You answer in one tap (with a two-second undo), the feed settles it minutes later from the live data, and your streak multiplies your points. Around that loop sits a full fan hub: 41 prediction markets spanning live windows, momentum moments, player props from the real lineups, halves, and full time; a match tab of whole-match markets re-priced as the game moves; head-to-head coin battles; a six-tier promotion ladder; global, friends, and per-match leaderboards; a 14-quest earn loop; a shop; and a trophy shelf of collectible moments earned by calling them.

The menu is game-aware by construction: a decided outcome is never offered, markets appear only when the game state creates them (a lead to defend, a deficit to chase), and framings adapt to the match (a goal drought turns "who scores next" into "will anyone score"). Momentum detection arms "call it now" moments on sustained sieges, penalties, and VAR reviews, offered for exactly as long as the moment is live.

  • Points are stake-independent, so money never buys rank; coins are play-only and never cashable.
  • 15 markets are fully provable on-chain (plus both-teams-to-score on its yes side, and the correct score provable per selection) via TxLINE's validate_stat instruction family, verified on mainnet in both directions before anything was labelled.
  • Placement is server-authoritative: the client sends market, selection, and stake; the server re-derives price, window, and tier at that instant, so timing and odds cannot be forged.
  • Installable PWA with web push: settle summaries (one push per pass, however many calls you had riding), kickoff reminders, live drama pings to exactly the users with skin in the game, and direct battle challenges.

How it works

TxLINE SSE firehoses (scores + odds)
        │
        ▼
 ingest worker (Railway)              Next.js web (Railway, onside.today)
 pure reducer → canonical state   ◄──── SSE relay ── browser (PWA)
 durable event log                     offer menu · placement · receipts
 settle loop · notifications           shop · battles · boards · profiles
        │        ▲
        ▼        │ LISTEN/NOTIFY
      Neon Postgres (18 tables)
        │
        ▼
 Solana mainnet · txoracle validate_stat / v2 / v3
 (free proof simulation + optional real, app-paid proof transactions)

Two persistent services share one repository and one database. The worker owns the TxLINE streams as a single writer: it maintains canonical match state through a pure reducer (the same reducer replays recordings for development and the demo), persists a durable event log and score-by-second change points, reconciles against snapshots on a timer (the stream is never assumed gap-free), settles calls and battles idempotently, and fires Postgres NOTIFY. The web tier serves the UI, places calls server-authoritatively, relays live state to browsers over SSE fed by a LISTEN on a direct database connection, and renders share cards server-side with zero external asset dependencies. Restarts are safe by construction: state, history, and finality reseed from the database, and every settlement, grant, purchase, and notification is idempotent by unique-index design, so replays and reconciles are no-ops.

The full picture, from the reducer's canonical-truth model to the proof-instruction plans per market, is in docs/TECHNICAL.md.

Exactly how TxLINE is used

The whole product runs on TxLINE. Every surface consumed, from lib/txline/:

Surface Use
POST /auth/guest/start guest JWT issue and refresh (the two-token auth model)
GET /api/scores/stream (SSE) the spine: every live scout event, consumed by the worker
GET /api/odds/stream (SSE) the live StablePrice consensus, ~200 ticks/second in play
GET /api/fixtures/snapshot (+ ?startEpochDay=) live tracking scope + the full slate for rounds and the bracket
GET /api/scores/snapshot/{id} reconcile source (10s live / 30s idle) + finished-score backfill
GET /api/scores/updates/{id} non-deduped recovery: scorer attribution survives coverage lapses
GET /api/scores/historical/{id} full play-by-play for recordings and the replay harness
GET /api/odds/snapshot/{id} + /updates/{id} odds seeding and recording
GET /api/scores/stat-validation (v1 + statKeys= v2) Merkle proof bundles, including period keys
GET /api/scores/stat-validation-v3 the v3 multiproof bundle (halves multi-stat compute, measured)
validate_stat / _v2 / _v3 (on-chain) one call certifies a settled market's exact claim against TxODDS's root

The product prices off the consensus line (a live over/under inverts to a remaining-goals mean; the 1X2 splits it between the teams given the current lead), resolves from the scout feed's canonical aggregates and player-attributed events, and proves eligible results against the on-chain roots. What the build surfaced about the feed along the way (the canonical Stats map, the per-event Id, the StatusId phase enum, review-label semantics, and more) shipped back to TxODDS as dated, evidence-backed feedback, per the hackathon's feedback requirement.

Provable fairness, drawn precisely

TxLINE anchors per-match team statistics as Merkle roots on Solana. Onside compiles a settled call's exact claim into predicates (the full-time result is goals₁ − goals₂ compared with zero; a correct score is two equality checks; half markets use the per-period stat keys) and runs the txoracle validation instruction. The label follows the returned verdict, and nothing else: "Verified on-chain" appears only after a verification ran and returned true, everything else says "Resolved from feed", and both-teams-to-score is labelled selection-aware because only its yes side is expressible. Verification is a free simulation; the receipt can also mint a real, branded, app-paid proof transaction, attributed to the caller's wallet in the memo, that the user never signs or pays for.

Real minted proofs from production: a full-time result proof and a first-half goals proof over period keys. Every proof path was verified on mainnet in both directions (true claims returning true, false claims returning false) before being trusted.

The tour

Home during a live match Home, live-first. A live hero card with the score, a ticking minute, and your personal count of calls waiting in the room, computed with the same feed logic as the room itself so the two never disagree. Never a dead screen: upcoming fixtures, countdowns, and the full bracket are always a tap away.
The live feed The match room. One swipeable hero call at a time, the next one peeking. Placing or flicking advances the queue; a flick is a timed snooze, and the queue refills as the match creates new situations. A pressure meter and a VAR beat ride the scoreboard, and the clock ticks client-side between server pushes.
Whole-match markets The match tab. Result, totals, both teams to score, winning margin, team totals, clean sheets, win to nil, half markets, half-time/full-time, and the correct-score grid, priced live off the consensus line and re-priced as the game moves.
Your card The payoff. At full time the room becomes your card: every call, points, coins, your rank among everyone who called the match, and a featured receipt that verifies on-chain while you watch, with a real explorer link and a mint-the-proof button.
Battles Battles and the ladder. Challenge a friend directly or by link, both stake coins in escrow, most post-accept correct calls takes the pot. Every completed knockout round, your division promotes its top third and relegates its bottom quarter, with guards that never punish sitting a round out.
The shelf The shop and the shelf. Badges are earned and money cannot buy them, by construction. Frames are bought and style your share card. Collectible moment cards drop when you call the moment, each stamped with its source call and carrying that call's on-chain provability.

Built for the data that exists

Two disciplines shaped the build. First, design to the real data: TxLINE's feed carries full play-by-play, lineups, VAR grammar, a possession ladder, and per-period stats, and Onside uses all of it, while features the feed cannot support (xG, player shot counts) are deliberately absent. Second, verify empirically before trusting: the two aggregate score representations disagree in both directions on real matches, review-outcome labels are directionally unreliable, goals arrive as bursts of enrichment copies, and retractions sometimes reference nothing, so the reducer counts a goal only when the canonical aggregate and the confirmed event stream agree, and momentum calls resolve only from observable consequences. Every settlement and money path carries a reconcile simulation that folds the same data repeatedly and asserts exactly-once outcomes; the full verification record is in docs/TECHNICAL.md §12.

Running it locally

Prerequisites: Node 22+, a Postgres database (Neon works well), a Privy app (Google + email login, Solana embedded wallets), a TxLINE API token, and a Solana RPC URL.

npm install
cp .env.example .env       # fill in the values; every key is documented in the file
npm run db:migrate         # applies the committed Drizzle migrations (0000-0005)
npm run dev                # the web app on :3000
npm run worker             # the ingest worker, in a second terminal

A TxLINE token comes from the free self-serve flow: a guest JWT, one free on-chain subscribe on Solana for service level 12 (real-time World Cup data), then token activation. The TxLINE quickstart walks it end to end.

No live match on? The worker has a replay mode that drives the entire pipeline, settlement included, from a recorded fixture: REPLAY_FILE=<recording.jsonl> npm run worker (plus REPLAY_SPEED and REPLAY_ODDS_FILE). Recordings are captured with npm run record -- <fixtureId> during the event. Recorded TxLINE data is not redistributed in this repo, per the hackathon data license.

Repository layout

lib/
  txline/     REST + SSE client (two-token auth, framing normalization), record types
  ingest/     the pure reducer (canonical truth), store, durable event log, replay driver
  engine/     offer menu (41 markets), pricing models, resolvers, settlement, scoring, feed runtime
  onchain/    the provable set, validate_stat v1/v2/v3 construction, proof routing + receipts
  shop/       catalog, guard-first purchase engine, earned badges, collectibles
  earn/       the 14-quest loop (live-computed progress, idempotent claims)
  battles/    escrow, lifecycle guards, post-accept tally settlement
  divisions/  the six-tier ladder (rank-based, self-healing round-end evaluation)
  notify/     the guarded-insert notification core, web push, worker trigger hooks
  db/         Drizzle schema (18 tables) + committed migrations + the direct LISTEN connection
worker/       the ingest worker (stream consumers, loops, replay mode) + record/replay CLIs
app/          Next.js App Router: 18 screens + 36 API routes + OG image rendering
components/   the client: match room, live feed, scorebar, boards, shop, shelf, PWA plumbing
public/       brand assets, flags, icons; sw.js is generated per build (stamp-sw.mjs)
docs/         TECHNICAL.md (the full technical documentation) + screenshots

Two lines Onside never crosses

Coins are never cashable, tradeable, or redeemable, which keeps the game a game. And the on-chain layer is read-only verification plus an optional app-paid proof transaction: no custody, no settlement of user funds, and the user's wallet never signs or pays for anything.


onside.today · powered by TxLINE live data · receipts on Solana mainnet

About

Live World Cup micro-prediction game on TxLINE real-time data, with results provable on-chain. TxODDS hackathon build.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages