Corner Clash
Corner Clash
Corner Clash pairs a broadcast‑style live football engine with on‑chain prediction markets that settle on provably‑verified match data. You watch a World Cup fixture play out as a stylized live simulation, and — while it’s live — bet on outcomes (winner, goals, corners, cards…) through Solana markets whose results are proven against TxLINE’s on‑chain‑verified stats.
It’s one product with two planes working together:
- The visual plane — play‑by‑play drives a real‑time isometric match animation (ball, players, events, xG, commentary). Built from TexLine earlier but had to move to ESPN bcz Texlin didn’t provide real time player and ball cordinates
- The truth plane — TxLINE (TxODDS) provides Merkle‑proof‑verifiable scores/stats that act as the settlement oracle for the markets. No trusting our word: outcomes are proven on‑chain.
What it does
- Live match engine — tune into a live World Cup match or replay a recent one as a stylized isometric simulation, streamed play‑by‑play over SSE. Scorebar, clock, xG, key‑moments feed, and broadcast‑style commentary.
- On‑chain prediction markets — for the match you’re watching, trade 8 standard markets. Connect a wallet (embedded or external), grab demo tokens, and place bets.
- Provable settlement — markets resolve against TxLINE’s verifiable score/stat feed, so winners are determined by cryptographically‑checkable facts, not an operator.
- Lazy market creation — markets don’t need pre‑seeding: the first bet opens the market on‑chain (the platform co‑signs the authorization, the bettor pays the fee) and places the bet atomically.
- Live trade activity — every on‑chain bet/claim is indexed from a Helius webhook and shown in a real‑time Activity feed per match.
How it works
┌──────────────────────────────────────────────┐
│ Railway │
TxLINE (World Cup) ───▶│ worker (cron) ─ python worker/main.py ─▶ ┐ │
│ │ │
ESPN play‑by‑play ─────▶│ web (Express) Postgres │ │
(positions, xG, │ ├─ /api/v2/live (SSE match stream) ▲ │ │
players, commentary) │ ├─ /api/fixtures/list (from DB) ───────┘ │ │
│ ├─ /api/trading/* (Solana markets) │ │
Helius webhook ────────▶│ └─ /api/webhooks/helius (trade indexer) │ │
(Solana tx) │ │ │
└───────────────┬──────────────────────────────┘ │
│ serves the SPA │
▼ │
React SPA (/ main · /v0 classic) │
├─ Match engine (canvas) ◀── ESPN SSE │
└─ Trading panel ──────────▶ Solana devnet ◀────┘
(corner_clash program)
- A cron worker (
worker/main.py) syncs World Cup fixtures from TxLINE into Postgres. - You open
/→ the SPA loads fixtures from the DB and auto‑plays a live match (or the most recent completed match’s recap). - The match engine opens an SSE stream and animates the isometric pitch from ESPN play‑by‑play (ball travels each pass, players are named, goals/cards/corners animate, commentary captions).
- For a live match, the trading panel shows the 8 markets. You connect a Privy Solana wallet, faucet some demo tokens, and bet.
- Your first bet on a market creates it on‑chain (platform co‑signs, you pay) and places the bet in one transaction; later bets just buy.
- A Helius webhook decodes the program’s events and stores each trade → shown in the Activity tab.
- After the match, markets settle against TxLINE‑verified stats; winners claim their payout.
Architecture
| Layer | What | Tech |
|---|---|---|
| Frontend | Single‑page app. / = match engine + trading panel side‑by‑side; /v0 = classic TxLINE replay. |
React 19, Vite 6, React Router 7, Privy, @solana/web3.js, Canvas 2D |
| Backend | Express server: serves the SPA + all APIs; proxies TxLINE; streams ESPN over SSE; builds Solana transactions; indexes trades. | Node 20+, Express 5, pg, Server‑Sent Events |
| Database | Sole source of truth — users, fixtures, markets, trades. No JSON files in the live path. | Railway Postgres |
| Worker | Cron job that syncs World Cup fixtures TxLINE → Postgres. | Python 3 (+ psycopg) |
| Indexer | Helius transaction webhook → decodes corner_clash events → stores the trade feed. |
Helius, Anchor event decoding |
| On‑chain | The betting program (markets, buy, settle, claim). Settlement verifies TxLINE proofs. | Solana devnet, Anchor 0.32, SPL tokens |
The match engine (why it’s built this way)
The pitch is a 1,500‑line imperative <canvas> renderer (IsometricMatch) — stadium, crowd, players, ball, particle effects, event animations. It’s driven from React via a ref and the useMatch hook (which owns the SSE streaming, replay controller, scoreboard/stat state). Keeping the canvas engine imperative and wrapping it in React is deliberate: reimplementing isometric rendering in the DOM would be slower and pointless. Everything around the canvas (scorebar, controls, feed, picker) is native React.
Two data sources, on purpose
- ESPN carries spatial data (ball/player field positions, player names, xG, commentary) that TxLINE does not — so ESPN drives the animation.
- TxLINE carries verifiable data (Merkle‑proof scores/stats) — so TxLINE drives settlement and the “verified on‑chain ✓” score chip.
They’re intentionally decoupled: the animation never blocks on settlement, and settlement never depends on ESPN.
The prediction markets
Eight standard markets are offered per fixture (lib/trading.js → marketPlans):
| Market | Outcomes |
|---|---|
| Match winner | Home · Draw · Away |
| Total goals | Over/Under 2.5 |
| {Home} goals | Over/Under 0.5 |
| Total corners | Over/Under 8.5 |
| {Home} corners | Over/Under 4.5 |
| Yellow cards total | Over/Under 3.5 |
| First‑half goals | Over/Under 0.5 |
| Any red card | Yes · No |
Each outcome is a predicate over TxLINE stat keys (goals, corners, cards, half‑time goals). Settlement submits the matching TxLINE proof on‑chain, so a market can only resolve to a fact the feed can prove.
On‑chain programs
corner_clash— the market program (create_market,buy_position,lock_market,settle_market,claim,void_market,refund). Deployed on devnet:G5usFnHwxSrCJCGpDUbVdYym6dR1b7UTuN4r3bk8dm2H.txoracle— TxLINE’s on‑chain oracle (6pW64gN1s2uqjHkn1unFeEjAwJkPGHoppGvS715wyP2J) providing the verified score/fixture Merkle roots + proofs.
Market creation is authorized by the config admin but paid by the bettor: the server co‑signs a create_market + buy_position transaction, the user signs as fee/rent payer, and submits it — so the first buyer opens the market without the platform funding account rent.
Tech stack
- Web: React 19 · Vite 6 · React Router 7 ·
@privy-io/react-auth(embedded + external Solana wallets) ·@solana/web3.js·lucide-react· HTML Canvas 2D - Server: Node ≥20 · Express 5 ·
pg(Postgres) · Server‑Sent Events - Chain: Solana (devnet) · Anchor 0.32 · SPL Token · Helius (indexing)
- Data / DB: Railway Postgres · ESPN API · TxLINE (TxODDS) API
- Worker: Python 3 ·
psycopg - Hosting: Railway (web service + Postgres + cron worker)
Project structure
server.mjs Express entry (mounts routers, serves the SPA)
core.mjs request handlers (TxLINE proxy, ESPN SSE, trading, webhook, static)
routes/ thin Express routers (misc · live · v2 · trading · webhooks)
lib/
trading.js Solana market logic (create/buy/settle/claim, market plans)
db.js Postgres pool + repositories
helius.js decode corner_clash events from webhook payloads
trading/ the React SPA (Vite root)
src/
main.jsx router + Privy provider
App.jsx trading panel
pages/ V2Page (main) · V0Page (classic) · Nav
match/ match engine (useMatch hook, MatchView, BottomTabs)
engine/ vendored canvas engine (simulation.js, replay.js, …)
public/ classic replay app + static assets
contracts/ Anchor workspace (programs/corner_clash)
worker/main.py fixtures sync (TxLINE → Postgres)
migrations/ SQL schema + runner (scripts/migrate.mjs)
scripts/ railway start/sync, db migrate, txline setup, devnet smokes
Local development
Prerequisites: Node ≥20, Python 3, a Postgres database (Railway), and (for contract work) the Solana + Anchor toolchains.
1. Install
npm install
2. Configure .env.local (copy from .env.example)
# Live data (TxLINE / TxODDS)
TXLINE_BASE_URL=https://txline-dev.txodds.com
TXLINE_JWT=...
TXLINE_API_TOKEN=...
# Database — REQUIRED (the app is DB‑only, no JSON fallback)
DATABASE_URL=postgresql://... # Railway Postgres (public proxy host for local)
# Wallet auth
PRIVY_APP_ID=... # dashboard.privy.io
# On‑chain admin (config authority) — co‑signs market creation, mints the demo faucet
ADMIN_SECRET_KEY=[12,34,...] # id.json byte array, or base58
# Trade indexer
HELIUS_WEBHOOK_AUTH=... # shared secret for POST /api/webhooks/helius
PORT=4173
HOST=127.0.0.1
3. Prepare the database
npm run db:migrate # create the schema
python worker/main.py # sync World Cup fixtures into Postgres (needs DATABASE_URL)
4. Run
# API server (serves the built SPA + APIs)
npm run serve # → http://localhost:4173
# Frontend with hot reload (proxies /api to the server above)
npm run web:dev
Open http://localhost:4173/. It auto‑selects a live match, or the most recent completed match’s recap.
Deployment (Railway)
Two services from this one repo, plus a Postgres plugin:
- web —
npm run build(build) →npm start(start). Serves the SPA + APIs. SetHOST=0.0.0.0. - worker — a cron service (
SERVICE_ROLE=worker, e.g.*/5 * * * *) that runsworker/main.pyto refresh fixtures in Postgres.
Set the env vars above in each service’s Variables. Create a Helius transaction webhook for the corner_clash program id pointing at POST /api/webhooks/helius (with the HELIUS_WEBHOOK_AUTH header) to power the Activity feed.
Redeploying the on‑chain program (from contracts/, needs the upgrade authority):
anchor build # or: cargo build-sbf
anchor deploy --provider.cluster devnet
Scripts
| Command | Purpose |
|---|---|
npm run serve |
Run the Express server (serves built SPA + APIs) |
npm run web:dev |
Vite dev server for the frontend (HMR) |
npm run build |
Build the SPA to dist/ |
npm run db:migrate / :status |
Apply / list Postgres migrations |
python worker/main.py |
Sync World Cup fixtures → Postgres |
npm run txline:setup |
Create a free TxLINE devnet subscription + credentials |
npm run contracts:smoke:devnet |
Devnet market create/buy smoke test |
npm test |
Node test runner |