Validation carry-forwards from the 3c2 live-submit work: two SKIPPED items (verified rationale) + operator Telegram alarms wired across three alarm sites + exits cursor seeded at arm-time. All 315 + 87 tests pass, 0 clippy errors.
Task b — SKIPPED: MarketData::resolve outcome validation
No code change required. MarketData::resolve calls ResolvedMarket::token_for(outcome_index, outcome_label) which already validates outcomes[idx] == label and is fail-closed before any reserve or trade action is attempted. The guard exists in market.rs from autotrade-market-data-trait (Task P2.3).
Task c — SKIPPED: neg_risk network round-trip inside build_order
Investigated crates/polymarket-clob/vendor/rs-clob-client-v2/src/clob/client.rs.
The vendor CLOB client exposes:
neg_risk(token_id: U256) -> Result<NegRiskResponse>— async, network call, has an in-memory cacheset_neg_risk(token_id, bool)— sync cache setter (no network)
The wrapper at crates/polymarket-clob/src/lib.rs:267 (ClobClient::neg_risk(&str) -> Result<bool>) is already wired.
Decision: leave neg_risk: bool as a caller argument in build_order. Calling ClobClient::neg_risk from inside build_order would inject an async network call into what is currently a synchronous signing path. This is YAGNI — the caller (the runner) already resolves neg_risk from the preflight gate before handing off to build_order.
Task d — DONE: Operator alarms via Telegram
Three alarm sites wired. Pattern: keep the existing tracing::warn! and ADD a notify(...) call alongside it so the operator gets a Telegram message on silent failures.
Site 1 — runner.rs::drain_pending_exits
Pending-exit drain read failure now calls:
notify("🚨 autotrade exits: pending-exit drain read failed: {err}")
Site 2 — runner.rs::run_exits
Two failure branches:
notify("🚨 autotrade exits: cursor read failed: {err}")
notify("🚨 autotrade exits: events read failed: {err}")
Site 3 — positions.rs::init_autotrade
Signature changed to accept cfg_copy: &CopyConfig (sourced from run_pmv2_positions):
init_autotrade(cfg, &cfg_copy, &pool).await?
The build_autotrade_deps failure branch now calls:
send_autotrade_dry(cfg, cfg_copy, "🚨 autotrade: deps init failed, skipping this run: {err}")
This ensures the operator receives a Telegram notification if dependency initialization silently fails at startup, rather than the run silently dropping the autotrade step.
Task e — DONE: Seed exits cursor at arm-time
File: crates/polymarket-fetch/src/pmv2/autotrade/commands.rs::arm_live
After config::arm_live_reset(pool) succeeds, check whether the 'autotrade_exits' cursor is unset and seed it to Utc::now() if so.
Reply text varies by case:
| State | Reply |
|---|---|
| First arm (cursor unset) | "✅ armed: mode -> live (breaker reset, exits cursor seeded to now)" |
| Re-arm (cursor already set) | "✅ armed: mode -> live (breaker reset)" |
Rationale: Without seeding the cursor on first arm, autotrade_exit_events would read from epoch-0 and drain every historical EXITED event on the first exits-runner tick, potentially triggering a flood of spurious sell orders. Seeding to now() at arm-time means only exits detected AFTER arming are acted on. Re-arms do not reset the cursor — the existing cursor position is correct (we want to continue processing from where we left off, not skip events that fired while disarmed).
For Agents
Cursor key
'autotrade_exits'is managed byautotrade_exits_cursor/advance_autotrade_exits_cursorinrepo.rs(see autotrade-repo-exit-helpers). It is distinct from'pmv2_exit_alert'(notifier, read-only alert cursor) and'overlap_watcher'.
Arm-time cursor seeding is load-bearing
Do not remove or make the cursor seed conditional on anything other than “cursor not yet set.” Any arm that skips seeding when the cursor is absent risks a historical-exit flood on the first run tick.
Gate result
315 + 87 tests pass, 0 clippy errors, on branch autotrade-foundation.