For Agents

Living index of themes for this project. Each H2 is a topic; bullets are wikilinks to related notes. Updated by obsidian-documenter when documenting work. Read by historian at bootstrap. Topics kept alphabetical.

Algorithm Design

  • cohort-algo-vs-polymarket-fetch-math-parity-auditcross-repo MATH PARITY AUDIT (2026-06-25): the NEW standalone algo (cohort_algo+pmv2-contracts) mirrors the PREVIOUS algo (polymarket-fetch/src/pmv2) — core decision math byte-for-byte identical on disk. Trader scoring (CONF_* 0.06/0.035/0.02/0.008, trader_score=e·(0.5·long_term+0.5), bet_confidence=((entry−lost)/(won−lost)).clamp(0,1)), consensus (fires at exactly holders.len()==2, 48h window, eligible_buy <=/eligible_sell strict >), and sizing (score_weight=clamp(score,0,1)·10+1; raw=base·score_weight·conviction; clamp) all identical. Genuine divergences = EV-floor gate DROPPED (the one possible accidental loss; min_round_trip_edge_bps loaded-but-inert; operator chose “report, don’t change”) + reference_price entry-band check dropped + wire v1→v2 redesign + safety hardening (fail-closed breaker, snapshot_trustworthy/should_record_exit exit guards) + operational constant tuning. Method = 4 parallel comparison agents, all math re-verified against disk.

  • tracked-alerts-consumerthe real-time tracked-ALERT filter design (LIVE 0f71f89): a 2nd-sharp-converges CONSENSUS gate — alert only when a 2nd positive-score cohort trader independently BUYS the same (condition_id,outcome_index) within 48h. Got there by elimination: per-fill flooded (~1/sec), then a conviction filter (notional ≥ 2× the trader's OWN avg, RELATIVE not absolute because the cohort is whales), then consensus. Distinction: this is a human-ALERT convergence gate, NOT the backtested “≥K agree” copy-trade thesis that xref-consensus-rejected-fresh-copy-wins REJECTED — different application (notify vs auto-copy) and mechanism (in-window co-buy vs backtested edge).

  • pmv2-two-tier-trader-scoringpmv2 two-tier scoring engine (design, 2026-06-01) — from-scratch rebuild (pmv2_* tables), Step 2 after leaderboard collection. Tier 1 trader_score (stored on pmv2_traders): min-50-resolved qualify gate, skill base edge = mean(result − entry_price) over resolved event bets (volume-weighted entry; chosen over raw W/L to kill favorite-bias), long-term weighting = min-of-halves track-record consistency × span_factor(active_days/365); trader_score = edge × (0.5 + 0.5·long_term) (long-term as a 0.5–1.0 trust multiplier, never zeroes a real edge). Tier 2 — RESOLVED 2026-06-01 = FIRST-MOVER, not consensus (user reversed their earlier consensus push): act on the first high-edge trader’s new event bet (no quorum) — the summed-over-holders single_bet_score consensus framing is dropped. bet_confidence (bet size between the trader’s avg LOST and avg WON bet, clamped 0–1, a conviction proxy) is repurposed as the position-SIZING input for that single first-mover copy, not a multi-trader trigger. Likely execution path: reuse the on-chain copy paper lane (copytrade-shipped) — already a first-mover detector on Polygon — feed it pmv2’s edge-selected cohort + add conviction sizing (wiring TBD). Key calls: avg bet sizes are trade-based (each BUY = a bet), not per-position/first-buy; long-term must multiply edge not W/L; cross-run persistence is a future longevity signal (every run tagged run_id). Migration 20260602010000_pmv2_scoring.sql (Tier-1 in progress); Tier-2 first-mover lane next. PHASE 1 SHIPPED (2026-06-02): pmv2-positions command + pmv2_positions current-state table (migration 20260602020000) snapshot the cohort’s open event positions — first prod run 35 wallets → ~2,546 open event positions / 32 traders / 757 events (≈3.3 markets per event), confirming the multi-version/variant reality. Detection = POLL the Polymarket Data API positions endpoint, NOT on-chain WSS. Built TDD + reviewed. PHASE 2a SHIPPED (2026-06-02) — the per-bet scoring engine (Tier-2 read-side): new pmv2-score-bet --bet <condition_id | market-slug | Polymarket URL> CLI + pmv2::scoring module (score_bet, group_into_bet_scores, parse_selector, BetHolder::conviction, format_bet_scores) + repo::load_bet_holders / latest_position_snapshot_ts. Per held bet bet_score = Σ trader_score × bet_confidence, grouped per (condition_id, outcome_index); event URLs/slugs fan out to variant markets. Read-only (pmv2_positions ⋈ pmv2_traders), no migration; TDD (26 pmv2 tests) + reviewed. Same single-bet-input function the future Telegram link-check AND the Phase-2b poller will call. Prod-validated: all 3 selector forms identical (bet_score 0.19870); MSTR “sells BTC by May 31” NO 16 holders (0.199) vs YES 4 (0.024), a $849k NO whale with negative trader_score correctly DRAGS the score down (skill-weighted, not money-weighted); output labeled “current cohort EXPOSURE (consensus), not a first-mover signal” + shows snapshot age. Remaining = Telegram link-handler + Phase 2b (poller → delta detection → auto-score → alerts + paper copies + resolution PnL).

  • xref-consensus-rejected-fresh-copy-winsCENTRAL FINDING (2026-06-01, verified twice): the cross-reference / “≥K top traders agree” consensus thesis is REJECTED; fresh single-wallet copying WINS. Same leakage-clean purged + embargoed harness as purged-backtest-gate-no-edge, EVENT-only (updown excluded), via a new backtest --xref co-occurrence mode. xref-K1 (copy the FIRST cohort BUY per event-market outcome) = +7.59% pooled, positive in all 3 folds (+6.7/+9.4/+8.4%), 34,957 fires; K2 +5.3–6.6% (worse); K3 ~−0.4 to −3.9%; K4 noise (~50). Event-only all-watched +2.39%, random +0.93%. Monotonic: more agreement → worse ROI; tighter window doesn’t help. Consensus is structurally LATE — by the time K wallets agree the price has moved, so you enter worse; the first informed entry is the well-priced one, agreement is the market catching up. GOAL VALIDATED (copy skilled traders: +7.59% vs +0.93% random); the cross-reference MECHANISM failed. Caveat: +7.59% is an UPPER BOUND (cohort’s own price + flat 200bps); tradeability UNPROVEN until real ask at detection + latency. Resolves best-consensus-algo-design: no consensus gate — copy the first mover, fast. Impl (uncommitted): backtest.rs::detect_xref_copies + K1..K4 arms, cli.rs --xref, output.rs::render_xref, TDD on the detector.

  • purged-backtest-gate-no-edgesets the Phase 2 ship bar. Leakage-clean purged walk-forward shows the current PnL/copy_score gate is anti-selective (−2.78%, loses to random +2.75% and all-watched +3.68% in 2 of 3 folds). So the bar for any new signal — the skill-gated selector (selector-vs-skill-gap) or the log-odds / independence aggregator (the skill-weighted log-opinion-pool of best-consensus-algo-design) — is beat ~+2.75% random under this same purged harness, not merely beat the (trivially low) old gate. Operationalizes the “required baseline arm.”

  • phase1-consensus-rebuild-implementedPhase 1 consensus rebuild, implemented & verified (uncommitted, 2026-06-01). WS1 (consensus.rs): new testable seam compute_consensus_groups_with(members, min_wallets, now, dust_floor_usd) (public wrapper supplies Utc::now() + DEFAULT_DUST_FLOOR_USD=5.0); eligibility filter drops redeemable / past-end_date (%Y-%m-%d then RFC3339; unparseable kept + warn!) / dust (<$5); bucket key gains asset so opposite-asset positions can’t inflate a long outcome’s count. WS2 (scheduled_run.rs/main.rs/repo.rs): scheduler runs the heavy short-term pipeline (trades→resolutions→short-term-markets→metrics→short-term-accuracy) ≤ once/24h behind a DB-backed gate (heavy_sync_is_due reads max(computed_at) in pm_short_term_trader_accuracy) — DB-backed because each ~30-min tick is a fresh process. Green: fmt/clippy clean, WS1 13 tests, WS2 4 tests. First concrete code response to the leakage/selection findings.

  • best-consensus-algo-designPhase 2+ consensus target: replace count>=2 with a skill-weighted LOGARITHMIC OPINION POOL (geometric mean of odds) over FRESH (from pm_trades trade_time, market-time decay), INDEPENDENT (effective-N via Kish design-effect + eigenvalue participation ratio over a co-entry/Jaccard dependence matrix) skilled wallets; weights = EB-shrunk edge_lower with a hard per-wallet CAP (neutralize the +30σ wallet) + bounded tanh stake-conviction; output calibrated p_agg, edge=p_agg−cur_price; trade only when residual edge survives fees+√-impact slippage AND skilled money isn’t split (disagreement→abstain). Sports noise dissolves (~0 edge_z → zeroed by weighting, no keyword filter). Caveat: edge thin/concentrated (~1–2 wallets); a simple equal-weight log-pool of edge_lower>0 wallets is a REQUIRED baseline arm (forecast-combination puzzle). Spec: docs/superpowers/specs/2026-06-01-best-consensus-algo-design.md.

  • copy-trade-laneon-chain copy-trade PAPER lane in pm-arb (commit b3f9e21, 2026-05-31). Strategic pivot from generating our own signal to copying the cohort’s cheap-outcome selection on crypto-updown markets: our longshot signal is negative-EV (t≈0.2 / 655 trades) but the top-5 cohort’s selection edge is real (+~18–30% ROI, t up to 9, all 5 wallets). Detection is on-chain (OrderFilled logs, ~1–3s) because the Data API is ~5 min late. Default-OFF, DB-configured, dormant until the kill-switch flips.

  • copytrade-paper-lane-latency-spread-fixes — two copy-lane analytics correctness fixes (working tree, uncommitted): carry ms-precise detect_latency_ms through evaluate_copy instead of truncate-then-recompute from whole seconds; synthesize the copy entry ask by no-arbitrage (ask=1−opposite_bid) instead of falling back to the mid. Closes two of the analytics niggles tracked in copy-trade-lane.

  • longshot-strategypm-arb implementation + tuning of Lane 1 (longshot reversion): buy ≤13¢ signal (≤~16.9¢ post-slippage) outcome late (tiw ∈ [0.66, 0.93]) in a 5m crypto window. Live params: 5¢ floor, 13¢ signal cap (bumped from 10¢ on 2026-05-30), 1.3× slippage guard, 0.93 tiw cap, 500 on 2026-05-30). Cohort sweet spot is 12–16¢, not 8–9¢ (3,206-trade study across 5 wallets); our paper model matches the 4/5 buy-only / hold-to-resolution top wallets, not 0x75cc3b’s sell-rotating outlier pattern.

  • cross-venue-pm-kalshi-arb — cross-venue YES/NO arb (Polymarket ↔ Kalshi) reality-checked against live books and shelved. Real lock existed only on June FOMC buckets (+0.36% / ~19d); edge is a 0.1¢-vs-1¢ tick-rounding gap, not free money. Sibling shelved strategy.

  • copy-trade-algorithm — per-trade edge → trader copy_score → live trade-signal scoring, with MM filter and time decay. Layers 1-4 shipped. Backtest verdict: gate works, copy_score magnitude doesn’t — kept as observation tool, magnitude ranking removed.

  • short-term-accuracy — edge-over-price ranking for crypto Up/Down markets; Wilson lower bound, split-half persistence

  • top-trader-edge — strategy v2 thesis for the latency-arb bot. Edge is late-window 5m longshot reversion + momentum-chase, signal = book state + time-in-window. Two lanes on the top wallets; 0xd189664c at 77.8% ROI is the purest expression.

  • selector-vs-skill-gapthe rigorous luck/skill stats are NOT wired into selection. Default selector top_n_intersection (selector.rs:183) ranks purely by cumulative PnL (only consistency control = 3-window presence gate); the Wilson/edge_z/persistence/Monte-Carlo-luck layer (short_term_accuracy.rspm_short_term_trader_accuracy) is measurement-only. Fix = a selector keyed on n_markets>=40 AND edge_lower>0. Ties to the no-edge backtest: PnL doesn’t persist (~0.21 vs luck ~0.23), yet PnL is exactly what selection targets.

  • polymarket-fetchcompute-metrics / ranked-traders CLI + pm_trader_metrics snapshots

  • correctness-pass-six-fix-commitsscoring-math corrections (operator-approved, 2026-06-03, shift ranking slightly): conviction() default 0.5 → 0.0 (no bet history = no weight, not a phantom median); consistency_factor returns 0 when overall edge ≤ 0 and dropped the 0.01 floor (break-even traders were getting inflated consistency; tiny edges were deflated by the floor); avg_won / avg_lost / avg_bet_size now computed per-POSITION (buys aggregated per condition/outcome) not per trade-fill — the correct baseline for bet_confidence. Plus the SHARED load_event_winners change (require payout>0 + deterministic tiebreak, dropping random winners on cancelled/0-payout markets) feeds compute_trader_metrics, so it was operator-approved and revalidated against pmv2-two-tier-trader-scoring. KEY SEMANTIC carried into display: band = skill, N× avg = conviction, orthogonal.

Backtesting / Validation

  • edge-discovery-2026-06-22two durable validation constraints the harness must bake in. (1) UMA resolves discretionary geopolitics to HEADLINE/SPIRIT, whale-voted ($120M Iran market resolved 99% YES AGAINST its literal “permanently cease” rule) ⇒ you cannot build a leakage-clean walk-forward on a discretionary resolver; resolution-source edges are only validatable on OBJECTIVE/deterministic oracles (FDA PDUFA, FAA license, Chainlink, ICAO, leaderboard URL). The often-cited UMA 98.5% proposer-win is the UNCONDITIONAL rate; the rate conditional on DVM escalation is unknown + structurally lower (the binding unknown that gates C7). (2) Data API trade-history is hard-capped ~3000 records (cf. the 3500/wallet ceiling in pmv2-two-tier-trader-scoring) ⇒ all deep-history validation must route via Gamma event dumps or on-chain Polygon log reconstruction, NOT Data API trade history. Also: portfolio-level correlated-resolution-timing risk is un-modeled (all CPI markets resolve together, daily-high weather at midnight, one regional war hits many geo positions). Every new lane must still beat ~+2.75% random under purged-backtest-gate-no-edge at 1.5–2× cost.
  • pmv2-two-tier-trader-scoringconsensus-vs-quorum question RESOLVED 2026-06-01 in favor of xref-consensus-rejected-fresh-copy-wins: Tier-2 is now first-mover copy (act on the first high-edge trader, no quorum); the summed single_bet_score consensus framing was dropped and bet_confidence repurposed as position-sizing. Now-open validation question is narrower: before real money, does the EDGE-selected cohort (pmv2 Tier-1) match or beat the old PnL-selected cohort under the same purged-backtest-gate-no-edge purged + embargoed harness (vs first-mover / random baselines)? Tier-1 edge is a point-in-time result − entry_price quantity (right shape), but consensus-validation-leakage’s discipline still applies. EMPIRICAL — Tier-1 first prod run (run 3, 2026-06-02): the edge ranking reshuffled the board as designed — favorite-riders sank (ArmageddonRewardsBilly 0.962 W/L, edge +0.003, #3→#21; “The Spirit of Ukraine>UMA” 0.890 W/L, edge −0.007; kinderSman 0.935 W/L, edge ~0) while edge-pickers rose (Dropper 0.723 W/L / 0.64 entry / edge +0.081 / 453d → #1; betwick 0.585 W/L / edge +0.042 / 377d → #3). KEY FINDING — the leaderboard is NOT a skill list: across 35 qualified (≥50-bet) traders, avg edge is −0.9pp, 11 have ≤0 edge, only ~24 are edge-positive and only ~5–8 clear 3pp ⇒ Tier-2’s copy cohort is small & sharp (~top 5–10), edge/score-gated. Diagnostics clean (0×429 @ concurrency 16; min-50 floor dropped 15/50 as noise; lone error = benign gamma empty-batch). Confirmed limit: trade history hard-capped at 3500/wallet by an API offset ceiling (NOT --trades-max-pages) ⇒ ~14 heavy traders scored on most-recent 3500 only. Span/long-term sound (0 zero-span; short spans = real burst-bettors). OPEN before Tier-2: -<timestamp>-suffixed wallet identities (e.g. 0xcF609D…-1771809916847 #13 n=1293, 0xE32F…) — sub-account vs parse/join quirk affecting trader identity.
  • xref-consensus-rejected-fresh-copy-winsthe --xref co-occurrence result on the same purged + embargoed harness, EVENT-only (negative control ≈0 across all cells, max |ROI| 1.31% within ±2% → leakage-clean; verified twice). xref-K1 = +7.59% (all 3 folds positive, 34,957 fires) ≫ random +0.93% / all-watched +2.39%; K2 +5.3–6.6%, K3 ~−0.4 to −3.9%, K4 noise. Monotonic decline in K ⇒ consensus is structurally late, the first mover is the well-priced entry. Clears the purged-backtest-gate-no-edge “beat random” bar by a wide margin. Same UPPER-BOUND caveat (measured at the cohort’s own price + flat 200bps → trust the relative ordering K1 > K2 > K3, not the absolute magnitude as live P&L).
  • purged-backtest-gate-no-edgethe leakage-clean verdict: the recommendation gate has NO edge and is ANTI-SELECTIVE. Purged + embargoed walk-forward (45d cutoff, 200bps slippage, 3 disjoint 15-day folds), pooled ROI: gate −2.78% · all-watched +3.68% · random +2.75% · raw-pnl top-20 (purged) −3.24% · negative-control −0.04% (≈0 → leakage-clean). Gate loses to baselines in 2 of 3 folds → PnL/copy_score selection picks worse-than-random trades. The old −43.37% gate was a LEAKAGE ARTIFACT (resolves the “−44% is provisional” caution in consensus-validation-leakage; this is its remedy steps 3+4 — purged CPCV + shuffled-label negative control — passing). Caveat: baselines use last-trade price as a mid proxy → UPPER BOUNDS; trust the relative ordering (gate < random < all-watched), not absolute magnitudes. Phase 2 bar: any new signal must beat ~+2.75% random under this same harness to ship.
  • consensus-validation-leakagethe edge numbers are PROVISIONAL (multi-agent re-validation 2026-06-01). The −44% copy-gate ROI and the persistence result (Spearman 0.53 / p=0.025 / 12-of-51 edge_z>2) rest on three leaking code paths: (1) consensus.rs:30 aggregates LIVE positions (post-entry+survivorship); (2) backtest.rs build_wallet_data + metrics.rs:132 pair_trades_to_resolved pair trades to payouts across the train/test cutoff (label leakage); (3) repo.rs:1429 fetch_short_term_bet_rows has NO time filter (survivorship+lookahead). Remedy (López de Prado): point-in-time signal from pm_trades, a real resolved_at, purged+embargoed CPCV, a shuffled-label negative control, deflate by DSR. Keep the price-conditioned Monte-Carlo luck baseline (short_term_accuracy.rs:259). Trust no current edge number until re-derived leakage-free.
  • best-consensus-algo-design — mandates a required baseline arm: a simple equal-weight log-pool of edge_lower>0 wallets, which the fancy skill-weighted version must beat leakage-free (via consensus-validation-leakage’s CPCV/DSR/shuffled-label controls) or be discarded.
  • longshot-strategy — paper strategy re-validated against 0x75cc3b’s actual pm_trades in the same band (4–10¢ late-window): both ~10.6–10.7% win / +50–60% ROI. Confirms the reverse-engineering captured the real edge; our +15% headline was a leaky rendering of the ~+50% core, fixed by floor/slippage/tiw tunes. Slip-ratio analysis (0.999 in 0.93–1.0) used to refute the execution-gap hypothesis. 2026-05-30 cohort study (3,206 trades, 5 wallets, 30d, same band+window): confirmed universal sweet spot is 12–16¢ not 8–9¢, motivating the cap bump to 0.13. Per-asset rolling-ROI gate killed by retrospective backtest (3 variants all worse than baseline; would have blocked +$320 cumulative profit — anti-correlation between recent and future performance in a reversion strategy is the durable lesson).
  • copy-trade-algorithm — multi-window backtest, Gamma API resolution data fix (61K markets), edge-persistence diagnostic (~0.21 Spearman), verdict: no demonstrated copy-trade edge vs naive PnL ranking
  • short-term-accuracy — split-half persistence test on 21 wallets (Spearman 0.34, p=0.08, underpowered); per-wallet edge_z up to +11.3 is solid; edge confirmed copyable (in-window latency arb, not post-resolution sniping)
  • selector-vs-skill-gap — the actionable corollary of the copy-trade-algorithm backtest: top-by-PnL persistence (~0.21 Spearman) did not beat the luck baseline (~0.23), yet the production default selector (top_n_intersection) selects on exactly that non-persistent PnL metric. The rigorous luck-screening machinery (Wilson edge_lower, edge_z, Monte-Carlo luck_p_value, split-half persistence at n>=40) exists in short_term_accuracy.rs but is measurement-only — never consulted by the selector.
  • cohort-consensus-backtest-2026-07-04point-in-time counterfactual of the LIVE consensus signal (fire on the 4th distinct cohort BUY, alerts_consumer.rs:100-116), flat €10, 2026-06-20→07-04. 98 fires → 93 resolved → 53W/40L (56.99%) → +€113.44 / +12.20% ROI (€930 staked; +€144.74/+14.77% incl. 5 open MtM). Median entry ~0.49, i.e. near-coin-flip prices with a >50% hit-rate = the shape of real edge OVER THIS WINDOW — but small sample (n=93), 2wk, World-Cup-clustered (correlated outcomes), and NOT a purged/embargoed harness (does not clear the purged-backtest-gate-no-edge +2.75%-random bar). Modeling optimism: enters at the 4th buyer’s FILL price (upper-bound vs the live best-ask) + holds to resolution (exit policy unwired) + flat €10 (≠ prod conviction sizing signals/sizing.rs). Does NOT overturn xref-consensus-rejected-fresh-copy-wins (consensus structurally late) — same upper-bound entry bias on a tiny correlated sample. No repo harness; standalone Python replay vs live APIs, artifacts ephemeral.

Architecture

  • cohort-mirror-featureNEW cohort /mirror per-trader copy-mirroring signal source (built + merged to main f385fbb, DORMANT). /mirror <user> <size> <limit> mirrors a single Polymarket user’s every trade — copies entries sized by conviction band (VLOW 0.2/LOW 0.5/NEUTRAL 1.0/HIGH 1.5/VHIGH 2.5 ×base; UNKNOWN→NEUTRAL), mirrors exits, caps concurrent open copies per user (skip at cap, no backfill). Third signal source source="mirror" reusing the producer/PM split. Second on-chain feed lane (Alchemy WSS run_feed, low latency) that is count-gated — a supervisor opens the WSS subscription only when ≥1 mirror is active (zero mirrors = zero CU), accepting the per-block Alchemy eth_subscribe(logs) CU cost (the same billing that drove the cohort lane to eth_getLogs polling, see watcher-onchain-gap-backfill) while active. Stable origin_ref = mirror|{wallet}|{condition_id}|{outcome_index}; PM matches exits by origin_ref+condition+outcome, NOT source. Tables pmv2_mirrors/pmv2_mirror_positions; 15-min cron resolves closed markets to free slots. New pmv2-contracts QueryKinds mirror/mirrorstop/mirrorlist. Dormant until POLYGON_WSS_URL set + PM adds a mirror pmv2_autotrade_sources row + contracts/connector land. Deferred: single-fill USD sizing under-sizes scale-in traders; /mirror stop leaves open copies to ride. Code crates/cohort_algo/src/pmv2/mirror/*.
  • cohort-algo-vs-polymarket-fetch-math-parity-auditcross-repo parity audit (2026-06-25): the NEW cohort_algo algo mirrors the PREVIOUS polymarket-fetch algo. Key ARCH insight — the old executor-side gate battery (chase/liveness/entry-band/category/min_conviction/ev_floor/stale) moved UPSTREAM into the producer (watch/cohort.rs signal_gate_decision+apply_io_gates) with identical formulas (matches the babylon #292 producer-owns-signal-gates split). Deliberate divergences = wire v1→v2 redesign (cohort.signalspmv2.order.{source}.{entry,exit}, schema 1→2, flat f64→nested market+Pricing/Decimal, mutually unparseable) + safety hardening (breaker fail-open→fail-closed; snapshot_trustworthy/should_record_exit exit guards). One possible accidental loss = the EV-floor gate dropped (operator chose report-only). Core decision math byte-for-byte identical (see the Algorithm Design entry).
  • pmv2-onchain-watch-crate-extractioncohort’s on-chain watch (decoder + resilient WSS feed) extracted into a new lean reusable git crate pmv2-onchain-watch so position_manager can run its OWN self-wallet watch by DEPENDING on it instead of copying the code (operator-directed, babylon #402, 2026-06-23; private github.com/wowjeeez/pmv2-onchain-watch @ canonical pin 1b8a0f9, was 5142d98). Review-pass cleanup (2026-06-23, pin 1b8a0f9, re-vendored at cohort 0fdfe8a, 295 tests green): a DRY/SOLID/bug pass (two parallel adversarial reviewers, findings verified vs disk) cut −378 lines (2079→1701) with the consumer surface UNCHANGED (safe in-place cleanup) — deleted 24 dead items carried verbatim from onchain.rs and unreachable from the one real entry run_feed (the whole newHeads/Detection/run_detector block-latency path + the SubCommand/dynamic-resubscribe path — run_log_feed_static hardwired a dummy cmd channel so the arm was dead — + a write-only ack registry + unused pub helpers order_filled_topic0/CONDITION_RESOLUTION_TOPIC0/watchlist_address_topics), extracted two DRY helpers (topic_hex for the ~5× 0x+hex pattern, watchlist_topic_slots as the single maker=slot2/taker=slot3 source for both subscribe + getLogs builders), and fixed three robustness bugs (proxied-hang trap: getLogs client now sets BOTH timeout+connect_timeout + OnceLock-reused; keepalive ping moved to a DEDICATED tokio::time::interval(PING_INTERVAL) arm — it was gated behind the 30s read-timeout arm so the 15s interval was dead under load; boot eth_block_number now retries 3×+warns instead of silently seeding the watermark to 0). Behavior preserved (only debug! observability added around the inherited price≥1 reject + both-watched maker-first pick). Lean: alloy-primitives+serde_json+tokio+reqwest+tokio-tungstenite, NO sqlx/gamma/data-api/NATS, #![forbid(unsafe_code)], edition 2024. Two modules: decode (moved out of polymarket-data/src/onchain.rs, renamed watchlist-neutral Cohort*Watched*: constants + OrderFilledEvent/decode_order_filled + classify + WatchedFill{party,side,token_id,price()/notional_usd()/shares()} + decode_watched_fill(event,&HashSet<Address,S>) generic-over-hasher + SeenLogs dedup + subscription builders) and feed (raw WSS loop moved + a NEW generalized run_feed(cfg:FeedConfig, watchlist:watch::Receiver<HashSet<Address>>, sink:mpsc::Sender<WatchedFill>) — subscribes OrderFilled filtered to the CALLER’s watchlist, decodes+dedups, delivers to caller’s sink; internalizes reconnect+backoff, stall watchdog 600s, live-watchlist resubscribe on watch change, backfill-on-reconnect over an eth_getLogs watermark, bounded drop-with-warn sink). cohort DOGFOODS it (cohort main @ 69a293a): dropped polymarket-data/src/onchain.rs + deleted watch/backfill.rs; vendored the crate as a workspace path-dep (crates/pmv2-onchain-watch, exactly the pmv2-contracts pattern); watch/ is now a thin run_feed consumer (spawn_cohort_refreshwatch::Sender cohort set 60s refresh → run_feedmpsc drains WatchedFills → handle_cohort). Behavior-preserving — logic byte-identical from markets.resolve onward, only the input type (OrderFilledEvent/CohortActionWatchedFill) changed; 306 tests, clippy -D warnings, fmt clean, dormant-safe. One feed, two callers (cohort first-mover/exit; PM self-wallet Plan 2, not built yet), zero copy. Supersedes the watcher internals in watcher-onchain-gap-backfill + Layout (now historical for cohort).
  • cohort-pre-live-signal-gatingcohort_algo PRE-LIVE SIGNAL-GATING: the 4 first-mover gates that were blocking arming are CLOSED (e444af0, 2026-06-23). All 4 sit in the first-mover ENTRY decision path and are FAIL-CLOSED (any fetch fail/timeout/missing ⇒ Skip, never emit blind — PM can’t catch them, no GammaClient): (1) category_filter (CategoryFilter{include,exclude}+passes_filter over esport/sport/geo/crypto/other; resolve_category cache-first pmv2_event_category→gamma event_tagscategory_bucket, empty event_slug short-circuits to skip a doomed RTT); (2) chase/max_entry_premium_bps (chase_gate: skip if best_ask>avg_price·(1+premium/10_000)); (3) market liveness (liveness_gate: active+accepting+!closed+!resolved+!resolution_proposed + min secs-to-resolution; status from the new CLOB read client, resolution-proposed from gamma umaResolutionStatuses); (4) the degenerate ev_floor_gate DROPPED (size cancels → price-blind bps>200; operators set max_entry_premium_bps net of cost; min_round_trip_edge_bps loaded-but-unused since pmv2_autotrade_config is FROZEN). signal_gate_decision made async + staged cheapest-first (trust gates → category cache → concurrent tokio::join! best_ask+market_status+resolution_proposed → pure apply_io_gates). Re-adds a read-only ClobReadClient to polymarket-data (best-ask via /book, market-status via /markets/{cond} — public endpoints, no signing/auth; DELIBERATELY relaxes cohort’s zero-CLOB invariant for best-ask accuracy). Closes deploy precondition (c); cohort still mode OFF.
  • cohort-algo-v2-wire-contract-cutovercohort_algo v2 WIRE-CONTRACT CUTOVER DONE (0986c4d, 2026-06-23). cohort emits the v2 trade contract (schema_version 2) on pmv2.order.first_mover_core.{entry,exit} instead of old cohort.signals schema_v1; vendored the shared pmv2-contracts crate (blocker resolved). Cohort is the Pricing::Derived arm (reference_price+conviction+source_score+size_usd → PM derives the limit). Producer now OWNS sizing (signals/sizing.rs, formula ported from the retired executor; portfolio caps EXCLUDED — those stay with PM) + signal-level gating (signals/gates.rs, per babylon #292: producer owns signal gates, PM owns mode/breaker/caps). 4 PRE-LIVE gating gaps flagged (category_filter / chase / market-status / a real EV floor) — must close before arming, PM can’t catch them.
  • cohort-algo-shared-db-migrator-ownershipbabylon #331 (2026-06-23): PM owns the FULL pmv2_autotrade_* migrator; producers DROP that DDL and treat the tables as a read-only precondition. Shared-DB safety = each repo uses a DISTINCT sqlx migration-history table (refines “one migrator per DB” → “one migrator per history table”). RESOLVES the position-manager-interface-design “physical relocation DEFERRED” hold. FREEZE on pmv2_autotrade_* schema until PM’s migrator ships.
  • telegram-connector-outbound-designDESIGN (approved 2026-06-22) — telegram_connector is the pmv2 fleet’s SINGLE Telegram I/O plane: sole bot-token holder + sole Telegram sender (single rate-limit authority), holds NO trading logic, NO DB, stateless v1. Own repo /coding/pmv2/telegram_connector. v1 = OUTBOUND ONLY: a durable JetStream PULL consumer tg_outbound on telegram.outbound (stream TELEGRAM_OUTBOUND, 1d, 5-min dedup) → Bot API sendMessage. A DUMB faithful relay — passes text + publisher parse_mode verbatim, NEVER re-escapes (escaping is the publisher’s job). Delivery: at-least-once, ack-AFTER-confirmed-send; transient (429+retry_after/5xx/net) → in-process retry then redeliver bounded by max_deliver; terminal (400 bad-format / 403 blocked) → drop+ack+ERROR + best-effort loop-guarded plain-text meta-alert. Sequential (one in-flight), reactive rate-limit only (no token bucket = YAGNI). Relay loop behind a MessageSource trait → broker-free unit tests; wiremock for Bot API; optional NATS testcontainer. Envelope TelegramOutbound{msg_id,chat:Option,text,parse_mode:Option,kind:Option} field-matched to cohort_algo/.../telegram_outbound.rs until pmv2-contracts publishes (then vendor). Inbound (getUpdates→auth→ops.*) = separate later spec, gated on @positionmanager locking ops.* (#311). Follows the fleet NATS conventions.
  • pmv2-fleet-infra-observability-nats-conventionsFLEET-WIDE REFERENCE (from @deploy, babylon pmv2 #313, 2026-06-22) — applies to ALL pmv2 services (cohort_algo, position_manager, weather, crypto, telegram_connector). Observability: traces+logs+metrics → SigNoz Cloud VIA the LOCAL OTel collector on host polymarket-infra (NEVER ship direct); OTLP gRPC 127.0.0.1:4317 / HTTP 4318, Docker host.docker.internal:4318; std env OTEL_SERVICE_NAME/OTEL_EXPORTER_OTLP_ENDPOINT/OTEL_EXPORTER_OTLP_PROTOCOL/OTEL_RESOURCE_ATTRIBUTES=service.namespace=pmv2,deployment.environment=prod (auto-injected for ansible apps-role containers; native/new repos opt in via SDK init). Logging: ERROR=operator-visible failures, WARN=non-fatal anomalies (rate-limit/retry/dedup-skip), INFO=state transitions only (metered, never per-message >~1/s), DEBUG=per-message behind RUST_LOG; log key fields as ATTRIBUTES not embedded strings; NEVER log secrets/PII (keys, NKey seeds, bot tokens, DB URLs — log the NKey PUBKEY not the seed). NATS: always set Nats-Msg-Id (5-min dedup window on all 3 streams); streams TELEGRAM_OUTBOUND(1d)/OPS(ops.commands+ops.results, 7d audit)/PMV2_ORDERS(pmv2.order.<source>.<entry|exit>, pending PM consumer)/legacy WEATHER_SIGNALS; each consuming service creates its OWN durable consumer with a stable name, producers DON’T create streams. Tracing: one span tree per signal lifecycle, span names=function role, data=attrs (tracing+tracing-opentelemetry+opentelemetry-otlp). Health golden rules: docker ps/systemctl is-active=ground truth, journald=universal fallback, SigNoz=second look, nats stream/consumer info w/ admin seed sparingly. Dashboards: “polymarket-infra — Host & Tailscale” / ”— App services (APM)” / ”— NATS + JetStream”. Coordination: pmv2-contracts confirmed standalone repo (#310); inbound ops.* contract still open w/ @positionmanager (#311).
  • cohort-algo-componentcohort_algo is now a STANDALONE repo/component (2026-06-22): /Users/levander/coding/pmv2/cohort_algo, private github.com/wowjeeez/pmv2-cohort-algo, main @ 2e150b1. The pmv2 fleet’s cohort-intelligence SIGNAL PRODUCER — does NOT place trades (zero CLOB dep). Five responsibilities: (1) scoring cron → pmv2_traders/leaderboard (gated by pmv2_runs); (2) consensus alerts (Pmv2Consensus cron + on-chain ConsensusTracker); (3) first-mover ENTRY (Core-tier first BUY → NATS action:Buy); (4) cohort EXIT (tracked trader sells → NATS action:Sell); (5) on-chain WSS watcher + 30s eth_getLogs gap-backfill. Own cargo workspace (edition 2024, resolver 3, unsafe_code=forbid, sqlx/tokio/anyhow/tracing/async-nats 0.49+nkeys; members crates/cohort_algo+crates/polymarket-data); vendors polymarket-data internally. 278 tests, clippy+fmt green, dormant-safe (POLYGON_WSS_URL/NATS_URL unset ⇒ idle, no panic). PORT = behavior-preserving from the retiring polymarket_fetch carve (483350c) — preserved internal crate::pmv2:: paths + the polymarket-data crate name ⇒ ZERO logic edits. Pre-port review fixed 4 bugs (empty-Data-API 200 [] mass-false-exit; dropped-publish-still-deletes-row; entry double-publish→watcher sole producer; autotrade/signals/ purge keeping only the shared pmv2_autotrade_* table names). ARCH (babylon #286): polymarket_fetch RETIRES → component repos in /coding/pmv2 on a NATS spine; producers own strategy+sizing; position_manager CONSUMES NATS DIRECTLY (no runner) + owns telegram.outbound, an ops.commands/ops.results kill-switch consumer, and the pmv2_autotrade_* migrator. v2 WIRE (babylon 292): subjects pmv2.order.<source>.<entry|exit>; ONE entry envelope w/ pricing discriminator limit (weather/crypto) | derived (cohort: PM derives limit from live book + $→shares + lot-size); lean exit-signal (condition_id+outcome_index+origin_ref); shared pmv2-contracts crate = source of truth (vendor it). Cohort gotchas: source MUST be the EXACT source_id key (first_mover_core — wrong value silently routes to Mode::Off); exit MUST carry origin_ref; producers OWN ev/chase/category gating (PM has no GammaClient). CURRENT = old cohort.signals schema_v1, telegram.outbound seam prepared but Direct-send default. NEXT (planned, BLOCKED on @positionmanager publishing pmv2-contracts): publish pmv2.order.cohort.{entry,exit}, schema_v2, vendor pmv2-contracts, ADD producer-side size_usd+valid_until_ts+ev/category gating. Ported FROM the in-monorepo carve cohort-signals-service-split-and-exit-signals; feeds the executor position-manager-interface-design.
  • cohort-signals-service-split-and-exit-signalsARCHITECTURE MILESTONE (2026-06-22) — pmv2 monorepo split into independent services BY CONCERN (execution=@deploy’s position_manager; signal producers; a notification service). This session carved ONE producer: cohort-signals (branch worktree-cohort-signals, from main @ 43023ba). (1) CARVE (8cd6f58, gate green): repo reduced to ONLY cohort scoring (Pmv2Collect, kept so the slice is self-contained), consensus (Pmv2Consensus cron + on-chain ConsensusTracker), first-mover ENTRY, cohort EXIT detection. ~13,400 lines DELETED (whole autotrade engine autotrade/{engine,execute,exits,runner,reconcile,settle,preflight,sizing,pricing,orders,…}, onchain_book triggers, Telegram bot, self-trade & overlap watchers, resolution-detection, trending, bet/market scoring, tracked-detector lane). 3 workspace crates dropped: polymarket-clob (CLOB signing — NOT a dependency of a signal producer at all), pm-ws, lag-probe. The pmv2-watch binary was ONE service mixing cohort-signal lanes (consensus/first-mover/exit) WITH execution-trigger lanes (self-trade/fill-reconcile/settlement) — exec lanes stripped, the rest + gap-backfill sweep preserved BYTE-FOR-BYTE. Method = compile-driven (delete callers, follow errors); KEEP-WHOLE files diff-clean vs base. (2) COHORT EXIT SIGNALS (e762f5d, gate green 153+107): “watch trade exits the same way as first-mover entries.” Prior on_cohort_sell was DB-only (log EXITED + delete rows). Now PUBLISHES action:Sell to NATS cohort.signals AND telegrams so the out-of-process executor closes the copied position. sources::exit_signal(PositionRow) (Execution::Context, reference_price=position.cur_price). CRUX — the exit signal_id carries an exit discriminator DISTINCT from the entry signal_id for the same (condition,outcome); else the consumer’s ON CONFLICT(dedup_key) silently swallows the Sell as a dup of the entry Buy = capital trap. Test-locked exit_signal_id_differs_from_enter_signal_for_same_market. DRY: entry+exit share publish_cohort_with_retry (retry/backoff + drop-alert). Ordering publish-BEFORE-delete. The eligible_sell+ExitDebounce gate kept unchanged (NOT narrowed to Core-tier). PROVISIONAL contract — @deploy’s position_manager must consume action:Sell idempotently; not yet coordinated. NOT merged/deployed. The PRODUCER half of the producer→executor split (executor = position-manager-interface-design); transport = the cohort.signals lane from cohort-firstmover-nats-migration.
  • position-manager-mode-control-implementedOUTCOME (2026-06-22): the MODE-CONTROL feature (off/dry/live kill switch) is IMPLEMENTED — 12 tasks via subagent-driven dev + per-task opus reviews on logic tasks + final whole-branch opus review (READY TO MERGE, no Critical/Important). Builds on the execution migration. position_manager is now the SINGLE execution authority: place_order resolves the authoritative mode fresh per call (global × per-source (mode,enabled) + breaker) and gates at the POST boundary — Off→Skipped (zero work), Dry→reserve dry_run row + build+sign rehearsal (no POST/caps/breaker), Live→reserve(live)+caps→place. Reserve+caps+breaker MOVED out of fetch’s engine/runner into PM (SQL byte-identical); the runner is now mode-agnostic (builds EntryIntent/ExitIntent, calls pm.place_order, maps the typed ExecOutcome to a NATS ack). Correctness: entries gate on effective_mode(global,source); EXITS gate on the row’s recorded mode (a live position stays sellable when global=Off — the E1 fix); enabled=false⇒Off now enforced (was dangling); pre-POST reserve/caps DB error→TransientNak(30s); recorded-failure/benign-race→Ack (no double-trade); fail-closed-to-Off two paths; cap:event literal. State: committed on main BOTH repos (PM 351a755, fetch 4fde695), NOT pushed, mode OFF. Gate: 66 PM offline + 532 fetch + 7 db-tests-vs-real-Postgres pass. OPEN (callout in note): (1) commit contamination — 4fde695 swept in the user’s unrelated in-progress exit_signal/cohort_exit_signal_id work (called by uncommitted firstmover/alerts code) — pending split/keep; (2) follow-ups: delete dead orders.rs/EngineConfig cap helpers, add host-injectable signer seam for live-POST e2e; (3) pre-arm: live-test PM notify→Telegram before arming.
  • position-manager-execution-migration-implementedOUTCOME (2026-06-21): the PM execution migration (Plan 1) is IMPLEMENTED — all 11 tasks via subagent-driven dev + opus reviews, final whole-branch review READY TO MERGE. New position_manager crate owns the polymarket-clob signing client (use_server_time OFF), place_order buy+sell, ledger code, onchain eth_call (+connect_timeout), load_caps/load_breaker, and the declared-but-unimplemented FillSink/FillEventSink watcher seams; polymarket_fetch rewired one-way, submit_live/auto_sell/execute.rs deleted. Behavior-preserving (11 ledger SQL + notify strings byte-identical, locked clob hash tests intact); gate green PM 43+5 / clob 11 / fetch 529-0. On main BOTH repos (PM cbd0f4c, fetch afc785d), NOT pushed, mode OFF, nothing armed. OPEN DECISIONS: caps expose-only DoD gap, pre-arm notify→Telegram live-test, accept breaker-vs-no-signer check-order flip. FOLLOW-UPS: SigningClient Clone, host-injectable signer seam.
  • pmv2-polymarket-clob-crate-migrationpmv2 Task 2 (2026-06-21, cdfcca8): polymarket-clob crate physically moved from polymarket_fetch into position_manager workspace; use_server_time flipped true→false (drops serial GET /time, ~20–100ms saved, EIP-712 bytes unchanged); self-referential include_str! test fixed with split-needle; all 11 tests green. polymarket_fetch intentionally left un-buildable — do NOT fix its Cargo.toml.
  • position-manager-interface-designDESIGN (2026-06-20) — new position_manager (PM) crate: a focused Polymarket EXECUTION+WATCH crate (NOT a full engine rehome) that the deferred autotrade→position_manager rename lands into. PM OWNS place_order (build/sign/POST+record), the position ledger, portfolio caps + circuit-breaker, self-wallet reconciliation, watched-tx detection. Strategy STAYS upstream in polymarket_fetch (detection, gates, EV/conviction/sizing, first-mover gating, NATS Pmv2Consumer, mode). Dependency is one-way in-process polymarket_fetch→position_manager; polymarket-clob (vendor rs-clob-client-v2) MOVES INTO PM (leaf crate). API: place_order(OrderRequest)->PlacedOutcome migrates BOTH today’s submit_live (buy) AND auto_sell (sell) — the whole lifecycle buy→submitted→filled/partial→open→selling→sold lives in PM (FAK, proxy sigType 1, V2 contracts preserved); OrderRequest carries reserve_id (acts on an already-inserted reserve row) + a buy/sell field split, NO provenance block. watch_self_wallet (CLOB /user WS + periodic on-chain balanceOf truth check; chain authoritative on divergence, emit reconcile never silent), watch_transactions (the UNBUILT Phase-2 OrderFilled WSS detector over a configurable address set → raw FillEvent). Sinks are TRAITS (FillSink/FillEventSink) so a later separate-watcher-process split is cheap. Post-audit (2026-06-20) — migration ownership WALKED BACK: PM owns the ledger CODE only and runs NO sqlx::migrate!; pmv2_autotrade_orders DDL stays with the single existing upstream migrator (it’s a sqlx trap to run two — see sqlx-shared-migrations-table-trap); physical relocation DEFERRED. pmv2_autotrade_config is CO-OWNED (PM reads caps, writes breaker counters); PM is the SOLE counter of exposure/open positions. Latency win folded in: use_server_time(false) on the clob client (polymarket-clob/src/lib.rs:329) drops a serial GET /time before every order POST (~20–100ms; signed-order bytes unchanged). Behavior-preserving, proven by GOLDEN-PARITY tests (same OrderRequest → same maker/taker/contract triple; buy transitions pending→submitted→filled|partial→open, sell open→selling→closed). Three sequenced plans: (1) execution migration [written], (2) self-wallet watcher, (3) watched-tx watcher. Preserves V2 hard-cutover correctness (V2 verifyingContract addrs, pUSD collateral) + the connect_timeout rule (BOTH timeout AND connect_timeout on every reqwest client — the 75-min silent-hang lesson). Source: spec + Plan 1 in pmv2/position_manager/docs/superpowers/.
  • bot-pmv2-native-v1-consensus-deletedbot + CLI self-status fully pmv2-native; LAST v1 consensus code DELETED (commit ec75491, 2026-06-03, net −156 lines). The v1 per-position “consensus alignment” view (read from frozen pm_selected_wallets/pm_safe_bets/pm_trader_metrics) is replaced by a pmv2 cohort-overlap view: per operator position, how many edge-cohort traders hold the same (condition_id, outcome_index) + conviction-weighted bet_score, reusing the bet-link scoring path. New CohortOverlap + overlap_by_position() (pmv2/scoring.rs), cohort_holders_for_conditions() (pmv2/repo.rs, batched WHERE condition_id = ANY($1)), a single shared fetch_self_overlap_sections feeding BOTH bot + CLI (DRY, best-effort: degrades to “no overlap”, never fails). Deleted the entire v1 consensus chain (ConsensusAlignment, fetch_consensus_alignment, classify_consensus, fetch_safe_bets_by_market, fetch_raw_watched_consensus, alignment renderers). This is the FINAL code-side step of the v1 decommission — v1 now fully gone from CODE (only dormant pm_* tables remain in DB). Resolves the “loose end carried into prod” noted on first-mover-overlap-watcher.
  • first-mover-overlap-watcherfirst-mover OVERLAP detection (Approach A — event-log join), shipped 2026-06-02 (4794a76, migration 20260603000000_pmv2_overlap in prod). Reframes the v1 self-wallet consensus watchers. run_pmv2_positions is the source of truth for cohort ENTER/EXIT → an append-only pmv2_position_events feed; detection/logging DECOUPLED from the copy gate (should_detect(prior_len)=prior_len>0), only the paper-copy ACTION stays gated on pmv2_copy_config.enabled (events populate with the copy lane OFF). New detect_exited; snapshot is a true current-state mirror (delete cohort positions absent from a fresh fetch) but scoped to wallets that successfully fetched (fetch_positions(rows, succeeded: HashSet)) so a timed-out wallet can’t fire false exits / mass-delete. Events denormalized at log time (run_id, trader_score, avg_won/lost_bet_size) so they outlive cohort membership. New module overlap_watcher.rs: cursor-based join (pm_detection_cursors key 'overlap_watcher') of the event feed against the operator’s own book → CONFIRM (same-side ENTER) / FOLLOW_OUT (same-side EXIT) / AGAINST (opposite-side ENTER); opposite-side EXIT NOT alerted; dedup via pm_overlap_alerts UNIQUE(event_id,self_wallet). Operator-side new-position alert (repo::self_new_positions, latest-two self_snapshot diff, first-run suppressed) → ACTIVE notifier (run_self_sync_for_scheduler takes both notifiers); self_trade_changes repointed to live DataApiClient::trades. Removed change_detection.rs + consensus_participant_changes.rs. Subagent-driven (12 tasks), 0 test failures.
  • pmv2-pipeline-bug-auditrepo::load_event_winners is SHARED between the watcher resolution/PnL pass (pmv2/positions.rs) and the validated edge/consistency trader scoring (pmv2/mod.rscompute_trader_metrics). The 50/50 payout-DESC tiebreak + NaN-drop guard are therefore a frozen interface: changing winner-selection shifts every trader’s edge and breaks the validated pmv2-two-tier-trader-scoring algo (any winner-selection change ⇒ re-validate scoring). Sharp edge for future resolution work.
  • pmv2-two-tier-trader-scoringv1 → pmv2 hard cut complete (2026-06-02): pmv2 is now the entire system. The legacy pm_* pipeline was deleted in code (~27k lines / 4 commits, ending 5ffc330) — the pm-arb crate, backtest/short-term/wallet-monitor, and the consensus/safe-bets/trades pipeline + polymarket-data consensus.rs/safe_bets.rs are gone. scheduled-run now runs the pmv2 pipeline (resolutions-sync → pmv2-collect → pmv2-positions → self-wallet steps; self-wallet retained). pm_* prod tables left dormant, not dropped.
  • copy-trade-lane — copy-lane data flow: detector (polymarket-data/onchain.rs, Polygon WSS eth_subscribe logs + newHeads block-ts cache) → copy_loop (pm-arb/copy_lane.rs, spawned in validate::run) → decode → token→market map (rotation poller) → gated evaluate_copy (pm-arb/copy.rs) → lane='copy' rows in pm_paper_trades held to resolution → resolver (subtracts PM crypto taker fee, no-op for existing lanes).
  • cohort-firstmover-nats-migrationcohort first-mover ENTRY ingestion moved from an in-process call chain (run_pmv2_positionsrun_autotrade_entryrunner::run_entry→engine) to a NATS publish on cohort.signals, consumed by the SAME Pmv2Consumer as @weather — one unified ingestion lane routed by the envelope source field; runner::run_entry deleted. Pairs with pmv2-position-manager-nats-consumer (which built that consumer + lane). Branch autotrade-foundation, NOT deployed. Transport-only change: the engine Context arm, wire::parse_signal, and submit_live are untouched.
  • watcher-onchain-gap-backfillwatcher on-chain GAP-BACKFILL sweep (shipped main 43023ba, 2026-06-20, NOT redeployed; 563+107 gate). A 30s periodic eth_getLogs reconciliation over [last_seen_block, latest] (new file crates/polymarket-fetch/src/pmv2/watch/backfill.rs) that recovers cohort/self/our-fill OrderFilled logs the Polygon WSS feed (run_log_feed, polymarket-data/onchain.rs) drops during disconnects — eth_subscribe auto-reconnects but NEVER replays gap logs, so they were permanently lost. Re-fetches the cohort across BOTH maker+taker topic slots (mirrors live build_logs_payload) and feeds each recovered log through the IDENTICAL worker path as a WatchEvent. New onchain.rs primitives: eth_block_number, eth_get_order_filled_logs, cohort_address_topics, build_get_logs_filter (modeled on eth_call_u128); reuses the HTTP POLYGON_RPC_URL (not the WSS feed). Periodic sweep, NOT an on-reconnect hook, on purpose: catches ALL gap causes (disconnect, silent frame drop, provider hiccup); lives entirely in the watcher (no surgery on the shared feed crate); simpler to test. Watermark last_seen_block: Arc<AtomicU64> advanced by fetch_max in handle_order_filled after decode; from = watermark INCLUSIVE (recovers intra-block partial-delivery gaps, dedup absorbs it); MAX_BACKFILL_BLOCKS=2000 cap with a log on hit; try_send so the sweep never blocks the worker. Plan docs/superpowers/plans/2026-06-20-pmv2-watcher-gap-backfill.md.
  • autotrade-source-extensibility-and-global-capsadding a Directive-shaped autotrade source is GENERIC (zero engine/runner/sizing changes): only (a) a parse branch in wire.rs::parse_signal producing a TradeSignal with the new source_id + (b) a row in pmv2_autotrade_sources. Mode resolves generically in runner.rs::per_message_mode (repo::autotrade_sourceconfig::effective_mode more-conservative floor off<dry<live); missing row + failed config load FAIL-CLOSED to Mode::Off. DRY: weather+crypto_shortterm_latency_test share one parse_directive(p,action,source_id) (2026-06-19). A mode='dry_run' row is a safe DRY-PIN (generic, any source) — can’t reach live until the row is explicitly flipped; but crypto_shortterm_latency_test is NOT dry-pinned — seeded mode='off' at rest yet LIVE-capable (dry can’t measure fill latency, no order submitted), so it runs LIVE under tight global caps. Pairs with pmv2-position-manager-nats-consumer (defines that routing + effective_mode).
  • polymarket-fetch — workspace layout, Selector trait (4 impls), auto-chain pipeline overview, trades fetcher, metrics layer, Telegram adapter
  • autotrade-authz-moduleauthz.rs Task 2 (2026-06-14): pure authorization module; Scope, Authz, OwnerSet, authorize(), scope_for(); the owner-only command gating reused by the autotrade engine. Consumed by Task 3 (run_command wiring).
  • pmv2-autotrade-engine-designpmv2 Auto-Trade Engine — the first LIVE-money lane (design LOCKED 2026-06-14). A source-agnostic engine running alongside (not replacing) the paper lane: sources emit a normalized TradeSignal, engine::ingest(&TradeSignal) is the single pluggable seam; first source = first-mover copy of Core-tier cohort traders, with a second exit source over pmv2_position_events (own cursor). New units: crates/polymarket-clob/ (vendored rs-clob-client-v2, the only place that signs) + pmv2/autotrade/{mod,sources,config,engine,orders,reconcile,commands,preflight,onchain}.rs + authz.rs. Isolation guarantee: frozen scoring/topbets/consensus/decide_copy math UNCHANGED — only additive read-path touches (tier into the cohort read, bet_confidence computed unconditionally, a new exit read). New economic gates live AROUND decide_copy, never inside. Spec at docs/superpowers/specs/2026-06-14-pmv2-autotrade-design.md (3 adversarial review passes).
  • pmv2-exit-alertsexit alerts (2026-06-04, module pmv2/exits.rs): a non-fatal scheduled step (after the trending alert) that READS the position watcher’s pmv2_position_events EXITED feed and pushes a ”🔴 TRADE EXITED” Telegram alert. Top-trader gated, cursor-deduped (pmv2_exit_alert), dust-filtered, budget-capped; shows last-seen P/L (cash_pnl). Migration 20260604000000_pmv2_event_pnl.sql denormalizes the P/L columns onto the event in insert_position_event (no call-site change). Caveat: exits are snapshot-detected, so the P/L is “last seen” not a realized fill.
  • category-consensuscategory consensus (2026-06-05, module pmv2/category_consensus.rs): async /consensus <category> [N] [K] — replies instantly + tokio::spawns a job that fetches a top-N category-leaderboard PNL cohort, fans out their positions (buffer_unordered(16) + 30s per-wallet timeout), filters to in-category bets via gamma event_tags, computes ≥K-wallet agreement (group by (condition_id, outcome_index), rank agreement-then-stake, top 25), persists (pm_category_consensus_runs/_rows, migration 20260605010000), pushes. 10-min cache. No edge/band (these traders aren’t in pmv2_traders). Replaced the short-lived tag-based tag_consensus SQL query. Distinct from v1 wallet-consensus AND pmv2 cohort-overlap.
  • pmv2-bot-startup-pingbot startup ping + build.rs SHA capture (2026-06-04): bot boot sends a best-effort NA CSUMI CSUMI CSUMI, bot STARTED, REF: <short-sha> via notifier::startup_message (build_startup_message is the pure builder, omits REF: when empty). A new crates/polymarket-fetch/build.rs bakes the short commit hash at compile time (env BUILD_GIT_HASHGIT_HASHGITHUB_SHAgit rev-parse --short HEAD→empty). Bot-only; scheduled-run untouched.
  • pmv2-trending-betstrending velocity signal (shipped 2026-06-03, 2c05c5a..4906eea): run_pmv2_positions joiner-seed fix (is_new_entry — only emit ENTERED for wallets already in the prior snapshot, so cohort reshuffles can’t fabricate entry bursts) → trending_bets query counting distinct high-score cohort wallets entering a bet in a 6h window vs prior → /trending bot command + pmv2-trending CLI + acceleration auto-alert (12h cooldown dedup in pm_trending_alerts, Telegram-size-guarded against an infinite re-alert loop).
  • pmv2-topbets-categories-filters/topbets categories + filters + price + multi-date (shipped 2026-06-03, 5c933a5..908824f): pmv2_event_category cache (populated by run_category_sync off gamma /events tags) feeds eq:/neq: bucket filters; entry→current price display; load_bet_holders resolves a selector to an event_slug set and returns all sibling markets (multi-date fix).
  • pmv2-exit-alertsexit alerts ”🔴 TRADE EXITED” (2026-06-04): a non-fatal scheduled step (after the trending alert) that READS the pmv2_position_events EXITED feed (logged by the position watcher) and pushes a Telegram alert when a top cohort trader leaves a bet. Top-trader gated (score > copy threshold), cursor-deduped (pm_detection_cursors key pmv2_exit_alert), dust-filtered (≥$50), Telegram-budget capped. Shows trader name/score/band/rank + market/outcome + P/L (last seen) = the position’s last-snapshot cash_pnl. Module pmv2/exits.rs; pmv2-exits CLI = read-only 24h preview.
  • category-consensusfetch-based async /consensus <category> [N] [K] (2026-06-05, module pmv2/category_consensus.rs): validates one of the 9 leaderboard categories, serves a 10-min cached run else replies instantly + tokio::spawns a job that fetches the top-N category leaderboard PNL cohort → fans out their positions (buffer_unordered(16) + 30s per-wallet timeout) → filters to in-category bets via gamma event_tags → consensus (group by (condition_id, outcome_index), ≥K distinct wallets, rank agreement-then-stake, top 25) → persist → push. No edge/band (these traders aren’t in pmv2_traders); a freshly-fetched, category-specific PNL cohort — distinct from v1 wallet-consensus AND pmv2 cohort-overlap. Replaced the short-lived tag-based tag_consensus SQL query. Does NOT touch topbets/scoring.
  • pmv2-bet-link-analyzer-flowthe bet-link auto-reply path end-to-end (verified main 2026-06-15, fallback shipped 22d7697): a posted Polymarket link → bot.rs::classify_messageMessageAction::BetLinkrun_score_bet_link/score_bet_link_body (bot.rs:611) → pmv2::scoring::score_bet (scoring.rs:414, reads pmv2_positionspmv2_traders via load_bet_holders) → format_bet_scores_telegram (scoring.rs:593). This reads the EDGE-scored pmv2 cohort (skill-weighted exposure gauge) — a DIFFERENT cohort from /consensus <category>’s freshly-fetched monthly-PNL cohort. Two distinct no-signal states: scores.is_empty() “No edge-cohort positions…” (scoring.rs:514) vs holders-all-below-GAUGE_FLOOR=0.005 → empty consolidate_gauge.sides “no high-edge cohort holders…” (scoring.rs:472, gate scoring.rs:379; consolidate_gauge is empty-safe). Cross-references the two coexisting category taxonomies and four gotchas. Category-cohort fallback IMPLEMENTED (22d7697): when !has_high_edge_signal, resolve_fallback_category + category_consensus_section (via run_and_persist) appends a consensus section to the same reply; with-signal replies unchanged.

Auto-Trade / Live Execution

  • cohort-firstmover-lane-mode-restart-gotchathe first-mover publish-lane mode (off/dry/live) is computed ONCE at process boot (watch/mod.rs build_first_mover_lane) and is NOT hot-reloaded — flipping the mode in pmv2_autotrade_config / the first_mover_core pmv2_autotrade_sources row requires a firstmover RESTART to take effect (confirm via the first_mover_core armed INFO appearing post-change). lane_active = source_enabled && effective_mode(global,source) != Off; both enabled=true AND effective mode≠Off build the NATS publisher (positions.rs:224), else it’s None + a DEBUG dormant log. Only the watchlist + breaker_tripped hot-reload; mode does not. (2026-06-25, operational runbook)
  • cohort-pre-live-signal-gatingthe 4 PRE-LIVE gating gaps blocking cohort from arming are CLOSED (e444af0, 2026-06-23) — this closes deploy precondition (c). category_filter + chase (max_entry_premium_bps) + market-liveness gates implemented FAIL-CLOSED in the first-mover ENTRY path; the degenerate ev_floor_gate DROPPED (operators set max_entry_premium_bps net of round-trip cost instead — one price-aware knob, no size-cancels-out math). Gates run producer-side because PM has no GammaClient to catch them. Re-added a read-only ClobReadClient (best-ask via /book, market-status via /markets/{cond}; NO signing — NOT the deleted execution client) so the chase gate compares against the REAL ask we’d pay; entry_price_band_gate repointed to gate best_ask not the trader’s fill. signal_gate_decision is now async + staged cheapest-first (skips the network calls when an earlier gate rejects). Startup announcement on telegram.outbound (component: first_mover_core started with version: <hash>, build.rs-stamped, restart-deduped). cohort STILL mode OFF; live-arming now waits only on PM’s migrator inserting the first_mover_core pmv2_autotrade_sources row + operator go-ahead.
  • cohort-algo-v2-wire-contract-cutovercohort_algo now emits the v2 LIVE-EXECUTION trade contract (0986c4d, 2026-06-23): schema_version 2 EntryOrder(Pricing::Derived) on pmv2.order.first_mover_core.entry + lean ExitSignal on …exit → PM (the executor that consumes these directly) derives the limit, does $→shares+lot-size, gates mode/breaker/caps. Producer-owned size_usd sizing (ported formula, caps EXCLUDED) + producer-owned signal gating (babylon #292). valid_until_ts=now+300s on entries (first-mover edge decays), None on exits. Mode still OFF; 4 PRE-LIVE gating gaps (category_filter / chase=max_entry_premium_bps / market-open-status / a real EV floor — the ported ev_floor_gate is degenerate) MUST close before arming because PM has no GammaClient to catch them.
  • telegram-connector-outbound-designthe fleet NOTIFICATION plane (2026-06-22). Trading-side producers (cohort_algo entry/exit alerts, position_manager fills, weather/crypto) NEVER call the Bot API directly — they publish TelegramOutbound to NATS telegram.outbound and telegram_connector (sole bot-token holder) relays it. This is the destination of the telegram.outbound seam prepared in cohort_algo (currently Direct-send default — see cohort-algo-component). The connector is dumb (verbatim relay, no escaping), at-least-once with ack-after-send, terminal-drop meta-alert so a swallowed trade alert stays visible. v1 outbound-only; inbound ops.* kill-switch relay (ops.commands/ops.results) is a later spec gated on the @positionmanager contract (#311).
  • cohort-algo-componentthe PRODUCER side made standalone (2026-06-22): cohort_algo (github.com/wowjeeez/pmv2-cohort-algo @ 2e150b1) emits first-mover ENTRY (action:Buy) + cohort EXIT (action:Sell) signals to NATS but NEVER signs/POSTs — zero CLOB dependency. Execution lives ENTIRELY on the executor side (position_manager, which CONSUMES NATS directly — no runner). The v2 wire-contract (babylon 292) makes cohort the derived pricing discriminator (reference_price+conviction+size_usd → PM derives the limit from the live book + $→shares + lot-size); the lean exit-signal carries condition_id+outcome_index+origin_ref → PM resolves OUR position row + full-exits. Cohort gotchas for arming: source MUST be the EXACT source_id key (first_mover_core, wrong value silently routes to Mode::Off); the exit MUST carry origin_ref (else the position can’t be auto-closed = capital trap); producers OWN ev/chase/category gating (PM has no GammaClient). Today on the OLD cohort.signals schema_v1; v2 cutover BLOCKED on @positionmanager publishing pmv2-contracts.
  • cohort-signals-service-split-and-exit-signalsthe PRODUCER/EXECUTOR boundary made concrete (2026-06-22, branch worktree-cohort-signals). The monorepo split BY CONCERN means execution now lives ENTIRELY on @deploy’s side (position_manager): this cohort-signals carve (8cd6f58) DELETES the whole autotrade engine + polymarket-clob from the producer (~13,400 lines; a signal producer never signs/POSTs). Live execution is no longer in this repo at all — it consumes TradeSignals off cohort.signals. The new piece is cohort EXIT signals (e762f5d): on_cohort_sell now publishes action:Sell (was DB-only per pmv2-phase4-chain-cohort-sell-fast-exit) so the executor can close a copied position, with the exit-discriminator dedup gotcha (entry Buy vs exit Sell must NOT collide on dedup_key). PROVISIONAL contract: executor must consume action:Sell idempotently — not yet coordinated. NOT merged/deployed.
  • position-manager-mode-control-implementedIMPLEMENTED (2026-06-22) — MODE-CONTROL (off/dry/live kill switch) DONE (12 tasks, per-task + whole-branch opus reviews, READY TO MERGE). position_manager is the SINGLE execution authority: place_order resolves the authoritative mode fresh per call (global × per-source (mode,enabled) + breaker) and gates at the POST boundary — Off→Skipped (zero work), Dry→reserve dry_run+build+sign rehearsal (no POST/caps/breaker), Live→reserve(live)+caps→place. Reserve+caps+breaker MOVED into PM (SQL byte-identical); the runner is mode-agnostic (EntryIntent/ExitIntentpm.place_order→typed ExecOutcome→NATS ack). Entries gate on effective_mode(global,source); EXITS gate on the row’s recorded mode (live position stays sellable when global=Off — E1 fix); enabled=false⇒Off enforced; pre-POST DB error→TransientNak(30s); recorded-failure/benign-race→Ack; fail-closed two paths; cap:event literal. Committed on main BOTH repos (PM 351a755, fetch 4fde695), NOT pushed, mode OFF. Gate: 66 PM offline + 532 fetch + 7 db-tests pass. OPEN (callout): commit contamination in 4fde695 (user’s exit_signal/cohort_exit_signal_id work, called by uncommitted firstmover/alerts code) pending split/keep; follow-ups (delete dead caps helpers, signer seam for live-POST e2e); pre-arm notify→Telegram live-test.
  • position-manager-execution-migration-implementedIMPLEMENTED (2026-06-21) — Plan 1 execution migration DONE (11 tasks, opus-reviewed, READY TO MERGE). Live buy+sell now route through PM::place_order (place_buysubmit_live, place_sellauto_sell); submit_live/auto_sell/execute.rs deleted upstream. Committed on main BOTH repos (PM cbd0f4c, fetch afc785d), NOT pushed, autotrade mode OFF, nothing armed/deployed. OPEN USER DECISIONS (callout in note): caps expose-only, pre-arm notify→Telegram live-test, breaker check-order flip. See note for full state + follow-ups.
  • position-manager-interface-designDESIGN (2026-06-20) — position_manager crate: focused execution+watch home for the deferred autotrade rename. Migrates today’s submit_live execution path (signing client + execute/ledger/onchain + the pmv2_autotrade_orders migration) out of polymarket_fetch behind PositionManager::place_order(OrderRequest)->PlacedOutcome, behavior-preserving (golden parity on built bytes + ledger transitions). Adds two watchers (self-wallet reconcile, watched-tx OrderFilled detector). Strategy/gates/sizing/NATS consumer/mode stay upstream. See Architecture + Database Schema topic entries for full detail.
  • 2026-06-20-fastest-polymarket-trade-submission★ RESEARCH (2026-06-20) — “fastest way to put trades into Polymarket.” Submission is ALREADY sub-second (warm pooled client, L1 signed once at connect, L2 creds cached, FAK, allowances preflighted) and is NOT the bottleneck — real latency is upstream DETECTION (~15-min REST poll), so the highest-leverage fix is Phase 2 first-mover (on-chain OrderFilled WSS → cohort.signals, see cohort-firstmover-nats-migration/autotrade-alert-latency-and-run-timeouts), NOT submission tuning. Biggest submission-path win = kill the 3 uncached per-trade GETs (market metadata + book + gamma resolution, ~150–600 ms) via the CLOB WS market-channel book mirror (vendored rs-clob-client-v2 has a DISABLED ws feature) + cached STATIC market metadata; WS is read-only, placement stays REST (read-over-WS, write-over-REST). Second win = disable use_server_time on signing (~20–100 ms/order). Cannot go faster on-chain (off-chain operator matching; CLOB API IS the fast path). WARNING — V2 hard cutover 2026-04-28 makes a stale-V1 order REJECTED not slow (verify domain version \"2\", new Order struct, pUSD collateral via Onramp 0x9307…, V2 exchange addrs). WARNING — any proxied reqwest MUST set BOTH timeout AND connect_timeout (75-min silent-hang lesson). Don’t-bother: sigType Proxy(1)→EOA(0) for speed (sub-ms diff + custody change), hand-rolling EIP-712 (SDK does it), pre-signing (already sub-ms). Colocation = GCP Frankfurt now; origin region UNDOCUMENTED (community-inferred AWS London eu-west-2, NOT us-east-1) — verify via traceroute before spend, Frankfurt→London already ~10–15 ms so upside small. Gaps: NO p50/p99 submit measurement exists; HTTP/1.1 (HTTP/2 untested, no benchmark); V2 operator ack latency unpublished (becomes the binding constraint after the WS/cache fixes). Priority: ① Phase 2 first-mover ② WS book mirror + metadata cache ③ disable use_server_time ④ verify V2 ⑤ measure p50/p99 ⑥ HTTP/2 + region-verified colocation only if measurement justifies.
  • autotrade-dry-skip-visibilityOBSERVABILITY — dry-mode gate-out visibility added to ingest_for_consumer in runner.rs (branch fix-dry-skip-visibility, GATE_EXIT=0, 334+87 tests). Previously a dry rehearsal that was gated out (stale gate OR Dispatched::Skipped) was silent and indistinguishable from “nothing arrived.” Now both skip paths surface notify(dry_skip_msg(...)) + tracing::info when eff == Mode::Dry; they degrade to tracing::debug otherwise. Key structural change: per_message_mode() hoisted above stale_gate so eff is available at both skip points. Helper dry_skip_msg(source_id, condition_id, reason) and a unit test locking the 4 output substrings added. Live paths untouched.
  • pmv2-phase4-chain-cohort-sell-fast-exit★ MONEY-CRITICAL DESIGN — Phase 4 (on-chain settlement/fills/cohort-exit plan, branch autotrade-onchain-book): a SELL branch in the tracked-alerts consumer (alerts_consumer.rs) that, on a REAL on-chain cohort SELL, accelerates that wallet’s exit to SECONDS after the fill (vs up to ~30 min for the cron). Gate GATE_EXIT=0 (508 binary [507/1 ignored] + 99 lib, clippy -D + fmt clean, 6 new tests); only 2 files changed (alerts_consumer.rs + the main.rs call-site). New symbols eligible_sell(sig,score) (gate: side=="SELL" + cohort + score>MIN_COHORT_SCORE=0.0), ExitDebounce (per-wallet 60s coalesce), on_cohort_sell(wallet); run_tracked_alerts gained cfg:&AppConfig and injects a DataApiClient (via cfg.build_data_client, which already sets connect_timeout), a tokio::Semaphore (cap 4) bounding fan-out, and ExitDebounce. THE DEDUP CRUX — no double alert / no double sell: the exit pipeline already has THREE consumers of an EXITED row, each with its OWN dedup — (1) cohort EXIT alert (exits::run_check, dedup = the pmv2_exit_alert timestamp cursor), (2) auto_sell (runner::run_exits, dedup = autotrade_exits cursor + the mark_selling open→selling CAS that serializes ALL sell triggers), (3) insert_position_event = a PLAIN INSERT, NO ON CONFLICT (the UNIQUE(proxy_wallet,condition_id,outcome_index) on pmv2_position_events was DROPPED in 20260603000000_pmv2_overlap.sql for enter→exit→re-enter, so duplicate EXITED rows ARE possible — you CANNOT dedup exits via a DB constraint). THE SAFETY DECISION: the chain SELL path does detect_exited scoped to {wallet} and for a confirmed exit LOGS the EXITED row (repo::insert_position_event) AND DELETES the prior pmv2_positions rows (repo::delete_positions) — mirroring exactly the cron’s run_pmv2_positions; it does NOT send a direct alert and does NOT call run_exits/auto_sell. Deleting the prior snapshot row means the cron’s next detect_exited won’t re-see it ⇒ exactly ONE EXITED row exists ⇒ the existing pmv2_exit_alert cursor alerts once + run_exits sells once (a stray duplicate is still safe via the mark_selling CAS). The chain path adds NO new alert source and NO new sell path — it makes the SAME row appear seconds early. REJECTED ALTERNATIVE: a DIRECT alert from the chain path — needs its own cross-process dedup against the pmv2_exit_alert cursor (impossible; the two rows carry different detected_ts, defeating the timestamp cursor ⇒ double-alert). Logging the row IS how you ride the existing cursor safely. IDEMPOTENCE: detect_exited (positions.rs:183) is intrinsically idempotent (a position already gone is never re-exited); cold-start (no prior snapshot) produces NO exit (tested). ACCEPTED LIMIT (spec, NOT a code comment): intra-gap round-trips (enter+exit within one cron gap, never snapshotted) get no fast-path exit — matches today’s cron coverage. Complements (supersedes nothing) pmv2-autotrade-engine-design; SELL/fast-exit counterpart to the BUY consensus work in tracked-alerts-consumer (same file); changes only WHEN the EXITED row appears, not the sell mechanism.
  • cohort-firstmover-nats-migration★ ARCHITECTURE CHANGE — the cohort first-mover ENTRY signal migrated from in-process ingestion to a NATS publish on subject cohort.signals, consumed by the SAME Pmv2Consumer that handles @weather (one unified lane routed by envelope source); in-process runner::run_entry DELETED. Branch autotrade-foundation (off main, NOT deployed), 4 commits 1166f51bdafd7f, gate green as reported (428+89); each task spec+code-quality reviewed, T4 also trade-safety = SAFE. Sits on top of pmv2-position-manager-nats-consumerwire::parse_signal, the engine Context arm, and the single submit_live site are UNTOUCHED; only cohort TRANSPORT changed (in-process call → NATS publish). T1 (1166f51): parse_signal routes source=="first_mover_core"Execution::Context, FAIL-CLOSED (Reject) on any capital-critical missing/empty field — origin_ref empty⇒cohort exits never fire (CAPITAL TRAP), outcome_label empty⇒token_for fails, +reference_price/conviction/outcome_index; max_price/valid_until_ts became Option (cohort omits them; valid_until_ts=None is EXACT parity + avoids clock-skew wrongful drops). T2 (3c589c1): serialize_cohort + the headline cohort_roundtrip_parity test = serialize_cohort ∘ parse_signal is byte-equal to sources::enter_signal over the full persisted/exit-read field set (fixtures use event_slug≠slug so a field-collapse is caught). SUBTLETY: watcher has no CLOB at detection so enter_signal leaves tick/min/neg_risk=None; serialize emits 0.0/0.0/false, parse rebuilds Some(0.0)/Some(false) — IMMATERIAL because the Context engine arm re-resolves these from the live CLOB and never reads the signal’s values. T3 (9505840): nats::publish_cohort — serialize then publish_with_headers w/ Nats-Msg-Id=signal_id (JetStream server-side dedup), then flush; reuses nats::connect. T4 cutover (bdafd7f, trade-safety reviewed): run_autotrade_entry (positions.rs) PUBLISHES instead of ingesting in-process — gated on active + Core tier + !breaker (the old !ready preflight clause DROPPED — preflight is now a CONSUME-time concern); bounded retry PUBLISH_RETRIES=3/200ms, a dropped publish after retries = logged MISSED entry = UNDER-trade, NEVER a double (no telegram/breaker); publisher built once/run from env NATS_URL+NATS_COHORT_NKEY_SEED (absent⇒dormant⇒nothing published); DELETED runner::run_entry, dead safety::ready, unused AutotradeDeps.mode. SAFE (no double-LIVE order): dedup on signal_id at BOTH the NATS header AND the DB ON CONFLICT(dedup_key) backstop; money-path untouched; dormant cohort (off) publishes nothing. ⚠️ LOAD-BEARING GOTCHA: deleting watcher-side autotrade_ready() removed in-process entry-preflight AND per-tick reconcile_entries — safe TODAY (cohort dormant/off, no live orders, /autotrade reconcile works manually) BUT the Pre-arm consumer-preflight gate (the blocker shared with weather) must NOW ALSO restore entry-reconcile + preflight before arming ANY lane live, else the first live lane has stuck pending/submitted orders nothing auto-reconciles. DEPLOY (NOT done, T5): stream stays WEATHER_SIGNALS (ADD cohort.signals subject, do NOT rename); pmv2 durable must consume BOTH subjects; provision a cohort-publisher NKey; put NATS_URL+NATS_COHORT_NKEY_SEED in the WATCHER env; @weather — cohort is a 2nd source, no schema change (fields Option-default at v1). Spec …/specs/2026-06-17-cohort-firstmover-nats-migration-design.md; plan …/plans/2026-06-17-cohort-firstmover-nats-migration.md.
  • tracked-alerts-consumer★ NEW FEATURE — Phase 2 of the pmv2 real-time tracked-detection lane: the pmv2-tracked-alerts consumer turns the Phase-1 on-chain detector’s tracked.signals stream into INSTANT first-mover buy AND sell Telegram alerts, recovering the first-mover edge lost to the ~15-min REST batch cadence. Branch autotrade-foundation, 2 commits 806ba78+e1d1f24, gate green (471+90, clippy -D warnings + fmt clean); NOT merged, NOT deployed. SEPARATE from the autotrade cohort.signalsWEATHER_SIGNALS lane (cohort-firstmover-nats-migration) — ALERT-ONLY (subject tracked.signals, stream TRACKED_SIGNALS, does NOT execute). Phase 1 (shipped) = always-on detector pmv2-tracked-detector publishing a TrackedEvent JSON per cohort buy/sell fill. Service: new long-running CLI pmv2-tracked-alerts = NATS JetStream pull consumer (durable alerts on TRACKED_SIGNALS); dormant-safe (exits 0, env-first no-DB-touch, if NATS_URL/NATS_TRACKED_ALERTS_NKEY_SEED unset) ⇒ deploy-safe before provisioning. Architecture: pure core decide_alert(payload,cohort)->AlertDecision{Send|Skip|Reject} (parse→lowercase-keyed cohort lookup→format) wrapped by a thin loop run_tracked_alerts; files src/pmv2/tracked/{event.rs (owned TrackedSignal mirror + fail-closed parse_tracked), alert.rs (format_tracked_alert), alerts_consumer.rs}. Two locked product decisions: (a) alert = trader-SCORED (wallet→name/rank/confidence-band from a 60s-refreshed cached scored cohort) + measured LATENCY + market/side/price/notional/link; DROPS the batch-only “N cohort holders” count + the paper-copy line; per-fill granularity (one alert per cohort order via order_hash dedup ⇒ alerts on ADDS not just first entries). (b) BOTH buys and sells — batch alert is buy-only so SELL alerts are NET-NEW. Isolation: frozen scoring/paper lane + batch format_entry_alert/send_entry_alert UNTOUCHED — batch alert stays as the safety net; retiring it deferred until prod parity (no zero-alert window). Review keeps (don’t “fix”): (1) #[allow(dead_code)] on TrackedSignal is CORRECT — canonical owned wire mirror; 6 fields (shares/fee_usd/block_number/tx_hash/order_hash/detected_at) unused by the alert but forward-looking for Phase-4; in a BINARY crate pub does NOT exempt never-read fields from dead_code, so removing the allow re-fails clippy. (2) cohort-cache staleness is a non-issue — refreshes lazily at the loop top BEFORE processing each message (≥60s) so the first message after a lull gets a fresh cohort; cohort changes slowly. (3) Reject→Telegram-notify KEPT (parity with autotrade nats::run; near-zero from our own publisher but real diagnostic value). DEPLOY (pending, folds into the Phase-1 detector deploy): provision NATS_TRACKED_ALERTS_NKEY_SEED (consume-only on tracked.signals); JetStream stream TRACKED_SIGNALS capturing tracked.signals w/ durable pull consumer alerts; run pmv2-tracked-alerts as a long-lived service (alongside pmv2-consumer). Spec …/specs/2026-06-18-pmv2-realtime-tracked-detection-design.md (§10.2); plan …/plans/2026-06-18-pmv2-realtime-tracked-alerts.md.
  • tracked-alerts-consumer★ UPDATE — went LIVE in prod; round-trip verified via SigNoz at sub-second latency, and the alert FILTER evolved through 3 stages. (1) per-fill no-filter = ~1 alert/sec FLOOD (the lane is per on-chain fill; the OLD batch lane hid churn via ≈30-min cadence + snapshot-diff + entry-only). (2) conviction filter (36a4264) = notional ≥ 2× the trader's OWN avg_bet_size, scored-only — KEY INSIGHT: the spammers were WHALES (avg bet 7.1K), so an absolute-$ floor would’ve been WRONG; conviction RELATIVE to each trader’s avg is right for a whale cohort. (3) consensus filter (CURRENT, 0f71f89) = alert ONLY when a 2nd positive-score “sharp” cohort trader independently BUYS the same (condition_id,outcome_index) within 48h (“sharps converging”); single-trader bets no longer alert. In-mem ConsensusTracker (HashMap<MarketKey,HashMap<wallet,Instant>>, 48h, pruned on 60s cohort-refresh), eligible_buy gate (BUY + score>0). Consts CONSENSUS_WINDOW=48h/MIN_TRADER_SCORE=0.0/2-trader. Observability: per-alert tracing::info!(target:"tracked_alerts", n=…, "consensus alert sent") ⇒ alert rate now visible in SigNoz (polymarket-fetch-pmv2-tracked-alerts). LIVE on 0f71f89, cohort=21, correctly SILENT so far (rare by design; knobs await first alerts). Later-patch: Telegram 429 drops sends with no retry ⇒ rare high-value alerts can be lost. Ops gotcha: pause→redeploy must use --deliver new (else replays backlog through the filter).
  • autotrade-source-extensibility-and-global-caps★ ARCHITECTURE/OPERATIONAL FACTS (branch autotrade-foundation) — (1) adding a Directive-shaped source is GENERIC: only a parse branch in wire.rs::parse_signal (new source_id) + a pmv2_autotrade_sources(source_id,enabled,mode) row; NO engine/runner/sizing changes. Mode resolves generically in runner.rs::per_message_mode via repo::autotrade_sourceconfig::effective_mode (const fn more-conservative floor off(0)<dry_run(1)<live(2)); missing source row + failed config load both FAIL-CLOSED to Mode::Off. DRY: weather+crypto_shortterm_latency_test share one parse_directive(p,action,source_id) helper (generalized from parse_weather, 2026-06-19). A row pinned mode='dry_run' is a safe DRY-PIN (generic capability, ANY source) — CANNOT reach live regardless of global mode (effective_mode floor); live flip = deliberate explicit row change (Andras’s call). crypto_shortterm_latency_test is the deliberate exception — NOT dry-pinned: seeded mode='off' at rest but LIVE-capable, because dry can’t measure fill latency (no order submitted in dry ⇒ no round-trip to time), so the test runs LIVE. (2) Caps/sizing are GLOBAL, not per-source — and this is DIRECTLY the crypto test’s safety surface. AutotradeConfig is one global row (pmv2_autotrade_config WHERE id=1): base_usd, min/max_size_usd, per_event_cap_usd, total_exposure_cap_usd, daily_spend_cap_usd, max_open_positions, max_copies_per_event, slippage/premium bps all global. Per-source granularity is ONLY mode+enabled; NO per-source cap columns. Arming the crypto latency test = a 3-part flip for the window: (a) crypto source row→mode='live', (b) global mode→live, (c) TIGHT global caps — because caps are global, those tight caps are the ONLY thing bounding the live test (no per-source caps to scope them). LOW-IMPACT today (global mode off; first-mover live trigger is Phase 2, not built/armed — nothing else produces live trades during the test window) but a REAL conflict once Phase 2 arms first-mover live (a tiny-caps latency-test window + the real first-mover strategy can’t have different caps simultaneously). Future fix = per-source cap columns if both must run live concurrently. Pairs with pmv2-position-manager-nats-consumer (defines the routing, Directive vs Context, effective_mode, the per-source mode column).
  • pmv2-position-manager-nats-consumer★ NEW FEATURE — generalizes the autotrade engine into a POSITION MANAGER that ingests MULTIPLE signal sources over NATS JetStream (first consumer = @weather); the reviewed money-path stays byte-for-byte, only ingestion + the order-derivation front-half generalize. Branch autotrade-foundation, 6 commits 4c0b7d4→74faf7d, gate-green, NOT deployed; module STAYS autotrade (rename deferred). T1 generalize TradeSignal: SignalAction::{Enter,Exit}Execution{Directive{max_price} | Context{reference_price,conviction,source_score}}; signal_id is the dedup key (dedup_key() returns it directly); LOAD-BEARING — cohort signal_id == legacy dedup_key(..,"enter",..) so dedup is byte-identical mid-migration (no double-orders). T2 wire.rs (trust boundary): parses @weather’s flat JSON envelope → ParseOutcome{Accept,Skip,Reject}, NEVER panics; fail-CLOSED on malformed/missing-required-common-field, fail-OPEN (Skip, never execute) on unknown source / SELL / future schema_version. T3 plan_order seam: ingest_entry dispatches Executionplan_context (cohort, unchanged) vs plan_directive (weather: token_id-direct, outcome_index from clob_token_ids, neg_risk verified vs CLOB, limit=floor_to_tick(min(max_price,ask)), NO cohort price gates — legit buys to 0.95); shared back-half untouched; stale_gate added. T4 per-source mode: pmv2_autotrade_sources.mode col, effective_mode=min(global,source) on off<dry<live, cohort backfilled live; LOAD-BEARING — build_autotrade_deps overrides engine_cfg.mode=eff so a dry source under live global can’t POST. T5 consumer seam + NATS adapter: ConsumerOutcome{Persisted,Skipped,Stale,Transient,Terminal} + ingest_for_consumer (staleness-FIRST, reuses dry_rehearsal/submit_live, breaker fail-safe); nats.rs ack discipline (ack_action: Persisted|Skipped→Ack, Transient→Nak, Stale|Terminal→Term), async-nats 0.49 raw NKey seed (no TLS), binds EXISTING pmv2 durable on WEATHER_SIGNALS; ack-only-after-persist + ON CONFLICT(dedup_key) DO NOTHING = at-least-once safe. T6 Pmv2Consumer subcommand: dedicated long-running process (engine is one-shot per-tick → a spawned loop would die); dormant-safe (exits 0 if NATS env absent); mode = weather effective (defaults off ⇒ skips all). GOTCHAS: async-nats 0.49 JetStream errors are Box<dyn StdError> (no anyhow::From) → nats_err map_err helper not bare ?; going live needs TWO operator actions (global=live AND weather.mode=live); env NATS_URL+NATS_NKEY_SEED (pubkey UDYV…ZEDD). REMAINING: deploy + @deploy launcher for the subcommand + manual dry-validate before arming weather live. Wire SoT = weather_bet docs/signal-schema.md @ 3187585.
  • autotrade-resolution-proposed-entry-guard★ Bug fix — the cohort first-mover lane DRY-RUN “bought” an ALREADY-DECIDED market (ITF tennis loser ~$0.002) because two entry gates each missed a UMA-proposed-but-not-closed market (2026-06-17, 824a22d+8b3c451, branch autotrade-foundation, NOT deployed). Gamma: closed=false/acceptingOrders=true BUT umaResolutionStatuses=["proposed"]+outcomePrices=["0.0005","0.9995"] = result in, not finalized. Hole 1: cohort price_bounds_gate validates the COPIED TRADER’s frozen reference_price=p.avg_price (in-band ~0.44), NEVER the live best_ask (0.001) → a post-entry collapse slips through. Hole 2: liveness_gate only skips FORMALLY resolved/closed/non-accepting; UMA-proposed is still acceptingOrders=true. Part A (ask-band, engine.rs::plan_context): after best_ask, skip when ask outside [min_entry,max_entry] (reuses price_bounds_gate on the ASK), reason "ask … outside entry band"; Cohort/Context-ONLY, NOT the weather Directive path (legit buys up to max_price=0.95); data-source-independent PRIMARY net. Part B (resolution-proposed, 8b3c451): GammaClient::market_resolution_proposed reads umaResolutionStatuses; MarketStatusView.resolution_proposed set FAIL-OPEN (30s timeout/warn/default-false — Gamma hiccup never breaks resolve()); liveness_gate skips, reason "market in resolution". ENTRY-ONLYis_terminal_market/exit_decision_for untouched (resolution-proposed position stays sellable). Gate green 503 workspace tests. See Gotchas + Polymarket API Quirks topics for the umaResolutionStatuses-is-a-stringified-array and MarketStatus.resolved==closed-misnomer caveats.
  • autotrade-alert-latency-and-run-timeouts★ OPS (2026-06-17, point-in-time from journalctl) — why dry-run autotrade alerts are ~20–38 min late + NEW intermittent scheduled-run 15-min KILLs. Latency = (1) REST-poll detection on the 30-min timer (mean ~15min) + (2) ~8min in-run before the autotrade step (collect+resolutions+leaderboard precede run_pmv2_positions) + (3) intermittent whole-cycle loss when a run hits TimeoutStartUSec=15min and is killed (status=130/SIGINT) — 3 of 48 runs in 24h, last two consecutive. Telegram itself is sub-second. Likely timeout cause = an occasional silently-hanging proxied reqwest w/o connect_timeout in a later stage (the 09:00 run died at 09:15 AFTER the autotrade step ran at 09:08); secondary = a growing EXITED backlog (exit-cursor read = 37,741 rows / 2.87s, cursor maybe near-NULL). Re-verify via @deploy on babylon, not SSH.
  • autotrade-3c2-round2-validation★ 3c-2 ROUND-2 validation pass (bug/DRY/redundancy/gap lenses, 2026-06-16) — found real issues the round-1 trade-safety review MISSED; final re-review CONFIRMED SAFE FOR STAGED ARM (334+87). LESSON: a flow can be structurally trade-safe yet BROKEN at a vendored value-validation boundary the safety lens never checks — round-1’s “SAFE FOR STAGED ARM” was premised on a SELL path that didn’t work. B1 BLOCKER (capital-trapping, e93dde1): vendored rs-clob-client-v2 Amount::shares rejects shares with normalize().scale() > LOT_SIZE_SCALE(=2) (0.01 lots, types/mod.rs:193-200 + order_builder.rs:25); auto_sell fed the RAW on-chain fill (locked vector 7.6219, scale 4) → build error → revert to open → EVERY fractional-share position unsellable (auto-exit + /autotrade sell), capital trapped till resolution; FIX = sell_shares = filled_size.trunc_with_scale(2) used for build_order+quantize_sell+assert_sell_amounts, sub-0.01 dust held to resolution (return before mark_selling); BUY unaffected (Amount::usdc scale ≤6, market builder no lot gate). 2E (false risk-limit, 84e3e34): max_copies_per_event shown in status but NEVER enforced → per-event_slug COUNT in the reserve config-lock tx (0=unlimited). 2C/I3/1D gaps (d4a2a85): settle alarms a held exit_pending; forward-only resume_cursor (first_failed−1µs) stops the exit cursor dropping a same-detected_ts sibling; reconcile CTF sweep bounded LIMIT 25+notify. DRY (efc69bc)+regression (0f06ec5): collecting_notify extraction blanked the /autotrade sell reply — Arc::try_unwrap drained while the closure held a 2nd Arc clone → always empty (visibility only; sell executed; CI green bc unasserted) → lock()-drain + regression test.
  • autotrade-3c2-complete-handoff★ 3c-2 COMPLETE — operator handoff/consolidation (2026-06-16). LIVE submit + lifecycle + safety DONE on autotrade-foundation (NOT merged); 17 commits 3c2.1→3c2.10; gate green 320+87; DEFAULT mode=off (3-layer enforced, prod no-op until armed). Consolidates Task 5 breaker / Task 6 reconcile (CTF balanceOf primary truth, decide_entry pure table, derived ready=preflight&&reconcile&&creds gate, per-tick) / Task 7 selling-state exits (auto_sell FAK, mark_selling open→selling CAS serializes all triggers, per-order-mode authority — live exits even with kill-switch off, exit_pending missed-exit queue, 1-click sell:<id>) / Task 8 settlement (gamma winners → mark_resolved, idempotent no-POST/redeem) / Task 9 exposure-cap TOCTOU re-check under config lock + filled/partial exposure + operator alarms + arm-time exit-cursor seed + module #![allow(dead_code)] removed. Final adversarial review (3 reviewers) found+FIXED: (A BLOCKER) live BUY never POSTed — build_order book-priced vs quantized plan.takerassert_amounts exact-taker bailed; FIX pin .price(plan.limit) on BUY / floor on SELL. (B) partial FAK orphaned residual + mis-booked PnL → FIX prorate_partial+record_partial_sell revert selling→open w/ reduced filled_size/cost + accumulated pnl. (C) stuck selling/filled never reconciled → FIX load_reconcilable_entries widened (selling: bal>0 revert / bal0 close+alarm; filled/partial: mark_open retry). Re-review: SAFE FOR STAGED ARM. Deferrals: fee_usd=0 (PostOrderResponse has no fee — cost understates/PnL overstates, fail-safe; calibrate or wire order_trades); cap-breach surfaces as benign “dedup” Skip (visibility gap, not fixed); balance-0 stuck-selling books pnl=0 + loud manual-verify alarm. STRATEGIC: first-mover TRIGGER is REST-polled ~30min (mean ~15min lag) — likely erodes most first-mover edge; real-time on-chain OrderFilled WSS recommended before scaling past tiny caps. Includes the staged-arm runbook (keys+RPC → /autotrade dryrun → small caps → /autotrade live preflight-gated → observe fills/reconcile/restart/exit/1-click/settlement; /autotrade off stops entries but live positions still exit).
  • autotrade-3c2-validation-carryforwards3c2.9 validation carry-forwards (2026-06-16): task b SKIPPED (token_for already fail-closed); task c SKIPPED (neg_risk stays caller arg — YAGNI); task d DONE (operator alarms at 3 sites: runner.rs::drain_pending_exits, runner.rs::run_exits, positions.rs::init_autotrade); task e DONE (exits cursor seeded to Utc::now() at first arm in arm_live). 315+87 tests pass.
  • autotrade-selling-state-foundationsTask 7a (2026-06-16): assert_sell_amounts + shared assert_contract in execute.rs; 5 CAS transitions (mark_selling/mark_sold/revert_selling/set_exit_pending/clear_exit_pending) + SellableOrderRow + 2 query helpers in repo.rs. Serialization gate for multi-trigger races; exit-pending race handler; zero-submit. 306+87 tests pass.
  • autotrade-breaker-literal-brace-fixBug fix (2026-06-16): first_lock_order_violation false-negatives from brace chars inside string/char/raw-string literals; neutralize_literal_braces char state machine; repo::bump_breaker_count dead_code removed; 3 regression tests; 285 pass.
  • autotrade-breaker-moduleTask 5 — circuit breaker consolidation (2026-06-15, branch autotrade-foundation). bump_breaker + BREAKER_THRESHOLD=5 extracted from execute.rs into new breaker.rs; SQL moved to repo.rs::bump_breaker_count (config-row-only FOR UPDATE tx, runs after the order-row tx commits). §3.4 lock-order safety invariant now enforced by a structural regression test in breaker.rs that scans all src/pmv2/autotrade/*.rs + src/pmv2/repo.rs on every cargo test run and fails if any function locks an order row before a config row. Pure refactor — zero behavior change, 282 tests pass.
  • autotrade-engine-implengine.rs — Plan 2 capstone (2026-06-14, branch autotrade-foundation). ingest_entry wires all gates via injected traits in a fixed sequence ending in try_reserve (the single mutation point). Key decisions: EngineConfig drops Clone; min_round_trip_edge_bps >200 required in tests; ingest_entry takes &(dyn Trait + Send + Sync) for tokio Send; category_gate passes None until Plan 3. 8/8 tests pass.
  • autotrade-market-data-traitmarket.rs — P2.3 of the autotrade foundation (2026-06-14, branch autotrade-foundation). MarketStatusView copy-struct + ResolvedMarket snapshot (condition_id, outcomes, clob_token_ids, tick_size, neg_risk, status, bid/ask) + ResolvedMarket::token_for(outcome_index, outcome_label) (3-guard validator → returns clob token id). MarketData async trait with single resolve(condition_id, outcome_index) -> Result<ResolvedMarket> method. 4 tests. Gate gotchas: clippy::unwrap_used in test bodies; rustfmt alphabetical import order; async-trait must appear in the crate’s own Cargo.toml.
  • autotrade-signal-typesignal.rs — P2.1 of the autotrade foundation (2026-06-14, branch autotrade-foundation). SignalAction::{Enter{reference_price},Exit} + TradeSignal struct (source_id, action, condition_id, outcome_index, outcome_label, event_slug, slug, origin_ref, conviction, source_score, tier, instance). dedup_key() delegates to orders::dedup_key(). Tier re-exported from pmv2::model. 2 tests pass. The single pluggable seam between any source and the engine.
  • autotrade-authz-moduleauthz.rs — Task 2 of the autotrade foundation (2026-06-14, branch autotrade-foundation). Pure authorization layer: Scope::{Public,Owner}, Authz::{Allowed,Denied}, OwnerSet (parses TELEGRAM_OWNER_IDS CSV of i64 user IDs), authorize(), scope_for() ("autotrade"Scope::Owner, else Scope::Public). Zero I/O, 6 unit tests, clippy pedantic+nursery clean. #![allow(dead_code)] is temporary — removed in Task 3 when run_command consumes the module.
  • pmv2-autotrade-engine-designthe design for writing REAL trades to Polymarket’s CLOB (LOCKED 2026-06-14). Execution = vendor+pin the official Polymarket/rs-clob-client-v2 Rust crate (alloy-based) — NOT a Python sidecar, NOT hand-rolled EIP-712 signing. Wallet = Bandi PROXY 0xdb0d23c2c3468da13c6e8abfe96edfecc72d9efe, signatureType=1 (maker=proxy, signer=EOA 0xfa7772b9aa1230a53d3bea9a11f5c52f110684f7); EOA key from /Users/levander/levandor/terraform/secrets/poly_private_key, in-memory only; gasless off-chain signing → no relayer/gas (proxy allowances pre-set). V1/V2 LANDMINE: two live PM contract generations differ by exchange address, EIP-712 domain version (1 vs 2), AND collateral (USDC.e vs pUSD) — resolved at RUNTIME via preflight + a non-deferrable verifyingContract-vs-neg_risk assert, NEVER hardcoded. Modes OFF/DRY_RUN/LIVE gate entries; exits follow the per-order mode, not the live kill-switch (closing must never be disabled). Safety machinery (the expensive part): marketable-limit FAK + slippage/chase/EV/price-bound/liveness gates; hard caps (exposure/daily-spend/per-event/max-copies) computed with in-flight reservations under a config-row (id=1) lock; a selling reserve state + global lock order (config-before-order) preventing double-sell + ABBA deadlock; on-chain CTF balanceOf(proxy,token_id) as the reconciliation ground truth (expected_order_hashclob_order_id correlation only best-effort); entries-only circuit breaker; derived (not latched) ready flag. Phase-0 SPIKE mandatory before engine — pin amount-quantization (taken VERBATIM from the client’s roundDown/roundNormal; the earlier “GCD of price and 1e6” guess was WRONG), order_hashorderID, and V1/V2 collateral, which cannot be settled on paper.
  • autotrade-config-repoconfig.rs — config repository layer (2026-06-15, branch autotrade-foundation). AutotradeConfig (sqlx::FromRow, BigDecimal NUMERIC fields, i32 INTEGER fields); load(pool) fetches the singleton id=1 row; update_numeric converts f64→BigDecimal via the established numeric() helper before binding; set_text for nullable text; set_source_enabled for pmv2_autotrade_sources; update_field_sql pure SQL builder; validate_set domain-bound checker for Telegram inputs. Runtime sqlx::query_as::<_, T> (no compile-time macros — no live DB at build). 5 tests pass.
  • autotrade-commands-bot-wiringcommands.rs + bot.rs/autotrade Telegram command wiring (2026-06-15, branch autotrade-foundation). apply_set (pure validate+column resolver), format_status (renders AutotradeConfig as a human-readable string; BigDecimal fields use {} directly), run_autotrade (async dispatcher → config::{load,update_numeric,set_text,set_source_enabled}), set_text_reply (private helper shared by SetMode/SetExit/Filter/Maker arms). "autotrade" arm added to run_command; send_with_timeout(..., None) (plain text, no parse_mode); owner-auth gate not duplicated (scope_for("autotrade") already at top of match). 3 tests, gate exit 0 (244 tests). Key gotcha: #[allow(clippy::unwrap_used)] required on test bodies.
  • autotrade-clob-crate-bindingsPlan 3b Task 1 — binding vendored rs-clob-client-v2 into polymarket-clob crate (2026-06-15, branch autotrade-foundation). Correct import paths (U256 at types::U256, Unauthenticated at auth::state::Unauthenticated, Client/Config at clob::); U256::from_str (no from_str_radix; to_string() is decimal); Client::new has no chain ID arg; bon builder returns struct directly; MarketResponse field type map; clippy landmines from enabling "clob" feature across the workspace (derivable_impls on vendored default, single_option_map, unused_self, allowexpect); vendored example required-features fixes.
  • autotrade-gatesgates.rs — P2.4 of the autotrade foundation (2026-06-14, branch autotrade-foundation). Five pure gate functions all returning GateOutcome::{Proceed, Skip(String)}: liveness_gate (fail-closed on unknown/epoch end_time; rejects resolved/closed/inactive/non-accepting/too-close-to-resolution), category_gate (delegates to crate::pmv2::category::passes_filter; None = Proceed), price_bounds_gate (outside [min_entry, max_entry] → skip), ev_floor_gate (skip unless size × bps/10_000 > est_fee; #[allow(clippy::cast_precision_loss)] for i64 as f64), chase_gate (skip if ask > reference_price × (1 + bps/10_000) — reference_price frozen at signal detection). Helper est_round_trip_cost_usd with ROUND_TRIP_COST_RATE=0.02. 9/9 tests pass, clippy clean. Gate-script exit=1 was a false positive (rtk intercepting -- -D warnings).
  • autotrade-category-resolveron-demand category resolver T3 (2026-06-15, branch autotrade-foundation). read_event_category(pool, event_slug) in repo.rs (runtime sqlx, Option<String> from pmv2_event_category cache); resolve_category(pool, gamma, event_slug) in category.rs (cache-hit-or-fetch: reads cache → gamma.event_tags(&[slug]) → frozen category_bucketupsert_event_category; Ok(None) if gamma doesn’t know the slug). Both carry #[allow(dead_code)] until T4/T8. Key gotcha: event_tags takes &[String] — bind let slugs = [event_slug.to_string()] before passing &slugs (temporary lifetime). GammaClient imported from polymarket_data crate. No staleness logic — batch run_category_sync owns refresh.
  • autotrade-persistence-engine-configT5 — PgPersistence + engine_config (2026-06-15, branch autotrade-foundation). engine_config() converts AutotradeConfig (BigDecimal/i32) → EngineConfig (f64/i64/usize), building CopyConfig from autotrade’s own sizing fields; category_filter via parse_topbets_filter. PgPersistence<'a> implements Persistence: dry-run inserts with terminal status dry_run (excluded from partial dedup index + exposure queries, never counts toward daily cap); live inserts as pending. Dedup uses ON CONFLICT (dedup_key) WHERE status NOT IN ('dry_run','closed','canceled','failed') DO NOTHING RETURNING id. FOR UPDATE lock on config row (id=1) prevents double-reservation races. OrderMode::as_str/from_db + OrderStatus::from_db mappers in orders.rs. BigDecimal conversions need both FromPrimitive + ToPrimitive in scope. Transaction executor pattern: &mut *tx (sqlx 0.7+). 249/87 tests, gate exit 0.
  • autotrade-reserve-outcome-enumENGINEERING DECISION — Persistence::try_reserve now returns ReserveOutcome enum (Reserved(i64)|CapExposure|CapDaily|CapMaxOpen|CapEvent|Dedup) instead of Result<Option<i64>> (2026-06-19, branch autotrade-phase2-firstmover, GATE_EXIT=0). Previously Ok(None) collapsed both an ON CONFLICT dedup-skip AND any cap-breach into the same IngestOutcome::Skipped("dedup") — a double-trade during Phase-2 staging would look identical to benign noise. Now each variant maps to a distinct cap:exposure/cap:daily/cap:max_open/cap:event/dedup skip reason; a Skipped("dedup") unambiguously means an ON CONFLICT hit. Pre-lock cap strings unified to the same cap:* namespace. MockStore::with_outcome(ReserveOutcome, rows, spend) is the correct constructor for new tests; new(Option<i64>,...) kept as a compat shim. Files: engine.rs, persistence.rs, runner.rs.
  • autotrade-firstmover-emit-hoistREFACTOR Phase 2 (2026-06-19, branch autotrade-phase2-firstmover, GATE_EXIT=0) — publish consts hoisted to nats.rs as pub consts; PublishOutcome enum + build_and_publish_first_mover_entry async fn in firstmover.rs is the single canonical emit path for both cron and future watcher (identical id+gating+retry by construction); run_autotrade_entry in positions.rs becomes a 1-call delegation; 3 new unit tests; regression pin guards the dedup key identity. Sits on top of cohort-firstmover-nats-migration and autotrade-sources-canonical-signal-id. Key design motivation: the watcher-triggered Phase 2 emit path must produce byte-identical signal_ids to the cron, otherwise the DB ON CONFLICT(dedup_key) backstop doesn’t fire and the same logical entry can place two live orders.
  • autotrade-sources-canonical-signal-idPhase 2 B1 (2026-06-19, branch autotrade-phase2-firstmover) — cohort_signal_id now lowercases origin_ref, condition_id, and instance before hashing; enter_signal uses condition_id as instance (replacing the unstable event_slug.unwrap_or(condition_id)). These two fixes make signal_id byte-identical across the cron and future watcher producers, ensuring the DB ON CONFLICT (dedup_key) DO NOTHING dedup fires correctly and the same logical entry never places two live orders. Two new parity tests; gate=0.
  • autotrade-sources-signal-emitterTask 6 — sources.rs first_mover_core signal emitter (2026-06-15, branch autotrade-foundation). Tier::from_label(s: &str) -> Option<Tier> case-insensitive parser added to model.rs. CohortTrader gained tier: Option<Tier> in positions.rs. latest_scored_cohort_with_scores in repo.rs SELECTs t.tier TEXT and maps via from_label. New sources.rs: SOURCE_ID = "first_mover_core", autotrade_active(mode, enabled) -> bool (const fn), enter_signal (freezes reference_price = p.avg_price, NOT cur_price — critical for the chase gate), format_dry_run_order. 3 new tests; total 352 tests, 0 failures, clippy clean.
  • autotrade-max-copies-per-eventBug fix — max_copies_per_event loaded+validated but never enforced (2026-06-16, branch autotrade-foundation). Added field to EngineConfig + ReserveCaps; engine_config() maps i32→usize with max(0, val) (zero=unlimited, diverges from max_open_positions where 0 blocks all, by design); in-tx per-event-slug COUNT query in try_reserve LIVE branch under the existing config FOR UPDATE lock; caps_from_cfg() const fn + pre_lock_cap_skip() private async helper extracted to keep ingest_entry under the 100-line Clippy limit; 2 new persistence tests (zero→unlimited, negative→zero). Gate: 327 tests, 0 failures.
  • autotrade-onchain-balanceofTask 6 — ERC-1155 balanceOf on-chain primitive (2026-06-15, branch autotrade-foundation). ctf_balance_of(proxy_wallet, token_id, rpc_url) encodes ERC-1155 calldata, fires eth_call, decodes the uint256 balance. Key facts: sol! generates balanceOfCall; SolCall::abi_encode(); alloy_primitives::hex re-exported (no separate crate); selector 0x00fdd58e; spike doc had hex-decode error (0x2c641ab6 = 744_757_942 not 744_144_054); binary-only crate needs --bin polymarket-fetch for tests; polygon_rpc_url in AppConfig (#[allow(dead_code)] until T7); 3 pure unit tests, 256 total pass.
  • autotrade-repo-exit-helpersTask 7 — autotrade repo exit helpers (repo.rs lines 1270–1359, 2026-06-15, branch autotrade-foundation). AutotradeExitEventRow (sqlx::FromRow over pmv2_position_events exit join); autotrade_exits_cursor (reads cursor key 'autotrade_exits', returns 0 if absent); advance_autotrade_exits_cursor (upsert via EXCLUDED.last_check_at); autotrade_exit_events (kind=‘EXITED’, detected_ts > cursor, joined to pmv2_autotrade_orders); autotrade_open_orders_for_origin (status=‘open’ orders by condition_id/outcome_index/origin_ref); autotrade_source_enabled (fail-closed bool from pmv2_autotrade_sources). All #[allow(dead_code)] until T8. Cursor key 'autotrade_exits' is distinct from 'pmv2_exit_alert' and 'overlap_watcher'. 253+87 tests, 0 failures.
  • autotrade-eip712-order-hashEIP-712 order hash implementation in polymarket-clob (Task 3, 2026-06-15, branch autotrade-foundation). OrderV2 is #[non_exhaustive] (sol! macro) — FRU syntax blocked from external crates; use field-by-field assignment on a Default instance. alloy 1.6 import paths: alloy_sol_types::{Eip712Domain, eip712_domain!, SolStruct as _} (NOT alloy::dyn_abi::Eip712Domain). Polymarket V2 domain: name="Polymarket CTF Exchange", version="2", chain_id=137, standard exchange 0xE111180000…, neg-risk 0xe2222d…. LazyLock<Address> with scoped #[allow(clippy::unwrap_used)] for const address parsing (workspace forbids expect_used). verifying_contract must be const fn or missing_const_for_fn nursery lint fires. alloy-sol-types needs explicit workspace dep even when vendored crate uses the alloy umbrella.
  • autotrade-signing-client-order-opsPlan 3a Task 1 — place_order/cancel/get_order/open_orders/order_trades on SigningClient (2026-06-15, branch autotrade-foundation). KEY FINDING: SignedOrder (vendored) has no Deserialize — the spec’s serde_json::from_value::<SignedOrder>(...) cannot compile. Fix: drop SignedPayload wrapper, hold signed_order: SignedOrder typed value directly in BuiltOrder. API shapes confirmed: OrdersRequest/TradesRequest both derive Default (no-filter = ::default()); Page<T>.data: Vec<T>; OpenOrderResponse: .id, .status (Display), .size_matched; TradeResponse: .taker_order_id is the CLOB order id (NOT an EIP-712 hash — TradeView.order_hash always None), .transaction_hash: B256; PostOrderResponse: .order_id (alias "orderID"), .making_amount/.taking_amount, .transaction_hashes Vec<B256>, .trade_ids Vec<String>; cancel_order returns (). 265 tests pass, EXIT:0.
  • copy-trade-lane — the PAPER cohort mirror the live auto-trade lane is built alongside (not replacing). The auto-trade engine reuses the same decide_copy sizing (wrapped, never changed) and the cohort first-mover thesis, but adds real CLOB execution + the full safety/reconcile/breaker stack documented in pmv2-autotrade-engine-design.
  • autotrade-dry-refactors-2026-06-16Four behavior-preserving DRY refactors (2026-06-16, branch autotrade-foundation, EXIT 0 / 333 tests). (1) collecting_notify() factory in commands.rs deduplicates Arc<Mutex<Vec<String>>> + closure from run_reconcile/run_sell. (2) AutotradeDeps::market(pool) method replaces 3 verbatim ClobMarketData struct literals across runner.rs/positions.rs/commands.rs; build_autotrade_deps promoted to pub. (3) FillOutcome enum + record_fill_then_open async fn in execute.rs — shared by record_placed_fill + heal_filled. (4) Dead functions removed: repo::mark_dry_run + repo::load_autotrade_order; AutotradeOrderRow retained.
  • autotrade-halt-command/autotrade halt — full-stop command (2026-06-16, branch autotrade-foundation). config::halt(pool) executes a single atomic UPDATE setting mode='off' AND exit_mode='notify' together; no intermediate state. Halt variant + run_halt + parse arm added to commands.rs. Distinct from /autotrade off (which stops entries but leaves exits on auto-sell); halt stops BOTH. Breaker state intentionally untouched. Resume = /autotrade live + /autotrade exits auto_sell.
  • autotrade-exit-cursor-404-wedge-fixBug fix — exit-cursor 404 WEDGE (2026-06-16, b5d5986, branch autotrade-foundation, NOT merged). run_exits broke on the FIRST exit-event whose CLOB /book 404’d and parked the cursor at first_failed−1µs; a RESOLVED/CLOSED market returns "No orderbook exists" PERMANENTLY → cursor wedged forever (re-fail + Telegram-spam every pass) AND blocked every exit-event behind it (once live = un-exited cohort positions). FIX = status-based classification: resolve() skips book_top for resolved||closed (empty BookTop, no 404); pure helpers is_terminal_market/ExitDecision{Skip,Exit,Retry}/exit_decision_for; both exit paths classify terminal→Skip (advance cursor / clear_exit_pending, quiet, no alert), open+bid→Exit, open+no-bid|transient→Retry (halt+alert, no cursor advance). Terminal positions close via the settlement sweep; entry path untouched (liveness_gate already skips terminal). Gate 345+87 (+10); trade-safety 7/7. See the Gotchas topic for the resolved==closed vendored-field aliasing caveat.

CI / Build

  • autotrade-clob-crate-bindingsenabling features = ["clob"] lints vendored code: four fixes required — #[expect(clippy::derivable_impls)] on vendored Config::default(), add "tracing" to four vendored example required-features, suppress clippy::single_option_map with #[expect] (rename alone not enough), convert fn foo(&self) that ignores self to an associated function. Also: existing #[allow] suppressions must become #[expect(..., reason = "...")] per workspace rules. See full list in the note.
  • autotrade-market-data-traitgate gotchas (2026-06-14): clippy::unwrap_used fires on unwrap() in test bodies (--all-targets includes tests); rustfmt reorders imports alphabetically (e.g. {bail, Result} not {Result, bail}); async-trait must be listed in the crate’s own Cargo.toml even when it’s in the workspace lock.
  • correctness-pass-six-fix-commitsPROCESS GOTCHA: the rtk shell wrapper MANGLES cargo clippy ... -- -D warnings (it passes -D as an input filename). The CI gate is clippy with -D warnings, so run it via the full cargo path: CARGO=$(which cargo); "$CARGO" clippy --workspace --all-targets -- -D warnings. Any warning fails CI.
  • cilocal CI gate scripts/gate.sh (added 2026-06-03): runs build + test + clippy --workspace --all-targets -- -D warnings + fmt in one shot. The rtk Claude-Code hook mangles cargo … -- -D warnings at the top level but NOT when cargo runs inside a shell script — so the script bypasses the mangling (durable workaround for the recurring -D-as-filename failure). Allow-listed locally to stop the per-run prompt. GitHub Actions pipeline: fmt, clippy, cargo-deny, Docker build. Key gotcha: pin rust-toolchain.toml (now 1.95.0) so local and CI clippy lint sets match. cargo-deny moved off the Alpine Docker action; RUSTSEC advisories cleared by bumping testcontainers rather than ignoring them.
  • autotrade-onchain-balanceofbinary-only crate gotcha: polymarket-fetch has no src/lib.rs so cargo test --lib fails; use cargo test --bin polymarket-fetch or just cargo test -p polymarket-fetch.
  • polymarket-fetch-deployDockerfile.polymarket-fetch multi-stage (rust:1-bookworm → debian:bookworm-slim) and .github/workflows/release.yml calling the reusable wowjeeez/terraform/.github/workflows/build-push.yml with WIF auth; pushes :latest + :<sha> to Artifact Registry europe-west3-docker.pkg.dev/aerobic-tesla-490112-r3/apps/polymarket-fetch.

Claude Code / Workflow

  • cirtk’s Claude-Code hook mangles cargo … -- -D warnings (rewrites -D into a filename) at the top level, but the mangling does NOT happen when cargo runs inside a shell script. scripts/gate.sh wraps the full gate (build/test/clippy-deny-warnings/fmt) precisely to bypass this; allow-list it locally so it stops prompting every run.
  • recovering-claude-session-400-on-resume — a crashed session 400s on claude --resume and can’t be cleared from the CLI; cause is a bloated, tool-block-unbalanced transcript (over token ceiling AND tool_use/tool_result mismatch). You can’t /compact out (400 fires pre-load). Recovery: reconstruct from AGENT_HANDOFF.md + historian, continue in a fresh session. Includes the .jsonl diagnostic command set.

CLI

  • polymarket-fetchrun / consensus / latest / diff / history / migrate / trades-since / trades-sync / compute-metrics / ranked-traders / telegram-test / telegram-discover, plus selector + CP flags and global --silent
  • pmv2-two-tier-trader-scoringpmv2 commands: pmv2-positions (snapshot the cohort’s open event positions into pmv2_positions; default --concurrency 4) and pmv2-score-bet --bet <condition_id | market-slug | Polymarket URL> (per-bet scoring engine, read-only — accepts all three selector forms, fans event slugs/URLs out to variant markets).
  • pmv2-trending-betspmv2-trending CLI + /trending bot command surface the velocity query (distinct high-score cohort wallets entering a bet in a 6h window vs prior).
  • pmv2-topbets-categories-filters/topbets filter syntax: eq:geo (single bucket) / neq:sport,esport (exclude buckets, comma-separated); buckets are esport/sport/geo/crypto/other.
  • pmv2-exit-alertspmv2-exits CLI: read-only 24h preview of cohort exits (sends no alerts — eyeballs what the live ”🔴 TRADE EXITED” alert would have fired on).
  • category-consensus/consensus <category> [N] [K] bot command (defaults N=50, K=3): async category-consensus over a freshly-fetched category-leaderboard PNL cohort; <category> must be one of the 9 leaderboard categories.

Copy Trading (cohort mirror)

  • pmv2-onchain-watch-crate-extractionthe cohort-mirror’s on-chain detection substrate (OrderFilled decoder + resilient WSS feed) is now a SHARED reusable git crate pmv2-onchain-watch (@ 5142d98, 2026-06-23, babylon #402). Rather than position_manager copying cohort’s battle-tested decoder + feed to watch its own self-wallets, the substrate was extracted to a watchlist-parameterized lib both services depend on (git dep, like pmv2-contracts); cohort dogfoods it — its watch/ collapsed to a thin run_feed consumer (watch::Sender cohort set @ 60s refresh → run_feedmpsc of WatchedFills → handle_cohort), behavior byte-identical from markets.resolve onward (only the input type OrderFilledEvent/CohortActionWatchedFill changed). The mirror’s detection logic is unchanged — this is library extraction, not a strategy change.
  • cohort-pre-live-signal-gatingthe cohort-mirror first-mover ENTRY now gates on the REAL price we’d pay + real market liveness before publishing (e444af0, 2026-06-23). chase_gate uses live best_ask vs the cohort trader’s fill (avg_price) — skip if best_ask > avg_price·(1+max_entry_premium_bps/10_000) — so a market that ran up since the copied trader entered isn’t chased; entry_price_band_gate repointed onto best_ask (our entry) not the trader’s frozen fill; liveness_gate skips resolution-proposed / non-accepting / near-resolution markets. category_filter restricts which buckets to mirror. All FAIL-CLOSED. Re-adds a read-only ClobReadClient (/book best-ask + /markets/{cond} status) — a deliberate CLOB re-coupling for best-ask accuracy over the free cur_price/gamma proxy. The mirror is still mode OFF.
  • cohort-algo-componentthe cohort-mirror producer is now its own component (cohort_algo, github.com/wowjeeez/pmv2-cohort-algo @ 2e150b1, 2026-06-22). It owns BOTH the first-mover ENTRY (Core-tier first BUY → action:Buy) and the cohort EXIT (tracked trader sells on-chain → action:Sell) signal production over NATS cohort.signals (schema_v1 today), plus the cohort scoring cron, consensus alerts, the on-chain watcher and the 30s gap-backfill — everything needed to PRODUCE the mirror signal, but not to execute it (zero CLOB dep; the executor mirrors). Ported behavior-preserving from the polymarket_fetch carve (preserved crate::pmv2:: paths + the polymarket-data crate name ⇒ zero logic edits); pre-port review fixed the empty-Data-API mass-false-exit + the dropped-publish-still-deletes-row lost-signal bug. Standalone home of the producer half documented in cohort-signals-service-split-and-exit-signals.
  • cohort-signals-service-split-and-exit-signalsthe copy lane is now a two-sided producer over NATS: first-mover ENTRY (action:Buy) AND cohort EXIT (action:Sell) both publish to cohort.signals for an out-of-process executor to mirror (2026-06-22, e762f5d). Closes the loop on “watch trade exits the same way as first-mover entries” — previously the EXIT was DB bookkeeping only (pmv2-phase4-chain-cohort-sell-fast-exit), so a copied position had no close signal once execution moved out-of-process. sources::exit_signal(PositionRow) builds the Sell (Execution::Context, reference_price=position.cur_price); entry+exit share publish_cohort_with_retry; publish-before-delete. GOTCHA: exit signal_id carries an exit discriminator distinct from the entry’s so the consumer’s ON CONFLICT(dedup_key) doesn’t swallow the Sell as a dup Buy (capital trap). Exit gate stays the score-based eligible_sell+ExitDebounce, NOT narrowed to Core-tier (you exit any cohort sell you’re mirroring).
  • bot-pmv2-native-v1-consensus-deletedoperator-overlap view in the bot/CLI: per operator position, how many edge-cohort traders hold the same (condition_id, outcome_index) + conviction-weighted bet_score (reuses the bet-link scoring path). Honestly an EXPOSURE/consensus read, not a first-mover trigger — the live first-mover signal is first-mover-overlap-watcher; this just annotates the operator’s own book. Replaced the v1 consensus-alignment view (ec75491, 2026-06-03).
  • first-mover-overlap-watcherthe read/alert side of the first-mover thesis (shipped 2026-06-02, 4794a76). Rather than a copy, the cohort’s ENTER/EXIT is logged to an append-only feed and joined against the operator’s OWN book to raise CONFIRM / FOLLOW_OUT / AGAINST alerts (annotated with the trader’s confidence score + avg won/lost bet sizes). Crucially the event feed + alerts run even with the paper-copy lane OFF (detection decoupled from pmv2_copy_config.enabled); the actual paper-copy ACTION remains independently enabled=false. Realizes the xref-consensus-rejected-fresh-copy-wins first-mover framing on the pmv2 pipeline as an operator-overlap signal.
  • pmv2-pipeline-bug-auditpre-release hardening of the pmv2 first-mover watcher (pmv2/positions.rs, default-OFF, unreleased): (1) load_copy_config fetch_onefetch_optional + CopyConfig::disabled() so a missing singleton no longer errors the whole positions snapshot; (2) insert_position_event now warns on error (was .ok()) + table gets UNIQUE(proxy_wallet,condition_id,outcome_index) + ON CONFLICT DO NOTHING (kills silent audit-log loss + dup ENTERED rows on overlapping runs); (3) per-event cap was bypassable within one cycle (event_cap_used read only committed rows) → fixed with an in-flight HashMap<event_id,f64> accumulator + pure within_event_cap helper + AND status='open' (cap = live exposure). Deferred sizing-formula tuning (score_weight=score·10+1 makes base_usd a near-no-op above score~0.4) for enable-time.
  • xref-consensus-rejected-fresh-copy-winsthe verified mandate for the copy lane: copy on ONE fill, fast — do NOT add a consensus/quorum gate. A purged --xref backtest (EVENT-only, verified twice) shows copying the first cohort BUY per outcome (xref-K1) = +7.59% (all 3 folds positive) while requiring agreement monotonically degrades it (K2 worse, K3 negative). So waiting for ≥2 cohort wallets is structurally late (price already moved). Vindicates copy-trade-lane’s single-fill, on-chain-fast design; refutes a quorum gate. +7.59% is an UPPER BOUND — the lane’s cohort_price / detect_latency_ms / up_mid_30s_ago / up_mid_60s_ago columns are exactly what’s needed to test how much survives real-ask-at-detection execution.
  • copy-trade-lane — the durable note for the on-chain copy lane: strategy basis (copy selection, not our signal), on-chain-vs-Data-API rationale, verified decode facts, architecture, the DB runbook (pm_copy_config / pm_copy_wallets, hot-reload), deploy state, and honest limits. Rejects cohort SELLs — mirrors cheap-outcome BUYs only.
  • copy-trade-algorithm — the other, unrelated copy idea: all-category, lifetime-PnL-edge tracker that backtested to no demonstrated edge. Do not confuse with the on-chain crypto-updown copy lane in copy-trade-lane.
  • pmv2-autotrade-engine-designthe LIVE-money first-mover copy lane (design LOCKED 2026-06-14). First signal source = first-mover entries from Core-tier cohort traders → real CLOB orders. Sizing reuses the frozen paper decide_copy (base × score_weight × conviction, clamped, per-event cap) wrapped with new economic gates (min-conviction, price bounds, EV-after-cost floor, market-liveness) + hard caps — gates live AROUND decide_copy, never inside it. reference_price is frozen at first detection (not the later blended avg_price) so the chase anchor reflects the copied entry. Distinct from the on-chain paper copy-trade-lane and from copy-trade-algorithm. Realizes the xref-consensus-rejected-fresh-copy-wins “copy the fast first-mover, don’t wait for quorum” mandate with real execution.
  • swisstony-hft-sports-market-makerworked example of a high-volume/high-PnL wallet that is explicitly NOT copyable. swisstony is Polymarket’s 1-by-volume HFT sports MARKET-MAKER; his ~1% margin accrues to the resting maker, so a follower mirroring his fills crosses the spread as a TAKER and pays away exactly the edge, then loses on gas/fees. Also 0% crypto (no fit with the crypto-updown first-mover lane) and our ~30-min REST trigger lag can’t track 13 fills/sec. Mirror-copy verdict: skip; treat as a flow/liquidity-concentration signal only.

Database Schema

  • cohort-algo-migration-squash-cleanupbabylon #333 IMPLEMENTED (a3329dc, 2026-06-23): cohort squashed its 44 inherited sqlx migrations → ONE idempotent CREATE TABLE IF NOT EXISTS baseline (20260624000000_baseline.sql) for ONLY the 12 tables it owns/reads (9 pmv2_* + 3 kept pm_*: pm_scheduled_runs/pm_category_consensus_rows/pm_category_consensus_runs); cumulative deployed schema (ALTERs folded), RLS preserved; dropped the pmv2_autotrade_* DDL (PM’s migrator 4712de9 recreates byte-identical so cohort’s READS survive) + ~25 dead pm_*. set_ignore_missing(true) on the Migrator (sqlx 0.8.6) in db.rs+schema test tolerates the deployed DB’s 44 now-orphaned applied versions (fresh DB: baseline creates all; deployed: safe no-op). KEY DIVERGENCE: the original #333 ask (a distinct sqlx history table) was UNNECESSARY — PM uses Supabase, not sqlx, so cohort is the SOLE sqlx owner; squash + ignore_missing supersedes the distinct-history-table approach + the sqlx-shared-migrations-table-trap framing for this DB. Validated via testcontainers+insta schema_snapshot golden (independently re-confirmed); gate green (310 tests/clippy/fmt); reviewed clean. Lessons: (1) count the actual sqlx migrators before isolating history tables — a Supabase/Diesel/psql peer is not a sqlx migrator; (2) set_ignore_missing(true) = THE technique to edit a deployed sqlx set (squash/prune/re-baseline) without checksum/version errors; (3) schema_snapshot validates columns only (not constraints/indexes/RLS) + omits 2 kept tables = coverage gap. Prod surgery (PM migrator first → pm_* DROPs → _sqlx_migrations reset) is @deploy’s (#380→#397).
  • cohort-algo-shared-db-migrator-ownershipbabylon #331 (2026-06-23, option (b) per data-ownership principle D3): PM owns the FULL pmv2_autotrade_* migrator (config/sources/orders + any v2 columns); producers (cohort) DROP all pmv2_autotrade_* DDL → read-only precondition (cohort READS config+sources, never touches orders). Shared-DB sqlx safety = each repo uses a DISTINCT migration-history table (custom Migrator table name) so histories can’t collide — REFINES sqlx-shared-migrations-table-trap “one migrator per DB” → “one migrator per (distinct) history table, disjoint physical tables still required.” Producer-private tables (scoring/consensus/positions) stay in the producer’s own migrator+history table. RESOLVES the position-manager-interface-design “physical relocation DEFERRED” hold. FREEZE on pmv2_autotrade_* schema until PM’s migrator ships. Cohort cleanup (NOT yet done): prune ~28 dead legacy pm_* tables + drop the autotrade DDL + set a distinct history table.
  • position-manager-interface-designSCHEMA SPLIT, REVISED post-audit (2026-06-20) — CODE ownership only, NO second migrator. PM owns the ledger CODE (ledger.rs runs sqlx queries against pmv2_autotrade_orders); the table DDL stays with the single existing upstream sqlx::migrate! (it already exists in prod). PM runs NO migrator — no migrations/ dir, sqlx migrate feature omitted, the applied upstream migration untouched. The earlier plan (PM owns an ADOPT-IF-EXISTS CREATE TABLE IF NOT EXISTS migration) was WALKED BACK — it is a sqlx trap (see sqlx-shared-migrations-table-trap). Physical table relocation DEFERRED to a coordinated single-migrator change. pmv2_autotrade_config is CO-OWNED: PM reads the caps subset (load_caps: total_exposure_cap_usd/daily_spend_cap_usd/max_open_positions/max_copies_per_event) AND writes the breaker counters (bump_breaker_count, config-only id=1 FOR UPDATE lock); strategy filters + mode stay upstream. PM is the SOLE counter of exposure/open positions. Split is ownership of MEANING per column/operation, not a table relocation.
  • sqlx-shared-migrations-table-trapREFERENCE/LESSON: sqlx 0.8 tracks ALL applied migrations in ONE hardcoded _sqlx_migrations table keyed by version (BIGINT PK) — so you cannot run two separate sqlx::migrate! sets against the same DB. Three failure modes: (1) two migrator sets → each throws MigrateError::VersionMissing on the OTHER’s applied versions (validate_applied_migrations defaults ignore_missing=false) → boot fails; (2) reusing a version integer across two dirs → VersionMismatch; (3) editing an ALREADY-APPLIED migration file changes its stored checksum → VersionMismatch → bricks boot on deployed DBs. CREATE TABLE IF NOT EXISTS does NOT save you — sqlx tracks by version ROW not table existence, so it still inserts the version row and collides. Takeaways: ONE migrator per DB/_sqlx_migrations table; never reuse version numbers; never edit applied files; owning a table’s CODE is separate from owning its DDL/migrator. This forced the position-manager-interface-design migration-ownership walk-back.
  • pmv2-position-manager-nats-consumertwo new migrations for the NATS position-manager (2026-06-17, branch autotrade-foundation, NOT applied to prod). 20260617000000_pmv2_source_mode.sql: ALTER TABLE pmv2_autotrade_sources ADD COLUMN mode TEXT NOT NULL DEFAULT 'off' CHECK (mode IN ('off','dry_run','live')), then UPDATE ... SET mode='live' WHERE source_id='first_mover_core' (cohort backfill so effective_mode==global, behavior preserved). 20260617010000_pmv2_weather_source.sql: seeds the weather source row (source_id,enabled,mode) VALUES ('weather',TRUE,'off') ON CONFLICT DO NOTHING — present but disabled (no-op until armed). effective_mode = min(global, source) on off<dry_run<live.
  • correctness-pass-six-fix-commitsalways filter redeemable=false when aggregating holdings (2026-06-03). pmv2_positions / pm_positions keep RESOLVED rows until the next snapshot prune, so unfiltered holder queries surface stale resolved positions as “held” with near-zero stakes that round to $0. Fix on cohort/holder queries = redeemable=false AND initial_value>1; self_positions got the same filter (it was raising false overlap alerts on resolved markets). Also: display rank must use the MONTH windowCOALESCE(rank_month, rank_week, rank_day), NOT rank_all — because the Polymarket leaderboard UI defaults to the month tab (fixed at all 5 query sites); and user_name subqueries need ORDER BY with their LIMIT 1 or the name is nondeterministic across the 4 leaderboard intervals.
  • first-mover-overlap-watchermigration 20260603000000_pmv2_overlap (applied to prod): forward ALTERs on the LIVE pmv2_position_eventsdropped the per-key UNIQUE (table is now an append-only enter→exit→re-enter lifecycle log), kind CHECK → ('ENTERED','EXITED'), added run_id / trader_score / avg_won_bet_size / avg_lost_bet_size; created pm_overlap_alerts (UNIQUE(event_id, self_wallet) for dedup). Overlap watcher cursor lives in pm_detection_cursors (key 'overlap_watcher'). GOTCHA: the auto-generated constraint name was TRUNCATED to ..._outcome_inde_key (not ..._index_key) — look up real names via pg_constraint before DROP CONSTRAINT.
  • pmv2-phase4-chain-cohort-sell-fast-exitCONSEQUENCE of the dropped pmv2_position_events UNIQUE (above): exit dedup CANNOT live in a DB constraint. insert_position_event is a PLAIN INSERT with NO ON CONFLICT, so duplicate EXITED rows are possible at the table level. The three EXITED-row consumers dedup via their own cursors + the mark_selling open→selling CAS, NOT the schema: cohort EXIT alert = the pmv2_exit_alert timestamp cursor; auto_sell = the autotrade_exits cursor + mark_selling. Phase 4’s chain fast-exit path safely rides those by LOGGING the EXITED row + DELETING the prior pmv2_positions snapshot (so the cron can’t log a 2nd row), instead of inserting a 2nd alert/sell source. (See the Auto-Trade topic for the full decision.)
  • pmv2-pipeline-bug-audit — pmv2 watcher migration 20260602030000_pmv2_watcher.sql gains UNIQUE(proxy_wallet, condition_id, outcome_index) on the position-events table (paired with ON CONFLICT DO NOTHING to stop duplicate ENTERED rows). Edited but NOT applied — apply before enabling the watcher.
  • purged-backtest-gate-no-edgecovering index for the backtest’s slowest query: idx_pm_pos_condition_enddate ON pm_positions (condition_id, end_date) turned fetch_market_end_dates from ~459s → ~8s (index-only scan, no heap access). Migration 20260601120000_pm_positions_condition_enddate_idx.sql, applied CONCURRENTLY to prod (no table lock) and reversible; cut the full purged+control backtest to ~2min. Durable pattern: a (condition_id, end_date) covering index gives an index-only scan; build prod indexes CONCURRENTLY.
  • phase1-consensus-rebuild-implemented — consensus bucket key changed (condition_id, outcome_index)(condition_id, outcome_index, asset) (so opposite-asset / negative-risk positions can’t inflate a long outcome’s wallet_count). WS2’s daily heavy-sync gate keys off max(computed_at) in pm_short_term_trader_accuracy — the table doubles as the durable “last heavy run” timestamp (no separate state table needed; survives the fresh-per-tick scheduler process).
  • consensus-validation-leakageschema gap blocking honest validation: pm_market_resolutions has only fetched_at (when we learned the outcome), no true resolved_at (when the market actually resolved) — so trade→resolution pairing can’t be cut at a real cutoff, which is prerequisite for purged+embargoed CPCV. Adding a real resolved_at is remedy step 2.
  • copy-trade-lanepm_copy_config (singleton: enabled kill-switch DEFAULT FALSE, cap_price, min_price, size_usd, min_window_left_secs, max_daily_notional_usd) + pm_copy_wallets (per-wallet enable, seeded with the 5 cohort wallets); copy fills land as lane='copy' rows in pm_paper_trades. Applied manually via Supabase MCP because pm-arb does not run sqlx migrate.
  • pmv2-topbets-categories-filterspmv2_event_category cache table (event_slug → category bucket), populated by run_category_sync from gamma /events tags (best-effort, bounded per-chunk tokio::timeout). Filters are inert until a scheduled run primes it.
  • pmv2-exit-alertsmigration 20260604000000_pmv2_event_pnl.sql: added cash_pnl / initial_value / current_value / cur_price / outcome / title columns to pmv2_position_events (captured in insert_position_event from the prior PositionRowno call-site change), denormalizing P/L context onto the event so the exit alert renders “P/L (last seen)” without a re-join.
  • pmv2-autotrade-engine-designthree new auto-trade tables (design): pmv2_autotrade_config (singleton id=1 — the cap/breaker serialization row, holds mode/exit_mode/maker_proxy + all sizing/gate/cap params + breaker_tripped/breaker_fail_count; seeded conservative, never auto-arms), pmv2_autotrade_sources (source_id PK + enabled, seeded first_mover_core), and pmv2_autotrade_orders (the order/position ledger: dedup_key with a unique partial index where status non-terminal, salt/expected_order_hash, mode/status, fills/fee_usd/cost_basis_usd/realized_pnl_usd, resolution_only/exit_pending flags, error_class). Terminal states dry_run|closed|canceled|failed; reservable/non-terminal pending|submitted|filled|partial|open|selling. DB is the source of truth for accounting; reservations count pending|submitted|selling. The exit source uses a new pm_detection_cursors row autotrade_exits (NOT the shared exits cursor).
  • category-consensusmigration 20260605010000_pm_category_consensus.sql: pm_category_consensus_runs (status running→done/error, cohort_size, top_n) + pm_category_consensus_rows. Persists each /consensus <category> run (10-min cache key).
  • topbets-enddate-1970-fixmigration 20260608000000 adds pmv2_event_category.end_date (the gamma EVENT end date, returned by event_tags, stored by upsert_event_category). The 3 cohort-holder queries COALESCE(p.end_date, ec.end_date) so positions whose market endDate is the 1970-01-01 epoch placeholder fall back to the real event date.
  • pmv2-trending-betspm_trending_alerts dedup table for the acceleration auto-alert (12h cooldown key).
  • polymarket-fetchpm_runs (now with selector / selector_params), pm_leaderboard_snapshots, pm_intersections, pm_traders, pm_positions, pm_consensus_positions, pm_selected_wallets, pm_trades, pm_trader_metrics
  • bot-pmv2-native-v1-consensus-deletedv1 tables pm_selected_wallets / pm_safe_bets / pm_trader_metrics are now fully DORMANT in code (no reader remains after ec75491), but left in the DB, not dropped per the standing “don’t drop pm_* prod tables” rule. With v1 fully removed from code, these are dead schema awaiting an eventual cleanup decision.

Deploy / Production

  • cohort-firstmover-roll-d851d44-2026-06-252026-06-25 ~20:19Z: apps-pmv2-cohort-algo-firstmover pin 8977b5bd851d44, ONLY the firstmover component (query-responder + scheduled-run untouched). On-chain feed migrated Alchemy WSS log-subscription → eth_getLogs HTTP polling via the 150-proxy rotating pool (boot log polygon poll feed armed interval_secs=5 max_blocks=2000); kills Alchemy WSS CU bleed on this lane. Same pin bumps consensus-alert threshold 2 → 4. Architecture caveat: PM self-watch (pmv2-onchain-watch) is on a SEPARATE WSS path and STILL uses Alchemy WSS — don’t conflate the two lanes
  • cohort-algo-migration-squash-cleanup@deploy prod-ops handoff for cohort’s migration squash (babylon #380 resolved → #397). The migration-source squash shipped (a3329dc); the deployed DB reconciliation is @deploy’s, strictly sequenced: (1) PM’s migrator runs FIRST (stands up the pmv2_autotrade_* family it now owns, 4712de9); (2) ~25 dead pm_* DROPs via psql — BEGIN/COMMIT, transcript-first, PM confirmed zero refs; (3) reset _sqlx_migrations to the baseline so deployed bookkeeping matches the squashed source. cohort does NOT run any of this (no-direct-prod-SSH rule).
  • pmv2-fleet-infra-observability-nats-conventionsFLEET DEPLOY/OPS CONTRACT (from @deploy, babylon pmv2 #313, 2026-06-22). Telemetry exporters point at the LOCAL OTel collector on polymarket-infra (127.0.0.1:4317 gRPC / 4318 HTTP; Docker host.docker.internal:4318), NEVER SigNoz Cloud directly. Containerized apps deployed via the ansible apps role get the std OTEL env auto-injected (OTEL_SERVICE_NAME/_ENDPOINT/_PROTOCOL/OTEL_RESOURCE_ATTRIBUTES=service.namespace=pmv2,deployment.environment=prod); native repos opt in. Streams are @deploy-owned/provisioned (TELEGRAM_OUTBOUND 1d, OPS 7d, PMV2_ORDERS, legacy WEATHER_SIGNALS; all 5-min dedup window) — services create their OWN durable consumers but NEVER create streams. Health golden rules: docker ps/systemctl is-active=ground truth, journalctl -u <svc> -f=universal fallback, SigNoz=second look, nats stream/consumer info w/ pmv2 admin seed sparingly. SigNoz dashboards: “polymarket-infra — Host & Tailscale” / ”— App services (APM)” / ”— NATS + JetStream”.
  • polygon-wss-provider-drpcPolygon WSS provider DECISION = dRPC over Alchemy (2026-06-20). TRIGGER: Alchemy creds drained; ROOT CAUSE = Alchemy bills 0.04 CU PER BYTE on the 24/7 eth_subscribe logs stream → byte/event-metered billing structurally drains an always-on subscription (cost scales with traffic volume, not request count). PICK = dRPC: flat per-request (20 CU/req), 210M CU/mo free tier, $6/1M CU PAYG with no monthly floor, multi-node routing; the new eth_getLogs backfill calls also benefit. Swap = a SINGLE env var POLYGON_WSS_URL, zero code change (same var the copy lane/watcher already read; unset = idles silently). Verify before cutover: dRPC per-notification CU cost + WSS concurrency limits on their dashboard. Runner-up = Alchemy PAYG (no re-integration but the per-byte stream bill is the drain); self-host not worth it at a single connection.
  • pmv2-position-manager-nats-consumerNEW long-running service unit pending (2026-06-17, branch autotrade-foundation, NOT deployed). The NATS consumer runs as a dedicated pmv2-consumer subcommand process — NOT part of scheduled-run (the engine is a one-shot per-tick batch, so a spawned NATS loop would die when the tick process exits). Dormant-safe: exits 0 with "NATS_URL/NATS_NKEY_SEED absent; consumer dormant, exiting 0" if the NATS env is unset. Needs: (1) @deploy to add the service unit + the env (NATS_URL tailnet nats.taild4189d.ts.net:4222 + NATS_NKEY_SEED, pmv2 identity pubkey UDYV…ZEDD, in POLYMARKET_FETCH_ENV); (2) the container DNS-resolution fix for the tailnet host (was pending at spec time); (3) manual dry-validate (publish a real weather signal → confirm a DRY-RUN rehearsal). Arming weather live = TWO operator actions: global mode live AND weather.mode=live (effective_mode=min(global,source)).
  • autotrade-alert-latency-and-run-timeouts★ CURRENT scheduler topology + ops latency/timeout finding (2026-06-17, supersedes the polymarket-fetch-deploy service table). ~/levandor/terraform/deploys/apps.yml on polymarket-infra: polymarket-fetch-scheduled-run job schedule:"*:0/30" (scheduled-run --dedup-seconds 300 --gap-warn-seconds 3600) + polymarket-fetch-bot service; wallet-monitor + pm-arb-validate now absent (decommissioned); weather-bet-run + daily weather-bet-resolve present. systemd Type=oneshot TimeoutStartUSec=15min. NEW: 3/48 runs in 24h killed at the 15-min cap (Jun16 20:00 / Jun17 08:30 / 09:00 UTC, last two consecutive); typical OK run ~6–9 min; a kill drops that cycle’s later-pipeline alerts (exit/sell run after positions). Autotrade live in DRY-RUN (mode=off/dry). Verify via @deploy on babylon.
  • bot-pmv2-native-v1-consensus-deleteddeploy target now ec75491 (Round 52b); 135 tests pass, clippy clean (3 pre-existing only), fmt clean. OPS INCIDENT (2026-06-03) — zombie bot container: two polymarket-fetch bot containers ran concurrently on the GCP VM — one on new code (overlap_watcher cursor advancing) + one on OLD pre-4794a76 code (participant_changes cursor STILL advancing every minute, a detector DELETED in 4794a76), spamming a v1 “watched wallets position dump”. Diagnosis via pm_detection_cursors: an old-code-only detector advancing = old container still live (same class as the Round-50 Telegram 409 getUpdates two-poller conflict). Fix is VM-side (kill stale container, leave one on HEAD), escalated to @deploy. GOTCHA: after a bot deploy, verify only ONE container runs AND that old-code-only detector cursors FREEZE — a still-advancing deleted-detector cursor is the signature of a zombie container.
  • first-mover-overlap-watcherGO-LIVE warning: the overlap / new-position / self-trade alerts are NOT default-off (unlike the copy lane). They fire the moment the binary deploys; the overlap gate uses pmv2_copy_config.score_threshold which defaults to 0 → ALL scored cohort traders qualify → very noisy. Set score_threshold > 0 (e.g. 0.03–0.05) before/after deploy. The paper-copy lane stays independently enabled=false. Loose end carried into prod: self-status CLI + bot /wallets still render v1 consensus alignment from the frozen pm_selected_wallets (safe-but-stale, no runtime error) — the consensus chain was intentionally kept because deleting it breaks self-status.
  • pmv2-two-tier-trader-scoringpmv2 hard-cut deploy (handed to @deploy, AGENT_HANDOFF.md Round 49, STATUS pending): deploy 5ffc330, decommission apps-pm-arb-validate (ends the rounds-44–48 copy-lane validation) + apps-polymarket-fetch-wallet-monitor, redeploy the bot (which lost /safebets //ranked //snapshot, gained the bet-link auto-reply). Code-only cut — pm_* prod tables left dormant, not dropped. Build/clippy/fmt green; 151 + 99 tests pass. Bet-link live paste-test still pending.
  • copy-trade-lane — copy lane ships default-OFF (dormant, zero-risk). Runtime config is DB-configured and hot-reloaded ~60s, no redeploy (UPDATE pm_copy_config SET enabled=true to start). Two manual prerequisites: migrations applied via Supabase MCP (pm-arb doesn’t run sqlx migrate), and new env var POLYGON_WSS_URL in the POLYMARKET_FETCH_ENV GH secret (until set, the lane idles even if enabled — logs a warning, doesn’t error). Handed to @deploy in AGENT_HANDOFF.md Round 42.
  • polymarket-fetch-deploy — three units (bot / scheduled-run / wallet-monitor) live on ops-vm in europe-west3-a since 2026-05-27 11:08 UTC. Docker via Artifact Registry, pulled by the generic apps Ansible role from ~/levandor/terraform/. Verification: scheduled-run on a 30-min systemd timer, wallet-monitor capturing ~2.6K rows / 30 min. Replaces the staged-but-never-loaded launchd plist.

Latency Arbitrage

  • edge-discovery-2026-06-22DURABLE VERDICT: every structural/latency arb is DEAD at this op’s speed — do NOT re-explore. Neg-risk convert / cross-side: ~2.7s avg windows, ~73% captured by sub-100ms bots, ~0.3% median spread, non-atomic convert (partial fill = naked exposure), “no longer profitable in 2026.” Conditional-chain (combinatorial) arb: ~**100/opportunity, in the zero-fee era (two-leg fees now invert most Politics cases). Cold-start: PM markets open with an EMPTY book set by snipe-bots (the most bot-contested instant, not unwatched). Breaking-news: free RSS ~15min late vs ~150ms PM news-bots — zero as a profit center, but the feed IS valuable as an adverse-selection kill-switch for the maker lanes. ESPN in-game is the SLOW feed (20–30s behind broadcast; backtests give false positives). Pinnacle-vs-PM is an already-productized bot archetype. The proven themes win precisely because they don’t require winning a sub-100ms race.
  • cross-venue-pm-kalshi-arb — cross-venue (not BTC-vs-book) arb between Polymarket and Kalshi. Shelved: only the Fed-decision family rule-matched, the edge is a tick-grid rounding gap, and with no atomic cross-venue fill the leg-out risk wipes 2–3× the profit. Reopen only with atomic-ish dual-venue execution + broader rule-matched coverage.
  • short-term-accuracy — high-edge Up/Down wallets thought to run a Binance→Polymarket latency arb; book drifts ~3-4c per 10% of window so a slightly-late copy keeps most of the +8.7pp edge. Thesis superseded — see top-trader-edge.
  • lag-probe — read-only harness measuring book lag vs true BTC price; captures 4 feeds + lag distribution. 14h capture (2026-05-23/24, real volatility) measured CLOB book lag vs direct Binance at p50 300 ms / mean 844 ms / p99 8 s. Edge window 300 ms to 2 s; sub-100 ms colo in eu-west-2 beats the book. RTDS Binance ~800 ms slow, RTDS Chainlink (settlement oracle) ~2.6 s slow. Caveat: clob_book throughput dropped ~20×, possible back-pressure — true lag may be tighter. Lag is real but not the source of the top-cohort edge — see top-trader-edge.
  • top-trader-edge — strategy v2 thesis. Naive lag-arb failed M0 at 46% win rate; real edge is late-window 5m longshot reversion (price ≤ 0–0.10 × late cell) + late-window momentum-chase (≥ 2¢ bid in last 30s, hit rate 73.4%, edge +4.7¢). Cross-asset (BTC+ETH+SOL+XRP). Validated on 0x75cc3b, purest on 0xd189664c (77.8% ROI / $9.4K PnL).

Fees / Cost Model

  • edge-discovery-2026-06-22P0 LIVE-CAPITAL CORRECTION + the canonical PM Fee Structure V2 table (verified mid-2026, eff. 2026-03-30). Taker fee = Theta·C·p·(1−p) (vanishes at p→0/1): Economics 0.05 / Crypto 0.07 / Politics 0.04 / Finance 0.04 / Tech 0.04 / Sports 0.03 / Esports 0.03 / Geopolitics 0 (free) / Culture 0.05 / Weather 0.05; maker rebates ~25% (~20% crypto). THE CORRECTION: Polymarket WEATHER is NOT fee-free (Theta 0.05, same as Culture, 2.25/$500)**; re-run the live weather deflated Sharpe NET of it — a thin margin may fall below the 1.5×-cost ship bar (purged-backtest-gate-no-edge). Extends/supersedes the partial schedule on cross-venue-pm-kalshi-arb (adds the missing Weather/Culture/Crypto/Sports/Esports rows). Worked math + rebate-trivial-at-extreme-p + rebate-adjusted-Kelly backlog in-note.
  • polygon-wss-provider-drpcRPC-provider cost model for the 24/7 Polygon WSS subscription (2026-06-20): Alchemy meters eth_subscribe logs at 0.04 CU PER BYTE — a byte/event-metered streaming bill drains an always-on subscription (scales with traffic, not call count), which is what drained our Alchemy creds. dRPC’s FLAT PER-REQUEST model (20 CU/req, 210M CU/mo free, $6/1M CU PAYG no floor) is the right shape for an always-on stream. Durable lesson: for a long-lived WSS log subscription, prefer a flat-per-request RPC provider over a per-byte/per-event one — the always-on byte stream is exactly the workload per-byte billing punishes; verify the per-notification charge + concurrency limits before committing.
  • cross-venue-pm-kalshi-arb2026 Polymarket fee schedule (PM is no longer zero-fee): taker fee Θ·C·p·(1−p), Economics Θ≈0.05 / Politics+Finance 0.04 / geopolitics 0. Kalshi ≈ ceil(0.07·C·p·(1−p)). Durable fact — invalidates older “PM is free” assumptions in short-term-accuracy / top-trader-edge.

Gotchas / Bugs Fixed

  • pmv2-self-watch-rustls-cryptoprovider-crashloop★ a tokio::spawn panic RESOLVES the parent’s &mut JoinHandle select arm (with JoinError) — it is NOT only a clean-return signal; this is what turned a rustls CryptoProvider panic into a silent zero-events crash-loop in PM’s self-watch feed (crate pmv2-onchain-watch pin 719dc32; crate-side fix NOT committed, 2026-06-24). PM’s self-watch feed connected to DRPC ogws then died ~200ms later, respawning every ~5s (FeedConfig::respawn_backoff) with ZERO events while backfill (separate reqwest path) kept working. ROOT CAUSE: tokio-tungstenite 0.29 (tls.rs:126) builds TLS via rustls ClientConfig::builder() → resolves the process-default CryptoProvider; rustls 0.23’s get_default_or_install_from_crate_features() only auto-picks when EXACTLY ONE of ring/aws_lc_rs is enabled, but PM’s Cargo.lock compiles rustls 0.23.40 with BOTH aws-lc-rs (via quinn-proto/reqwest http3) AND ring and never calls install_default → the first wss:// handshake panics. The panic unwinds (default panic=unwind) killing only the spawned feed task → run_feed’s _ = &mut handle => arm RESOLVES with a JoinError → logs the generic respawn line + respawns. That mechanism explains the 200ms timing, the 5s cadence, AND the absence of any crate log (the panic bypasses all tracing). Two repro traps: (a) python-websockets uses OpenSSL — a different TLS stack that never exercises rustls provider resolution, so probing the endpoint looks healthy (server IS healthy; bug is client-side); (b) a single-provider standalone build (ring only) does NOT panic — the dual-provider condition only materializes in the full consumer dependency graph, so a standalone-crate repro is misleadingly green. SAME class as the earlier cohort_algo “Detector rustls CryptoProvider fix” (cohort_algo/src/main.rs:57 + telegram_connector/src/main.rs:7 install ring as the first line of main(); PM was missing it). FIX = make the crate robust: idempotent ensure_crypto_provider() (via std::sync::Once, install-if-absent) at the top of connect_once, + rustls single-provider (ring,std) in the crate Cargo.toml. Durable lessons: (1) a panicked spawned task makes the parent’s &mut JoinHandle select arm fire — a respawn loop spinning with NO error logs = suspect a swallowed panic in the spawned task, not a clean return; (2) rustls dual-provider panics are an AGGREGATE-graph property — ring+aws-lc-rs both compiled with none installed → panic on first handshake; check the CONSUMER’s Cargo.lock, not the crate’s; (3) a different TLS stack (OpenSSL) is not a valid client repro for a rustls provider bug; (4) make libraries robust to the consumer’s main() via an idempotent install-if-absent on the connect path rather than relying on every binary to call install_default.
  • pmv2-onchain-watch-crate-extraction★ a keepalive WSS ping gated behind the read-timeout arm is SILENTLY INEFFECTIVE under load — its configured interval never fires (review-pass cleanup, pin 1b8a0f9, 2026-06-23). In pmv2-onchain-watch’s feed, the WSS ping was emitted from the same select-arm as the 30s read-timeout, so on a busy socket (frames always arriving before the read-timeout) the 15s PING_INTERVAL never fired and liveness drifted to ~90s. FIX: a dedicated tokio::time::interval(PING_INTERVAL) select-arm so the ping cadence is independent of traffic (restores the intended ~45s). Two companion fixes in the same pass: (a) the eth_getLogs/eth_block_number reqwest client now sets BOTH timeout AND connect_timeout and is OnceLock-reused (the standing proxied-hang trap — see the connect_timeout gotcha below); (b) a failing boot eth_block_number used to silently seed the backfill watermark to 0 (disabling backfill until the first live fill) — now retries 3×+warns. Two durable review lessons: (1) verbatim module extraction carries dead weight — moving onchain.rs→crate brought TWO whole entry points (run_detector block-latency path + the SubCommand resubscribe path; run_log_feed_static hardwired a dummy cmd channel so its arm was dead) + unused helpers that the new single consumer run_feed never touches; a dead-code sweep against the ACTUAL consumer surface shed ~⅓ of the crate (−378 lines). (2) a reviewer’s SEVERITY can be wrong — verify it against disk too: this ping issue was flagged “Critical (600s detection + frame loss)”, but tracing connect_once showed the read-timeout arm DOES fire on silence so stalls were still detected (~90s) → downgraded Critical→Important (a cadence fix, not an emergency). Keepalive ping cadence must live on its own timer, not behind a read-timeout arm.
  • pmv2-onchain-watch-crate-extraction★ a gap-recovery watermark must advance on the SCAN, not only on delivery — else a quiet watchlist can PERMANENTLY miss a later real fill (caught in review, fixed before publish d94d7c5, 2026-06-23). When extracting cohort’s on-chain feed into pmv2-onchain-watch, the new run_feed runner first advanced last_seen_block only on a delivered (watchlist-matched) fill. Combined with the 2000-block backfill cap (max_backfill_blocks), a long quiet stretch containing only non-deliverable watched OrderFilled events stalled the watermark — and a later reconnect’s [watermark, latest] span would exceed 2000 blocks and silently drop a real fill in the gap. FIX: backfill_once advances the watermark to the scanned to UNCONDITIONALLY after each successful scan, via a const fn next_watermark(current, scanned_to) -> u64 (monotonic max, never rewinds on out-of-order delivery; feed.rs:638). This also fixed an inherited flaw in the original cohort backfill.rs — the extraction surfaced a latent bug in the source. Durable lesson: a gap-recovery watermark tracks what you’ve EXAMINED, not what you’ve ACTED on — advancing it only on delivery means quiet periods silently shrink the recovery window until a real event falls off the edge of the backfill cap. (Two companion extraction lessons in the note: watchlist-parameterization made the generalization near-free because the decoder was already generic over HashSet<Address,S>; and the gamma resolver was kept OUT of the lean crate so PM’s dep stays light.)
  • pmv2-onchain-watch-crate-extraction★ a dedup key with a CONSTANT component collapses to its remaining fields — PM’s self-wallet alert dedup (wallet, transaction_hash, outcome_index) silently became per-WALLET because transaction_hash was always "" (crate fix re-pinned 1b8a0f9719dc32, babylon #430, 2026-06-24). PM’s self-wallet (Balu/Bandi) instant-alert watch persists a row in pm_self_trade_alerts keyed by (wallet, transaction_hash, outcome_index) so it won’t re-alert across restarts. But the pmv2-onchain-watch WatchedFill struct carried no tx_hash, forcing PM to set transaction_hash=""+outcome_index=0 for EVERY fill — so after the FIRST fill of a wallet was recorded, is_alerted returned true for all subsequent fills of that wallet = permanent one-alert-per-wallet suppression (PM-side adversarial review caught it). No sound PM-side interim key existed: token_id+amounts isn’t unique, and now() breaks restart-idempotency — the only durable per-fill identity is the on-chain (tx_hash, log_index), which only the decoder has, so the fix HAD to be in the crate. FIX = expose the identity triple on WatchedFill (tx_hash: B256, log_index: u64, block_number: u64; already on the in-scope OrderFilledEvent at decode.rs:53-64, plumbed through decode_watched_fillrun_feed); PM now keys on ("{:#x}"(tx_hash), log_index) instead of constants. Durable lesson: a composite dedup key is only as unique as its LEAST-varying component — if any field is pinned to a constant ("", 0, now() truncated, etc.) the key silently collapses onto the rest; verify every component genuinely varies per-row, and when the only stable per-event identity lives upstream (here the on-chain tx_hash/log_index), expose it from the producer rather than fabricating a weak key downstream.
  • cohort-pre-live-signal-gatingthe ported ev_floor_gate was DEGENERATE — size_usd cancels out, reducing a “real EV floor” to a price-blind constant bps>200 (caught + DROPPED, 2026-06-23). edge = size_usd·(bps/10_000) vs cost = size_usd·0.02size_usd divides out → the gate is just bps > 200, completely ignoring the price. It was a placeholder shaped like an EV floor. FIX = remove it, don’t rebuild it: operators set max_entry_premium_bps (the chase gate) net of round-trip cost — one price-AWARE knob. min_round_trip_edge_bps is left loaded-but-unused (NOT deleted) because the pmv2_autotrade_config schema is FROZEN (PM-owned, babylon #333). Also: GET /book returns asks DESCENDING, so .min() is load-bearing for the best ask (first-element = worst ask). Also: an empty event_slug can never resolve a categoryresolve_category short-circuits to None to skip a guaranteed-wasted gamma RTT on the latency-sensitive first-mover hot path. Durable lesson: an “EV floor” where the size term appears on BOTH sides isn’t an EV floor — verify the formula doesn’t algebraically collapse before trusting a gate; a price-blind gate that LOOKS economic is worse than no gate because it gives false confidence.
  • cohort-firstmover-lane-mode-restart-gotchaa config value read ONCE at boot is a silent restart-required trap: the first-mover lane mode (off/dry/live) is fixed at process startup in build_first_mover_lane and never re-read, so a DB mode flip looks applied but the running process keeps its boot-time publisher state until restarted. Compounded by an OBSERVABILITY gap — no startup INFO announces the effective off/dry/live mode (only first_mover_core armed + cohort count), so dry-vs-live can’t be confirmed from SigNoz logs, only from the DB rows; and rare/silently-filtered entry signals mean an absence of publish logs is NOT proof the lane is broken. Durable lesson: distinguish hot-reloaded config (watchlist, breaker) from boot-fixed config (signal mode) when reasoning about whether a DB change took effect; if a flip isn’t reflected, suspect a value captured once at startup and restart the process. (2026-06-25)
  • autotrade-sources-canonical-signal-idPhase 2 B1: signal_id was NOT byte-identical across producers due to two independent bugs in sources.rs (2026-06-19, branch autotrade-phase2-firstmover). (1) cohort_signal_id passed origin_ref (wallet address), condition_id, and instance verbatim to orders::dedup_key — on-chain addresses are always lowercase hex, but the cron side can carry checksummed (mixed-case) wallet addresses, producing two different hash inputs for the same logical wallet. (2) enter_signal used p.event_slug.unwrap_or(condition_id) as instanceevent_slug is Option<String> and absent on the watcher detection path (no CLOB context at detection time), so cron and watcher computed different instance values for the same position. Together these breaks the DB ON CONFLICT (dedup_key) DO NOTHING dedup, meaning the same logical position could result in TWO live orders. FIX: lowercase all three inputs before hashing + use condition_id directly as instance (always present and on-chain stable). Durable lesson: any multi-producer dedup-by-hash scheme must canonicalize EVERY input — address casing and optional fields are the two most common silent divergences; use condition_id as the canonical market discriminator, not event_slug which is Optional and may be absent on fast-path detectors.
  • cohort-firstmover-nats-migration★ async-nats CORE publish is FIRE-AND-FORGET — it silently drops into nothing on a missing/misfiltered subject (post-cutover 3-lens review, 2026-06-18, 39c01dd). The cohort publisher first used async_nats::Client::publish_with_headers(...) + client.flush() (the core client). Core NATS publish returns NO JetStream PubAck and does NOT error when no stream’s subject filter captures the subject — so if cohort.signals was never bound into the WEATHER_SIGNALS stream (the T5 deploy step), every entry published “successfully” into nothing: consumer never sees it, zero error anywhere, operator believes the lane is live. The code could not detect it. FIX: publish via the JetStream contextasync_nats::jetstream::new(client.clone())publish_with_headers(...).awaitPublishAckFuture.await the ack, under a 5s tokio::time::timeout. Compiler-pinned against vendored async-nats 0.49.1: a missing/misfiltered subject yields NO_RESPONDERS → StreamNotFound = a loud Err the bounded retry surfaces; persistence is now confirmed (PubAck) instead of assumed. Durable lesson: async_nats::Client::publish* (core) gives NO ack and NO error on a missing subject — Msg-Id stream-dedup still applies but delivery is UNCONFIRMED; whenever you need delivery confirmation OR loud-on-missing-subject behavior, publish through async_nats::jetstream::Context::publish* and .await the PublishAckFuture (ideally under a timeout). Companion operator-visibility fixes in the same commit: an “armed-but-unconfigured” lane (cohort active but watcher NATS_URL/NATS_COHORT_NKEY_SEED unset) now warns + telegrams instead of dropping entries at debug (previously indistinguishable from intentional dormancy), and retry-exhaustion telegrams a “first-mover entry LOST” alarm — because re-detection next tick is impossible (the end-of-run snapshot upserts unconditionally), so a dropped publish is a PERMANENT miss (benign for capital — never a double — but not self-healing, so it must be visible). A pure unit-tested publisher_intent(at_active, has_url, has_seed) -> {Dormant, Unconfigured, Ready} classifier drives the distinction. T5 addendum: the cohort-publisher NKey must permit JetStream publish to cohort.signals (publish + receive PubAck on its inbox), not merely core publish. Gate green 429 polymarket-fetch + 89 polymarket-data, trade-safety reviewed SAFE; branch autotrade-foundation, NOT deployed.
  • cohort-firstmover-nats-migration★ LOAD-BEARING: deleting the watcher-side autotrade_ready() (when moving cohort entry to NATS, 2026-06-18, bdafd7f) silently removed TWO things — the in-process entry-PREFLIGHT and the per-tick reconcile_entries. It is safe TODAY only by coincidence (cohort lane dormant / global mode off, no live orders exist, /autotrade reconcile still works manually). But it shifts a hidden dependency onto the Pre-arm consumer-preflight gate (the blocker already shared with @weather): that gate must NOW ALSO restore per-tick entry-reconcile + preflight before arming ANY lane live — otherwise the first live lane (cohort OR weather) will accumulate stuck pending/submitted orders that nothing auto-reconciles. Durable lesson: when you delete a watcher-side function during a transport migration, enumerate EVERY responsibility it carried — a preflight/reconcile gate often piggybacks on the same per-tick hook as the thing you meant to move; losing it is invisible while the lane is dormant and only bites at the first live arm. Also a clean parity pattern worth reusing: the cohort_roundtrip_parity test (serialize ∘ parse byte-equal to the legacy emitter, with event_slug≠slug fixtures) is how a transport swap is proven not to change engine inputs.
  • autotrade-resolution-proposed-entry-guarda Polymarket market can be ALREADY DECIDED yet still closed=false + acceptingOrders=true — its UMA result is “proposed” but not finalized, and it keeps accepting orders at the collapsed price (2026-06-17, 824a22d+8b3c451). The cohort lane DRY-RUN “bought” an ITF tennis LOSER at ~$0.002 because (1) the entry price_bounds_gate checked the COPIED TRADER’s frozen reference_price (~0.44, in-band), not the live best_ask (0.001), and (2) liveness_gate only rejected FORMALLY resolved/closed/non-accepting markets — a UMA-proposed one passes. Fix = two complementary ENTRY-ONLY guards: an ask-band gate (engine.rs::plan_context, reuses price_bounds_gate on the live ask, Cohort/Context-only — NOT the weather Directive path that buys up to 0.95) and a resolution-proposed guard (GammaClient::market_resolution_proposed reads umaResolutionStatusesMarketStatusView.resolution_proposed, FAIL-OPEN with the ask-band as backstop → liveness_gate skip). TWO durable gotchas: (a) umaResolutionStatuses is a JSON-ENCODED STRING ("[]" / "[\"proposed\"]"), NOT a native JSON array — same encoding as outcomes/outcomePrices; parse via parse_json_string_array — a Vec<String> deserialize silently fails. (b) in the vendored polymarket-clob, MarketStatus.resolved is set resolved: m.closed — a MISNOMER (just a copy of closed), so the CLOB meta carries NO real UMA-resolution signal; only Gamma does (same family as the is_terminal_market = resolved||closed aliasing under the 404-wedge fix). Durable lesson: a price-band entry gate must validate the price YOU will execute at (the live ask), not the reference price the copied trader entered at; and “result proposed to the oracle” is a distinct pre-closed state that only Gamma’s umaResolutionStatuses exposes — the CLOB cannot tell you. Keep the guard entry-only so an already-held position stays exitable. Gate green 503 workspace tests; branch autotrade-foundation, NOT deployed.
  • autotrade-exit-cursor-404-wedge-fixa RESOLVED/CLOSED Polymarket market returns CLOB /book 404 "No orderbook exists for the requested token id" PERMANENTLY — so a cursor-driven loop that treats the first /book failure as a hard stop WEDGES forever on that event (2026-06-16, b5d5986). run_exits (runner.rs) break/return false’d on the first exit-event that 404’d and resume_cursor parked the autotrade_exits cursor at first_failed−1µs; a settled market’s permanent 404 re-failed every pass (Telegram-spamming) AND blocked every exit-event queued behind it — once live, cohort-mirror exits for positions behind the wedge would never fire (capital-trapping class, same family as the SELL-never-sells lot-size blocker). FIX = stop conflating “transient read failure” with “this market is terminal”: market_data.rs::resolve() returns an empty BookTop for resolved||closed (never calls book_top → no 404); pure exit_decision_for(market) returns ExitDecision::{Skip (terminal),Exit (open+bid),Retry (open+no-bid or transient resolve error)} with advances_cursor()=Skip|Exit; terminal → advance cursor / clear_exit_pending, quiet debug, no alert; transient → still halts (no advance, retried). GOTCHA-IN-A-GOTCHA: in the vendored CLOB client (polymarket-clob/src/lib.rs:251 ClobClient::status_from) BOTH MarketStatus.resolved AND .closed are mapped from the single /markets field m.closed — so is_terminal_market = resolved||closed effectively reduces to “closed” today; if a future change populates resolved independently, re-verify the terminal predicate and everything riding on it. Durable lesson: distinguish a PERMANENT terminal-state error from a transient one in any cursor/queue advance decision — a permanent error that doesn’t advance the cursor is an eternal wedge that also starves everything behind it; and audit each value-validation boundary against what the upstream actually returns for a settled market. Trade-safety review 7/7 (transient still halts; breaker untouched by read errors; zero entry/reconcile blast radius). NOT on main/VM (VM bf4b628) — live engine still wedged until merge+redeploy.
  • autotrade-3c2-round2-validation★ HIGH-VALUE: the vendored CLOB share-amount constructor enforces a 0.01 LOT SIZE — feeding it a raw on-chain fill silently breaks every fractional SELL (2026-06-16, BLOCKER e93dde1). rs-clob-client-v2 Amount::shares(value) rejects any share amount whose value.normalize().scale() > LOT_SIZE_SCALE (=2) (types/mod.rs:193-200, re-applied order_builder.rs:25). auto_sell passed the RAW on-chain CTF fill (full precision — locked test vector 7.6219 shares, scale 4) straight into build_order → Amount::shares → build errorauto_sell reverted the row to openEVERY real fractional-share position was un-exitable (both auto-exit and the manual /autotrade sell), capital trapped until on-chain resolution. The round-1 SAFETY review missed it — it checked the project’s own assert_sell_amounts but not the vendored lot-size guard one layer deeper, so the assertion passed while the constructor it fed rejected. FIX: quantize oncesell_shares = filled_size.trunc_with_scale(2) — and use that single value for build_order + quantize_sell(expected_shares) + assert_sell_amounts; sub-0.01-share dust is held to resolution (return before mark_selling, so it never enters selling). BUY is unaffected (Amount::usdc allows scale ≤ 6; the market-order builder has no lot gate). Durable lesson: a Polymarket SELL size must be trunc_with_scale(2) (0.01 lot); the full-precision on-chain balanceOf fill must NEVER be handed raw to any share-amount constructor — and when you vendor a value-validating SDK, audit every value across that boundary against the SDK’s own accept/reject rules, separately from the flow’s safety logic. Companion bug in the same pass: a behavior-preserving DRY extraction (collecting_notify) drained via Arc::try_unwrap while the notify closure still held a 2nd Arc clone → always empty → /autotrade sell swallowed its SOLD/REJECTED/TIMEOUT reply (visibility only, CI green because nothing asserted output); fixed to lock()-drain + a regression test (0f06ec5).
  • autotrade-max-copies-per-eventmax_copies_per_event was wired (DB load, validation, conversion) but never consulted at placement (2026-06-16). The cap existed in AutotradeConfig and was mapped by engine_config(), but neither EngineConfig nor ReserveCaps carried the field, and no query checked in-flight event counts before INSERT. Zero-means-unlimited (distinct from max_open_positions’s zero-blocks-all); negative DB values clamp to zero (unlimited) to prevent accidental total block from a default row. The fix adds the field, the clamped mapping, and the in-tx COUNT in the LIVE branch only (dry-run orders don’t count toward the cap).
  • autotrade-signing-client-order-opsSignedOrder has no Deserialize — serde round-trip via serde_json::from_value::<SignedOrder>(...) does not compile (2026-06-15). The vendored type derives only Debug + Builder + PartialEq. Do NOT attempt to serialize/deserialize SignedOrder. Fix: hold the typed value directly in BuiltOrder.signed_order: SignedOrder and drop the SignedPayload(Value) wrapper. Also: TradeResponse carries NO order_hashTradeView.order_hash is always None; correlate trades to an order via taker_order_id == clob_order_id, not the EIP-712 hash. PostOrderResponse.order_id maps from JSON "orderID" (camelCase alias in serde).
  • pmv2-bet-link-analyzer-flowfour cohort/category gotchas + category-cohort fallback (shipped 22d7697, verified main 2026-06-15): (1) the /consensus cache only warms on status='done'recent_category_consensus selects WHERE status='done' (repo.rs:1601) and a run is only readable after complete_category_run(..,'done') (repo.rs:1542); computing + inserting rows without completing the run leaves it 'running' and INVISIBLE (the next /consensus recomputes from scratch). (2) no scheduler warms that cacheschedule.rs/scheduled_run.rs have no category-consensus job, so it is usually cold and the first /consensus per category pays full fetch+fan-out latency. (3) parse_selector (scoring.rs:91) returns the LAST URL path segment, so a deep /event/<event>/<market> link yields the MARKET slug, not the event slug (test scoring.rs:993-995); gamma.event_tags() (client.rs:505) takes EVENT slugs → deriving an event’s category from parse_selector output silently fails on deep links — prefer BetScore.event_slug (DB truth) or the segment after /event/. (4) TWO empty-states in the bet reply mean different thingsscores.is_empty() (nobody holds it) vs all-sides-below-GAUGE_FLOOR=0.005 (held but no high-edge side); consolidate_gauge is empty-safe so the latter renders cleanly, not a panic. Durable lesson: the edge-scored bet-link cohort and the monthly-PNL /consensus cohort are different populations on different category taxonomies — don’t cross-feed slugs or buckets without the category_tags/GEO_TAGS/CRYPTO_TAGS bridge.
  • autotrade-eip712-order-hash#[non_exhaustive] on sol!-generated OrderV2 blocks FRU from external crates (2026-06-15). Both OrderV2 { field, .. } and OrderV2 { field, ..Default::default() } fail to compile from outside the defining crate when the type is #[non_exhaustive]. Fix: let mut o = OrderV2::default(); o.field = val; o. Also: alloy_sol_types::Eip712Domain is the correct type for EIP-712 work — alloy::dyn_abi::Eip712Domain is a different internal type; mixing them causes type-mismatch errors. missing_const_for_fn fires on any function returning only 'static str literals — add const. expect_used/unwrap_used are forbidden workspace-wide; use LazyLock statics with a tight #[allow(clippy::unwrap_used)] per static for hardcoded-constant parsing.
  • topbets-enddate-1970-fix/topbets showed ends 1970-01-01 (2026-06-08). ROOT CAUSE: the data-api positions feed returns the epoch placeholder 1970-01-01 as the market endDate for markets whose end date lives on the gamma EVENT (market-level endDate is null → serialized as epoch). FIX: capture the gamma event end_date into pmv2_event_category.end_date (migration 20260608000000); normalize_end_date in map_position drops empty/pre-2000 → NULL; cohort-holder queries COALESCE(p.end_date, ec.end_date); /topbets shows ends {date} (unresolved) for past-dated-but-held. Verified: kansas-governor→2026-11-03, openai-ipo→2026-12-31. Durable lesson: a Polymarket position endDate of 1970-01-01 is an epoch placeholder — the real date is on the parent gamma event, not the market. DISTINCT from the correctness-pass UTC-midnight-countdown end_date issue.
  • self-sync-held-only-fix/wallets showed 0 positions despite holding several (2026-06-08). SAME past-date class as the 1970 fix: repo::is_position_inactive(position, today) dropped any position that was redeemable OR past its end-date, so still-HELD bets on past-but-unresolved markets (elections) were filtered out → the whole book collapsed to 0. FIX: replaced with is_position_redeemed(position) = position.redeemable ONLY; both the stored upsert_positions_for_wallet and the live /wallets fetch_self_overlap_sections_live now keep currently-held positions (redeemable=false, size>0) regardless of end-date (drop only resolved + dust). DECISION: “currently-held only” (resolved bets stay hidden; widening is a one-liner). Durable lesson: held-ness is redeemable=false, NOT “before end-date” — a past end-date does not mean the position closed.
  • category-leaderboard-apiFALSE ALARM: the UPPERCASE category query param is NOT a “lowercase bug” (2026-06-05). A reviewer flagged polymarket_data’s Category::as_query_value() rendering OVERALL/WEATHER (uppercase) as a bug. The data-api category param is case-insensitive (category=WEATHER and OVERALL both work; timePeriod=month/MONTH both work), and prod already runs with uppercase OVERALL successfully. Do NOT “fix” to lowercase — zero behavior change, pure churn on a working path. Durable lesson: verify a claimed casing bug against the live API before changing a prod query.
  • correctness-pass-six-fix-commitscodebase-wide correctness pass, 6 fix commits (HEAD f42c7f4, 2026-06-03), triggered by operator-spotted bot data inaccuracies; ~50 issues across 6 domains, operator-visible + correctness-critical fixed, revalidated clean. Durable gotchas: (1) rank display must match the PM UI’s MONTH default (COALESCE(rank_month,…) not rank_all); (2) filter redeemable=false when aggregating holdings or resolved dust shows as $0 “held”; (3) consolidate_gauge must group by (event_merge_key, outcome_index) not outcome_index alone, else distinct candidates merge into “No — for what?” (merge-key strips date/year/integer tokens; KNOWN EDGE: it strips the token “may”); (4) band = SKILL (edge×consistency), · N× avg = conviction (stake/avg_bet_size); they are orthogonal — surface both. Other fixes: copy_pnl ÷0 (entry_price 0→NaN→copy stuck ‘open’); load_event_winners random winner on cancelled/0-payout markets corrupting SCORING → require payout>0+deterministic tiebreak (SHARED winner path, see pmv2-pipeline-bug-audit); leaderboard names unescaped HTML (Telegram <>& parse fail); clamp_to_telegram_limit byte-vs-char; pmv2 collect/positions made non-fatal in execute_pipeline (transient API error was aborting the whole scheduled run); non-pmv2 flatten_resolution_rows missing dedup (#371-class crash). Scoring-math (operator-approved, shifts ranking): conviction() default 0.5→0.0; consistency_factor returns 0 when edge ≤0 + dropped the 0.01 floor; avg_won/lost/bet_size now per-POSITION. Mop-up (Fix-D): anonymous-sender rate-limit now denies; ex-cohort prune delete_positions_not_in_wallets DOUBLE-guarded against empty-set whole-table wipe (<> ALL('{}') is TRUE for all rows → the empty-slice early-return is load-bearing); copy-cap event_id=None bucketed under condition_id; mark_copy_resolved idempotent; partial-fetch min-success floor (collect bails if >25% of fetches fail). Deferred: ~3500-trade API offset cap truncates prolific traders (structural); date-only end_date as UTC midnight (countdown ≤24h off).
  • bot-pmv2-native-v1-consensus-deletedzombie deploy container signature (2026-06-03): two bot containers ran at once on the VM (new + old pre-4794a76 code); the OLD one’s participant_changes cursor — a detector DELETED in 4794a76 — was STILL advancing in pm_detection_cursors, spamming a v1 position dump. Durable lesson: a still-advancing cursor for a detector that no longer exists in HEAD is proof an old-code container is still live. Same root-cause class as the Round-50 Telegram 409 getUpdates two-poller-one-token conflict. After any bot deploy: confirm exactly ONE container runs AND that old-code-only detector cursors FREEZE.
  • first-mover-overlap-watcherPostgres auto-generated constraint names can be TRUNCATED (2026-06-02). The overlap migration’s DROP CONSTRAINT had to target ..._outcome_inde_key, NOT the assumed ..._index_key — Postgres truncates generated identifiers to 63 chars and the column-list rendering dropped mid-word. Durable lesson: never assume an auto-named constraint from the column list — query pg_constraint for the real name before DROP. Also captures the timed-out-fetch = false-exit trap: a current-state-mirror snapshot that DELETEs “missing” rows must scope the delete to wallets that actually returned data (fetch_positions(rows, succeeded: HashSet)), or a timeout reads as “exited everything” and wipes the snapshot (same class as the silent connect-timeout hang).
  • pmv2-pipeline-bug-auditany chunked ON CONFLICT (...) DO UPDATE upsert MUST dedup its input by the conflict key first (2026-06-02, 2nd occurrence of this class). upsert_market_resolutions/flatten_resolution_rows (crates/polymarket-fetch/src/pmv2/repo.rs) had no dedup before a chunked ON CONFLICT (condition_id, outcome_index) DO UPDATE — a duplicate key in one chunk crashes with ON CONFLICT DO UPDATE command cannot affect row a second time, the exact crash that killed scheduled-run #371 (positions table). Fixed with dedup_resolution_rows (keep-last, order-preserving — behavior-identical to what DO UPDATE converges to). Durable lesson: INSERT ... ON CONFLICT DO UPDATE can’t touch the same target row twice in one statement; chunked batches make this live whenever the source can repeat a key — dedup keep-last by the conflict key before chunking.
  • pmv2-two-tier-trader-scoringdon’t trust truncated slugs from display queries (2026-06-02, Phase 2a). An ad-hoc left(slug, 40) in a diagnostic truncated ...by-may-31-2026 to ...by-may-3 — a different, wrong identifier. Durable lesson: match bet selectors on the full slug / condition_id, never on a left()/terminal-width-truncated display render.
  • pmv2-two-tier-trader-scoringproxied reqwest clients need connect_timeout, not just timeout (2026-06-02). pmv2-positions hung ~75 min silently at --concurrency 8 (zero rotation/retry/eviction warns, no completion) because a proxy accepting the TCP connection then stalling the HTTPS CONNECT tunnel hangs past the total .timeout(30s) — endpoint/proxies/data were all fine. Fix (defense-in-depth): .connect_timeout(10s) on the shared build_http_client (DataApiClient + GammaClient) and build_proxy_client, a per-wallet tokio::time::timeout(90s) in fetch_positions, and default --concurrency 8→4. No deterministic repro, so the two timeouts are belt-and-braces. Durable lesson: always set BOTH timeout and connect_timeout on networked/proxied clients; bound fan-out fetches with a per-item timeout; the “silent stall, zero warns” signature points at a stuck connect, not retries/rotation.
  • lag-probecapture tests are flaky under parallel workspace load: load_grids_buckets_events_onto_grid and load_grids_skips_blank_and_malformed_lines (crates/lag-probe/src/capture.rs) PASS in isolation but FAIL under full-workspace cargo test — the temp file is named process_id + timestamp_nanos (capture.rs:150), so concurrent tests race on the path (remove_file().unwrap() panics; parsed-count assert fails). Pre-existing, not a Phase 1 regression (lag-probe was untouched). Fix with the tempfile crate / unique dirs. Durable lesson: don’t rely on pid+nanos for temp-file uniqueness under a parallel test runner — use tempfile.
  • phase1-consensus-rebuild-implementedconsensus wallet_count could be inflated by opposite-asset positions: keying buckets only on (condition_id, outcome_index) let negative-risk / opposite-asset holdings aggregate with longs. Fix = add asset to the bucket key. Also: redeemable / past-end_date / dust (<$5) positions were counted as live votes — now filtered out. Durable lesson: a consensus/agreement count must aggregate only genuinely same-side, live, non-dust exposure; partition by asset and gate on eligibility, or the count overstates agreement. (Note: end_date parse fails open — unparseable kept + warn! — to avoid silently shrinking consensus on a formatting quirk.)
  • consensus-validation-leakagethree lookahead/survivorship leaks silently inflating the edge evidence (verified by reading source 2026-06-01): (1) crates/polymarket-data/src/consensus.rs:30 compute_consensus_groups uses LIVE position fields (current_value/avg_price) → post-entry + survivorship, not as-of-trade; (2) crates/polymarket-fetch/src/backtest.rs build_wallet_data + crates/polymarket-data/src/metrics.rs:132 pair_trades_to_resolved pair trades to resolution payouts across the train/test cutoff → label leakage; (3) crates/polymarket-fetch/src/repo.rs:1429 fetch_short_term_bet_rows has NO time filter → survivorship + lookahead. Durable lesson: build features point-in-time from pm_trades; never feed live snapshots or unbounded payout joins into a backtest/skill metric.
  • copytrade-paper-lane-latency-spread-fixes — two copy-lane analytics bugs (uncommitted): (1) detect_latency_ms always 0 because copy_lane.rs truncated the ms-precise upstream Detection.latency to whole seconds (block_ts) and copy.rs recomputed from those — sub-second latency floored to 0 (0 not NULL = block_time was populated). Lesson: carry a precisely-computed upstream value through; don’t truncate-then-recompute downstream. (2) negative chase_spread because the entry ask fell back to the MID. Durable no-arbitrage rule: on a binary up/down market ask_up = 1 − bid_down and ask_down = 1 − bid_up; synthesize a real ask from the opposite-side bid — the mid is NOT a valid ask. Refuted en route: the “gamma token-order inversion” hypothesis (4 rows matched cohort price to the cent). Residual deferred: a −15¢ stale-book outlier — book-age isn’t stored and a staleness threshold interacts with the planned low-traffic <60s lane.
  • copy-trade-lane — copy-lane deploy gotchas: (1) pm-arb does NOT run sqlx migrate (only polymarket-fetch does) — copy migrations had to be applied manually via Supabase MCP, and any future copy-schema change must too; (2) real OrderFilled topic0 is 0xd543adfd…, NOT the canonical-ABI 0xd0a08e8c — filtering on the canonical hash returns zero logs; (3) missing POLYGON_WSS_URL makes the lane idle silently (logs a warning, does not error) — it looks “enabled but doing nothing” until the env var is set; (4) you cannot trade on-chain — PM matches off-chain and only settles on-chain, so on-chain detection ≠ on-chain execution.
  • longshot-strategy — the 0.93 tiw cap must be longshot-only, not a global gate: in evaluate_v2 (strategy_v2.rs:210-216) it wraps only try_longshot; try_momentum runs unconditionally after because momentum’s signal IS the last-30s move (tiw≈0.9–1.0), so a global cap would kill it (pinned by test momentum_not_suppressed_by_longshot_max_time_in_window). Also the analytical correction: a 0.93–1.0 loss that looked like an execution/slippage gap was actually a signal problem (clean fills, 0 wins) — slippage can’t turn a winning $1 share into a loss. 2026-05-30 additions: (1) sqlx migrations must be idempotent (CREATE TABLE IF NOT EXISTS, ADD COLUMN IF NOT EXISTS, DROP POLICY IF EXISTS … ; CREATE POLICY …) — non-idempotent migrations compound when tracking drifts, today’s pm_self_trade_alerts feature was silently broken in prod for hours from this; (2) main.rs:2376-2380 swallows sqlx MigrateError so failures degrade silently — task #15 to fix (log at ERROR + non-zero exit so systemd restarts); (3) Supabase MCP direct prod surgery (CREATE TABLE IF NOT EXISTS + insert into _sqlx_migrations with computed SHA-384 checksums) is the standard unblock when migration tracking drifts; (4) auto-catch (per @deploy Round 36) applies to pm-arb only — polymarket-fetch services need explicit redeploys; (5) in a reversion strategy, “skip recent losers” gates of any shape are anti-correlated with future performance — per-asset rolling-ROI gate killed for this reason.
  • cross-venue-pm-kalshi-arb — resolution-rule-mismatch trap: two venues listing the “same” event can resolve on different measures (CPI = Polymarket YoY level vs Kalshi MoM change). Diff the resolution rules, not the titles, before treating a cross-venue pair as fungible.
  • polymarket-fetch — run-status leak, auto-chain self-skip, paginator infinite loop, DB password URL encoding, percent_pnl numeric overflow, dead-position pollution, strict 3-of-3 starvation, negative×negative copy_score gate
  • short-term-accuracy — survivorship bias from joining outcomes on the traded side (losing-only markets dropped, hit rates inflated); always derive market outcome authoritatively
  • polymarket-fetch-deploy — four deploy-time gotchas (cross-linked to gcp-terraform-ansible-gotchas 15-18): Supabase IPv6 vs Docker IPv4 (the killer — fix is session pooler URL, applies to every future Supabase-using app on this rig), .dockerignore killing sqlx::migrate!() (commit 834bc66), gcloud GPG key .asc extension, Tailscale name collision after VM reprovision.
  • recovering-claude-session-400-on-resume — Claude Code session 400s on --resume from a 26 MB / 14k-message transcript with unbalanced tool blocks; recover via AGENT_HANDOFF.md + historian in a fresh session rather than reviving it.

Infrastructure / Colocation

  • short-term-accuracy — Polymarket order matching is off-chain (centralized CLOB); Polygon only settles. No “Jito for Polymarket” — the race is network latency to Polymarket’s CLOB API on AWS eu-west-2 (London). Colocate in eu-west-2 / London-Dublin Equinix; US-East ~130ms is too slow.

NATS / Messaging

  • cohort-pre-live-signal-gatingstartup-announcement dedup via a version-stamped Nats-Msg-Id (e444af0, 2026-06-23): the boot announcement to telegram.outbound sets Nats-Msg-Id = startup|first_mover_core|<build-hash>, so the stream’s 5-min duplicate_window swallows repeated boots on the SAME build (a flapping restart announces once) while a NEW build (new hash) gets a fresh announcement. A clean reuse of the fleet Msg-Id dedup convention for an idempotent boot ping.
  • cohort-algo-v2-wire-contract-cutoverfirst producer to emit on the v2 pmv2.order.<source>.<entry|exit> subjects (0986c4d, 2026-06-23): cohort publishes schema_version 2 on pmv2.order.first_mover_core.entry + …exit (source_id="first_mover_core"), replacing the now-DELETED cohort.signals schema_v1 lane (signals/wire.rs + signals/signal.rs removed). Types come from the vendored shared pmv2-contracts crate (single source of truth — don’t hand-roll). The executor (position_manager) consumes these directly. (source MUST be the EXACT source_id key or it silently routes to Mode::Off; the exit’s origin_ref is load-bearing or the position can’t be auto-closed.)
  • pmv2-fleet-infra-observability-nats-conventionsFLEET NATS/JetStream CONVENTIONS (@deploy, babylon pmv2 #313, 2026-06-22). Always set Nats-Msg-Id on publish (in-stream dedup within duplicate_window, currently 5 min on all 3 streams). Streams (all @deploy-provisioned — producers NEVER create streams): TELEGRAM_OUTBOUND (subject telegram.outbound, 1d), OPS (ops.commands+ops.results, 7d — the control plane needs audit), PMV2_ORDERS (pmv2.order.<source>.<entry|exit>, pending PM consumer), legacy WEATHER_SIGNALS (weather.signals/cohort.signals/weather.shadow, until polymarket_fetch retires). Each consuming service creates its OWN durable consumer with a stable name (e.g. tg_outbound). NATS health via nats stream info / nats consumer info with the pmv2 admin seed (sparingly). NKey identity is logged as PUBKEY, never the seed.
  • telegram-connector-outbound-designtelegram_connector is a durable JetStream PULL consumer (tg_outbound) on telegram.outbound/stream TELEGRAM_OUTBOUND (2026-06-22). Dedup is the STREAM’s job (Nats-Msg-Id = the envelope msg_id within the 5-min duplicate_window) — the connector keeps no dedup state. It binds-or-creates its OWN durable consumer (explicit ack, ack_wait, max_deliver) but does NOT create the stream. Delivery is at-least-once with ack-AFTER-confirmed-send; transient errors → no-ack → JetStream redelivers bounded by max_deliver, exhaustion → dead-letter + ERROR. Connects via nkey auth. Concretely applies the fleet conventions in pmv2-fleet-infra-observability-nats-conventions.
  • cohort-firstmover-nats-migrationasync-nats CORE publish is FIRE-AND-FORGET (no PubAck, no error on a missing/misfiltered subject) — publish through the JetStream Context and .await the PublishAckFuture (under a timeout) whenever you need delivery confirmation or loud-on-missing-subject behavior (2026-06-18, 39c01dd, async-nats 0.49.1). The cohort producer’s lane to cohort.signals; pairs with the consumer pmv2-position-manager-nats-consumer and the transport migration that moved cohort entry ingestion onto NATS. The full gotcha (silent drop into nothing, the JetStream-context fix, and the “first-mover entry LOST” visibility alarm) is under Gotchas / Bugs Fixed.
  • pmv2-position-manager-nats-consumerthe dedicated pmv2-consumer long-running process (NOT part of scheduled-run, which is a one-shot per-tick batch) consuming the unified signal lane, routed by the envelope source field; dormant-safe (exits 0 when NATS_URL/NATS_NKEY_SEED unset) (2026-06-17, branch autotrade-foundation). The consumer half that cohort-firstmover-nats-migration publishes into. Tailnet NATS host nats.taild4189d.ts.net:4222, pmv2 identity pubkey UDYV…ZEDD.
  • query-commands-restorationthe query.request/query.results request/reply channel (cohort’s first INBOUND NATS surface, 2026-06-24): connector publishes QueryRequest{request_id,actor,kind} on query.request; cohort’s query-responder runs the ported pipeline and JetStream-publishes QueryReply{request_id,rendered} on query.results with Nats-Msg-Id={request_id}-result. Carries /topbets, /consensus, /filters.
  • bet-link-inspector-restored-pmv2additive QueryKind::BetInspect{url} rides the same query channel (2026-06-24) — NO new subjects/permits; serde back-compat (unknown variant to an old cohort = fail-safe skip). The fourth surface on the request/reply channel above.

Notifications / Telegram

  • cohort-pre-live-signal-gatingcohort_algo now publishes a STARTUP ANNOUNCEMENT on watcher boot (operator directive #354, e444af0, 2026-06-23): one telegram.outbound message component: first_mover_core started with version: <hash> <title> — version captured at BUILD time via build.rs (BUILD_GIT_HASH/BUILD_GIT_TITLE, the same compile-time SHA-capture pattern as pmv2-bot-startup-ping). Nats-Msg-Id = startup|first_mover_core|<hash> so same-version restarts DEDUP (the 5-min stream duplicate_window swallows a flapping restart on the same build; a new build → new hash → fresh announcement). Rides the Notifier::Outbound plane the v2 cutover flipped on → relayed by telegram_connector.
  • cohort-algo-v2-wire-contract-cutovercohort_algo FLIPPED its Telegram routing to the outbound NATS plane (0986c4d, 2026-06-23): Notifier::for_lane now constructs the Outbound variant (publishes TelegramOutbound to telegram.outbound, consumed by telegram_connector) when NATS is configured; Direct-send is fallback only (NATS absent). This completes the telegram.outbound seam that was prepared-but-Direct-defaulted in cohort-algo-component.
  • telegram-connector-outbound-designthe fleet’s SINGLE Telegram sender (telegram_connector, own repo /coding/pmv2/telegram_connector, design approved 2026-06-22). Sole bot-token holder + single rate-limit authority — every pmv2 service now publishes TelegramOutbound{msg_id,chat?,text,parse_mode?,kind?} to NATS telegram.outbound instead of calling the Bot API; the connector durably consumes (tg_outbound on stream TELEGRAM_OUTBOUND) and relays to sendMessage. A DUMB faithful relay — text + parse_mode passed verbatim, NEVER re-escaped (escaping is the publisher’s job, e.g. cohort_algo’s notifier). At-least-once, ack-AFTER-send; transient (429+retry_after/5xx/net) → retry then JetStream redeliver bounded by max_deliver; terminal (400 bad-format / 403 blocked) → drop+ack+ERROR + a best-effort loop-guarded plain-text meta-alert to TELEGRAM_CHAT_ID (META_ALERT_ON_DROP, default on) so a swallowed alert stays visible — a failed meta-alert is only logged, never meta-alerted. v1 OUTBOUND ONLY; inbound ops.* kill-switch relay is a later spec gated on @positionmanager (#311). Supersedes the per-service fire-and-forget Telegram clients (e.g. polymarket-fetch telegram.rs/notifier.rs) as the fleet consolidates onto the spine.
  • cohort-signals-service-split-and-exit-signalscohort EXIT now sends a Telegram alert alongside the action:Sell publish (2026-06-22, e762f5d). Previously on_cohort_sell neither alerted nor published — it only logged an EXITED row + deleted the snapshot (the pmv2-exit-alerts ”🔴 TRADE EXITED” alert rode that row’s cursor in-process). With the executor moved out-of-process, the exit handler itself now alerts + publishes. The shared publish_cohort_with_retry carries a drop-alert on give-up so an exhausted exit publish is never silent.
  • pmv2-bet-link-analyzer-flowthe bet-link AUTO-REPLY (no command — fires on any posted Polymarket URL via bot.rs::classify_messageMessageAction::BetLink): replies with a per-side skill-weighted cohort gauge built from pmv2_positionspmv2_traders (format_bet_scores_telegram, scoring.rs:593). Honestly labeled “current cohort EXPOSURE (consensus), not a first-mover signal”. Two distinct no-signal replies — “No edge-cohort positions match…” (nobody holds it, scoring.rs:514) vs “no high-edge cohort holders…” (held but all below GAUGE_FLOOR=0.005, scoring.rs:472). Contrast with the explicit /consensus <category> command (a different cohort entirely). Category-cohort fallback shipped 22d7697: when no signal, bot appends a category-consensus section to the SAME reply (25s-bounded, cache-first).
  • bet-link-inspector-restored-pmv2the monolith bet-link auto-reply RESTORED into pmv2 (2026-06-24) over the NATS query channel: connector detects a non-slash polymarket.com/ message → QueryKind::BetInspect{url} → cohort render_bet_inspect (score_bet(url) + gauge; !has_high_edge_signalresolve_fallback_category + category_consensus_section) → QueryReply{rendered} → connector posts HTML. 1:1 verbatim port of the monolith pmv2-bet-link-analyzer-flow; pins pmv2-contracts c6b3a15 + cohort_algo 1d573b8. Connector non-slash arm pending (@pmv2_telegram, babylon #517).
  • correctness-pass-six-fix-commitstwo Telegram-render bugs fixed (2026-06-03): leaderboard-update trader names were sent as unescaped HTML → a name with <, > or & triggered a Telegram parse failure (escape user-supplied text before HTML-mode send); and clamp_to_telegram_limit had a byte-vs-char length bug. Also the gauge/first-mover/overlap cells now show · N× avg (conviction) next to the skill band — see Gotchas / Bugs Fixed.
  • pmv2-trending-betsacceleration auto-alert (trending velocity) is Telegram-size-guarded to prevent an infinite re-alert loop: an over-limit unsent alert never records its 12h-cooldown dedup row in pm_trending_alerts, so the next cycle re-detects and re-attempts forever — the size guard ensures the alert is clamped/recorded so the cooldown takes hold.
  • pmv2-exit-alerts”🔴 TRADE EXITED” alert (2026-06-04): pushes a Telegram alert when a top cohort trader exits a bet (reads the pmv2_position_events EXITED feed). Shows trader name/score/band/rank, market+outcome, and P/L (last seen). CAVEAT: exits are snapshot-detected so the real fill price is unknown — the P/L is the last-snapshot unrealized cash_pnl, honestly labeled (last seen). Top-trader gated, cursor-deduped (pmv2_exit_alert), dust-filtered (≥$50), Telegram-budget capped.
  • pmv2-bot-startup-pingbot boot ping (2026-06-04): on polymarket-fetch bot boot, notifier::startup_message sends a best-effort NA CSUMI CSUMI CSUMI, bot STARTED, REF: <short-sha> (the REF: segment is omitted when no hash). The short SHA is baked at compile time by crates/polymarket-fetch/build.rs (env BUILD_GIT_HASHGIT_HASHGITHUB_SHAgit rev-parse --short HEAD→empty). build_startup_message is the pure builder. Bot-only. Deploy gotcha: a Docker image built without .git and without one of those build-args reports NO sha — pass GIT_HASH/GITHUB_SHA at image build to populate REF.
  • polymarket-fetchtelegram.rs + notifier.rs fire-and-forget, lifecycle/milestone/error notifications, --silent global, BotFather setup flow, rich per-wallet messages
  • pmv2-autotrade-engine-designowner-only /autotrade command surface + a reusable authz.rs seam (design). Scope::{Public,Owner} + OwnerSet + authorize()/scope_for(); owner ids from TELEGRAM_OWNER_IDS into BotConfig.owner_ids (mirrors allowed_chats, which is preserved — owner gate is additive); authorize once at the shared run_command chokepoint (message + callback both converge there), Denied → ⛔ not authorized. Commands (all Scope::Owner): status|off|dryrun|live|exits auto|notify|source <id> on|off|filter <expr>|set <param> <value>|maker <addr>|reconcile|resolve <id> canceled|filled, plus a sell:<order_id> inline-button callback (owner-revalidated, re-loads under lock). Subcommand routing is hand-written (parse_command only splits the leading token).

Observability / Telemetry

  • pmv2-fleet-infra-observability-nats-conventionsFLEET OBSERVABILITY CONTRACT (@deploy, babylon pmv2 #313, 2026-06-22). All 3 signals (traces/logs/metrics) → SigNoz Cloud VIA the LOCAL OTel collector on host polymarket-infra, NEVER direct. Endpoints: OTLP gRPC 127.0.0.1:4317, HTTP 127.0.0.1:4318; from Docker http://host.docker.internal:4318. Std env OTEL_SERVICE_NAME/OTEL_EXPORTER_OTLP_ENDPOINT/OTEL_EXPORTER_OTLP_PROTOCOL/OTEL_RESOURCE_ATTRIBUTES=service.namespace=pmv2,deployment.environment=prod (auto-injected for ansible apps-role containers; native repos opt in via SDK init). Logging discipline: ERROR=operator-visible failures, WARN=non-fatal anomalies (rate-limit/retry/dedup-skip), INFO=state transitions only (metered, NEVER per-message above ~1/s), DEBUG=per-message behind RUST_LOG. Log key fields as ATTRIBUTES not embedded strings (source/source_id/signal_id/order_id/condition_id/outcome_index/dedup_key/subject). NEVER log secrets/PII (private keys, NKey seeds, bot tokens, DB URLs) — log identity (NKey PUBKEY) not the seed. Tracing: one span tree per end-to-end signal lifecycle; span names = function role not data; data as attributes; Rust = tracing + tracing-opentelemetry + opentelemetry-otlp. SigNoz dashboards: “polymarket-infra — Host & Tailscale” / ”— App services (APM)” / ”— NATS + JetStream”.
  • telegram-connector-outbound-designtelegram_connector telemetry init follows the fleet contract (2026-06-22): one telemetry module call from main wires tracing+tracing-opentelemetry+opentelemetry-otlp to the local collector. Logs use structured attributes (subject/nats_msg_id/chat/kind/error_code/retry_after); span names by role (relay_message, send_message); INFO for state transitions (consumer bind, message sent) but NEVER per-message above ~1/s; DEBUG per-message behind RUST_LOG; NEVER logs the bot token / nkey seed. See pmv2-fleet-infra-observability-nats-conventions.
  • cohort-firstmover-lane-mode-restart-gotchaGAP: the firstmover service (pmv2-cohort-algo-firstmover, ns apps, prod host polymarket-infra.taild4189d.ts.net) emits NO startup INFO line announcing its effective lane mode (off/dry/live) — only first_mover_core armed (target pmv2_watch) with the cohort count. So dry-vs-live is UNVERIFIABLE from SigNoz logs alone; you must read the DB config rows. A startup line printing effective mode would make dry/live verifiable from telemetry — worth adding. Also: the cohort publisher dormant (lane inactive) log is DEBUG (usually un-exported), so an inactive lane is effectively invisible in SigNoz. (2026-06-25)
  • tracked-alerts-consumerend-to-end signal latency confirmed SUB-SECOND via SigNoz (the pmv2-tracked-detectortracked.signalspmv2-tracked-alerts→Telegram round-trip was traced and verified in SigNoz). An example of the per-signal-lifecycle tracing the fleet observability contract codifies.

On-Chain Detection

  • pmv2-self-watch-rustls-cryptoprovider-crashloopthe WSS feed itself can crash-loop with ZERO events while backfill keeps working — a rustls CryptoProvider panic on the FIRST wss:// handshake in PM’s self-watch (crate pmv2-onchain-watch src/feed.rs, pin 719dc32, 2026-06-24). The on-chain feed connects to DRPC ogws then dies ~200ms later and respawns every ~5s delivering nothing, because tokio-tungstenite’s TLS builder resolves the process-default rustls provider and PM’s graph has BOTH ring+aws-lc-rs compiled with none installed → panic on the first handshake → the spawned feed task dies → run_feed’s &mut JoinHandle arm resolves (JoinError) → respawn. The eth_getLogs backfill rides a separate reqwest path and is unaffected — so “backfill fine, live WSS dead” is the diagnostic split. Crate-side fix = idempotent ensure_crypto_provider() on the connect path + single-provider rustls; same class as the cohort_algo/telegram_connector install_default() first-line-of-main fix. See the Gotchas topic for the full mechanism + the “panicked spawned task resolves the JoinHandle arm” lesson.
  • pmv2-onchain-watch-crate-extractionthe on-chain OrderFilled decoder + the resilient WSS feed are now a STANDALONE reusable crate pmv2-onchain-watch (extracted from cohort, canonical pin 1b8a0f9 after a 2026-06-23 review-pass cleanup, was 5142d98, babylon #402). A post-publish DRY/SOLID/bug pass cut −378 lines with the consumer surface unchanged — dead-code sweep (the whole newHeads block-latency path + the SubCommand resubscribe path were unreachable from run_feed) + DRY (topic_hex, watchlist_topic_slots) + 3 robustness fixes (proxied connect_timeout/OnceLock; keepalive ping moved to its own timer — it was dead behind the read-timeout arm under load; boot block-number retry instead of silent watermark-0). Re-vendored at cohort 0fdfe8a (295 tests green). decode (moved out of polymarket-data/src/onchain.rs) keeps the verified facts — EXCHANGE_ADDRESSES (CTF V2), ORDER_FILLED_TOPIC0, classify (maker/taker give-got → side+collateral), decode_watched_fill(event, &HashSet<Address,S>) yielding the watched maker-OR-taker leg, (tx_hash,log_index) dedup via SeenLogs. feed exposes run_feed(cfg, watchlist: watch::Receiver, sink: mpsc::Sender<WatchedFill>) — subscribes OrderFilled filtered to the CALLER’s watchlist (maker+taker), and internalizes reconnect+backoff, a 600s stall watchdog, live-watchlist resubscribe, AND backfill-on-reconnect (the eth_getLogs gap-recovery mechanic, now generalized). GAP-RECOVERY BUG caught + fixed before publish (d94d7c5): the watermark must advance on the SCAN, not only on delivery — see the Gotchas topic. Generic over the watchlist (caller supplies a watch::Receiver + an mpsc sink); cohort + PM-self-wallet are two callers of one feed. The cohort watch/backfill.rs + polymarket-data onchain primitives in watcher-onchain-gap-backfill are now historical for cohort (substrate moved to the crate).
  • pmv2-onchain-watch-crate-extractionWatchedFill now carries the on-chain IDENTITY TRIPLE tx_hash/log_index/block_number (crate re-pinned 1b8a0f9719dc32, cohort re-vendored 0fdfe8a4a3f8b2, babylon #430, 2026-06-24). Added to fix a permanent one-alert-per-wallet suppression in PM’s self-wallet (Balu/Bandi) watch: WatchedFill had no tx_hash, so PM was forced to set transaction_hash=""+outcome_index=0 for every fill, collapsing its (wallet,tx,outcome) dedup in pm_self_trade_alerts to per-wallet (see Gotchas). The fields were already on the OrderFilledEvent in scope (decode.rs:53-64), so decode_watched_fill just plumbs them through and run_feed emits them with no extra change; regression test watched_fill_carries_event_identity_fields. PM maps transaction_hash=format!("{:#x}",fill.tx_hash) + outcome_index=fill.log_index. The cohort consumer needed NO change (it uses decode_watched_fill and has zero WatchedFill { … } literals — additive fields are call-site-safe). DEFERRED out of the lean crate: block_time (block→timestamp RPC; derive from block_number) + a TRUE condition_id/outcome_index (gamma token→market enrichment stays consumer-side). Gates green both sides (crate 37 tests/clippy/fmt; cohort full suite/clippy/fmt).
  • cohort-signals-service-split-and-exit-signalsthe cohort on-chain detection substrate is now a SELF-CONTAINED signal producer (2026-06-22, branch worktree-cohort-signals, 8cd6f58). The pmv2-watch binary used to mix cohort-signal lanes (consensus, first-mover, exit) with execution-trigger lanes (self-trade alerts, fill/reconcile, settlement) in ONE service; the carve STRIPS the execution-trigger lanes and preserves the cohort consensus + first-mover + exit detection + the gap-backfill sweep BYTE-FOR-BYTE. The on-chain ConsensusTracker and the OrderFilled cohort detection feed stay; what they DROVE in-process (fills→reconcile→settle, self-trade alerts, the onchain_book triggers) is deleted — detection now emits TradeSignals to NATS instead of triggering execution locally.
  • watcher-onchain-gap-backfillthe WSS eth_subscribe logs feed NEVER replays logs emitted during a disconnect — run_log_feed reconnects with backoff but only resubscribes for NEW logs, so cohort/self/our-fill OrderFilled logs in the gap are lost permanently (2026-06-20, main 43023ba). Fix = a 30s periodic eth_getLogs reconciliation sweep over [last_seen_block, latest] that re-fetches the cohort (both maker+taker topic slots, mirroring build_logs_payload) and feeds each recovered log through the IDENTICAL worker path as a WatchEvent. KEY SAFETY INSIGHT — feeding backfilled logs is PROVABLY safe because every downstream consumer is ALREADY idempotent: consensus fires on the 2nd distinct holder by WALLET, first-mover dedups at ON CONFLICT(dedup_key), self-trade alerts dedup by (wallet,tx,outcome), our-fill reconcile shares the worker’s seen set keyed by (tx_hash,log_index) — live/backfill overlap is harmless, structurally equivalent to the existing first-mover match-cron backstop. Durable pattern: an eth_subscribe (or any push-subscription) feed needs a periodic eth_getLogs reconciliation backstop over a persisted watermark to cover disconnect/frame-drop gaps; a periodic sweep beats a literal on-reconnect hook because it catches silent drops too, and you can make it free of dedup machinery if every consumer is already idempotent. New onchain.rs primitives eth_block_number/eth_get_order_filled_logs/cohort_address_topics/build_get_logs_filter (modeled on eth_call_u128); watermark Arc<AtomicU64> via fetch_max; from INCLUSIVE (intra-block partial-delivery recovery); MAX_BACKFILL_BLOCKS=2000; try_send non-blocking; reuses HTTP POLYGON_RPC_URL.
  • tracked-alerts-consumerthe on-chain detector substrate now drives a LIVE Telegram alert lane (Phase 2): pmv2-tracked-detector publishes per-fill tracked.signals → consumer pmv2-tracked-alerts → Telegram, round-trip verified at SUB-SECOND latency via SigNoz. Because the lane is per on-chain fill (buy+sell, no batch damping) it flooded at ~1 alert/sec, forcing a filter evolution that landed on a 2nd-sharp-converges CONSENSUS gate (current 0f71f89). See the Notifications/Telegram topic for the full filter journey.
  • copy-trade-lane — Polygon on-chain OrderFilled log detection for near-real-time (~1–3s) cohort fill capture, vs the Data API’s ~5-min surfacing lag (only ~2% within 30s). Verified facts: real OrderFilled topic0 is 0xd543adfd… (NOT canonical-ABI 0xd0a08e8c); cohort settles on CTF Exchange V2 0xE111180000d2663C0091e4f400237545B87B996B; on-chain maker/taker address == pm_trades.proxy_wallet for all 5 cohort wallets; decode per leg = got-token+gave-USDC=BUY / gave-token+got-USDC=SELL, price=USDC/token (both 6-decimals), token→outcome via gamma clobTokenIds. Detector keeps a newHeads block-timestamp cache (logs carry no wall-clock). Detection only — execution still needs off-chain EIP-712 CLOB orders (PM matches off-chain, settles on-chain).
  • copytrade-paper-lane-latency-spread-fixes — the detector already computes a ms-precise Detection.latency (received_at − block_time) in onchain.rs; the copy lane was throwing that precision away and re-deriving from whole seconds (→ detect_latency_ms=0). Fix carries the ms value through. Detection latency is genuinely sub-second (~200–900ms expected), confirming the newHeads block-time cache works.
  • pmv2-autotrade-engine-designon-chain reads are the ground truth for the live trade lane (design). pmv2/autotrade/onchain.rs does Polygon RPC reads — CTF balanceOf(proxy, token_id) is the PRIMARY reconciliation truth (balance present → a fill happened; balance absent + no matching trade → safe to cancel + release reservation; genuinely ambiguous → stay reserved + alarm loudly), plus the proxy owner-arg + allowance/collateral checks at preflight. The CLOB expected_order_hashclob_order_id correlation is best-effort secondary (the hash↔orderID identity is spike-verified or calibrated on first fill, never a hard gate). NOTE: PM matches off-chain and only settles on-chain — execution is gasless off-chain EIP-712 CLOB order signing, so on-chain reads here are for reconciliation/preflight, not execution.

Polymarket API Quirks

  • cohort-pre-live-signal-gatingthe read-only ClobReadClient (re-added to polymarket-data 2026-06-23) — response structs grounded in REAL captured fixtures (CLOB was NOT geoblocked). Three wire facts: (1) the live CLOB wire is snake_case (not camelCase); (2) GET /book returns asks in DESCENDING price order, so .min() over the asks is LOAD-BEARING to get the actual best (lowest) ask — taking the first element gives the WORST ask; (3) market_status via GET /markets/{cond}. These are PUBLIC read endpoints (no signing/auth/order-execution surface — explicitly NOT the deleted execution client). NOTE: resolution-proposed-ness is still a Gamma-only fact (the CLOB binding’s resolved is just closed re-aliased — see autotrade-resolution-proposed-entry-guard), so the liveness gate fetches umaResolutionStatuses from gamma separately.
  • 2026-06-20-fastest-polymarket-trade-submissionCLOB V2 went live 2026-04-28 as a HARD CUTOVER — V1 SDKs/orders are dead in prod, so a stale-V1 order is REJECTED (infinite latency), not slow. V2 changes: EIP-712 domain version "2" (was "1"); Order struct drops taker/expiration/nonce/feeRateBps, adds timestamp/metadata/builder; collateral is pUSD (ERC-20 backed by USDC, wrap USDC.e→pUSD via Collateral Onramp 0x93070a847efEf7F70739046A929D47a521F5B8ee), NOT USDC.e; V2 exchange addrs (re-verify byte-for-byte vs ctf-exchange-v2): Standard CTF 0xE111180000d2663C0091e4f400237545B87B996B, Neg Risk CTF 0xe2222d279d744050d28e00520010520000310F59; neg_risk is a hard runtime branch (verifyingContract/domain/collateral all differ — mismatch = valid-but-rejected sig); fees now collected on-chain at match (signed order no longer carries feeRateBps). The CLOB WebSocket market channel (wss://ws-subscriptions-clob.polymarket.com/ws/market, subscribe by assets_ids) streams a book snapshot + price_change/tick_size_change deltas — read-only, so you can mirror top-of-book locally but placement stays REST. tick_size changes when price >0.96 or <0.04. Operator matches OFF-CHAIN (no on-chain shortcut for taking liquidity); V2 operator match/ack latency is UNPUBLISHED. POST /orders batch max 15/request; rate limits POST /order 5,000/10s burst. Polymarket origin region is undocumented (Cloudflare anycast; community-inferred AWS London eu-west-2, NOT us-east-1).
  • autotrade-resolution-proposed-entry-guarda Gamma market record has a pre-closed “result is in” state: umaResolutionStatuses=["proposed"] while closed=false + acceptingOrders=true (and outcomePrices already pinned ~["0.0005","0.9995"]) — the match/event is decided and proposed to the UMA oracle but not yet formally settled, so the CLOB still accepts orders at the collapsed price. umaResolutionStatuses is returned as a JSON-ENCODED STRING ("[]" / "[\"proposed\"]"), NOT a native JSON array — same stringified encoding as outcomes/outcomePrices; parse with parse_json_string_array, never a plain Vec<String> deserialize (which silently fails). Query: GET https://gamma-api.polymarket.com/markets?limit=1&condition_ids=<condition_id>. The CLOB binding gives you NOTHING herepolymarket-clob’s MarketStatus.resolved is just m.closed re-aliased, so resolution-proposed-ness is a Gamma-only fact.
  • category-leaderboard-apidata-api has a per-CATEGORY leaderboard (verified live 2026-06-05): GET https://data-api.polymarket.com/v1/leaderboard?timePeriod=month&orderBy=PNL&category=<slug>&limit=N&offset=0 (also /v1/biggest-winners?...&category=). Accepts ONLY 9 fixed categories — politics, sports, crypto, finance, culture, mentions, economics, weather, tech (+ OVERALL global); arbitrary gamma tags (iran/elections/geopolitics) return empty/global. category and timePeriod are case-insensitive (WEATHER/OVERALL, MONTH all work). timePeriod∈{day,week,month,all}, orderBy∈{PNL,VOL}. The 9 leaderboard categories are a separate namespace from gamma event_tags (don’t map 1:1). Verified: weather→ShyGuy1/HighTempTation ~467k, OVERALL→Inaccuratestake $1.36M. Found via the UI /leaderboard “All Categories” dropdown (route /leaderboard/{category}/{window}/{metric}). In code: polymarket_data Category enum already has all 9 + Overall; as_query_value() sends UPPERCASE (works — see the false-alarm under Gotchas).
  • topbets-enddate-1970-fixa position endDate of 1970-01-01 is an EPOCH PLACEHOLDER: the data-api positions feed returns it when the market-level endDate is null and the real end date lives on the parent gamma event. Get the real date from gamma /events (event_tags end_date), and normalize the 1970-01-01 placeholder to NULL before display.
  • pmv2-bet-link-analyzer-flowgamma.event_tags() (polymarket-data/src/client.rs:505) takes EVENT slugs, but parse_selector (scoring.rs:91) hands you the MARKET slug on deep links — for /event/<event>/<market> URLs parse_selector returns the last path segment (the market), which is NOT a valid event slug, so categorizing off parse_selector output silently returns no tags (test scoring.rs:993-995). Extract the segment after /event/ or use the DB-truth BetScore.event_slug. Also captures the two coexisting category namespaces (9 leaderboard categories category_consensus.rs:13 vs the coarse pmv2_event_category.category bucket esport/sport/geo/crypto/other) and their bridge (category_tags + GEO_TAGS/CRYPTO_TAGS).
  • pmv2-gamma-tags-categorizationbet categorization comes ONLY from gamma /events?slug=<event_slug> (a tags array of {label, slug}); the /markets endpoint returns category=null + no tags and is useless for it (verified live 2026-06-03). Multi-slug batching ?slug=a&slug=b works. GOTCHA: esports events are ALSO tagged sports → bucket precedence must be esport→sport→geo(politics/elections/geopolitics/world)→crypto→other or every esport mis-buckets. Verified: MLB→sports/games/mlb/baseball, LA mayoral→politics/elections/…, IEM CS2→iem-cologne/sports/counter-strike-2/esports/major, crypto→crypto.
  • polymarket-fetch — uppercase query params, vol vs volume, stringified rank, OVERALL category sports bias, trades endpoint offset hard-capped at 3000
  • pmv2-two-tier-trader-scoringthe trades-endpoint offset cap bites trader scoring: per-wallet trade history is hard-capped at ~3500 trades by the API’s offset ceiling — confirmed on the pmv2 Tier-1 run 3 (2026-06-02) that bumping --trades-max-pages to 100 changed nothing, so it is the API offset ceiling, NOT our paging knob. Consequence: ~14 heavy traders are scored on their most-recent 3500 trades only (earlier history truncated). Durable: don’t expect --trades-max-pages to lift the per-wallet ceiling.
  • cross-venue-pm-kalshi-arbKalshi public market-data endpoints (no auth for read): markets api.elections.kalshi.com/trade-api/v2/markets + order book .../markets/{ticker}/orderbook. Tick grids differ: Polymarket 0.1¢, Kalshi 1¢.
  • lag-probe — RTDS filters must be object/array-shaped or it 400s the whole subscription; Gamma 403s the default urllib User-Agent; find the active btc-updown market by computing its slug on 300/900s boundaries, not paging closed=false

Reverse-Engineering Top Traders

  • category-leaderboard-apiper-category top-trader discovery: the data-api /v1/leaderboard?category=<one of 9>&orderBy=PNL lets you pull the month’s top PNL traders for a specific category (weather/crypto/politics/…), not just the global board. category-consensus uses this to build a freshly-fetched, category-specific PNL cohort on demand.
  • xref-consensus-rejected-fresh-copy-winsthe cohort’s exploitable edge is EARLINESS, not agreement. A purged --xref backtest (EVENT-only, verified twice) shows the first cohort BUY per outcome is the well-priced entry (xref-K1 = +7.59%, all 3 folds positive); each additional agreeing wallet enters later/pricier → lower ROI (monotonic in K). So the durable RE lesson is: the first informed entry carries the edge; subsequent cohort agreement is the market catching up to it. Don’t model the cohort’s edge as “consensus” — model it as “be the fast first-mover copier.”
  • copy-trade-lane — the actionable conclusion of the RE: the cohort’s edge is cheap-outcome SELECTION (+~18–30% ROI, t up to 9, all 5 wallets, crypto-updown-specific), so the copy lane mirrors which outcome they buy rather than re-deriving a signal. Our own autonomous longshot signal is negative-EV by contrast.
  • longshot-strategy2026-05-30 cohort study (5 wallets, 3,206 trades, 30d) establishes the actual top-5 ranking by ROI in our band+window: 0xb27bc932 (+48.1% / +295.5% 24h) > 0x75cc3b (+40.1% / +36.2% 24h) > 0xeebde7a0 > 0xb55fa129 > 0xce25e214. 4/5 are buy-only / hold-to-resolution (the template our paper model matches); 0x75cc3b is the sell-rotating outlier (28.1% SELLs). Universal cohort sweet spot is 12–16¢ (not 8–9¢ as previously documented). Three candidate tunes from a 0xb27bc932 deep-dive (asset+side bias, sub-tiw [0.85–0.90] goldmine, 8–14 UTC hour clustering) pending cohort validation before shipping. Our 24h posture (~7.4% win / −5.3% ROI on 94 trades) is below mid-tier — the regime-variance excuse is dead, the cohort is crushing the same window.
  • top-trader-edge — full pass on 0x75cc3b’s 43,734 trades cross-validated on 8 other high-edge wallets. Falsified the earlier 3,397-markets / BTC-only / 15%-into-window characterization; identified two lanes (longshot reversion + momentum-chase), both 5m-only, both late-window. At least 2 distinct edge-bearing strategies coexist in the cohort. 2026-05-30 correction: 0x75cc3b was framed as “the top trader” but they are actually wallet #2 by 30-day ROI in our band+window; 0xb27bc932 is #1. RE captured real edge, but specific parameters were biased to 0x75cc3b’s personal pattern.
  • swisstony-hft-sports-market-makerprofile of swisstony (proxy 0x204f72…95e14, “Frail-Possible”), Polymarket’s 1-by-volume (~990M) / #5-by-PnL (9.93M) ~100%-SPORTS trader — an algorithmic HFT MARKET-MAKER bot, not a human bettor (CORRECTS the retired “favorite-lay / draw-fade sportsbook” framing). Evidence: 3,497 trades / 3.3h, 13 fills in one second, unique tx per fill, 100% BUY in-window, full-board two-sided saturation, sub-363k unrealized). NOT copyable (a follower becomes the spread-paying taker) — use only as a flow / liquidity-concentration signal.

Scheduler / Cron

  • phase1-consensus-rebuild-implementedWS2 daily heavy-sync gate. The 30-min scheduler (scheduled_run.rs, systemd timer OnCalendar=*:0/30) now also runs the heavy short-term pipeline in dependency order (trades→resolutions→short-term-markets→metrics→short-term-accuracy), throttled to ≤ once per 24h. The gate (heavy_sync_is_due) is DB-backed — it reads max(computed_at) from pm_short_term_trader_accuracy rather than an in-memory timer, because the scheduler is a fresh process on every ~30-min tick so an in-memory “last run” would never persist. The existing 30-min consensus_refresh path is unchanged. Durable pattern: gate periodic work in a fresh-per-tick process off a DB timestamp, not process memory.

Selectors

  • pmv2-bet-link-analyzer-flowparse_selector (scoring.rs:91) returns the LAST URL path segment (rsplit('/').next()): for a /event/<event>/<market> link that’s the MARKET slug, not the event slug (test scoring.rs:993-995). Fine for score_bet/load_bet_holders (they resolve to the event’s sibling markets downstream), but a TRAP if you feed it straight to gamma.event_tags() (which wants event slugs).
  • polymarket-fetch — TopN, Manual, MinTenure wrapper, ConsistentlyProfitable + score JSON shapes, --require-windows 1..3 loosening
  • selector-vs-skill-gap — the default is top_n_intersection (selector.rs:183), ranking purely by cumulative PnL with only a 3-window presence gate (no luck/sample-size/edge control). consistently_profitable (selector.rs:342) still filters on PnL/volume, not edge/Wilson/persistence, and isn’t default. No selector consults pm_short_term_trader_accuracy. Proposed: an edge-based selector (n_markets>=40, edge_lower>0, optional edge_z floor).

Spin-off Projects

  • contract-scanner — separate project scoped as a side-thread on 2026-05-30 during a polymarket-fetch session; auto-runs smart-contract static analysis (Slither MVP, Aderyn/Mythril/Halmos later) over DeFi protocols on a chain and ranks likely-vulnerable contracts for white-hat research. Provenance lives here for the trail; design + implementation context live in the contract-scanner folder.

Strategy Parameters

  • edge-discovery-2026-06-22the 3 viable NEW lanes from the edge-discovery workflow (all pilot-small, default-OFF, must clear the purged harness): (1) C3’ Sports/Esports Liquidity-Rewards MAKER farming re-pointed from geopolitics (which has NO LR pool — the pool is in Sports/Esports, ~5–20k justified**; net-after-adverse-selection UNPROVEN, the AS measurement is make-or-break; we ARE the resting maker, distinct from mirroring swisstony); (2) C4-weather maker-flip of the LIVE weather signal only (maker-first/taker-fallback, +10–20% over taker not +30–50%, drop the crypto-1b & copy-entry sub-variants — negatively selected); (3) C-Tech resolution-source mispricing on OBJECTIVE named oracles only (FDA PDUFA / FAA license / leaderboard snapshot; Theta 0.04; distinct from the dead C1 precisely because it screens out UMA-discretionary resolution). +2 research-more: C7 UMA dispute-window fade, C15 on-chain MM cancel-event signal.
  • copy-trade-lane — copy-lane params live in the DB (pm_copy_config, hot-reloaded ~60s): enabled (kill-switch, DEFAULT FALSE), cap_price, min_price, size_usd, min_window_left_secs, max_daily_notional_usd; per-wallet enable in pm_copy_wallets (seeded with the 5 cohort wallets). Resolver applies the PM crypto taker fee shares×0.072×p×(1−p) (≈6.4% of stake at ~11¢). No code redeploy needed to tune.
  • longshot-strategy — live params (2026-05-30): longshot_min_price=0.05, longshot_cap_price=0.13 (bumped from 0.10, commit a2de509), longshot_max_time_in_window=0.93, min_time_in_window=0.66, max_slippage_mult=1.3, max_daily_notional_usd=$700 (bumped from 10flat. Mid-history columns onpm_paper_tradesshipped in commitb677ce2(recording only). Source of truth:crates/pm-arb/src/strategy_v2.rs; CLI overrides in main.rs`.

Trades Pipeline

  • polymarket-fetchpm_trades table, trades-sync CLI with --all-watched / --since / --concurrency, per-wallet incremental resume, 3000-offset cap handling. Live: 53k trades / 24 wallets.

TODO / Backlog

  • phase1-consensus-rebuild-implementedcommit + deploy WS1/WS2 (still in the working tree, verified green: fmt/clippy clean, WS1 13 tests, WS2 4 tests). Follow-ups: (1) DRY — hoist the duplicated parse_end_date (%Y-%m-%d-then-RFC3339 lenient parser) out of both consensus.rs and repo.rs into polymarket-data; (2) watch the behavior change — asset-in-key + $5 dust floor will reduce consensus group counts (expected/correct: drops dead/dust/synthetic-short), don’t mistake for a bug post-deploy; (3) scheduled-run is the systemd-timer unit that picks up the WS2 gate.
  • copytrade-paper-lane-latency-spread-fixescommit + deploy the two copy-lane fixes (still in the working tree). Post-deploy: confirm detect_latency_ms shows real sub-second values and the mid-as-ask bias is gone with fill volume preserved on single-sided feeds. Deferred: a book-staleness guard for the −15¢-class stale-book outlier — requires persisting book-age and picking a threshold that coexists with the planned low-traffic <60s lane (needs data first).
  • polymarket-fetch — copy regenerated types into CRM packages/shared/src/database.types.ts (scheduling is now done — see polymarket-fetch-deploy)
  • polymarket-fetch-deploy — Telegram bot end-to-end smoke test, Tailscale admin cleanup of offline ops-vm node, merge spec2a-app-deploy to main in ~/levandor/terraform/
  • copy-trade-algorithm — phase 5: real-time trade-signal scoring + Telegram alerts; phase 6: threshold backtest tuning
  • longshot-strategy — active tasks: #3 forward-validate longshot tunes (in progress); #9 bound scheduled-run’s pm_trades WHERE proxy_wallet=$1 query (24k–112k rows/cycle/wallet); #13 trend-filter backtest (waiting on 2–3 days of mid-history data); #15 stop swallowing sqlx MigrateError in main.rs:2376-2380; #16 cohort-validate the 3 0xb27bc932 candidate tunes (asset+side bias, sub-tiw goldmine, hour-of-day) before shipping.
  • copy-trade-lane — copy-lane next steps: Gate-0 = after @deploy sets POLYGON_WSS_URL and the kill-switch is flipped (UPDATE pm_copy_config SET enabled=true), confirm lane='copy' rows actually appear in pm_paper_trades (definitive end-to-end proof). Then close the analytics niggles (up_mid_at_signal stores entry ask not up-mid; detect_latency_ms measures block→eval; no stale-book guard on the copy ask; copy counters absent from the hourly summary). Real-money execution (EIP-712 signed CLOB orders) is a separate future project.