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 to main). 17 commits 3c2.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: 334 polymarket-fetch + 87 polymarket-data tests. 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/ across engine.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_core emitter), commands.rs (/autotrade Telegram), plus the runner loop and repo.rs (CAS transitions, cursors, reconcile/exit queries). The single seam from any source into the engine is engine::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_amounts exact-taker check before submission. Root cause: build_order passed no price, so the SDK book-priced the order. Meanwhile plan.taker had been quantized against the limit. The two disagreed, so the pure assert_amounts exact-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 equals quantize_buy exactly. 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_sell revert selling → open with a reduced filled_size/cost and accumulated pnl, so the residual is tracked and PnL reflects only the portion actually sold.

(C) Stuck selling/filled rows were never reconciled

Rows wedged in selling or filled/partial could never recover. Fix: load_reconcilable_entries widened to include them. Reconcile now recovers:

  • selling: on-chain balance > 0 → revert to open; balance == 0 → close + alarm.
  • filled/partial: mark_open so the exit path can retry.

Known Deferrals / Limitations

fee_usd = 0 (accounting bias is fail-safe)

PostOrderResponse exposes no fee, so fee_usd is recorded as 0. 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 wire order_trades fees 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 selling row reconciles with on-chain balance == 0, the proceeds are unknown, so reconcile books pnl = 0 and 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_core trigger is REST-polled roughly every ~30 minmean ~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-chain OrderFilled WSS 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.

  1. Provision secrets — set POLY_PRIVATE_KEY (or POLY_PRIVATE_KEY_FILE) + POLYGON_RPC_URL.
  2. /autotrade dryrun — observe DRY-SIGNED rehearsals; confirm the pre-POST assert passes (this is exactly where blocker (A) would have failed).
  3. /autotrade set … — set small caps (exposure / daily-spend / per-event / max-copies).
  4. /autotrade live — flips to live; preflight-gated (ready must be true).
  5. 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 off stops 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 of ingest_entry, and the live preflight ready gate. Production stays a no-op until all three see live + valid creds.