https://github.com/edycutjong/agentduel
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.
https://github.com/edycutjong/agentduel
agent-payments ai-agents cctp hackathon injective nextjs prediction-market settlement typescript usdc world-cup x402
Last synced: 4 days ago
JSON representation
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.
- Host: GitHub
- URL: https://github.com/edycutjong/agentduel
- Owner: edycutjong
- Created: 2026-07-12T01:53:11.000Z (8 days ago)
- Default Branch: main
- Last Pushed: 2026-07-12T11:48:17.000Z (8 days ago)
- Last Synced: 2026-07-16T08:33:42.682Z (4 days ago)
- Topics: agent-payments, ai-agents, cctp, hackathon, injective, nextjs, prediction-market, settlement, typescript, usdc, world-cup, x402
- Language: TypeScript
- Homepage: https://agentduel.edycu.dev
- Size: 11.1 MB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README

AgentDuel — two agents, one match, on-chain settlement
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.
[](https://github.com/edycutjong/agentduel/actions/workflows/ci.yml) [](#quickstart) [](PITCH.pdf)  
📊 Pitch deck (PDF) · interactive HTML (arrow keys · press P to print)
> **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)**).
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 payout is funds-gated.** 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.
- **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
```bash
npm install
npm test # 52 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 EdgeLedger'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):
```json
{ "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.
```mermaid
flowchart TD
WG["wallet_generate ×2 (MCP)"] --> CCTPFUND["USDC CCTP fund
Base burn → Iris → cctp_mint"]
CCTPFUND -.->|"gassed · funds-gated"| RED
CCTPFUND -.->|"gassed · funds-gated"| CYAN
RED["🔴 RED duelist
buys EdgeLedger edge via x402 → HOME"]
CYAN["🔵 CYAN duelist
free consensus odds → contrarian AWAY"]
RED -->|"POST /api/duel/enter"| GATE
CYAN -->|"POST /api/duel/enter"| GATE
GATE{{"x402 entry gate — 0.10 USDC
receipt = pre-kickoff commitment"}}
GATE --> LEDGER[("duel ledger
duels · slots · payouts")]
CLOCK["⚽ football-data.org — the referee"]
CLOCK -->|"kickoff locks entries"| LEDGER
CLOCK -->|"final score"| WORKER
LEDGER --> WORKER["settlement worker
decideSettlement — pure fn"]
WORKER --> PAY{"payWinner()"}
PAY -->|"default"| MOCK["MOCK payout
mock-tx-… · is_mock:true"]
PAY -.->|"ALLOW_PAYOUT=1 + funded wallet"| REAL["REAL USDC transfer
0x… on Injective EVM"]
WORKER --> PROOF["/api/duel/:id/proof + replay.ts
decision_hash reproduces"]
LEDGER --> WEB["Next.js arena page
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)` 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 (EdgeLedger 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/ 52 vitest
```
## 🧑⚖️ 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 install` → `npm test` (52 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/enter`
→ **HTTP 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: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 arena
wallet holds 13 USDC on Base only — see **[STATUS.md](STATUS.md)**); the identical code
path runs the moment it's funded. So a judge can reproduce every claim today except the
literal on-chain transfer — which is honestly marked *pending funding*, not faked.
## 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-EdgeLedger.