Derby
The problem: a friend in Lagos and a friend in London want to bet a friendly $10 on tonight's match. Venmo/PayPal don't cross borders cleanly, nobody wants to be the "holds everyone's money" friend, a
ビデオ
テックスタック
説明
# Derby — trustless friendly wagers for World Cup matches
Built for the [Injective Global Cup](https://www.hackquest.io/hackathons/The-Injective-Global-Cup).
**The problem:** a friend in Lagos and a friend in London want to bet a
friendly $10 on tonight's match. Venmo/PayPal don't cross borders cleanly,
nobody wants to be the "holds everyone's money" friend, and someone has to
manually check the score and pay out.
**What Derby does:** anyone opens a pool for a fixture, friends join by
staking the same buy-in on home/draw/away, and once the match is final the
whole pot splits evenly across whoever picked the winning side — no
organizer ever touches the money, and no one has to manually check the score.
## Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ Agent Skill (skills/derby/SKILL.md) │
│ lets Claude (or any MCP client) drive the whole flow │
│ conversationally — "set up a pool for tonight's match" │
├─────────────────────────────────────────────────────────────┤
│ MCP Server (mcp-server/) │
│ 6 tools: create_pool, join_pool, check_score, settle_pool, │
│ list_pools, get_pool │
│ + an x402-gated /score HTTP endpoint (httpServer.ts) that │
│ any client — not just this MCP server — can call directly │
├─────────────────────────────────────────────────────────────┤
│ derby-escrow contract (contracts/derby-escrow/, CosmWasm/Rust)│
│ pari-mutuel pool escrow: create/join/submit-result/settle/ │
│ cancel/refund. No house rake. No organizer-withdraw path. │
└─────────────────────────────────────────────────────────────┘
│
▼
Injective (testnet in this build)
```
## Injective tech used, and why
- **MCP Server** — the actual interface. Every action (create a pool, join,
check a score, settle) is a typed tool an agent can call directly, not a
wrapper bolted onto a web UI afterward.
- **x402** — `GET /score/:fixtureId` is genuinely payment-gated. It returns
HTTP 402 with an x402-shaped payment requirement until the caller has paid
`PAY_TO_ADDRESS` the quoted USDC amount and retries with the resulting tx
hash as proof. See "x402 on Injective" below for why this is a from-scratch
implementation rather than an off-the-shelf middleware package.
- **Agent Skills** — `skills/derby/SKILL.md` teaches an agent the actual
domain flow (find the fixture, create, join, settle) and the custody model,
not just the raw tool signatures.
**CCTP was deliberately left out of this build.** Cross-chain funding (a
friend on Base, a friend on Arbitrum, both joining an Injective-native pool)
is the natural next integration, but wiring up Circle's CCTP flow plus the
UI for it is a multi-day task on its own, and this was built in two days.
Every pool in this build is funded directly in the Injective-native buy-in
denom. See "Roadmap" below.
## x402 on Injective
The official x402 middleware packages (`@x402/express`, `@x402/evm`,
`@x402/svm`, `@x402/avm`) cover Base, Solana, Stacks, and Algorand as of this
build — not Injective/Cosmos. Rather than force an unsupported network
string into a scheme built for a different curve, `mcp-server/src/x402.ts`
implements the protocol directly against Injective's LCD:
1. Client requests `/score/:fixtureId` with no `X-PAYMENT` header.
2. Server replies `402` with an x402-shaped `accepts` array.
3. Client pays the quoted amount to `payTo` with a normal bank-module
`MsgSend` (Injective finalizes in ~1 block, so this checks a *settled* tx
rather than the sign-but-don't-broadcast pattern some x402 schemes use for
slower chains).
4. Client retries with `X-PAYMENT: <tx hash>`. The server fetches that tx
from the LCD (`/cosmos/tx/v1beta1/txs/{hash}`), confirms a `transfer`
event paying `payTo` at least the required amount in the required denom,
and only then serves the resource.
`src/x402Client.ts` is the matching client half, used by the `check_score`
tool so the round trip is invisible to whoever's driving the agent — but any
other client (a curl script, a competing app) can hit `/score/:fixtureId`
directly and would have to pay the same way. The check is a real chain
query, not a signature the server takes on faith.
## Custody model — read before running this
This is a two-day hackathon build, so it makes one deliberate trust
trade-off: **the MCP server derives four demo accounts (oracle, alice, bob,
carol) from one relayer mnemonic** via HD paths `m/44'/60'/0'/0/{0,1,2,3}`,
so a judge can run the full create → join → submit-result → settle flow
through MCP tool calls alone, without standing up a separate wallet UI first.
The *contract* itself is genuinely non-custodial regardless of who signs:
`JoinPool` has no special-casing for these demo accounts, there is no
organizer-withdraw path, and `ClaimRefund` lets any participant recover their
own stake if the oracle goes dark after the refund deadline. Swapping the
alice/bob/carol demo signers for real user wallets (Keplr/Leap/MetaMask via
Injective's wallet strategies) is a frontend change, not a contract change —
see Roadmap.
## Repo layout
```
contracts/derby-escrow/ Rust/CosmWasm escrow contract
src/state.rs Config, Pool, Side, PoolStatus
src/msg.rs Instantiate/Execute/Query messages
src/contract.rs entry points + all execute/query logic
tests/integration.rs 6 tests: full settle flow, no-winners refund,
refund-deadline claim, cancel+refund, and two
tests that lock in the exact JSON wire format
the TS server relies on
mcp-server/ TypeScript MCP server + x402 score API
src/chain.ts Injective signing/querying (eth_secp256k1 —
see comment at top of file for why plain
CosmJS wallets don't work here)
src/x402.ts / x402Client.ts the x402 implementation described above
src/scoreApi.ts TheSportsDB live-score lookup
src/tools/ one file per MCP tool
src/index.ts MCP entrypoint (stdio transport)
src/httpServer.ts the paywalled /score endpoint
skills/derby/SKILL.md Agent Skill for conversational use
```
## Running it
### 1. Contract
```bash
cd contracts/derby-escrow
cargo test # 6 tests, all passing as of this build
rustup target add wasm32-unknown-unknown # if not already installed
cargo wasm # or: docker run --rm -v "$(pwd)":/code \
# cosmwasm/optimizer:0.16.0
injectived tx wasm store artifacts/derby_escrow.wasm --from <key> \
--chain-id injective-888 --node <testnet-rpc> --gas auto --gas-adjustment 1.3
injectived tx wasm instantiate <code_id> \
'{"admin":"<oracle-injective-address>","denom":"<testnet-usdc-denom>"}' \
--from <key> --label derby-escrow --no-admin --chain-id injective-888
```
Fill the resulting contract address into `mcp-server/.env`.
### 2. MCP server
```bash
cd mcp-server
cp .env.example .env # fill in CONTRACT_ADDRESS, DENOM, RELAYER_MNEMONIC, PAY_TO_ADDRESS
npm install
npm run typecheck # confirms clean as of this build
```
Fund all four derived demo accounts (see Custody model) from the
[Injective testnet faucet](https://testnet.faucet.injective.network/) before
running any flow — `create_pool`/`join_pool`/`settle_pool` all pay gas.
Run the x402 score API and the MCP server in separate terminals:
```bash
npm run http # the paywalled /score endpoint, needed by check_score
npm run mcp # the MCP server itself (stdio) — point your MCP client at this
```
### 3. Agent Skill
Point Claude (or any MCP-aware agent) at the running MCP server and load
`skills/derby/SKILL.md`. Then just talk to it: "set up a $10 pool for
tonight's Nigeria match, I'll take home, put bob on away."
## What's real, what's not
Real: the contract logic (tested), the MCP tool wiring (typechecked end to
end against the contract's actual serialized wire format — see the two
JSON-shape tests in `tests/integration.rs`), the x402 protocol implementation
against Injective's LCD, the eth_secp256k1 signing via `@injectivelabs/sdk-ts`.
Not yet run against a live chain: this build was verified with `cargo test`
(contract) and `tsc --noEmit` (server) inside a sandboxed environment without
network access to a running Injective node or a wasm32 Rust target, so the
`cargo wasm` / `injectived tx wasm store` / faucet-funded live flow above is
written but untested end-to-end. Treat the deploy steps as the next thing to
run, not something already confirmed on testnet.
## Security note: a supply-chain incident in this exact dependency
`@injectivelabs/sdk-ts` (and ~17 sibling `@injectivelabs/*` packages) had a
version briefly compromised — a malicious hook on `PrivateKey.fromMnemonic`
that exfiltrated mnemonics — published as `1.20.21` and fixed in `1.20.23`.
`package.json` pins `^1.20.23` specifically so npm's resolver can never land
on the compromised version. If you're auditing this repo, check that pin
before running anything that touches `RELAYER_MNEMONIC`.
## Roadmap
- **CCTP**: let a friend on Base or Arbitrum join an Injective-native pool
by depositing USDC on their own chain; Circle's CCTP handles the burn/mint
into the pool's denom.
- **Real user wallets**: drop the alice/bob/carol demo signers, have
`join_pool` return an unsigned `MsgExecuteContractCompat` for the user's own
wallet (Keplr/Leap/MetaMask) to sign, so the MCP server never touches
participant funds — only the oracle key remains server-side.
- **Independent oracle**: replace the single relayer-as-oracle with a real
sports-data oracle module or an M-of-N multisig of independent reporters,
so no single key can misreport a result.
ハッカソンの進行状況
Built and working:
Contract logic — full CosmWasm escrow (create/join/submit-result/settle/cancel/refund) with 6 passing tests, including two that lock in the exact JSON wire format the TS server depends on
MCP server — all 6 tools (create_pool, join_pool, check_score, settle_pool, list_pools, get_pool), typechecks clean end-to-end against the contract's actual serialized format
x402 payment-gating — a real from-scratch implementation against Injective's LCD (not an off-the-shelf package, since none of the official x402 middlewares support Injective/Cosmos yet) — genuinely gates the
/scoreendpoint on a settled on-chain payment, not just a signatureAgent Skill — the conversational flow ("set up a $10 pool for tonight's match...") is written and wired to the MCP tools
Custody model — non-custodial by design: no organizer-withdraw path, refund claim available if the oracle goes dark
資金調達の状況
N/A