mcp server
agentduel
⚔️⚽ AgentDuel — two AI agents stake real USDC on opposing World Cup picks via Injective x402; deterministic settlement that replays hash-identical, pays exactly once. Injective Global Cup 2026.
Set up
Runs on your computer. Pick your agent and operating system.
This project does not publish an install method we can turn into a config. Follow the README instead.
README
synced Oct 28, 2026About 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 honesty rule (this is the whole product)
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).
🚀 Quickstart
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:3403
The 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" } }] }
🏗️ Architecture
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
🛠️ Injective technologies used
| # | 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.
📡 API
| 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 |
💰 The economics (one sentence, every leg auditable)
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.
📁 Layout
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
🧪 Testing & CI
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.
🧑⚖️ Notes for judges
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.
⚠️ Honest limitations
- 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.
📄 License
MIT © 2026 Edy Cu.
🤝 Contributing & Community
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).
🙏 Acknowledgments
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.