Trustless peer-to-peer World Cup prop bets on Solana, settled by cryptographic proof — no oracle multisig, no admin keys, no trust.
Built for the TxLINE World Cup hackathon. Two people disagree about a match stat ("There will be more than 9 corners in Brazil vs France"). GoalPost lets them stake tokens on it peer-to-peer, and the smart contract itself verifies the outcome against TxODDS' Merkle-proofed match data anchored on Solana — then pays the winner automatically.
- Create — Alice locks 25 USDT in a program-owned vault and defines a predicate over TxLINE stat keys:
corners(home) + corners(away) > 9. She's betting YES. - Accept — Bob disagrees. He matches her 25 USDT before kickoff. He's NO. The 50 USDT pot sits in a vault PDA that only the program can move.
- Settle — when the outcome is provable, anyone (in practice our keeper bot) fetches a Merkle proof from TxLINE's API and submits it on-chain. Our program CPIs into TxLINE's
validate_statinstruction, which hashes the proof against the daily Merkle root that TxODDS publishes on-chain. If a single byte of the proof is fake, the whole transaction reverts. If it verifies, our program's deterministic logic picks the winner and pays the entire pot in the same transaction. - Receipt — the market page shows the full proof chain, decoded straight from the settlement transaction. Anyone can re-verify. Nobody had to trust GoalPost, the keeper, or even TxLINE's web API.
validate_stat proves the stat value is authentic, not that the match is over. GoalPost layers two deterministic rules on top (in lib.rs, settle()):
- Finality: every proven stat leaf carries the game phase at update time. Phase 5/10/13 = full-time/AET/pens → the value is final, so the predicate result decides the winner in either direction.
- Monotonicity: goals/cards/corners only ever go up. So a TRUE
GreaterThanpredicate can settle mid-match — a goal can't be un-scored. (Addpreserves this;Subtractdoesn't, so handicap markets wait for full-time.) - A FALSE predicate on an in-play proof proves nothing → transaction reverts (
ProofNotConclusive). - Backstops:
settle_timeout(NO side collects if nothing was provable by the deadline),cancel_market(unmatched refund),mutual_void(abandoned fixtures).
- Node 20+ and npm (Windows is fine for everything except the Rust build)
- WSL2 with the Solana toolchain — only needed to (re)build/deploy the program:
anchor-cli 0.31.1,solana-cli 2.x(already set up on this machine) wallet.jsonat repo root — devnet keypair with a few SOL (already present; airdrop viasolana airdrop 2in WSL if low).envat repo root with TxLINE credentials (already present; recreate withnpm run onboardif the token expires)
npm install # installs all workspaces (web, keeper, shared)npm run onboard # guest JWT → subscribe() on-chain → activate API token → writes .env
npm run smoke # sanity-check every endpoint we depend on# in WSL, from /mnt/d/tx-odds-hackathon
anchor build # produces target/deploy/goalpost.so + target/idl/
solana program deploy target/deploy/goalpost.so \
--program-id target/deploy/goalpost-keypair.json --keypair wallet.json --url devnet
# then copy the fresh IDL into the web app (from Windows):
cp target/idl/goalpost.json apps/web/src/idl/goalpost.json
⚠️ Gotchas that already bit us (don't rediscover them):
idls/txoracle.jsonhas TxLINE'spubkeyconstants stripped — re-vendoring the IDL verbatim breaks the build (Anchor macro bug, FEEDBACK #13).- Don't
cargo update— Cargo.lock pins many crates for the platform-tools rustc (MSRV hell).
npx tsx tests/goalpost.test.ts # from repo root, plain Windows terminal is fineCreates a fresh mint + taker each run, exercises every instruction against the deployed program with real Merkle proofs from finished fixture 17952170.
npm run dev -w web # → http://localhost:3000
npm run build -w web # production build (what Vercel runs)Needs apps/web/.env.local (gitignored, already present):
TXLINE_API_ORIGIN=https://txline-dev.txodds.com
TXLINE_JWT=... # same values as root .env
TXLINE_API_TOKEN=...
NEXT_PUBLIC_STAKE_MINT=... # demo mint (DEMO_MINT in root .env)Browsing is walletless; connect Phantom/Solflare (set to devnet) to create/accept markets.
| What | Address |
|---|---|
| GoalPost program | 5GH4qp7L5LtfjLNU5ybhQ1RBsZTdDvaQVsoqGp71M5uG |
| TxLINE txoracle program | 6pW64gN1s2uqjHkn1unFeEjAwJkPGHoppGvS715wyP2J |
| Dev wallet (creator/keeper) | 67iphNsk8gDjhMymedYwhvkGSXYKkiyMa5JPCqX1ah3E |
| Replay fixture | 17952170 (finished 2026-06-04, 1-1, corners 11+2) |