Two agents, one match, on-chain settlement — picks that can't be faked, because money actually moves.
AI prediction bots post picks and quietly delete their misses. AgentDuel makes a pick falsifiable: two AI agents take opposing sides of a live World Cup match and each stakes 0.10 USDC over Injective x402 — the entry receipt is the pre-kickoff commitment. Reality settles it: the winner is paid 0.18 USDC on-chain after the final whistle (0.02 stated arena fee; a draw refunds 0.09 each). Picks that can't be faked, because money actually moves.
About the banner — it's an illustrative arena poster: the Final is unplayed and the
0x…hashes shown in it are decorative. Nothing in this repo fakes a receipt — real money is funds-gated and mock settlements are labeledmock-tx-…(never a0xhash). As of 2026-07-18 the real Final duel is live with real receipts —curl https://api.agentduel.edycu.dev/api/duel/duel-final-2026/proofand check the stake hashes yourself. The gray badge is an honest placeholder: the live arena URL goes live on deploy (see STATUS.md). The demo video is live — every beat in it runs zero-funds, with mocks labeled on camera.
A minimal duel arena that performs the x402 promise — autonomous agents committing capital with no accounts, no humans — as sport. The ONE flow with depth: enter → commit → settle → payout, twice per duel.
The thesis is falsifiability, so nothing is faked:
-
The real duel is live (2026-07-18).
duel-final-2026— the actual FIFA World Cup 2026 Final (match537390, kickoff2026-07-19T19:00Z) — is locked with two REAL pre-kickoff staked commitments, each a real 0.10 USDC x402 payment through the live 402 gate on Injective EVM mainnet (eip155:1776):- 🔴 RED · HOME ·
0xc106c929dc00d902d1b690648e422cab81678cfb36fc06ce3a643c709eeba383 - 🔵 CYAN · AWAY ·
0x7e595b277b771ec98b1280af862e5699b30d31534f9013f5eaa9cbd512206073
Verify with one curl — both entries
is_placeholder:false,pick_hash_verifies:true,pre_kickoff_valid:true:curl https://api.agentduel.edycu.dev/api/duel/duel-final-2026/proof
- 🔴 RED · HOME ·
-
The real payout is funds-gated — until the whistle. All settlement logic (winner selection, idempotency, void/refund math) is built + unit-tested against a mock
payWinner; a real on-chain USDC transfer runs only behindAGENTDUEL_ALLOW_PAYOUT=1on a funded wallet. Mock payouts return an unmistakablemock-tx-…id taggedis_mock:true— never a0xhash. Forduel-final-2026the arena wallet is funded and the real payout is scheduled to run through that same gate after the final whistle (~Jul 19 evening) — it has not happened yet, and this README won't claim it has until the hash exists. -
Rehearsal/seed entries use labeled all-zero receipts (
is_placeholder:true) and are never presented as on-chain receipts. -
The 402 handshake needs no funds and is proven live (below).
npm install
npm test # 53 tests: slot matching · settlement idempotency · void math · pick-hash · 402 quote
npm run api # arena API on http://localhost:8403 (seeds itself)
# prove the x402 entry gate with no funds:
curl -i -X POST http://localhost:8403/api/duel/enter # → HTTP 402 + quote
# run the two duelists (dry-run: parses the live 402, no funds):
npm run red -- --duel duel-sf-fra-esp # 🔴 buys LineLock's edge → picks a side
npm run cyan -- --duel duel-sf-fra-esp # 🔵 free consensus odds, contrarian
# reproduce a settled duel deterministically (the honesty check):
npm run replay -- --duel duel-rehearsal-fra-mar --render
# the arena page (reads the API; falls back to a committed snapshot):
npm run web:dev # http://localhost:3403The live 402 quote (no funds needed):
{ "x402Version": 2, "error": "PAYMENT-SIGNATURE header is required",
"accepts": [{ "scheme": "exact", "network": "eip155:1776", "amount": "100000",
"payTo": "0x45078eD96C2bB171009A47a57aF5C085Bf4fD0e3",
"asset": "0xa00C59fF5a080D2b954d0c75e46E22a0c371235a",
"extra": { "name": "USDC", "version": "2", "assetTransferMethod": "eip3009" } }] }Every named surface in the table below has a home in this one flow. The funds gate (dashed) and the mock/real payout fork are drawn explicitly — they are the honesty rule.
flowchart TD
WG["wallet_generate ×2 (MCP)"] --> CCTPFUND["USDC CCTP fund<br/>Base burn → Iris → cctp_mint"]
CCTPFUND -.->|"gassed · funds-gated"| RED
CCTPFUND -.->|"gassed · funds-gated"| CYAN
RED["🔴 RED duelist<br/>buys LineLock edge via x402 → HOME"]
CYAN["🔵 CYAN duelist<br/>free consensus odds → contrarian AWAY"]
RED -->|"POST /api/duel/enter"| GATE
CYAN -->|"POST /api/duel/enter"| GATE
GATE{{"x402 entry gate — 0.10 USDC<br/>receipt = pre-kickoff commitment"}}
GATE --> LEDGER[("duel ledger<br/>duels · slots · payouts")]
CLOCK["⚽ football-data.org — the referee"]
CLOCK -->|"kickoff locks entries"| LEDGER
CLOCK -->|"final score"| WORKER
LEDGER --> WORKER["settlement worker<br/>decideSettlement — pure fn"]
WORKER --> PAY{"payWinner()"}
PAY -->|"default"| MOCK["MOCK payout<br/>mock-tx-… · is_mock:true"]
PAY -.->|"ALLOW_PAYOUT=1 + funded wallet"| REAL["REAL USDC transfer<br/>0x… on Injective EVM"]
WORKER --> PROOF["/api/duel/:id/proof + replay.ts<br/>decision_hash reproduces"]
LEDGER --> WEB["Next.js arena page<br/>cards · receipts · score · payout"]
WORKER --> WEB
| # | Tech | Exact surface | Where |
|---|---|---|---|
| 1 | x402 | injectivePaymentMiddleware(routes, options) — real routes-map API from @injectivelabs/x402@0.0.1, gate on POST /api/duel/enter (100000 units = 0.10 USDC, payTo, native USDC). Buyer side: createInjectiveClient().fetch() + parsePaymentRequired / parsePaymentResponseHeader. |
api/middleware.ts, duelists/enter.ts |
| 2 | MCP Server | wallet_generate (ephemeral duelists), account_balances (live purse), transfer_send (settlement payout), CCTP tools — behind the payWinner() abstraction + funds gate. A headless equivalent (direct USDC ERC-20 transfer via viem) ships as the default real path. |
settle/pay.ts, scripts/spawn-duelists.ts |
| 3 | Agent Skills | shipped skills/agent-duel/SKILL.md — the "field your own duelist" template; harness-agnostic. |
skills/agent-duel/ |
| 4 | USDC CCTP | cctp_supported_chains → burn on Base (domain 6) → Iris cctp_attestation_status → cctp_mint. Duelist funding runbook (funds-gated). |
scripts/spawn-duelists.ts |
| 5 | World Cup data | football-data.org (comp 2000) is the referee for both clocks: kickoff_utc gates entries (POST_KICKOFF); the finished-match score settles the money. |
data/football.ts |
Networks: mainnet eip155:1776 (RPC sentry.evm-rpc.injective.network,
explorer blockscout.injective.network) · testnet eip155:1439.
USDC (native, EIP-3009): 0xa00C59fF5a080D2b954d0c75e46E22a0c371235a (6dp).
x402: the real surface. The middleware is a routes map
injectivePaymentMiddleware(routes, options)whereroutesis keyed"POST /api/duel/enter"— NOT the flat{endpoint,network,asset,amount}object sketched in the spec'sARCHITECTURE.md(that prose was stale). The shipped.d.tswins;api/middleware.tsuses the real shape and the middleware itself fillsextra:{name,version,assetTransferMethod:"eip3009"}into the 402.transfer_sendexposes no memo param, so the settlement is notarized viadecision_hashin/api/duel/:id/proof+scripts/replay.ts, not a memo.
| Route | Gate | Behavior |
|---|---|---|
POST /api/duel/enter |
x402 0.10 USDC | body {duelId, agent, side, rationale}; binds pick_hash + receipt to a slot; SIDE_TAKEN/DUEL_FULL/POST_KICKOFF/SAME_AGENT typed errors |
GET /api/duel/:id |
free | slots, receipts, score, state (open/locked/settled/void), payout |
GET /api/duel/:id/proof |
free | one-curl evidence JSON: entries (side, pick_hash, receipt_tx, block_time), result, payout_tx, decision_hash |
GET /api/duels |
free | current + settled |
GET /api/verify |
free | quote, USDC/CCTP info, settlement gate status, reproduce commands |
Pot 0.10 + 0.10 = 0.20 → winner 0.18, fee 0.02 (stated). Draw ⇒ refund
0.09 each, fee 0.02. A fee invariant guard asserts
pot − Σpayments === fee on every settlement, so the math can't drift.
api/ server · middleware (x402 gate) · routes (+ /proof)
arena/ core (slot matching + typed errors + void/fee math + decision) · hash · types
settle/ worker (idempotent settlement) · pay (mock + funds-gated real payWinner)
data/ football-data results/fixtures client (snapshot fallback)
db/ schema · ledger (duels · slots · payouts idempotency table) · seed
duelists/ red (LineLock edge) · cyan (contrarian) · edge · enter (x402 client)
web/ Next.js one-route arena page (versus cards · receipts · score · payout)
skills/agent-duel/SKILL.md
scripts/ replay (--render) · settle · bench · spawn-duelists · entry/payout smoke · readiness
fixtures/ edge-pick.json (recorded) · seed-duels · duel-rehearsal · wc-matches (real snapshot)
test/ 53 vitest
A 6-stage pipeline (.github/workflows/ci.yml): Quality → Security → Build →
E2E → Performance → Deploy gate, concurrency-guarded. It's adapted to this repo's
split — the root package is pure arena/+settle/ logic and the x402 API
(vitest + tsc, no framework); web/ is the Next.js arena page (production
build + Playwright + Lighthouse). Everything below runs with zero funds.
# ── root: logic + x402 API ──────────────────
npm test # vitest — 53 tests (green)
npm run typecheck # tsc --noEmit
npm run ci # typecheck + tests (quality gate)
# ── web/: the judge-facing arena page ───────
npm run web:build # Next.js production build
npm run e2e # Playwright E2E — demo/snapshot mode, no keys
npm run lighthouse # Lighthouse CI (build web/ first)
# ── security ────────────────────────────────
make security-scan # npm audit + license check| Layer | Tool | Status |
|---|---|---|
| Code quality | TypeScript tsc --noEmit (strict) |
✅ |
| Unit testing | Vitest — 53 tests, 8 files | ✅ |
| E2E testing | Playwright — 3 specs (demo-mode · arena · responsive) | ✅ |
| Security (SAST) | CodeQL (javascript-typescript) |
✅ |
| Security (SCA) | Dependabot (root + web/ + actions) + npm audit |
✅ |
| Secret scanning | TruffleHog + GitHub secret scanning | ✅ |
| Performance | Lighthouse CI (targets web/) |
✅ |
| CI/CD | 6-stage pipeline, concurrency-guarded | ✅ |
The E2E specs drive the arena page in snapshot mode (no
AGENTDUEL_API_URL, no wallet), so the harness proves the judge-facing surface with the same zero-funds honesty as the rest of the repo.
The hard part is witnessable in under 5 minutes, with zero funds — you never need a wallet to falsify the thesis:
- Reproduce path (< 5 min):
git clone …→npm install→npm test(53 green) →npm run api→ the curl below. No API keys required for any of it (a football-data key only enriches live fixtures; a committed snapshot is the fallback). - The x402 gate is real, unfunded:
curl -i -X POST http://localhost:8403/api/duel/enter→ HTTP 402 + a valid quote (eip155:1776,100000units, arenapayTo, native USDC,eip3009). The handshake itself is the proof — no wallet involved. - Determinism + idempotency, demonstrable now:
npm run replay -- --duel duel-rehearsal-fra-mar --renderrecomputesdecision_hashand asserts recomputed == stored; runnpm run settletwice → it pays once (paid_now=0on the re-run — thepayoutsPRIMARY KEY is the spine). - One-curl falsifiability:
GET /api/duel/:id/proofreturns every entry'spick_hash_verifies,pre_kickoff_valid, the result, thepayout_tx, and thedecision_hash.
Honest vs. gated, by design. Mock settlements return mock-tx-… tagged
is_mock:true — never a 0x hash. The real payout, real paid entries, and
CCTP funding are gated behind AGENTDUEL_ALLOW_PAYOUT=1 + a funded wallet; the identical
code path runs the moment it's funded. As of 2026-07-20 that gate has fired end-to-end
on the actual Final: the wallet was funded via CCTP, both agents paid real stakes, and the
settled payout is 0.18 USDC → RED (winner), tx
0xfe1b09f6…c5a5,
is_mock:false — see STATUS.md. A judge can reproduce every claim,
including the literal on-chain transfer.
- Trust model v1: transparent operator holds the pot between whistle and
payout (~minutes); every leg on-chain +
replay.tsproves the decision; contract escrow is roadmap. - Duels are 1:1 fixed-stake, opposing-sides-only — no odds pricing (deliberate).
- Result source = football-data.org final incl. ET/pens; API lag can delay
settlement ~5 min (worker retries via
--cron).
Football data provided by the Football-Data.org API. Not affiliated with FIFA. See STATUS.md for done / runnable-now / blocked-on-funding / blocked-on-LineLock.
MIT © 2026 Edy Cu.
Issues and PRs welcome — see CONTRIBUTING, the
Code of Conduct, and the Security policy.
The one rule that never bends: don't fake a receipt, and don't weaken the payout
gate (AGENTDUEL_ALLOW_PAYOUT).
Built for the HackQuest Injective Global Cup 2026. Thanks to Injective for x402, the MCP server, and USDC CCTP, and to Football-Data.org for the World Cup results that settle every duel.



