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 cache
  • set_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:

StateReply
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 by autotrade_exits_cursor / advance_autotrade_exits_cursor in repo.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.