Skip to content

Latest commit

 

History

30 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

AgentDuel

AgentDuel

Two agents, one match, on-chain settlement — picks that can't be faked, because money actually moves.

AgentDuel arena poster: Agent RED (paid edge — France) and Agent CYAN (contrarian — Argentina) commit opposing 0.10 USDC x402 stakes on the World Cup Final; a row of on-chain hashes (CCTP, two stakes, payout) settles on Injective EVM mainnet.

Live Site Pitch Deck Demo Video Demo Script Live API HackQuest · Injective Global Cup 2026


TypeScript Node.js Express Next.js Vitest Injective EVM x402 CI

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 labeled mock-tx-… (never a 0x hash). As of 2026-07-18 the real Final duel is live with real receiptscurl https://api.agentduel.edycu.dev/api/duel/duel-final-2026/proof and 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 x402 stake gate — HTTP 402 quoting a 0.10 USDC entry on Injective EVM
The x402 stake gate, live — HTTP 402 quotes a 0.10 USDC entry on eip155:1776
Idempotent settlement — double-run pays once, mock settlements labeled mock-tx
Deterministic settlement — idempotent (double-run pays once), mocks labeled mock-tx-…
Replay verification — identical decision hash reproduced from scratch
Replay ✓ IDENTICAL — anyone can reproduce the decision hash from scratch
The AgentDuel arena — RED vs CYAN duelists
The arena — Agent RED vs Agent CYAN, opposing stakes on one match

🔒 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 (match 537390, kickoff 2026-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):

    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
  • 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 behind AGENTDUEL_ALLOW_PAYOUT=1 on a funded wallet. Mock payouts return an unmistakable mock-tx-… id tagged is_mock:true — never a 0x hash. For duel-final-2026 the 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
Loading

🛠️ 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_statuscctp_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) where routes is keyed "POST /api/duel/enter" — NOT the flat {endpoint,network,asset,amount} object sketched in the spec's ARCHITECTURE.md (that prose was stale). The shipped .d.ts wins; api/middleware.ts uses the real shape and the middleware itself fills extra:{name,version,assetTransferMethod:"eip3009"} into the 402. transfer_send exposes no memo param, so the settlement is notarized via decision_hash in /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:

  1. Reproduce path (< 5 min): git clone …npm installnpm 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).
  2. The x402 gate is real, unfunded: curl -i -X POST http://localhost:8403/api/duel/enterHTTP 402 + a valid quote (eip155:1776, 100000 units, arena payTo, native USDC, eip3009). The handshake itself is the proof — no wallet involved.
  3. Determinism + idempotency, demonstrable now: npm run replay -- --duel duel-rehearsal-fra-mar --render recomputes decision_hash and asserts recomputed == stored; run npm run settle twice → it pays once (paid_now=0 on the re-run — the payouts PRIMARY KEY is the spine).
  4. One-curl falsifiability: GET /api/duel/:id/proof returns every entry's pick_hash_verifies, pre_kickoff_valid, the result, the payout_tx, and the decision_hash.

Honest vs. gated, by design. Mock settlements return mock-tx-… tagged is_mock:truenever 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

  1. Trust model v1: transparent operator holds the pot between whistle and payout (~minutes); every leg on-chain + replay.ts proves the decision; contract escrow is roadmap.
  2. Duels are 1:1 fixed-stake, opposing-sides-only — no odds pricing (deliberate).
  3. 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.

About

⚔️⚽ 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.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages