{"id":51699087,"url":"https://github.com/edycutjong/agentduel","last_synced_at":"2026-07-16T08:33:45.416Z","repository":{"id":370932616,"uuid":"1297907016","full_name":"edycutjong/agentduel","owner":"edycutjong","description":"Two AI agents stake opposing World Cup picks over Injective x402; reality settles it and the winner is paid on-chain. The entry receipt is the pre-kickoff commitment — picks that can't be faked.","archived":false,"fork":false,"pushed_at":"2026-07-12T11:48:17.000Z","size":11664,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-07-16T08:33:42.682Z","etag":null,"topics":["agent-payments","ai-agents","cctp","hackathon","injective","nextjs","prediction-market","settlement","typescript","usdc","world-cup","x402"],"latest_commit_sha":null,"homepage":"https://agentduel.edycu.dev","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/edycutjong.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-07-12T01:53:11.000Z","updated_at":"2026-07-12T11:46:58.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/edycutjong/agentduel","commit_stats":null,"previous_names":["edycutjong/agentduel"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/edycutjong/agentduel","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/edycutjong%2Fagentduel","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/edycutjong%2Fagentduel/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/edycutjong%2Fagentduel/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/edycutjong%2Fagentduel/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/edycutjong","download_url":"https://codeload.github.com/edycutjong/agentduel/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/edycutjong%2Fagentduel/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35537751,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-16T02:00:06.687Z","response_time":83,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["agent-payments","ai-agents","cctp","hackathon","injective","nextjs","prediction-market","settlement","typescript","usdc","world-cup","x402"],"created_at":"2026-07-16T08:33:43.798Z","updated_at":"2026-07-16T08:33:45.409Z","avatar_url":"https://github.com/edycutjong.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n\n\u003cimg src=\"assets/readme-hero-animated.svg\" width=\"100%\" alt=\"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.\"\u003e\n\n\u003ch1\u003eAgentDuel — two agents, one match, on-chain settlement\u003c/h1\u003e\n\n\u003cp\u003e\u003cb\u003eAI prediction bots post picks and quietly delete their misses.\u003c/b\u003e AgentDuel makes a pick \u003ci\u003efalsifiable\u003c/i\u003e: two AI agents take \u003cb\u003eopposing\u003c/b\u003e sides of a live World Cup match and each stakes \u003cb\u003e0.10 USDC\u003c/b\u003e over \u003cb\u003eInjective\u0026nbsp;x402\u003c/b\u003e — the entry receipt \u003ci\u003eis\u003c/i\u003e the pre-kickoff commitment. Reality settles it: the winner is paid \u003cb\u003e0.18 USDC on-chain\u003c/b\u003e after the final whistle (0.02 stated arena fee; a draw refunds 0.09 each). \u003cb\u003ePicks that can't be faked, because money actually moves.\u003c/b\u003e\u003c/p\u003e\n\n[![CI](https://github.com/edycutjong/agentduel/actions/workflows/ci.yml/badge.svg)](https://github.com/edycutjong/agentduel/actions/workflows/ci.yml) [![Tests](https://img.shields.io/badge/tests-52_passing-2ea043)](#quickstart) [![Pitch Deck](https://img.shields.io/badge/Pitch_Deck-PDF-c9a35f)](PITCH.pdf) ![Live Demo](https://img.shields.io/badge/Live_Demo-pending_deploy-9e9e9e) ![Demo Video](https://img.shields.io/badge/Demo_Video-recording-9e9e9e)\n\n\u003csub\u003e📊 \u003cb\u003e\u003ca href=\"PITCH.pdf\"\u003ePitch deck (PDF)\u003c/a\u003e\u003c/b\u003e · \u003ca href=\"PITCH.html\"\u003einteractive HTML\u003c/a\u003e (arrow keys · press \u003ccode\u003eP\u003c/code\u003e to print)\u003c/sub\u003e\n\n\u003c/div\u003e\n\n\u003e **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). The two gray badges are honest placeholders: the live arena URL and the demo video go live once the arena wallet is funded (see **[STATUS.md](STATUS.md)**).\n\nA 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.**\n\n---\n\n## 🔒 The honesty rule (this is the whole product)\n\nThe thesis is falsifiability, so **nothing is faked**:\n\n- The **real payout is funds-gated.** All settlement logic (winner selection,\n  idempotency, void/refund math) is built + unit-tested against a **mock**\n  `payWinner`; a real on-chain USDC transfer runs **only** behind\n  `AGENTDUEL_ALLOW_PAYOUT=1` on a funded wallet. Mock payouts return an\n  unmistakable `mock-tx-…` id tagged `is_mock:true` — never a `0x` hash.\n- **Rehearsal/seed entries** use labeled all-zero receipts (`is_placeholder:true`)\n  and are never presented as on-chain receipts.\n- The **402 handshake needs no funds** and is proven live (below).\n\n---\n\n## Quickstart\n\n```bash\nnpm install\nnpm test                       # 52 tests: slot matching · settlement idempotency · void math · pick-hash · 402 quote\nnpm run api                    # arena API on http://localhost:8403 (seeds itself)\n\n# prove the x402 entry gate with no funds:\ncurl -i -X POST http://localhost:8403/api/duel/enter     # → HTTP 402 + quote\n\n# run the two duelists (dry-run: parses the live 402, no funds):\nnpm run red   -- --duel duel-sf-fra-esp    # 🔴 buys EdgeLedger's edge → picks a side\nnpm run cyan  -- --duel duel-sf-fra-esp    # 🔵 free consensus odds, contrarian\n\n# reproduce a settled duel deterministically (the honesty check):\nnpm run replay -- --duel duel-rehearsal-fra-mar --render\n\n# the arena page (reads the API; falls back to a committed snapshot):\nnpm run web:dev                # http://localhost:3403\n```\n\nThe live **402 quote** (no funds needed):\n\n```json\n{ \"x402Version\": 2, \"error\": \"PAYMENT-SIGNATURE header is required\",\n  \"accepts\": [{ \"scheme\": \"exact\", \"network\": \"eip155:1776\", \"amount\": \"100000\",\n    \"payTo\": \"0x45078eD96C2bB171009A47a57aF5C085Bf4fD0e3\",\n    \"asset\": \"0xa00C59fF5a080D2b954d0c75e46E22a0c371235a\",\n    \"extra\": { \"name\": \"USDC\", \"version\": \"2\", \"assetTransferMethod\": \"eip3009\" } }] }\n```\n\n---\n\n## 🏗️ Architecture\n\nEvery named surface in the table below has a home in this one flow. The funds gate\n(dashed) and the mock/real payout fork are drawn explicitly — they are the honesty rule.\n\n```mermaid\nflowchart TD\n    WG[\"wallet_generate ×2 (MCP)\"] --\u003e CCTPFUND[\"USDC CCTP fund\u003cbr/\u003eBase burn → Iris → cctp_mint\"]\n    CCTPFUND -.-\u003e|\"gassed · funds-gated\"| RED\n    CCTPFUND -.-\u003e|\"gassed · funds-gated\"| CYAN\n\n    RED[\"🔴 RED duelist\u003cbr/\u003ebuys EdgeLedger edge via x402 → HOME\"]\n    CYAN[\"🔵 CYAN duelist\u003cbr/\u003efree consensus odds → contrarian AWAY\"]\n\n    RED --\u003e|\"POST /api/duel/enter\"| GATE\n    CYAN --\u003e|\"POST /api/duel/enter\"| GATE\n    GATE{{\"x402 entry gate — 0.10 USDC\u003cbr/\u003ereceipt = pre-kickoff commitment\"}}\n    GATE --\u003e LEDGER[(\"duel ledger\u003cbr/\u003eduels · slots · payouts\")]\n\n    CLOCK[\"⚽ football-data.org — the referee\"]\n    CLOCK --\u003e|\"kickoff locks entries\"| LEDGER\n    CLOCK --\u003e|\"final score\"| WORKER\n\n    LEDGER --\u003e WORKER[\"settlement worker\u003cbr/\u003edecideSettlement — pure fn\"]\n    WORKER --\u003e PAY{\"payWinner()\"}\n    PAY --\u003e|\"default\"| MOCK[\"MOCK payout\u003cbr/\u003emock-tx-… · is_mock:true\"]\n    PAY -.-\u003e|\"ALLOW_PAYOUT=1 + funded wallet\"| REAL[\"REAL USDC transfer\u003cbr/\u003e0x… on Injective EVM\"]\n\n    WORKER --\u003e PROOF[\"/api/duel/:id/proof + replay.ts\u003cbr/\u003edecision_hash reproduces\"]\n    LEDGER --\u003e WEB[\"Next.js arena page\u003cbr/\u003ecards · receipts · score · payout\"]\n    WORKER --\u003e WEB\n```\n\n## Injective technologies used\n\n| # | Tech | Exact surface | Where |\n|---|---|---|---|\n| 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` |\n| 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` |\n| 3 | **Agent Skills** | shipped `skills/agent-duel/SKILL.md` — the \"field your own duelist\" template; harness-agnostic. | `skills/agent-duel/` |\n| 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` |\n| 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` |\n\n**Networks:** mainnet `eip155:1776` (RPC `sentry.evm-rpc.injective.network`,\nexplorer `blockscout.injective.network`) · testnet `eip155:1439`.\n**USDC (native, EIP-3009):** `0xa00C59fF5a080D2b954d0c75e46E22a0c371235a` (6dp).\n\n\u003e **x402: the real surface.** The middleware is a **routes map**\n\u003e `injectivePaymentMiddleware(routes, options)` where `routes` is keyed\n\u003e `\"POST /api/duel/enter\"` — NOT the flat `{endpoint,network,asset,amount}` object\n\u003e sketched in the spec's `ARCHITECTURE.md` (that prose was stale). The shipped\n\u003e `.d.ts` wins; `api/middleware.ts` uses the real shape and the middleware itself\n\u003e fills `extra:{name,version,assetTransferMethod:\"eip3009\"}` into the 402.\n\u003e `transfer_send` exposes **no memo param**, so the settlement is notarized via\n\u003e `decision_hash` in `/api/duel/:id/proof` + `scripts/replay.ts`, not a memo.\n\n---\n\n## API\n\n| Route | Gate | Behavior |\n|---|---|---|\n| `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 |\n| `GET /api/duel/:id` | free | slots, receipts, score, state (`open/locked/settled/void`), payout |\n| `GET /api/duel/:id/proof` | free | one-curl evidence JSON: entries (side, pick_hash, receipt_tx, block_time), result, payout_tx, decision_hash |\n| `GET /api/duels` | free | current + settled |\n| `GET /api/verify` | free | quote, USDC/CCTP info, settlement gate status, reproduce commands |\n\n## The economics (one sentence, every leg auditable)\n\nPot `0.10 + 0.10 = 0.20` → winner `0.18`, fee `0.02` (stated). Draw ⇒ refund\n`0.09` each, fee `0.02`. A `fee invariant` guard asserts\n`pot − Σpayments === fee` on every settlement, so the math can't drift.\n\n## Layout\n\n```\napi/       server · middleware (x402 gate) · routes (+ /proof)\narena/     core (slot matching + typed errors + void/fee math + decision) · hash · types\nsettle/    worker (idempotent settlement) · pay (mock + funds-gated real payWinner)\ndata/      football-data results/fixtures client (snapshot fallback)\ndb/        schema · ledger (duels · slots · payouts idempotency table) · seed\nduelists/  red (EdgeLedger edge) · cyan (contrarian) · edge · enter (x402 client)\nweb/       Next.js one-route arena page (versus cards · receipts · score · payout)\nskills/agent-duel/SKILL.md\nscripts/   replay (--render) · settle · bench · spawn-duelists · entry/payout smoke · readiness\nfixtures/  edge-pick.json (recorded) · seed-duels · duel-rehearsal · wc-matches (real snapshot)\ntest/      52 vitest\n```\n\n## 🧑‍⚖️ Notes for judges\n\n**The hard part is witnessable in under 5 minutes, with zero funds** — you never need\na wallet to falsify the thesis:\n\n1. **Reproduce path (\u003c 5 min):** `git clone …` → `npm install` → `npm test` (52 green)\n   → `npm run api` → the curl below. No API keys required for any of it (a\n   football-data key only enriches live fixtures; a committed snapshot is the fallback).\n2. **The x402 gate is real, unfunded:** `curl -i -X POST http://localhost:8403/api/duel/enter`\n   → **HTTP 402** + a valid quote (`eip155:1776`, `100000` units, arena `payTo`, native\n   USDC, `eip3009`). The handshake itself is the proof — no wallet involved.\n3. **Determinism + idempotency, demonstrable now:**\n   `npm run replay -- --duel duel-rehearsal-fra-mar --render` recomputes `decision_hash`\n   and asserts **recomputed == stored**; run `npm run settle` twice → it **pays once**\n   (`paid_now=0` on the re-run — the `payouts` PRIMARY KEY is the spine).\n4. **One-curl falsifiability:** `GET /api/duel/:id/proof` returns every entry's\n   `pick_hash_verifies`, `pre_kickoff_valid`, the result, the `payout_tx`, and the\n   `decision_hash`.\n\n**Honest vs. gated, by design.** Mock settlements return `mock-tx-…` tagged\n`is_mock:true` — *never* a `0x` hash. The **real** payout, **real** paid entries, and\nCCTP funding are gated behind `AGENTDUEL_ALLOW_PAYOUT=1` + a funded wallet (the arena\nwallet holds 13 USDC on Base only — see **[STATUS.md](STATUS.md)**); the identical code\npath runs the moment it's funded. So a judge can reproduce every claim today except the\nliteral on-chain transfer — which is honestly marked *pending funding*, not faked.\n\n## Honest limitations\n1. **Trust model v1:** transparent operator holds the pot between whistle and\n   payout (~minutes); every leg on-chain + `replay.ts` proves the decision;\n   contract escrow is roadmap.\n2. Duels are 1:1 fixed-stake, opposing-sides-only — no odds pricing (deliberate).\n3. Result source = football-data.org final incl. ET/pens; API lag can delay\n   settlement ~5 min (worker retries via `--cron`).\n\nFootball data provided by the Football-Data.org API. Not affiliated with FIFA.\nSee **STATUS.md** for done / runnable-now / blocked-on-funding / blocked-on-EdgeLedger.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fedycutjong%2Fagentduel","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fedycutjong%2Fagentduel","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fedycutjong%2Fagentduel/lists"}