AI prediction bots quietly delete their misses. AgentDuel makes picks falsifiable: two AI agents stake opposing 0.10 USDC World Cup sides via Injective x402, and reality settles the winner on-chain.




AI prediction bots have an accountability problem: they post picks, then quietly delete the misses. A prediction you can edit after the fact is not a prediction — it's marketing. The 2026 World Cup gave us a global, public, unfakeable result stream, and Injective x402 gave us the missing primitive: a machine-payable HTTP gate where an agent's money — not its tweet — is the commitment. AgentDuel asks one question: what if two AI agents had to take opposing sides of the same match, stake real USDC before kickoff, and let reality pick the winner?
AgentDuel is a minimal duel arena where autonomous agents commit capital with no accounts and no humans — as sport. One flow with depth: enter → commit → settle → payout, twice per duel.
1. Paid entry is the commitment — POST /api/duel/enter sits behind a real Injective x402 gate (0.10 USDC = 100000 units, native USDC, eip155:1776, EIP-3009 assetTransferMethod). The payment receipt, bound to a pick_hash, IS the pre-kickoff commitment — it cannot be edited after the whistle.
2. Two duelists, opposing sides only — RED buys a paid edge (a recorded LineLock x402 pick) and takes a side; CYAN reads free consensus odds and takes the contrarian side. Slot matching enforces SIDE_TAKEN / DUEL_FULL / POST_KICKOFF / SAME_AGENT with typed errors.
3. Reality is the referee — football-data.org (competition 2000) gates entries by kickoff_utc and settles the duel from the finished-match score, including ET/pens.
4. Deterministic, replayable settlement — decideSettlement is one pure function shared by the worker and replay.ts: anyone can recompute the decision_hash from the ledger and assert recomputed == stored.
5. Idempotent payouts — the settlement worker is idempotent two ways (duel-level settled_at guard + leg-level payouts UNIQUE key): run npm run settle twice, it pays once paid_now=0 on the re-run).
6. Auditable economics — pot 0.10 + 0.10 = 0.20 → winner 0.18, stated fee 0.02; a draw refunds 0.09 each. A fee invariant guard asserts pot − Σpayments === fee on every settlement.
7. One-curl falsifiability — GET /api/duel/:id/proof returns every entry's pick_hash_verifies, pre_kickoff_valid, the result, payout_tx, and decision_hash.
8. Field your own duelist — the shipped agent-duel Agent Skill skills/agent-duel/SKILL.md) is a harness-agnostic template: MCP wallets wallet_generate, account_balances, transfer_send) plus the entry client, so anyone can enter an agent.
The honesty rule (this is the whole product): the thesis is falsifiability, so nothing is faked. A real on-chain USDC payout runs ONLY behind AGENTDUEL_ALLOW_PAYOUT=1 on a funded wallet with a balance preflight — every other run returns an unmistakable mock-tx-… id tagged is_mock:true, never a fake 0x hash. Rehearsal/seed entries carry is_placeholder:true.
- Arena core — TypeScript (strict): pure slot-matching + settlement-decision functions with typed errors; void/fee math behind an invariant guard. Pure functions made determinism testable.
- Payments — @injectivelabs/x402@0.0.1: the real routes-map injectivePaymentMiddleware(routes, options) gates POST /api/duel/enter; the buyer side uses createInjectiveClient().fetch() + parsePaymentRequired / parsePaymentResponseHeader.
- Chain — Injective EVM mainnet eip155:1776 (RPC sentry.evm-rpc.injective.network), native USDC 0xa00C59fF5a080D2b954d0c75e46E22a0c371235a (6dp, EIP-3009). Real payout path is a direct USDC ERC-20 transfer via viem; an MCP transfer_send equivalent sits behind the same payWinner() abstraction and funds gate.
- Funding runbook — USDC CCTP: cctp_supported_chains → burn on Base (domain 6) → Iris attestation → cctp_mint on Injective scripts/spawn-duelists.ts, funds-gated).
- API + ledger — Express + better-sqlite3 (duels · slots · payouts idempotency table); self-seeds from committed fixtures, incl. a labeled settled rehearsal (real QF FRA 2-0 MAR) so the page never demos empty.
- Data — football-data.org client with a committed real snapshot fallback, so zero keys are needed to reproduce.
- Web — Next.js 14 one-route arena page (versus cards, receipt seals, score, payout chip, /verify evidence + trust box); GitHub Pages landing + pitch deck; API on Railway at a custom domain.
- Quality & Security Engineering — the production harness:
- Code quality — TypeScript strict tsc --noEmit + ESLint, zero warnings.
- Unit testing — Vitest: 53 tests green across 8 files (hash · slot-matching · settlement · idempotency · pay-gate · football · quote · proof).
- E2E — Playwright: 3 specs driving the arena page in zero-funds snapshot mode; Lighthouse CI on web/.
- Security — CodeQL (SAST) + Dependabot (root, web/, actions) + TruffleHog + GitHub secret scanning + npm audit.
- CI/CD — 6-stage GitHub Actions pipeline (Quality → Security → Build → E2E → Performance → Deploy gate), concurrency-guarded, with semantic-release auto-versioning (currently v1.1.0) and Railway/Pages deploy jobs.
- Observability — /api/verify self-describes the quote, USDC/CCTP info, settlement-gate status, and reproduce commands; npm run readiness runs an 11-check pre-submission gate; npm run bench reports 402 round-trip p50/p95.
1. The SDK's real shape beat the spec's prose. Our architecture notes sketched a flat {endpoint, network, asset, amount} x402 config — the shipped @injectivelabs/x402 .d.ts is actually a routes-map keyed "POST /api/duel/enter", and the middleware itself fills extra:{name,version,assetTransferMethod:"eip3009"} into the 402. We rewrote api/middleware.ts against the real types and added a test that parses our own live 402 with parsePaymentRequired.
2. Notarizing settlement without a memo field. MCP transfer_send exposes no memo param, so we couldn't tag the payout tx with the decision. Fix: notarize via determinism instead — a shared pure decideSettlement, a stored decision_hash, and replay.ts that recomputes it from the ledger and asserts identity.
3. A time-bomb in CI. A test asserting "this fixture is not FINISHED" would flip red whenever the committed World Cup snapshot refreshed. Fixed (commit f2dfcb4) by making the test snapshot-refresh-proof — that's what took main CI green.
4. Deploy gate without leaking secrets. The Railway deploy job broke forks and secretless runs secrets context is invalid in a job-level if); reworked it to green-skip when RAILWAY_TOKEN is absent while still gating real deploys.
## What we learned
Honesty is a feature you can engineer. The moment we adopted one rule — "never emit a fake 0x hash" — the architecture fell out of it: funds-gated payWinner(), labeled mock-tx-… ids, is_placeholder seeds, a pure settlement function anyone can replay, and a /proof endpoint that lets a stranger falsify every claim with one curl and zero funds. Verifiability is a stronger demo than a screenshot.
- Fund one real duel end-to-end — arena wallet funded → AGENTDUEL_ALLOW_PAYOUT=1 → two real 0.10 USDC x402 entries + one real on-chain payout on a live Final-stage match (the identical, already-tested code path).
- Contract escrow (trust model v2) — replace the transparent-operator window between whistle and payout with an on-chain escrow so the pot never touches an EOA.
- Open the arena — public duelist registration via the agent-duel Skill, multiple concurrent duels per matchday, and LineLock wired live as RED's paid-edge feed.
Everything was built during the hackathon window, solo. Honest log:
- Jul 12 — the full system built and verified locally in one build day: arena core (slot matching, typed errors, void/fee invariant), idempotent settlement worker with the funds-gated payWinner(), real x402 routes-map gate emitting a valid 402 quote with zero funds, RED/CYAN duelists, replay.ts determinism check, Next.js arena page, agent-duel Skill, seed data with a labeled settled rehearsal (real QF FRA 2-0 MAR). 52 tests green at that point (53 now).
- Jul 17 — repo made public at github.com/edycutjong/agentduel (initial commit 63d7509); fixed a snapshot-time-bomb test f2dfcb4) → 6-stage CI green on main.
- Jul 17 — deployed live: API on Railway at api.agentduel.edycu.dev /api/duels 200, real x402 402 on /api/duel/enter), landing + pitch deck on GitHub Pages at agentduel.edycu.dev; CI deploy jobs hardened to green-skip without RAILWAY_TOKEN 8694a1a).
- Jul 17 — semantic-release automation landed b94e9cc, v1.0.0) and cut v1.1.0 after the fight-night landing + pitch-deck redesign 4fb354d, cbbfe32).
- Jul 17 — 2:03 demo video recorded and published: https://youtu.be/Q7hxQKvnxyY — the zero-funds cut, with mock settlements labeled on camera.
What is deliberately NOT claimed: no real on-chain stake or payout has been executed yet — the arena wallet is unfunded on Injective, so those paths are funds-gated and every mock is labeled mock-tx-…, is_placeholder), never faked.
Bootstrapped, solo-built, and not raising. No external funding, no token, no revenue yet. The stated 0.02 USDC arena fee per duel is the intended sustainability model once real-money duels go live.