Consolidation + operator handoff for the pmv2 auto-trade 3c-2 (LIVE submit + lifecycle + safety) phase. This is the single reference for understanding what 3c-2 built and for arming the engine. Per-task notes already exist (breaker, reconcile, exits, settle, carry-forwards) — this note links them and records the final state, the adversarial-review fixes, the known deferrals, and the staged-arm runbook.
State at a glance
3c-2 is COMPLETE on branch
autotrade-foundation(NOT merged tomain). 17 commits3c2.1 → 3c2.10, plus a round-2 validation pass (bug/DRY/redundancy/gap lenses) that hardened a capital-trapping SELL-path blocker the safety review missed → see autotrade-3c2-round2-validation. Gate green: 334polymarket-fetch+ 87polymarket-datatests. DEFAULT mode = off (enforced at 3 layers) — production is a complete no-op until armed. Arming requires deliberate operator action (keys + RPC +/autotrade live), documented in the runbook below.Two review passes have signed off: round-1 (trade-safety lens) found+fixed the BUY-never-POSTed blocker; round-2 (bug/DRY/gap lens) found+fixed the SELL-never-POSTed blocker — round-1’s “SAFE FOR STAGED ARM” verdict had been premised on a SELL path that was actually broken at the vendored lot-size guard. Both passes now CONFIRMED SAFE FOR STAGED ARM.
What 3c-2 Built
3c-2 took the foundation (Plans 1/2/3a/3b — the DRY engine, gates, signing, CLOB bindings, all strictly no-submit) and added: real order submission, the full position lifecycle (entry → reconcile → exit → settle), and the safety machinery that makes live trading recoverable. Tasks 5–9 below; each links its per-task note.
Task 5 — Circuit Breaker (own module)
bump_breaker + BREAKER_THRESHOLD=5 extracted from execute.rs into a dedicated breaker.rs; SQL moved to repo.rs::bump_breaker_count (config-row-only FOR UPDATE, runs after the order-row tx commits). The §3.4 lock-order safety invariant (config row before order row — prevents ABBA deadlock) is enforced by a structural regression test that scans every src/pmv2/autotrade/*.rs + src/pmv2/repo.rs on every cargo test and fails if any function locks an order row before a config row.
→ autotrade-breaker-module · scanner literal-brace fix: autotrade-breaker-literal-brace-fix
Task 6 — Reconcile (on-chain ground truth)
CTF balanceOf(proxy, token_id) is the primary source of truth — the on-chain ERC-1155 balance, not the CLOB order state. decide_entry is a pure decision table over (on-chain balance, recorded status). A derived gate ready = preflight_ok && reconcile_ok && creds_valid governs LIVE entries (derived per-tick, never latched). /autotrade reconcile runs it on demand; the runner also runs it per-tick.
→ on-chain primitive: autotrade-onchain-balanceof · design rationale: pmv2-autotrade-engine-design
Task 7 — Selling-State Exits
The auto_sell FAK SELL engine. mark_selling performs an open → selling CAS that serializes all sell triggers (auto-sell, 1-click, settlement) — exactly one wins, the rest see rows_affected == 0 and abort. Exits run under per-order-mode authority: a live row exits even with the kill-switch off (closing must never be disabled). Off-mode exit gating still runs exits when has_open_live_orders. A missed-exit queue (exit_pending) catches signals that arrive before an order settles to open. An Owner-gated 1-click Sell button (sell:<id>) lets the operator force-close from Telegram.
→ foundations (assertions + CAS + queries): autotrade-selling-state-foundations · repo exit helpers + cursor: autotrade-repo-exit-helpers · pre-POST assertions / fee accounting: 2026-06-15-autotrade-execute-rs
Task 8 — Resolution Settlement
settle_open_positions reads the gamma winners map and performs a mark_resolved open → closed transition. Idempotent, no POST, no redeem — it only books the resolved outcome locally (CTF auto-redeems on-chain). /autotrade resolve is the operator escape-hatch.
→ design: pmv2-autotrade-engine-design
Task 9 — Exposure-Cap Hardening + Operator Alarms + Integration Cleanup
Exposure-cap TOCTOU re-check under the config lock, now counting filled/partial exposure (not just reservations). Operator Telegram alarms wired at three silent-failure sites. Arm-time exit-cursor seed to Utc::now() (so the first exits tick doesn’t drain every historical EXITED event). Removed the module-level #![allow(dead_code)] as an integration check — confirmed no dead seams remain (15 targeted, justified #[allow]s remain).
→ carry-forwards (alarms + cursor seed + skipped-item rationale): autotrade-3c2-validation-carryforwards
For Agents — module map
Live-submit path lives in
crates/polymarket-fetch/src/pmv2/autotrade/acrossengine.rs(gate sequence →try_reserve),execute.rs(assert_amounts/assert_sell_amounts, fee/PnL),breaker.rs(failure accounting),onchain.rs(ctf_balance_of),sources.rs(first_mover_coreemitter),commands.rs(/autotradeTelegram), plus the runner loop andrepo.rs(CAS transitions, cursors, reconcile/exit queries). The single seam from any source into the engine isengine::ingest(TradeSignal).
Final Adversarial Review (3 Parallel Reviewers)
Three independent reviewers ran against the completed phase. Three issues found; all FIXED; a re-review confirmed SAFE FOR STAGED ARM, no regressions.
(A) BLOCKER — a live BUY would never have POSTed
Symptom: every live BUY bailed at the
assert_amountsexact-taker check before submission. Root cause:build_orderpassed no price, so the SDK book-priced the order. Meanwhileplan.takerhad been quantized against the limit. The two disagreed, so the pureassert_amountsexact-taker assertion (got_taker == expected_taker) failed and the order was never sent. Fix: pin.price(plan.limit)on BUY (and.price(floor)on SELL) so the SDK’s taker equalsquantize_buyexactly. With the price pinned, SDK taker == expected taker and the assertion passes.
(B) Partial FAK sell orphaned the residual + mis-booked PnL
A partial FAK SELL left shares behind but the code booked PnL as if fully closed. Fix:
prorate_partial+record_partial_sellrevertselling → openwith a reducedfilled_size/costand accumulatedpnl, so the residual is tracked and PnL reflects only the portion actually sold.
(C) Stuck
selling/filledrows were never reconciledRows wedged in
sellingorfilled/partialcould never recover. Fix:load_reconcilable_entrieswidened to include them. Reconcile now recovers:
selling: on-chain balance > 0 → revert toopen; balance == 0 →close+ alarm.filled/partial:mark_openso the exit path can retry.
Known Deferrals / Limitations
fee_usd = 0 (accounting bias is fail-safe)
PostOrderResponseexposes no fee, sofee_usdis recorded as0. Effect: cost basis understates and realized PnL overstates — i.e. the bias is in the fail-safe direction (you think you made slightly less margin than you did, never more). Calibrate against real fills or wireorder_tradesfees during rollout.
Cap-breach reported as benign "dedup" Skip (visibility gap)
When the exposure cap is hit at the reservation step, the engine reports a benign “dedup” Skip. The operator cannot distinguish exposure-saturation from a genuine dedup. This is a visibility gap, not a correctness bug — and it is not fixed in 3c-2.
Balance-0 stuck-selling reconcile books pnl = 0
If a
sellingrow reconciles with on-chain balance == 0, the proceeds are unknown, so reconcile bookspnl = 0and fires a loud “verify manually” alarm. Treat any such alarm as a manual-bookkeeping action item.
Strategic — the first-mover TRIGGER is REST-polled (likely erodes the edge)
The
first_mover_coretrigger is REST-polled roughly every ~30 min → mean ~15 min detection lag. For a first-mover copy strategy that lag likely erodes most of the first-mover edge. Recommendation: a real-time on-chainOrderFilledWSS trigger is recommended as a prerequisite to scaling beyond tiny caps. Arm small, treat current caps as a correctness/operability shakedown, not an alpha test.
Staged-Arm Runbook
How to arm — do this in order
Arming is deliberate and staged. At every step, watch Telegram and the logs before proceeding.
- Provision secrets — set
POLY_PRIVATE_KEY(orPOLY_PRIVATE_KEY_FILE) +POLYGON_RPC_URL./autotrade dryrun— observe DRY-SIGNED rehearsals; confirm the pre-POST assert passes (this is exactly where blocker (A) would have failed)./autotrade set …— set small caps (exposure / daily-spend / per-event / max-copies)./autotrade live— flips to live; preflight-gated (readymust be true).- Observe the full lifecycle on tiny caps:
- first fills,
/autotrade reconcile(confirm on-chain balance matches),- an intentional restart (confirm state recovers cleanly),
- an exit (auto-sell),
- the 1-click Sell button (
sell:<id>),- a settlement (gamma winner →
mark_resolved).Kill-switch:
/autotrade offstops entries but live positions still exit (per-order-mode authority — Task 7). Off is not a freeze; it’s “no new entries, keep closing.”
For Agents — enforcement layers
mode=off is enforced at 3 layers so a single missed check can’t go live: source
autotrade_active(mode, enabled)(const-fn false when Off), the engine mode-check at the top ofingest_entry, and the live preflightreadygate. Production stays a no-op until all three seelive+ valid creds.
Related
- autotrade-3c2-round2-validation — the round-2 bug/DRY/gap validation pass (SELL-path blocker B1, per-event cap, settle/cursor/reconcile gaps)
- pmv2-autotrade-engine-design — the locked design (wallet, V1/V2 landmine, safety machinery)
- autotrade-breaker-module · autotrade-breaker-literal-brace-fix — Task 5 circuit breaker
- autotrade-onchain-balanceof — Task 6 CTF
balanceOfreconcile primitive - autotrade-selling-state-foundations · autotrade-repo-exit-helpers — Task 7 exits
- autotrade-3c2-validation-carryforwards — Task 9 alarms + cursor seed
- 2026-06-15-autotrade-execute-rs — pre-POST assertions + fee/PnL accounting
- autotrade-signing-client-order-ops —
place_order/cancel/order_tradesbindings - autotrade-sources-signal-emitter —
first_mover_coretrigger (the REST-polled one) - autotrade-commands-bot-wiring —
/autotradeTelegram surface - copy-trade-lane — the paper lane built alongside this live lane