A last-resort tool for disputing wrong UMA Optimistic Oracle proposals (mostly Polymarket markets on Polygon) when the usual tools and APIs are down. It talks to the chain through your own RPC. There are two ways to use it:
- In a terminal: review open proposals one by one and dispute the wrong ones.
- With agents: Claude Code or Codex review proposals in parallel from a shared queue (through a local MCP server), and a human sends the disputes with one command.
Disputes go out in batches: through the DisputeBatcher on the permissionless OOv2, and through the Managed OOv2's
own multicall. If batching fails, it falls back to one transaction per dispute.
yarn install --frozen-lockfile && yarn buildThen configure the RPC and the key (the chain defaults to Polygon). Either copy .env.example to .env and fill it
in, or export them in the shell:
export NODE_URLS_137='["https://<rpc-1>","https://<rpc-2>"]'read -rs PRIVATE_KEY && export PRIVATE_KEYThe second command asks for the key without echoing it and keeps it out of your shell history. Use a dedicated wallet (see Wallet, funds and keys).
Then pick a path:
| Situation | Use |
|---|---|
| A handful of proposals to check, by hand | A. Terminal: yarn dispute -p |
| Hundreds or thousands (e.g. election night) | B. Agents: MCP review + yarn apply --watch |
| First time, or before an incident | C. Rehearse on a fork: yarn fork:up, nothing real is touched |
Always try --dry-run first: it reads, re-checks and simulates everything, but approves and sends nothing.
- Node.js 22.13 or later (the agent queue uses the built-in
node:sqlite).yarn disputealone also runs on Node 18. - yarn 1.22.
- A Polygon RPC that serves
eth_getLogs(ranges it rejects are split automatically). Configure two providers inNODE_URLS_137so one can fail. - For fork rehearsals: Foundry (
anvil) and an RPC that serves historical state.
- A dedicated hot wallet, holding only what it may post.
- Bonds: each dispute posts the proposer's bond plus UMA's final fee, in the bond token (USDC.e for Polymarket).
applyshows it per proposal. Midterm markets bond $750 (House) to $2,750 (statewide) each, and the 515 midterm events total about $5.5M in bonds. Plan for the share you expect to dispute. - Gas: POL. A batch of 20 disputes uses up to ~7M gas.
- Approvals: the first send asks to approve the bond token for the contract that pulls the bond (the
DisputeBatcheror the Managed OOv2). Approving ahead of time avoids that prompt mid-incident. - Key handling:
- Only
yarn disputeandyarn applyreadPRIVATE_KEY, from the environment or a.env(gitignored). - The MCP server that agents use never takes it, even if a
.envdefines it. - Never paste the key into an agent chat.
- Only
- DisputeBatcher: deployed on Polygon at
0xa7f9aa0DFaeDdd51fa75180F3467ea1bB0ffE8fC(UMAprotocol/mcp-otb#2826) and used by default for the permissionless OOv2. The CLI checks at start-up that it is bound to the OOv2. Approve the bond token for it ahead of time.DISPUTE_BATCHER_ADDRESS=nonesends one dispute per transaction instead. Managed OOv2 disputes are batched through its ownmulticalleither way.
yarn dispute -p --dry-runThe command lists open Polymarket proposals on both oracles, most urgent first. For each one you answer:
| Key | Action |
|---|---|
y |
queue the dispute |
s |
queue it and send the queue now |
n |
skip |
q |
stop reviewing and send what is queued |
The queue is also sent when it's full, or when a queued proposal is about to expire. Drop --dry-run to send for
real. Managed OOv2 proposals past their liveness are still shown: they can be disputed until the resolver settles
them.
A known list of question ids. When you already know which markets to dispute (for example a list from another team), skip both the prompts and the agents:
yarn dispute --question-ids ids.txt --dry-run- Input:
ids.txtholds question ids: one per line, a JSON array, or a selection file withinclude.question_ids. Either id works:- the UMA question id: keccak256 of the ancillary data, Gamma's
negRiskRequestIDfor neg-risk markets; - Polymarket's
questionID, which differs for neg-risk markets (every midterm race) and is looked up on chain through the NegRiskOperator.
- the UMA question id: keccak256 of the ancillary data, Gamma's
- What it does:
- matches the ids against the open proposals in the lookback window;
- writes the ones it can't find (settled, already disputed, not proposed yet, or older than
BLOCK_LOOKBACK_PERIOD) toids.txt.missing.txt; - shows the count and the total bond;
- asks once for the whole list.
- Sending: each proposal is still re-checked on chain, and disputes go out in batches of 20.
- Unattended:
--yes --max-disputes N --max-bond Xsends without asking, within those caps. - Re-running: proposals already disputed no longer match, so running it again is safe.
Agents review; you send. Nothing is hosted: the agent starts the MCP server (dist/mcp-entry.js) as a local
process, and it keeps a shared queue in .oo-dispute/review-137.sqlite in this repo.
-
Set up the MCP server for Claude Code and Codex, from this folder:
yarn agents:setup --rpc "https://<rpc-1>,https://<rpc-2>" --selection selections/midterms-2026.resolved.jsonIt builds if needed and registers the server with absolute paths to this repo and to the
noderunning it, so the desktop apps find it even with nvm or mise:- Claude Code: a local-scope
oo-disputeserver for this folder, through theclaudeCLI or the one bundled with the macOS desktop app; - Codex:
[mcp_servers.oo-dispute]in~/.codex/config.toml, after a backup, and thereview-disputesskill in~/.codex/skills/. The server's tools are pre-approved (default_tools_approval_mode = "approve"): they only touch the local review queue and never sign, and Codex subagents can't ask for approval; - Settings:
.oo-dispute/mcp.env, if it doesn't exist yet. The server reads the chain and RPC from this file, which wins over shell variables. It never takesPRIVATE_KEY.
--selectionmakes the midterms the default scope; leave it out to review everything. Re-run the command after switching Node versions or moving the repo. An agent can run it too: "runyarn agents:setup". - Claude Code: a local-scope
-
Check
.oo-dispute/mcp.envif it already existed: it must not still point at a fork from an earlier rehearsal. -
Open a new session in this folder (Claude Code or Codex) and ask: "call
progresson oo-dispute". The first call scans the chain and can take a few minutes. The server runs with--only-polymarket. -
Start the review, optionally with a scope, a group limit and a number of parallel reviewers:
- Claude Code:
/review-disputes permissionless 20, 3 reviewers. It launchesdispute-reviewersubagents, which only have the review tools and web search, with no shell. - Codex: "Use $review-disputes on scope permissionless with 3 reviewers". The skill starts the reviewers as
Codex subagents (
spawn_agent). - A plain "revisa las propuestas en liveness" works in both.
- Scopes:
permissionless(the OOv2, where the midterms are and liveness is final),managed(the Managed OOv2, disputable until settled),midterms-2026andall. - Several sessions, in Claude Code and Codex at once, can share the queue: the MCP server gives each session's
agent names their own suffix (e.g.
r1@3fa2c1), so they never share or release each other's claims. Prefer a strong model for real decisions.
- Claude Code:
-
Send the disputes from a terminal, with the same
CHAIN_ID/RPC and your key. It shares the agents' queue,.oo-dispute/review-137.sqlitein this repo, from whatever folder you run it:yarn apply --watch --scope selections/midterms-2026.resolved.json --batch-size 50 --dry-run
- Passing the file (rather than the scope name) works even if
applystarts before the agents. Drop--dry-runto send. It asks before each batch, unless you pass--auto --max-disputes N --max-bond X. - The default gas budget is 1,000,000 per dispute, allowing headroom for neg-risk markets with long rules.
Remove an older
GAS_PER_DISPUTE=500000override or set it to1000000. In the dry run, check forwould dispute via DisputeBatcher in a batch of N.retrying one per transactionmeans batching fell back to individual sends; check the preceding simulation errors and gas budget before sending. - Stop it with Ctrl+C.
- If reviewers disagree with each other (a
conflict), it's never sent automatically:applyreports it, and you decide.
- Passing the file (rather than the scope name) works even if
Midterms: selections/midterms-2026.json lists the 515 midterm events. The committed
selections/midterms-2026.resolved.json is a snapshot with 4,552 markets in 512 events. Re-resolve it shortly before
the election, while Polymarket's API is up:
yarn select resolve selections/midterms-2026.jsonIt lets the review filter and group a race's markets even if Polymarket's APIs are down. For other selections, see docs/review-queue.md.
Back up your .oo-dispute/mcp.env first: fork:up replaces it and fork:down deletes it.
FORK_URL=<polygon archive rpc> yarn fork:upThis starts a local fork with 11 test proposals about past events, 5 of them wrong. Then:
- Review them with path B in a new Claude Code session.
- Send the disputes with
source .oo-dispute/fork/apply.env && yarn apply --watch. The key in that file is anvil's public test key and only works on the fork. - Check the result on chain with
yarn fork:status. - Clean up with
yarn fork:down.
To stress-test the queue with thousands of simultaneous proposals, without spending model tokens, use
yarn fork:load. See docs/fork-testing.md.
- Two working Polygon RPCs in
NODE_URLS_137, both servingeth_getLogs. - A dedicated wallet funded with USDC.e for bonds and POL for gas, with approvals done.
-
yarn select resolve selections/midterms-2026.jsonre-run in the last day. - A rehearsal done (
yarn fork:up…yarn fork:down), and.oo-dispute/mcp.envpoints at mainnet again. - Claude Code / Codex sees the
oo-disputeserver, andprogressanswers. -
yarn apply --dry-run --scope selections/midterms-2026.resolved.json --batch-size 50runs and reports batches.
| Symptom | Fix |
|---|---|
secp256k1 unavailable, reverting to browser version |
Harmless. |
No RPC configured |
Export NODE_URLS_137, put it in .env (CLI), or in .oo-dispute/mcp.env (agents). |
exceed maximum block range, Log response size exceeded, slow scans |
Rejected ranges are halved automatically; if scans stay slow, set MAX_BLOCK_LOOK_BACK=1000 (or lower), or use another provider. |
oo-dispute server missing or "failed to start" (Claude Code or Codex) |
Run yarn agents:setup (again after switching Node versions), open a new session in this folder, and check .oo-dispute/mcp.env. |
Codex says the oo-dispute tools aren't available and offers to start the server itself |
Stop it and run yarn agents:setup; Codex only loads MCP servers when a session starts. |
| The agents see fork proposals during a real incident | A leftover .oo-dispute/mcp.env from a rehearsal: run yarn fork:down and recreate it. |
apply finds no candidates |
Use the same CHAIN_ID/RPC and --scope as the agents, and don't point --db/REVIEW_DB elsewhere. |
Another apply is already running |
Only one apply per database. Ctrl+C releases it at once; after a kill -9, wait 2 minutes. |
A proposal shows as conflict |
Reviewers disagree: it's never sent automatically. Decide by hand (yarn dispute), or ask a reviewer to re-check. |
| Polymarket V2 proposals are not reviewed | V2 (OOReporter, JSON ancillary data) isn't supported yet, and --only-polymarket excludes it. |
Approve unlimited? prompt |
The bond token isn't approved for the contract that pulls the bond. Answer y, or approve ahead of time. |
| Command | Purpose |
|---|---|
yarn dispute [options] |
Interactive terminal review and dispute (path A) |
yarn apply [options] |
Send the disputes the agent reviews call for (path B) |
yarn agents:setup [--rpc urls] [--selection file] [--claude] [--codex] |
Register the MCP server with Claude Code and Codex (path B) |
yarn mcp [options] |
The MCP review server; agents start it themselves |
yarn select resolve <file> [-o out] |
Resolve a selection's slugs/tags to ids through Gamma |
yarn fork:up / fork:status / fork:down |
Local rehearsal fork |
yarn fork:load |
Load test on a fork |
yarn test |
Unit tests (no network) |
Shared by dispute and apply:
| Option | Default | |
|---|---|---|
-p, --only-polymarket |
off | Only Polymarket requesters (excludes V2 for now) |
-o, --oracle <all|oov2|managed> |
all | Which oracles to scan (managed is Polygon only) |
-m, --multiplier <n> |
4 | Multiplier on the priority fee (decimals allowed) |
--no-batch |
One dispute per transaction | |
--batch-size <n> |
20 | Disputes per batch transaction |
-d, --dry-run |
off | Simulate; approve and send nothing |
dispute only:
| Option | Default | |
|---|---|---|
-t / -b / -r / -a |
-t |
Sort by time left / bond / random / event order |
--flush-before <min> |
10 | Send the queue when a queued proposal expires within this many minutes |
--question-ids <file> |
Dispute this list of question ids directly, with one confirmation | |
-y, --yes |
off | With --question-ids: send without asking (needs --max-disputes and --max-bond) |
--max-disputes <n> / --max-bond <amount> |
none | With --question-ids: caps for this run (bond in token units) |
apply only:
| Option | Default | |
|---|---|---|
-w, --watch |
off | Keep running and send new candidates |
--interval <s> |
20 | Polling interval in watch mode |
--scope <name|file> |
all | Only this scope's disputes |
--auto --max-disputes <n> --max-bond <x> |
Send without asking, within these limits (bond in token units) | |
--db <path> |
.oo-dispute/review-<chain>.sqlite in the repo |
Review database |
--min-confidence <n> / --confirmations <n> |
0.8 / 1 | Dispute policy (stored in the database, shared with the MCP server) |
MCP server (dist/mcp-entry.js):
| Option | Default | |
|---|---|---|
-o, --oracle / -p, --only-polymarket |
all / off | As above (.mcp.json passes -p) |
--select <files…> |
Selection files to load as scopes; the first becomes the default scope | |
--scope <name> |
Default scope for tool calls | |
--db, --min-confidence, --confirmations |
As for apply |
|
--lease-minutes <n> |
10 | How long a claimed group stays reserved |
--no-market-data |
Don't query Polymarket's public API |
| Variable | Used by | Default / notes |
|---|---|---|
CHAIN_ID |
all | 137 (Polygon) |
NODE_URLS_137 / NODE_URL_137 |
all | Required. A JSON array of RPCs tried in order (wins), or a single URL |
NODE_RETRIES, NODE_RETRY_DELAY, NODE_TIMEOUT |
all | 2 retries, 1 s, 60000 ms |
PRIVATE_KEY |
dispute, apply |
Required there; never read by the MCP server |
DISPUTE_BATCHER_ADDRESS |
dispute, apply |
Polygon's DisputeBatcher (0xa7f9…E8fC); another address to override, none for one OOv2 dispute per transaction. Not defaulted when OOV2_ADDRESS is set |
BLOCK_LOOKBACK_PERIOD |
all | 43200 (~24 h) blocks scanned on a fresh start |
START_BLOCK |
MCP, apply |
First block a fresh review queue scans (overrides the lookback) |
MAX_BLOCK_LOOK_BACK |
all | 3499 blocks per eth_getLogs call |
MIN_PRIORITY_FEE_GWEI |
sending | 30 on Polygon (ethers v5 assumes 1.5) |
GAS_PER_DISPUTE |
sending | 1000000 gas budgeted per dispute in a batch |
RPC_CONCURRENCY |
all | 10 calls in flight |
REVIEW_DB |
MCP, apply |
.oo-dispute/review-<chain>.sqlite in the repo |
REVIEW_SELECTION |
MCP | Comma-separated selection files loaded at start-up |
SCAN_INTERVAL_SECONDS |
MCP, apply |
15 |
OO_DISPUTE_ENV_FILE |
MCP | .oo-dispute/mcp.env in the repo; settings file read at start-up (it wins over the environment) |
OOV2_ADDRESS / MANAGED_OOV2_ADDRESS |
all | Override the oracle addresses (testnets, forks) |
POLYMARKET_CLOB_URL / POLYMARKET_GAMMA_URL |
MCP, select |
Polymarket's public APIs |
- docs/review-queue.md: how the agent queue works, the dispute policy, scopes, MCP tools and the security model.
- docs/fork-testing.md: the rehearsal fork, the load test and its results, and the scripts behind them.
- AGENTS.md: instructions for agents and the Codex setup.