`polymarket-us-expansion`) has deeper background; the repo is authoritative.
**The goal:** a supervised minimum-size live test — deposit → copy a followed
bet → mirror a sell before resolution → hold to resolution (win, loss, AND
50/50 refund) → withdraw — with worst-case loss bounded by the ~$30 exposure
cap. Only after every edge case in Phase 5 passes does sizing-up get discussed.
---
## Phase 0 — invariants (read, don't skip)
These were each earned the hard way. Violating any of them voids the test.
| # | invariant | why |
|---|-----------|-----|
| 0.1 | **Live orders only from a geo-TRADABLE box.**`python3 host/geocheck.py` must print `VERDICT: TRADABLE`. The Fly worker (`wwf-copybot`, Stockholm) passes; a Mac in the US gets 403-geo-blocked on the order POST. | Polymarket IP-blocks order placement (README gotcha 10). |
| 0.2 | **The user decides WHEN to arm** (jurisdictional posture: they trade live from Colombia months, paper from US months). The agent prepares everything; the arming step and its timing are user-only. | The machine's location doesn't relocate the person. |
| 0.3 | **One writer per book.** The live book uses its own state (`copybot_state.live.json`) and must use its own feed file (Phase 1.1 — today both modes write `live/copybot_live.json`, which would clobber the July paper test's dashboard). The paper worker keeps running untouched throughout. | The paper July test is the control group; don't contaminate it. |
| 0.4 | **Settlement truth is the chain.** CLOB `winner` flags never populate for operator-resolved in-play markets, and 50/50 refunds (28% of follow-set markets!) pay $0.50/share. `resolution_price()` in copybot.py already does CLOB→chain-payout tiering; live mode requires `ALCHEMY_RPC_URL` set or refunded/operator-resolved positions never settle. | 2026-07-06 audit: 4 positions sat locked for hours. |
| 0.5 | **The ledger must reconcile to the cent.**`ledger_drift` in the feed must stay 0.00; in live mode it additionally must match the on-chain USDC balance (Phase 1.4). Any drift >1¢ = stop, diagnose, fix before the next order. | Real money makes bookkeeping bugs unrecoverable. |
| 0.6 | **Hard caps stay at test size until Phase 6.**`config.live.json` risk block: $5/trade, $5/position, $25/day, $30 exposure, 6 open, prices 0.05–0.95, FAK orders. Never raise mid-test. | Worst case ≈ the $30 exposure cap. |
| 0.7 | **Paper-mode safety interlocks stay intact**: live needs `mode:"live"` + `--live` + typed phrase. Never bypass, never automate the phrase. | The phrase is the human checkpoint. |
---
## Phase 1 — code prep (agent; all verifiable in paper mode, no money)
Each item is a small PR-sized change with its own verification. Do them in order.
### 1.1 Feed separation
`copybot.py``FEED` is a module constant (`live/copybot_live.json`) — a live run
overwrites the paper feed. Make it config-driven: `cfg.get("feed_path",
"live/copybot_live.json")`, and set `"feed_path": "live/copybot_live_real.json"`
in `config.live.json`. Same for `FILL_LOG` → `copybot_fills.live.jsonl`.
**Verify:** run paper poll-once with a scratch config setting `feed_path`; confirm
the alternate file is written and `live/copybot_live.json` is untouched.
### 1.2 Secrets via env for the worker path
`config.live.json` is gitignored — the Fly worker clones the repo and won't have
it. Extend `load_cfg`/`normalize_follow_config` so `LIVE_PRIVATE_KEY`,
`LIVE_FUNDER_ADDRESS`, `LIVE_SIGNATURE_TYPE` env vars override `cfg["live"]`.
- [ ] Polygonscan: the fill's on-chain transfer matches shares/price ±rounding
If any link fails: stop (Ctrl-C), snapshot state + logs, diagnose before re-arming.
---
## Phase 5 — edge-case matrix (the actual test)
Run days-to-weeks until every row has a ✓ with evidence (log line + feed row +
chain tx where applicable). Most trigger naturally from the follow set's flow;
patience beats forcing.
| case | trigger | acceptance evidence |
|------|---------|---------------------|
| BUY copy | any conviction signal | Phase-4 checklist |
| ADD (signal scales in) | whale re-buys same market | position grows ≤ stake rule; feed cost/shares aggregate |
| **SELL before resolution** | signal fully exits (they exit most positions — hours, not days) | `EXIT … [LIVE] sell` + proceeds land in balance + status `closed`/SOLD + 1.6 retry logs if book thin |
| hold → WIN | resolution | `SETTLE … WON` + **auto-redeem tx hash on Polygonscan** + USDC balance up by shares×$1 |
| hold → LOSS | resolution | `SETTLE … LOST`, no redeem attempted, book realizes −cost |
| hold → **REFUND 50/50** | ITF walkover/abandonment (≈28% of markets — will happen) | `SETTLE … REFUND ↩` + redeem tx at $0.50/share + cash up by shares×0.5 |
| missed (price guard) | price runs >5% in the copy lag | `skip … price moved` + missed row on dashboard, later settled hypothetically |
| missed (cash gate) | exposure cap hit | `capital fully deployed` skip + missed row |
| downtime reconcile | stop the live app 30+ min during flow, restart | boot logs `reconcile:` lines; downtime entries → missed("bot offline"); exits mirrored or settled |
| FAK no-fill | thin book (observe, don't force) | order killed cleanly, no resting order in the UI, retry per 1.6 |
| neg-risk market | if one gets copied | `⚠ NEG-RISK … redeem manually` warning; manual UI redeem works |
| **WITHDRAW** | user, at the end | USDC withdrawn via UI back to source; final book cash ≈ withdrawal ± dust; screenshot + final feed snapshot committed |
Throughout: `ledger_drift` 0.00 every heartbeat, CASH≠CHAIN silent, and the
paper book running in parallel as control.
---
## Phase 6 — wind-down + verdict (user + agent)
1. Stop the live app; final feed + state snapshot committed (tagged `live-test-1`).
2. Write `FINDINGS.md` addendum: measured live slippage vs paper's −3.6%/65s,
fee accuracy, fill rate (FAK kill rate), any stuck exits, refund handling.