For Agents
Living index of themes for this project. Each H2 is a topic; bullets are wikilinks to related notes. Updated by
obsidian-documenterwhen documenting work. Read byhistorianat bootstrap. Topics kept alphabetical.
Algorithm Design
-
cohort-algo-vs-polymarket-fetch-math-parity-audit — cross-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 exactlyholders.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_bpsloaded-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_exitexit guards) + operational constant tuning. Method = 4 parallel comparison agents, all math re-verified against disk. -
tracked-alerts-consumer — the 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-scoring — pmv2 two-tier scoring engine (design, 2026-06-01) — from-scratch rebuild (
pmv2_*tables), Step 2 after leaderboard collection. Tier 1trader_score(stored onpmv2_traders): min-50-resolved qualify gate, skill baseedge= 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-holderssingle_bet_scoreconsensus 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 taggedrun_id). Migration20260602010000_pmv2_scoring.sql(Tier-1 in progress); Tier-2 first-mover lane next. PHASE 1 SHIPPED (2026-06-02):pmv2-positionscommand +pmv2_positionscurrent-state table (migration20260602020000) 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): newpmv2-score-bet --bet <condition_id | market-slug | Polymarket URL>CLI +pmv2::scoringmodule (score_bet,group_into_bet_scores,parse_selector,BetHolder::conviction,format_bet_scores) +repo::load_bet_holders/latest_position_snapshot_ts. Per held betbet_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 negativetrader_scorecorrectly 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-wins — CENTRAL 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 --xrefco-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-edge — sets the Phase 2 ship bar. Leakage-clean purged walk-forward shows the current PnL/
copy_scoregate 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-implemented — Phase 1 consensus rebuild, implemented & verified (uncommitted, 2026-06-01). WS1 (
consensus.rs): new testable seamcompute_consensus_groups_with(members, min_wallets, now, dust_floor_usd)(public wrapper suppliesUtc::now()+DEFAULT_DUST_FLOOR_USD=5.0); eligibility filter drops redeemable / past-end_date(%Y-%m-%dthen RFC3339; unparseable kept +warn!) / dust (<$5); bucket key gainsassetso 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_duereadsmax(computed_at)inpm_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-design — Phase 2+ consensus target: replace
count>=2with a skill-weighted LOGARITHMIC OPINION POOL (geometric mean of odds) over FRESH (frompm_tradestrade_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-shrunkedge_lowerwith a hard per-wallet CAP (neutralize the +30σ wallet) + boundedtanhstake-conviction; output calibratedp_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 (~0edge_z→ zeroed by weighting, no keyword filter). Caveat: edge thin/concentrated (~1–2 wallets); a simple equal-weight log-pool ofedge_lower>0wallets is a REQUIRED baseline arm (forecast-combination puzzle). Spec:docs/superpowers/specs/2026-06-01-best-consensus-algo-design.md. -
copy-trade-lane — on-chain copy-trade PAPER lane in
pm-arb(commitb3f9e21, 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 (OrderFilledlogs, ~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_msthroughevaluate_copyinstead 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-strategy —
pm-arbimplementation + 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, not0x75cc3b’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_scoremagnitude 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;
0xd189664cat 77.8% ROI is the purest expression. -
selector-vs-skill-gap — the 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.rs→pm_short_term_trader_accuracy) is measurement-only. Fix = a selector keyed onn_markets>=40ANDedge_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-fetch —
compute-metrics/ranked-tradersCLI +pm_trader_metricssnapshots -
correctness-pass-six-fix-commits — scoring-math corrections (operator-approved, 2026-06-03, shift ranking slightly):
conviction()default0.5 → 0.0(no bet history = no weight, not a phantom median);consistency_factorreturns 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_sizenow computed per-POSITION (buys aggregated per condition/outcome) not per trade-fill — the correct baseline forbet_confidence. Plus the SHAREDload_event_winnerschange (requirepayout>0+ deterministic tiebreak, dropping random winners on cancelled/0-payout markets) feedscompute_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-22 — two 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-scoring — consensus-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_scoreconsensus framing was dropped andbet_confidencerepurposed 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-1edgeis a point-in-timeresult − entry_pricequantity (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-wins — the
--xrefco-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-edge — the 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_scoreselection 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-leakage — the 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:30aggregates LIVE positions (post-entry+survivorship); (2)backtest.rsbuild_wallet_data+metrics.rs:132pair_trades_to_resolvedpair trades to payouts across the train/test cutoff (label leakage); (3)repo.rs:1429fetch_short_term_bet_rowshas NO time filter (survivorship+lookahead). Remedy (López de Prado): point-in-time signal frompm_trades, a realresolved_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>0wallets, 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_tradesin 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 (Wilsonedge_lower,edge_z, Monte-Carloluck_p_value, split-half persistence atn>=40) exists inshort_term_accuracy.rsbut is measurement-only — never consulted by the selector. - cohort-consensus-backtest-2026-07-04 — point-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 sizingsignals/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-feature — NEW cohort
/mirrorper-trader copy-mirroring signal source (built + merged to mainf385fbb, 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 sourcesource="mirror"reusing the producer/PM split. Second on-chain feed lane (Alchemy WSSrun_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 Alchemyeth_subscribe(logs)CU cost (the same billing that drove the cohort lane toeth_getLogspolling, see watcher-onchain-gap-backfill) while active. Stableorigin_ref = mirror|{wallet}|{condition_id}|{outcome_index}; PM matches exits by origin_ref+condition+outcome, NOT source. Tablespmv2_mirrors/pmv2_mirror_positions; 15-min cron resolves closed markets to free slots. New pmv2-contracts QueryKindsmirror/mirrorstop/mirrorlist. Dormant untilPOLYGON_WSS_URLset + PM adds amirrorpmv2_autotrade_sourcesrow + contracts/connector land. Deferred: single-fill USD sizing under-sizes scale-in traders;/mirror stopleaves open copies to ride. Codecrates/cohort_algo/src/pmv2/mirror/*. - cohort-algo-vs-polymarket-fetch-math-parity-audit — cross-repo parity audit (2026-06-25): the NEW
cohort_algoalgo mirrors the PREVIOUSpolymarket-fetchalgo. 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.rssignal_gate_decision+apply_io_gates) with identical formulas (matches the babylon #292 producer-owns-signal-gates split). Deliberate divergences = wire v1→v2 redesign (cohort.signals→pmv2.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_exitexit 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-extraction — cohort’s on-chain watch (decoder + resilient WSS feed) extracted into a new lean reusable git crate
pmv2-onchain-watchsoposition_managercan run its OWN self-wallet watch by DEPENDING on it instead of copying the code (operator-directed, babylon #402, 2026-06-23; privategithub.com/wowjeeez/pmv2-onchain-watch@ canonical pin1b8a0f9, was5142d98). Review-pass cleanup (2026-06-23, pin1b8a0f9, re-vendored at cohort0fdfe8a, 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 fromonchain.rsand unreachable from the one real entryrun_feed(the whole newHeads/Detection/run_detectorblock-latency path + theSubCommand/dynamic-resubscribe path —run_log_feed_statichardwired a dummy cmd channel so the arm was dead — + a write-only ack registry + unusedpubhelpersorder_filled_topic0/CONDITION_RESOLUTION_TOPIC0/watchlist_address_topics), extracted two DRY helpers (topic_hexfor the ~5×0x+hex pattern,watchlist_topic_slotsas the single maker=slot2/taker=slot3 source for both subscribe + getLogs builders), and fixed three robustness bugs (proxied-hang trap: getLogs client now sets BOTHtimeout+connect_timeout+OnceLock-reused; keepalive ping moved to a DEDICATEDtokio::time::interval(PING_INTERVAL)arm — it was gated behind the 30s read-timeout arm so the 15s interval was dead under load; booteth_block_numbernow retries 3×+warns instead of silently seeding the watermark to 0). Behavior preserved (onlydebug!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 ofpolymarket-data/src/onchain.rs, renamed watchlist-neutralCohort*→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 +SeenLogsdedup + subscription builders) andfeed(raw WSS loop moved + a NEW generalizedrun_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 onwatchchange, backfill-on-reconnect over aneth_getLogswatermark, bounded drop-with-warn sink). cohort DOGFOODS it (cohortmain@69a293a): droppedpolymarket-data/src/onchain.rs+ deletedwatch/backfill.rs; vendored the crate as a workspace path-dep (crates/pmv2-onchain-watch, exactly thepmv2-contractspattern);watch/is now a thinrun_feedconsumer (spawn_cohort_refresh→watch::Sendercohort set 60s refresh →run_feed→mpscdrainsWatchedFills →handle_cohort). Behavior-preserving — logic byte-identical frommarkets.resolveonward, only the input type (OrderFilledEvent/CohortAction→WatchedFill) 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-gating — cohort_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_filterover esport/sport/geo/crypto/other;resolve_categorycache-firstpmv2_event_category→gammaevent_tags→category_bucket, empty event_slug short-circuits to skip a doomed RTT); (2) chase/max_entry_premium_bps(chase_gate: skip ifbest_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 gammaumaResolutionStatuses); (4) the degenerateev_floor_gateDROPPED (size cancels → price-blindbps>200; operators setmax_entry_premium_bpsnet of cost;min_round_trip_edge_bpsloaded-but-unused sincepmv2_autotrade_configis FROZEN).signal_gate_decisionmade async + staged cheapest-first (trust gates → category cache → concurrenttokio::join!best_ask+market_status+resolution_proposed → pureapply_io_gates). Re-adds a read-onlyClobReadClienttopolymarket-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-cutover — cohort_algo v2 WIRE-CONTRACT CUTOVER DONE (
0986c4d, 2026-06-23). cohort emits the v2 trade contract (schema_version 2) onpmv2.order.first_mover_core.{entry,exit}instead of oldcohort.signalsschema_v1; vendored the sharedpmv2-contractscrate (blocker resolved). Cohort is thePricing::Derivedarm (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-ownership — babylon #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 onpmv2_autotrade_*schema until PM’s migrator ships. - telegram-connector-outbound-design — DESIGN (approved 2026-06-22) —
telegram_connectoris 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 consumertg_outboundontelegram.outbound(streamTELEGRAM_OUTBOUND, 1d, 5-min dedup) → Bot APIsendMessage. A DUMB faithful relay — passestext+ publisherparse_modeverbatim, 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 bymax_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 aMessageSourcetrait → broker-free unit tests; wiremock for Bot API; optional NATS testcontainer. EnvelopeTelegramOutbound{msg_id,chat:Option,text,parse_mode:Option,kind:Option}field-matched tocohort_algo/.../telegram_outbound.rsuntilpmv2-contractspublishes (then vendor). Inbound (getUpdates→auth→ops.*) = separate later spec, gated on @positionmanager lockingops.*(#311). Follows the fleet NATS conventions. - pmv2-fleet-infra-observability-nats-conventions — FLEET-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 gRPC127.0.0.1:4317/ HTTP4318, Dockerhost.docker.internal:4318; std envOTEL_SERVICE_NAME/OTEL_EXPORTER_OTLP_ENDPOINT/OTEL_EXPORTER_OTLP_PROTOCOL/OTEL_RESOURCE_ATTRIBUTES=service.namespace=pmv2,deployment.environment=prod(auto-injected for ansibleapps-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 behindRUST_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 setNats-Msg-Id(5-min dedup window on all 3 streams); streamsTELEGRAM_OUTBOUND(1d)/OPS(ops.commands+ops.results, 7d audit)/PMV2_ORDERS(pmv2.order.<source>.<entry|exit>, pending PM consumer)/legacyWEATHER_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 infow/ admin seed sparingly. Dashboards: “polymarket-infra — Host & Tailscale” / ”— App services (APM)” / ”— NATS + JetStream”. Coordination:pmv2-contractsconfirmed standalone repo (#310); inboundops.*contract still open w/ @positionmanager (#311). - cohort-algo-component —
cohort_algois now a STANDALONE repo/component (2026-06-22):/Users/levander/coding/pmv2/cohort_algo, privategithub.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 bypmv2_runs); (2) consensus alerts (Pmv2Consensuscron + on-chainConsensusTracker); (3) first-mover ENTRY (Core-tier first BUY → NATSaction:Buy); (4) cohort EXIT (tracked trader sells → NATSaction:Sell); (5) on-chain WSS watcher + 30seth_getLogsgap-backfill. Own cargo workspace (edition 2024, resolver 3,unsafe_code=forbid, sqlx/tokio/anyhow/tracing/async-nats 0.49+nkeys; memberscrates/cohort_algo+crates/polymarket-data); vendorspolymarket-datainternally. 278 tests, clippy+fmt green, dormant-safe (POLYGON_WSS_URL/NATS_URLunset ⇒ idle, no panic). PORT = behavior-preserving from the retiringpolymarket_fetchcarve (483350c) — preserved internalcrate::pmv2::paths + thepolymarket-datacrate name ⇒ ZERO logic edits. Pre-port review fixed 4 bugs (empty-Data-API200 []mass-false-exit; dropped-publish-still-deletes-row; entry double-publish→watcher sole producer;autotrade/→signals/purge keeping only the sharedpmv2_autotrade_*table names). ARCH (babylon #286):polymarket_fetchRETIRES → component repos in/coding/pmv2on a NATS spine; producers own strategy+sizing;position_managerCONSUMES NATS DIRECTLY (no runner) + ownstelegram.outbound, anops.commands/ops.resultskill-switch consumer, and thepmv2_autotrade_*migrator. v2 WIRE (babylon 292): subjectspmv2.order.<source>.<entry|exit>; ONE entry envelope w/ pricing discriminatorlimit(weather/crypto) |derived(cohort: PM derives limit from live book + $→shares + lot-size); lean exit-signal (condition_id+outcome_index+origin_ref); sharedpmv2-contractscrate = source of truth (vendor it). Cohort gotchas:sourceMUST be the EXACTsource_idkey (first_mover_core— wrong value silently routes toMode::Off); exit MUST carryorigin_ref; producers OWN ev/chase/category gating (PM has no GammaClient). CURRENT = oldcohort.signalsschema_v1, telegram.outbound seam prepared butDirect-send default. NEXT (planned, BLOCKED on @positionmanager publishingpmv2-contracts): publishpmv2.order.cohort.{entry,exit}, schema_v2, vendorpmv2-contracts, ADD producer-sidesize_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-signals — ARCHITECTURE 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(branchworktree-cohort-signals, frommain@43023ba). (1) CARVE (8cd6f58, gate green): repo reduced to ONLY cohort scoring (Pmv2Collect, kept so the slice is self-contained), consensus (Pmv2Consensuscron + on-chainConsensusTracker), first-mover ENTRY, cohort EXIT detection. ~13,400 lines DELETED (whole autotrade engineautotrade/{engine,execute,exits,runner,reconcile,settle,preflight,sizing,pricing,orders,…},onchain_booktriggers, 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. Thepmv2-watchbinary 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.” Prioron_cohort_sellwas DB-only (log EXITED + delete rows). Now PUBLISHESaction:Sellto NATScohort.signalsAND telegrams so the out-of-process executor closes the copied position.sources::exit_signal(PositionRow)(Execution::Context,reference_price=position.cur_price). CRUX — the exitsignal_idcarries anexitdiscriminator DISTINCT from the entrysignal_idfor the same (condition,outcome); else the consumer’sON CONFLICT(dedup_key)silently swallows the Sell as a dup of the entry Buy = capital trap. Test-lockedexit_signal_id_differs_from_enter_signal_for_same_market. DRY: entry+exit sharepublish_cohort_with_retry(retry/backoff + drop-alert). Ordering publish-BEFORE-delete. Theeligible_sell+ExitDebouncegate kept unchanged (NOT narrowed to Core-tier). PROVISIONAL contract — @deploy’sposition_managermust consumeaction:Sellidempotently; not yet coordinated. NOT merged/deployed. The PRODUCER half of the producer→executor split (executor = position-manager-interface-design); transport = thecohort.signalslane from cohort-firstmover-nats-migration. - position-manager-mode-control-implemented — OUTCOME (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_manageris now the SINGLE execution authority:place_orderresolves the authoritative mode fresh per call (global × per-source(mode,enabled)+ breaker) and gates at the POST boundary — Off→Skipped(zero work), Dry→reservedry_runrow + 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 (buildsEntryIntent/ExitIntent, callspm.place_order, maps the typedExecOutcometo a NATS ack). Correctness: entries gate oneffective_mode(global,source); EXITS gate on the row’s recordedmode(a live position stays sellable when global=Off — the E1 fix);enabled=false⇒Offnow enforced (was dangling); pre-POST reserve/caps DB error→Transient→Nak(30s); recorded-failure/benign-race→Ack(no double-trade); fail-closed-to-Off two paths;cap:eventliteral. State: committed onmainBOTH repos (PM351a755, fetch4fde695), NOT pushed, mode OFF. Gate: 66 PM offline + 532 fetch + 7 db-tests-vs-real-Postgres pass. OPEN (callout in note): (1) commit contamination —4fde695swept in the user’s unrelated in-progressexit_signal/cohort_exit_signal_idwork (called by uncommitted firstmover/alerts code) — pending split/keep; (2) follow-ups: delete deadorders.rs/EngineConfigcap helpers, add host-injectable signer seam for live-POST e2e; (3) pre-arm: live-test PM notify→Telegram before arming. - position-manager-execution-migration-implemented — OUTCOME (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_managercrate owns the polymarket-clob signing client (use_server_timeOFF),place_orderbuy+sell, ledger code, onchaineth_call(+connect_timeout),load_caps/load_breaker, and the declared-but-unimplementedFillSink/FillEventSinkwatcher seams;polymarket_fetchrewired one-way,submit_live/auto_sell/execute.rsdeleted. Behavior-preserving (11 ledger SQL + notify strings byte-identical, locked clob hash tests intact); gate green PM 43+5 / clob 11 / fetch 529-0. OnmainBOTH repos (PMcbd0f4c, fetchafc785d), 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:SigningClientClone, host-injectable signer seam. - pmv2-polymarket-clob-crate-migration — pmv2 Task 2 (2026-06-21,
cdfcca8):polymarket-clobcrate physically moved frompolymarket_fetchintoposition_managerworkspace;use_server_timeflippedtrue→false(drops serialGET /time, ~20–100ms saved, EIP-712 bytes unchanged); self-referentialinclude_str!test fixed with split-needle; all 11 tests green.polymarket_fetchintentionally left un-buildable — do NOT fix its Cargo.toml. - position-manager-interface-design — DESIGN (2026-06-20) — new
position_manager(PM) crate: a focused Polymarket EXECUTION+WATCH crate (NOT a full engine rehome) that the deferredautotrade→position_managerrename lands into. PM OWNSplace_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, NATSPmv2Consumer,mode). Dependency is one-way in-processpolymarket_fetch→position_manager;polymarket-clob(vendorrs-clob-client-v2) MOVES INTO PM (leaf crate). API:place_order(OrderRequest)->PlacedOutcomemigrates BOTH today’ssubmit_live(buy) ANDauto_sell(sell) — the whole lifecycle buy→submitted→filled/partial→open→selling→sold lives in PM (FAK, proxy sigType 1, V2 contracts preserved);OrderRequestcarriesreserve_id(acts on an already-inserted reserve row) + a buy/sell field split, NOprovenanceblock.watch_self_wallet(CLOB/userWS + periodic on-chainbalanceOftruth check; chain authoritative on divergence, emit reconcile never silent),watch_transactions(the UNBUILT Phase-2OrderFilledWSS detector over a configurable address set → rawFillEvent). 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 NOsqlx::migrate!;pmv2_autotrade_ordersDDL 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_configis 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 serialGET /timebefore every order POST (~20–100ms; signed-order bytes unchanged). Behavior-preserving, proven by GOLDEN-PARITY tests (same OrderRequest → same maker/taker/contract triple; buy transitionspending→submitted→filled|partial→open, sellopen→selling→closed). Three sequenced plans: (1) execution migration [written], (2) self-wallet watcher, (3) watched-tx watcher. Preserves V2 hard-cutover correctness (V2verifyingContractaddrs, pUSD collateral) + the connect_timeout rule (BOTHtimeoutANDconnect_timeouton every reqwest client — the 75-min silent-hang lesson). Source: spec + Plan 1 inpmv2/position_manager/docs/superpowers/. - bot-pmv2-native-v1-consensus-deleted — bot + CLI
self-statusfully pmv2-native; LAST v1 consensus code DELETED (commitec75491, 2026-06-03, net −156 lines). The v1 per-position “consensus alignment” view (read from frozenpm_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-weightedbet_score, reusing the bet-link scoring path. NewCohortOverlap+overlap_by_position()(pmv2/scoring.rs),cohort_holders_for_conditions()(pmv2/repo.rs, batchedWHERE condition_id = ANY($1)), a single sharedfetch_self_overlap_sectionsfeeding 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 dormantpm_*tables remain in DB). Resolves the “loose end carried into prod” noted on first-mover-overlap-watcher. - first-mover-overlap-watcher — first-mover OVERLAP detection (Approach A — event-log join), shipped 2026-06-02 (
4794a76, migration20260603000000_pmv2_overlapin prod). Reframes the v1 self-wallet consensus watchers.run_pmv2_positionsis the source of truth for cohort ENTER/EXIT → an append-onlypmv2_position_eventsfeed; detection/logging DECOUPLED from the copy gate (should_detect(prior_len)=prior_len>0), only the paper-copy ACTION stays gated onpmv2_copy_config.enabled(events populate with the copy lane OFF). Newdetect_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 moduleoverlap_watcher.rs: cursor-based join (pm_detection_cursorskey'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 viapm_overlap_alerts UNIQUE(event_id,self_wallet). Operator-side new-position alert (repo::self_new_positions, latest-twoself_snapshotdiff, first-run suppressed) → ACTIVE notifier (run_self_sync_for_schedulertakes both notifiers);self_trade_changesrepointed to liveDataApiClient::trades. Removedchange_detection.rs+consensus_participant_changes.rs. Subagent-driven (12 tasks), 0 test failures. - pmv2-pipeline-bug-audit —
repo::load_event_winnersis SHARED between the watcher resolution/PnL pass (pmv2/positions.rs) and the validated edge/consistency trader scoring (pmv2/mod.rs→compute_trader_metrics). The 50/50 payout-DESC tiebreak + NaN-drop guard are therefore a frozen interface: changing winner-selection shifts every trader’sedgeand 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-scoring — v1 → 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, ending5ffc330) — thepm-arbcrate, backtest/short-term/wallet-monitor, and the consensus/safe-bets/trades pipeline +polymarket-dataconsensus.rs/safe_bets.rsare gone.scheduled-runnow 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 WSSeth_subscribelogs +newHeadsblock-ts cache) →copy_loop(pm-arb/copy_lane.rs, spawned invalidate::run) → decode → token→market map (rotation poller) → gatedevaluate_copy(pm-arb/copy.rs) →lane='copy'rows inpm_paper_tradesheld to resolution → resolver (subtracts PM crypto taker fee, no-op for existing lanes). - cohort-firstmover-nats-migration — cohort first-mover ENTRY ingestion moved from an in-process call chain (
run_pmv2_positions→run_autotrade_entry→runner::run_entry→engine) to a NATS publish oncohort.signals, consumed by the SAMEPmv2Consumeras @weather — one unified ingestion lane routed by the envelopesourcefield;runner::run_entrydeleted. Pairs with pmv2-position-manager-nats-consumer (which built that consumer + lane). Branchautotrade-foundation, NOT deployed. Transport-only change: the engineContextarm,wire::parse_signal, andsubmit_liveare untouched. - watcher-onchain-gap-backfill — watcher on-chain GAP-BACKFILL sweep (shipped
main43023ba, 2026-06-20, NOT redeployed; 563+107 gate). A 30s periodiceth_getLogsreconciliation over[last_seen_block, latest](new filecrates/polymarket-fetch/src/pmv2/watch/backfill.rs) that recovers cohort/self/our-fillOrderFilledlogs the Polygon WSS feed (run_log_feed, polymarket-data/onchain.rs) drops during disconnects —eth_subscribeauto-reconnects but NEVER replays gap logs, so they were permanently lost. Re-fetches the cohort across BOTH maker+taker topic slots (mirrors livebuild_logs_payload) and feeds each recovered log through the IDENTICAL worker path as aWatchEvent. New onchain.rs primitives:eth_block_number,eth_get_order_filled_logs,cohort_address_topics,build_get_logs_filter(modeled oneth_call_u128); reuses the HTTPPOLYGON_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. Watermarklast_seen_block: Arc<AtomicU64>advanced byfetch_maxinhandle_order_filledafter decode;from = watermarkINCLUSIVE (recovers intra-block partial-delivery gaps, dedup absorbs it);MAX_BACKFILL_BLOCKS=2000cap with a log on hit;try_sendso the sweep never blocks the worker. Plandocs/superpowers/plans/2026-06-20-pmv2-watcher-gap-backfill.md. - autotrade-source-extensibility-and-global-caps — adding a Directive-shaped autotrade source is GENERIC (zero engine/runner/sizing changes): only (a) a parse branch in
wire.rs::parse_signalproducing aTradeSignalwith the newsource_id+ (b) a row inpmv2_autotrade_sources. Mode resolves generically inrunner.rs::per_message_mode(repo::autotrade_source→config::effective_modemore-conservative flooroff<dry<live); missing row + failed config load FAIL-CLOSED toMode::Off. DRY:weather+crypto_shortterm_latency_testshare oneparse_directive(p,action,source_id)(2026-06-19). Amode='dry_run'row is a safe DRY-PIN (generic, any source) — can’t reach live until the row is explicitly flipped; butcrypto_shortterm_latency_testis NOT dry-pinned — seededmode='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-module —
authz.rsTask 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_commandwiring). - pmv2-autotrade-engine-design — pmv2 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 overpmv2_position_events(own cursor). New units:crates/polymarket-clob/(vendoredrs-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_copymath UNCHANGED — only additive read-path touches (tierinto the cohort read,bet_confidencecomputed unconditionally, a new exit read). New economic gates live AROUNDdecide_copy, never inside. Spec atdocs/superpowers/specs/2026-06-14-pmv2-autotrade-design.md(3 adversarial review passes). - pmv2-exit-alerts — exit alerts (2026-06-04, module
pmv2/exits.rs): a non-fatal scheduled step (after the trending alert) that READS the position watcher’spmv2_position_eventsEXITEDfeed 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). Migration20260604000000_pmv2_event_pnl.sqldenormalizes the P/L columns onto the event ininsert_position_event(no call-site change). Caveat: exits are snapshot-detected, so the P/L is “last seen” not a realized fill. - category-consensus — category 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 gammaevent_tags, computes ≥K-wallet agreement (group by(condition_id, outcome_index), rank agreement-then-stake, top 25), persists (pm_category_consensus_runs/_rows, migration20260605010000), pushes. 10-min cache. No edge/band (these traders aren’t inpmv2_traders). Replaced the short-lived tag-basedtag_consensusSQL query. Distinct from v1 wallet-consensus AND pmv2 cohort-overlap. - pmv2-bot-startup-ping — bot startup ping + build.rs SHA capture (2026-06-04):
botboot sends a best-effortNA CSUMI CSUMI CSUMI, bot STARTED, REF: <short-sha>vianotifier::startup_message(build_startup_messageis the pure builder, omitsREF:when empty). A newcrates/polymarket-fetch/build.rsbakes the short commit hash at compile time (envBUILD_GIT_HASH→GIT_HASH→GITHUB_SHA→git rev-parse --short HEAD→empty). Bot-only; scheduled-run untouched. - pmv2-trending-bets — trending velocity signal (shipped 2026-06-03,
2c05c5a..4906eea):run_pmv2_positionsjoiner-seed fix (is_new_entry— only emit ENTERED for wallets already in the prior snapshot, so cohort reshuffles can’t fabricate entry bursts) →trending_betsquery counting distinct high-score cohort wallets entering a bet in a 6h window vs prior →/trendingbot command +pmv2-trendingCLI + acceleration auto-alert (12h cooldown dedup inpm_trending_alerts, Telegram-size-guarded against an infinite re-alert loop). - pmv2-topbets-categories-filters —
/topbetscategories + filters + price + multi-date (shipped 2026-06-03,5c933a5..908824f):pmv2_event_categorycache (populated byrun_category_syncoff gamma/eventstags) feedseq:/neq:bucket filters; entry→current price display;load_bet_holdersresolves a selector to an event_slug set and returns all sibling markets (multi-date fix). - pmv2-exit-alerts — exit alerts ”🔴 TRADE EXITED” (2026-06-04): a non-fatal scheduled step (after the trending alert) that READS the
pmv2_position_eventsEXITEDfeed (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_cursorskeypmv2_exit_alert), dust-filtered (≥$50), Telegram-budget capped. Shows trader name/score/band/rank + market/outcome + P/L (last seen) = the position’s last-snapshotcash_pnl. Modulepmv2/exits.rs;pmv2-exitsCLI = read-only 24h preview. - category-consensus — fetch-based async
/consensus <category> [N] [K](2026-06-05, modulepmv2/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 gammaevent_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 inpmv2_traders); a freshly-fetched, category-specific PNL cohort — distinct from v1 wallet-consensus AND pmv2 cohort-overlap. Replaced the short-lived tag-basedtag_consensusSQL query. Does NOT touch topbets/scoring. - pmv2-bet-link-analyzer-flow — the bet-link auto-reply path end-to-end (verified
main2026-06-15, fallback shipped22d7697): a posted Polymarket link →bot.rs::classify_message→MessageAction::BetLink→run_score_bet_link/score_bet_link_body(bot.rs:611) →pmv2::scoring::score_bet(scoring.rs:414, readspmv2_positions⋈pmv2_tradersviaload_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→ emptyconsolidate_gauge.sides“no high-edge cohort holders…” (scoring.rs:472, gatescoring.rs:379;consolidate_gaugeis 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(viarun_and_persist) appends a consensus section to the same reply; with-signal replies unchanged.
Auto-Trade / Live Execution
- cohort-firstmover-lane-mode-restart-gotcha — the first-mover publish-lane mode (off/dry/live) is computed ONCE at process boot (
watch/mod.rsbuild_first_mover_lane) and is NOT hot-reloaded — flipping the mode inpmv2_autotrade_config/ thefirst_mover_corepmv2_autotrade_sourcesrow requires a firstmover RESTART to take effect (confirm via thefirst_mover_core armedINFO appearing post-change).lane_active = source_enabled && effective_mode(global,source) != Off; bothenabled=trueAND effective mode≠Off build the NATS publisher (positions.rs:224), else it’s None + a DEBUG dormant log. Only the watchlist +breaker_trippedhot-reload; mode does not. (2026-06-25, operational runbook) - cohort-pre-live-signal-gating — the 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 degenerateev_floor_gateDROPPED (operators setmax_entry_premium_bpsnet of round-trip cost instead — one price-aware knob, nosize-cancels-out math). Gates run producer-side because PM has no GammaClient to catch them. Re-added a read-onlyClobReadClient(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_gaterepointed to gate best_ask not the trader’s fill.signal_gate_decisionis now async + staged cheapest-first (skips the network calls when an earlier gate rejects). Startup announcement ontelegram.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 thefirst_mover_corepmv2_autotrade_sourcesrow + operator go-ahead. - cohort-algo-v2-wire-contract-cutover — cohort_algo now emits the v2 LIVE-EXECUTION trade contract (
0986c4d, 2026-06-23): schema_version 2EntryOrder(Pricing::Derived) onpmv2.order.first_mover_core.entry+ leanExitSignalon…exit→ PM (the executor that consumes these directly) derives the limit, does $→shares+lot-size, gates mode/breaker/caps. Producer-ownedsize_usdsizing (ported formula, caps EXCLUDED) + producer-owned signal gating (babylon #292).valid_until_ts=now+300son 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 portedev_floor_gateis degenerate) MUST close before arming because PM has no GammaClient to catch them. - telegram-connector-outbound-design — the 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
TelegramOutboundto NATStelegram.outboundandtelegram_connector(sole bot-token holder) relays it. This is the destination of thetelegram.outboundseam prepared incohort_algo(currentlyDirect-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; inboundops.*kill-switch relay (ops.commands/ops.results) is a later spec gated on the @positionmanager contract (#311). - cohort-algo-component — the 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 thederivedpricing 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:sourceMUST be the EXACTsource_idkey (first_mover_core, wrong value silently routes toMode::Off); the exit MUST carryorigin_ref(else the position can’t be auto-closed = capital trap); producers OWN ev/chase/category gating (PM has no GammaClient). Today on the OLDcohort.signalsschema_v1; v2 cutover BLOCKED on @positionmanager publishingpmv2-contracts. - cohort-signals-service-split-and-exit-signals — the 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): thiscohort-signalscarve (8cd6f58) DELETES the whole autotrade engine +polymarket-clobfrom the producer (~13,400 lines; a signal producer never signs/POSTs). Live execution is no longer in this repo at all — it consumesTradeSignals offcohort.signals. The new piece is cohort EXIT signals (e762f5d):on_cohort_sellnow publishesaction:Sell(was DB-only per pmv2-phase4-chain-cohort-sell-fast-exit) so the executor can close a copied position, with theexit-discriminator dedup gotcha (entry Buy vs exit Sell must NOT collide ondedup_key). PROVISIONAL contract: executor must consumeaction:Sellidempotently — not yet coordinated. NOT merged/deployed. - position-manager-mode-control-implemented — IMPLEMENTED (2026-06-22) — MODE-CONTROL (off/dry/live kill switch) DONE (12 tasks, per-task + whole-branch opus reviews, READY TO MERGE).
position_manageris the SINGLE execution authority:place_orderresolves the authoritative mode fresh per call (global × per-source(mode,enabled)+ breaker) and gates at the POST boundary — Off→Skipped(zero work), Dry→reservedry_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/ExitIntent→pm.place_order→typedExecOutcome→NATS ack). Entries gate oneffective_mode(global,source); EXITS gate on the row’s recordedmode(live position stays sellable when global=Off — E1 fix);enabled=false⇒Offenforced; pre-POST DB error→Transient→Nak(30s); recorded-failure/benign-race→Ack; fail-closed two paths;cap:eventliteral. Committed onmainBOTH repos (PM351a755, fetch4fde695), NOT pushed, mode OFF. Gate: 66 PM offline + 532 fetch + 7 db-tests pass. OPEN (callout): commit contamination in4fde695(user’sexit_signal/cohort_exit_signal_idwork, 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-implemented — IMPLEMENTED (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_buy≡submit_live,place_sell≡auto_sell);submit_live/auto_sell/execute.rsdeleted upstream. Committed onmainBOTH repos (PMcbd0f4c, fetchafc785d), 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-design — DESIGN (2026-06-20) —
position_managercrate: focused execution+watch home for the deferred autotrade rename. Migrates today’ssubmit_liveexecution path (signing client + execute/ledger/onchain + thepmv2_autotrade_ordersmigration) out of polymarket_fetch behindPositionManager::place_order(OrderRequest)->PlacedOutcome, behavior-preserving (golden parity on built bytes + ledger transitions). Adds two watchers (self-wallet reconcile, watched-txOrderFilleddetector). 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
OrderFilledWSS →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 WSmarket-channel book mirror (vendoredrs-clob-client-v2has a DISABLEDwsfeature) + cached STATIC market metadata; WS is read-only, placement stays REST (read-over-WS, write-over-REST). Second win = disableuse_server_timeon 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 Onramp0x9307…, V2 exchange addrs). WARNING — any proxied reqwest MUST set BOTHtimeoutANDconnect_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 Londoneu-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-visibility — OBSERVABILITY — dry-mode gate-out visibility added to
ingest_for_consumerinrunner.rs(branchfix-dry-skip-visibility, GATE_EXIT=0, 334+87 tests). Previously a dry rehearsal that was gated out (stale gate ORDispatched::Skipped) was silent and indistinguishable from “nothing arrived.” Now both skip paths surfacenotify(dry_skip_msg(...))+tracing::infowheneff == Mode::Dry; they degrade totracing::debugotherwise. Key structural change:per_message_mode()hoisted abovestale_gatesoeffis available at both skip points. Helperdry_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). GateGATE_EXIT=0(508 binary [507/1 ignored] + 99 lib, clippy -D + fmt clean, 6 new tests); only 2 files changed (alerts_consumer.rs+ themain.rscall-site). New symbolseligible_sell(sig,score)(gate:side=="SELL"+ cohort +score>MIN_COHORT_SCORE=0.0),ExitDebounce(per-wallet 60s coalesce),on_cohort_sell(wallet);run_tracked_alertsgainedcfg:&AppConfigand injects aDataApiClient(viacfg.build_data_client, which already setsconnect_timeout), atokio::Semaphore(cap 4) bounding fan-out, andExitDebounce. THE DEDUP CRUX — no double alert / no double sell: the exit pipeline already has THREE consumers of anEXITEDrow, each with its OWN dedup — (1) cohort EXIT alert (exits::run_check, dedup = thepmv2_exit_alerttimestamp cursor), (2) auto_sell (runner::run_exits, dedup =autotrade_exitscursor + themark_sellingopen→selling CAS that serializes ALL sell triggers), (3)insert_position_event= a PLAIN INSERT, NOON CONFLICT(theUNIQUE(proxy_wallet,condition_id,outcome_index)onpmv2_position_eventswas DROPPED in20260603000000_pmv2_overlap.sqlfor 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 doesdetect_exitedscoped to{wallet}and for a confirmed exit LOGS the EXITED row (repo::insert_position_event) AND DELETES the priorpmv2_positionsrows (repo::delete_positions) — mirroring exactly the cron’srun_pmv2_positions; it does NOT send a direct alert and does NOT callrun_exits/auto_sell. Deleting the prior snapshot row means the cron’s nextdetect_exitedwon’t re-see it ⇒ exactly ONE EXITED row exists ⇒ the existingpmv2_exit_alertcursor alerts once +run_exitssells once (a stray duplicate is still safe via themark_sellingCAS). 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 thepmv2_exit_alertcursor (impossible; the two rows carry differentdetected_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 SAMEPmv2Consumerthat handles @weather (one unified lane routed by envelopesource); in-processrunner::run_entryDELETED. Branchautotrade-foundation(offmain, NOT deployed), 4 commits1166f51→bdafd7f, 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-consumer —wire::parse_signal, the engineContextarm, and the singlesubmit_livesite are UNTOUCHED; only cohort TRANSPORT changed (in-process call → NATS publish). T1 (1166f51):parse_signalroutessource=="first_mover_core"→Execution::Context, FAIL-CLOSED (Reject) on any capital-critical missing/empty field —origin_refempty⇒cohort exits never fire (CAPITAL TRAP),outcome_labelempty⇒token_forfails, +reference_price/conviction/outcome_index;max_price/valid_until_tsbecameOption(cohort omits them;valid_until_ts=Noneis EXACT parity + avoids clock-skew wrongful drops). T2 (3c589c1):serialize_cohort+ the headlinecohort_roundtrip_paritytest =serialize_cohort ∘ parse_signalis byte-equal tosources::enter_signalover the full persisted/exit-read field set (fixtures useevent_slug≠slugso a field-collapse is caught). SUBTLETY: watcher has no CLOB at detection soenter_signalleaves 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 thenpublish_with_headersw/Nats-Msg-Id=signal_id(JetStream server-side dedup), then flush; reusesnats::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!readypreflight clause DROPPED — preflight is now a CONSUME-time concern); bounded retryPUBLISH_RETRIES=3/200ms, a dropped publish after retries = logged MISSED entry = UNDER-trade, NEVER a double (no telegram/breaker); publisher built once/run from envNATS_URL+NATS_COHORT_NKEY_SEED(absent⇒dormant⇒nothing published); DELETEDrunner::run_entry, deadsafety::ready, unusedAutotradeDeps.mode. SAFE (no double-LIVE order): dedup onsignal_idat BOTH the NATS header AND the DBON CONFLICT(dedup_key)backstop; money-path untouched; dormant cohort (off) publishes nothing. ⚠️ LOAD-BEARING GOTCHA: deleting watcher-sideautotrade_ready()removed in-process entry-preflight AND per-tickreconcile_entries— safe TODAY (cohort dormant/off, no live orders,/autotrade reconcileworks 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 staysWEATHER_SIGNALS(ADDcohort.signalssubject, do NOT rename);pmv2durable must consume BOTH subjects; provision a cohort-publisher NKey; putNATS_URL+NATS_COHORT_NKEY_SEEDin 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-alertsconsumer turns the Phase-1 on-chain detector’stracked.signalsstream into INSTANT first-mover buy AND sell Telegram alerts, recovering the first-mover edge lost to the ~15-min REST batch cadence. Branchautotrade-foundation, 2 commits806ba78+e1d1f24, gate green (471+90, clippy -D warnings + fmt clean); NOT merged, NOT deployed. SEPARATE from the autotradecohort.signals→WEATHER_SIGNALSlane (cohort-firstmover-nats-migration) — ALERT-ONLY (subjecttracked.signals, streamTRACKED_SIGNALS, does NOT execute). Phase 1 (shipped) = always-on detectorpmv2-tracked-detectorpublishing aTrackedEventJSON per cohort buy/sell fill. Service: new long-running CLIpmv2-tracked-alerts= NATS JetStream pull consumer (durablealertsonTRACKED_SIGNALS); dormant-safe (exits 0, env-first no-DB-touch, ifNATS_URL/NATS_TRACKED_ALERTS_NKEY_SEEDunset) ⇒ deploy-safe before provisioning. Architecture: pure coredecide_alert(payload,cohort)->AlertDecision{Send|Skip|Reject}(parse→lowercase-keyed cohort lookup→format) wrapped by a thin looprun_tracked_alerts; filessrc/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 viaorder_hashdedup ⇒ 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 + batchformat_entry_alert/send_entry_alertUNTOUCHED — 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)]onTrackedSignalis 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 cratepubdoes NOT exempt never-read fields fromdead_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 autotradenats::run; near-zero from our own publisher but real diagnostic value). DEPLOY (pending, folds into the Phase-1 detector deploy): provisionNATS_TRACKED_ALERTS_NKEY_SEED(consume-only ontracked.signals); JetStream streamTRACKED_SIGNALScapturingtracked.signalsw/ durable pull consumeralerts; runpmv2-tracked-alertsas a long-lived service (alongsidepmv2-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-memConsensusTracker(HashMap<MarketKey,HashMap<wallet,Instant>>, 48h, pruned on 60s cohort-refresh),eligible_buygate (BUY + score>0). ConstsCONSENSUS_WINDOW=48h/MIN_TRADER_SCORE=0.0/2-trader. Observability: per-alerttracing::info!(target:"tracked_alerts", n=…, "consensus alert sent")⇒ alert rate now visible in SigNoz (polymarket-fetch-pmv2-tracked-alerts). LIVE on0f71f89, 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 inwire.rs::parse_signal(newsource_id) + apmv2_autotrade_sources(source_id,enabled,mode)row; NO engine/runner/sizing changes. Mode resolves generically inrunner.rs::per_message_modeviarepo::autotrade_source→config::effective_mode(const fnmore-conservative flooroff(0)<dry_run(1)<live(2)); missing source row + failed config load both FAIL-CLOSED toMode::Off. DRY:weather+crypto_shortterm_latency_testshare oneparse_directive(p,action,source_id)helper (generalized fromparse_weather, 2026-06-19). A row pinnedmode='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_testis the deliberate exception — NOT dry-pinned: seededmode='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.AutotradeConfigis 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 ONLYmode+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 modeoff; 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,DirectivevsContext,effective_mode, the per-sourcemodecolumn). - 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 STAYSautotrade(rename deferred). T1 generalizeTradeSignal:SignalAction::{Enter,Exit}→Execution{Directive{max_price} | Context{reference_price,conviction,source_score}};signal_idis the dedup key (dedup_key()returns it directly); LOAD-BEARING — cohortsignal_id == legacy dedup_key(..,"enter",..)so dedup is byte-identical mid-migration (no double-orders). T2wire.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. T3plan_orderseam:ingest_entrydispatchesExecution→plan_context(cohort, unchanged) vsplan_directive(weather: token_id-direct, outcome_index fromclob_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_gateadded. T4 per-source mode:pmv2_autotrade_sources.modecol,effective_mode=min(global,source)on off<dry<live, cohort backfilledlive; LOAD-BEARING —build_autotrade_depsoverridesengine_cfg.mode=effso 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.rsack discipline (ack_action: Persisted|Skipped→Ack, Transient→Nak, Stale|Terminal→Term), async-nats 0.49 raw NKey seed (no TLS), binds EXISTINGpmv2durable onWEATHER_SIGNALS; ack-only-after-persist +ON CONFLICT(dedup_key) DO NOTHING= at-least-once safe. T6Pmv2Consumersubcommand: 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 areBox<dyn StdError>(no anyhow::From) →nats_errmap_err helper not bare?; going live needs TWO operator actions (global=live AND weather.mode=live); envNATS_URL+NATS_NKEY_SEED(pubkey UDYV…ZEDD). REMAINING: deploy + @deploy launcher for the subcommand + manual dry-validate before arming weather live. Wire SoT = weather_betdocs/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-closedmarket (2026-06-17,824a22d+8b3c451, branchautotrade-foundation, NOT deployed). Gamma:closed=false/acceptingOrders=trueBUTumaResolutionStatuses=["proposed"]+outcomePrices=["0.0005","0.9995"]= result in, not finalized. Hole 1: cohortprice_bounds_gatevalidates the COPIED TRADER’s frozenreference_price=p.avg_price(in-band ~0.44), NEVER the livebest_ask(0.001) → a post-entry collapse slips through. Hole 2:liveness_gateonly skips FORMALLY resolved/closed/non-accepting; UMA-proposedis stillacceptingOrders=true. Part A (ask-band,engine.rs::plan_context): afterbest_ask, skip when ask outside[min_entry,max_entry](reusesprice_bounds_gateon the ASK), reason"ask … outside entry band"; Cohort/Context-ONLY, NOT the weather Directive path (legit buys up tomax_price=0.95); data-source-independent PRIMARY net. Part B (resolution-proposed,8b3c451):GammaClient::market_resolution_proposedreadsumaResolutionStatuses;MarketStatusView.resolution_proposedset FAIL-OPEN (30s timeout/warn/default-false — Gamma hiccup never breaksresolve());liveness_gateskips, reason"market in resolution". ENTRY-ONLY —is_terminal_market/exit_decision_foruntouched (resolution-proposed position stays sellable). Gate green 503 workspace tests. See Gotchas + Polymarket API Quirks topics for theumaResolutionStatuses-is-a-stringified-array andMarketStatus.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 intermittentscheduled-run15-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 precederun_pmv2_positions) + (3) intermittent whole-cycle loss when a run hitsTimeoutStartUSec=15minand 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 proxiedreqwestw/oconnect_timeoutin a later stage (the 09:00 run died at 09:15 AFTER the autotrade step ran at 09:08); secondary = a growingEXITEDbacklog (exit-cursor read = 37,741 rows / 2.87s, cursor maybe near-NULL). Re-verify via@deployon 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): vendoredrs-clob-client-v2Amount::sharesrejects shares withnormalize().scale() > LOT_SIZE_SCALE(=2)(0.01 lots,types/mod.rs:193-200+order_builder.rs:25);auto_sellfed the RAW on-chain fill (locked vector 7.6219, scale 4) → build error → revert toopen→ EVERY fractional-share position unsellable (auto-exit +/autotrade sell), capital trapped till resolution; FIX =sell_shares = filled_size.trunc_with_scale(2)used forbuild_order+quantize_sell+assert_sell_amounts, sub-0.01 dust held to resolution (return beforemark_selling); BUY unaffected (Amount::usdcscale ≤6, market builder no lot gate). 2E (false risk-limit,84e3e34):max_copies_per_eventshown in status but NEVER enforced → per-event_slugCOUNT in the reserve config-lock tx (0=unlimited). 2C/I3/1D gaps (d4a2a85): settle alarms a heldexit_pending; forward-onlyresume_cursor(first_failed−1µs) stops the exit cursor dropping a same-detected_tssibling; reconcile CTF sweep boundedLIMIT 25+notify. DRY (efc69bc)+regression (0f06ec5):collecting_notifyextraction blanked the/autotrade sellreply —Arc::try_unwrapdrained 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 (CTFbalanceOfprimary truth,decide_entrypure table, derivedready=preflight&&reconcile&&credsgate, per-tick) / Task 7 selling-state exits (auto_sellFAK,mark_sellingopen→selling CAS serializes all triggers, per-order-mode authority — live exits even with kill-switch off,exit_pendingmissed-exit queue, 1-clicksell:<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_orderbook-priced vs quantizedplan.taker→assert_amountsexact-taker bailed; FIX pin.price(plan.limit)on BUY / floor on SELL. (B) partial FAK orphaned residual + mis-booked PnL → FIXprorate_partial+record_partial_sellrevert selling→open w/ reducedfilled_size/cost+ accumulatedpnl. (C) stuckselling/fillednever reconciled → FIXload_reconcilable_entrieswidened (selling: bal>0 revert / bal0 close+alarm; filled/partial:mark_openretry). Re-review: SAFE FOR STAGED ARM. Deferrals:fee_usd=0(PostOrderResponse has no fee — cost understates/PnL overstates, fail-safe; calibrate or wireorder_trades); cap-breach surfaces as benign “dedup” Skip (visibility gap, not fixed); balance-0 stuck-selling bookspnl=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-chainOrderFilledWSS recommended before scaling past tiny caps. Includes the staged-arm runbook (keys+RPC →/autotrade dryrun→ small caps →/autotrade livepreflight-gated → observe fills/reconcile/restart/exit/1-click/settlement;/autotrade offstops entries but live positions still exit). - autotrade-3c2-validation-carryforwards — 3c2.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-foundations — Task 7a (2026-06-16):
assert_sell_amounts+ sharedassert_contractinexecute.rs; 5 CAS transitions (mark_selling/mark_sold/revert_selling/set_exit_pending/clear_exit_pending) +SellableOrderRow+ 2 query helpers inrepo.rs. Serialization gate for multi-trigger races; exit-pending race handler; zero-submit. 306+87 tests pass. - autotrade-breaker-literal-brace-fix — Bug fix (2026-06-16):
first_lock_order_violationfalse-negatives from brace chars inside string/char/raw-string literals;neutralize_literal_braceschar state machine;repo::bump_breaker_countdead_code removed; 3 regression tests; 285 pass. - autotrade-breaker-module — Task 5 — circuit breaker consolidation (2026-06-15, branch
autotrade-foundation).bump_breaker+BREAKER_THRESHOLD=5extracted fromexecute.rsinto newbreaker.rs; SQL moved torepo.rs::bump_breaker_count(config-row-onlyFOR UPDATEtx, runs after the order-row tx commits). §3.4 lock-order safety invariant now enforced by a structural regression test inbreaker.rsthat scans allsrc/pmv2/autotrade/*.rs+src/pmv2/repo.rson everycargo testrun and fails if any function locks an order row before a config row. Pure refactor — zero behavior change, 282 tests pass. - autotrade-engine-impl —
engine.rs— Plan 2 capstone (2026-06-14, branchautotrade-foundation).ingest_entrywires all gates via injected traits in a fixed sequence ending intry_reserve(the single mutation point). Key decisions:EngineConfigdropsClone;min_round_trip_edge_bps>200 required in tests;ingest_entrytakes&(dyn Trait + Send + Sync)for tokio Send;category_gatepassesNoneuntil Plan 3. 8/8 tests pass. - autotrade-market-data-trait —
market.rs— P2.3 of the autotrade foundation (2026-06-14, branchautotrade-foundation).MarketStatusViewcopy-struct +ResolvedMarketsnapshot (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).MarketDataasync trait with singleresolve(condition_id, outcome_index) -> Result<ResolvedMarket>method. 4 tests. Gate gotchas:clippy::unwrap_usedin test bodies; rustfmt alphabetical import order;async-traitmust appear in the crate’s own Cargo.toml. - autotrade-signal-type —
signal.rs— P2.1 of the autotrade foundation (2026-06-14, branchautotrade-foundation).SignalAction::{Enter{reference_price},Exit}+TradeSignalstruct (source_id, action, condition_id, outcome_index, outcome_label, event_slug, slug, origin_ref, conviction, source_score, tier, instance).dedup_key()delegates toorders::dedup_key().Tierre-exported frompmv2::model. 2 tests pass. The single pluggable seam between any source and the engine. - autotrade-authz-module —
authz.rs— Task 2 of the autotrade foundation (2026-06-14, branchautotrade-foundation). Pure authorization layer:Scope::{Public,Owner},Authz::{Allowed,Denied},OwnerSet(parsesTELEGRAM_OWNER_IDSCSV of i64 user IDs),authorize(),scope_for()("autotrade"→Scope::Owner, elseScope::Public). Zero I/O, 6 unit tests, clippy pedantic+nursery clean.#![allow(dead_code)]is temporary — removed in Task 3 whenrun_commandconsumes the module. - pmv2-autotrade-engine-design — the design for writing REAL trades to Polymarket’s CLOB (LOCKED 2026-06-14). Execution = vendor+pin the official
Polymarket/rs-clob-client-v2Rust crate (alloy-based) — NOT a Python sidecar, NOT hand-rolled EIP-712 signing. Wallet = Bandi PROXY0xdb0d23c2c3468da13c6e8abfe96edfecc72d9efe,signatureType=1(maker=proxy, signer=EOA0xfa7772b9aa1230a53d3bea9a11f5c52f110684f7); 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-deferrableverifyingContract-vs-neg_riskassert, NEVER hardcoded. ModesOFF/DRY_RUN/LIVEgate entries; exits follow the per-ordermode, 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; asellingreserve state + global lock order (config-before-order) preventing double-sell + ABBA deadlock; on-chain CTFbalanceOf(proxy,token_id)as the reconciliation ground truth (expected_order_hash→clob_order_idcorrelation 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’sroundDown/roundNormal; the earlier “GCD of price and 1e6” guess was WRONG),order_hash↔orderID, and V1/V2 collateral, which cannot be settled on paper. - autotrade-config-repo —
config.rs— config repository layer (2026-06-15, branchautotrade-foundation).AutotradeConfig(sqlx::FromRow,BigDecimalNUMERIC fields,i32INTEGER fields);load(pool)fetches the singleton id=1 row;update_numericconverts f64→BigDecimal via the establishednumeric()helper before binding;set_textfor nullable text;set_source_enabledforpmv2_autotrade_sources;update_field_sqlpure SQL builder;validate_setdomain-bound checker for Telegram inputs. Runtimesqlx::query_as::<_, T>(no compile-time macros — no live DB at build). 5 tests pass. - autotrade-commands-bot-wiring —
commands.rs+bot.rs—/autotradeTelegram command wiring (2026-06-15, branchautotrade-foundation).apply_set(pure validate+column resolver),format_status(rendersAutotradeConfigas a human-readable string;BigDecimalfields 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 torun_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-bindings — Plan 3b Task 1 — binding vendored
rs-clob-client-v2intopolymarket-clobcrate (2026-06-15, branchautotrade-foundation). Correct import paths (U256attypes::U256,Unauthenticatedatauth::state::Unauthenticated,Client/Configatclob::);U256::from_str(nofrom_str_radix;to_string()is decimal);Client::newhas no chain ID arg; bon builder returns struct directly;MarketResponsefield type map; clippy landmines from enabling"clob"feature across the workspace (derivable_implson vendored default,single_option_map,unused_self,allow→expect); vendored examplerequired-featuresfixes. - autotrade-gates —
gates.rs— P2.4 of the autotrade foundation (2026-06-14, branchautotrade-foundation). Five pure gate functions all returningGateOutcome::{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 tocrate::pmv2::category::passes_filter;None= Proceed),price_bounds_gate(outside[min_entry, max_entry]→ skip),ev_floor_gate(skip unlesssize × bps/10_000 > est_fee;#[allow(clippy::cast_precision_loss)]fori64 as f64),chase_gate(skip if ask >reference_price × (1 + bps/10_000)— reference_price frozen at signal detection). Helperest_round_trip_cost_usdwithROUND_TRIP_COST_RATE=0.02. 9/9 tests pass, clippy clean. Gate-scriptexit=1was a false positive (rtk intercepting-- -D warnings). - autotrade-category-resolver — on-demand category resolver T3 (2026-06-15, branch
autotrade-foundation).read_event_category(pool, event_slug)inrepo.rs(runtime sqlx,Option<String>frompmv2_event_categorycache);resolve_category(pool, gamma, event_slug)incategory.rs(cache-hit-or-fetch: reads cache →gamma.event_tags(&[slug])→ frozencategory_bucket→upsert_event_category;Ok(None)if gamma doesn’t know the slug). Both carry#[allow(dead_code)]until T4/T8. Key gotcha:event_tagstakes&[String]— bindlet slugs = [event_slug.to_string()]before passing&slugs(temporary lifetime).GammaClientimported frompolymarket_datacrate. No staleness logic — batchrun_category_syncowns refresh. - autotrade-persistence-engine-config — T5 —
PgPersistence+engine_config(2026-06-15, branchautotrade-foundation).engine_config()convertsAutotradeConfig(BigDecimal/i32) →EngineConfig(f64/i64/usize), buildingCopyConfigfrom autotrade’s own sizing fields;category_filterviaparse_topbets_filter.PgPersistence<'a>implementsPersistence: dry-run inserts with terminal statusdry_run(excluded from partial dedup index + exposure queries, never counts toward daily cap); live inserts aspending. Dedup usesON CONFLICT (dedup_key) WHERE status NOT IN ('dry_run','closed','canceled','failed') DO NOTHING RETURNING id.FOR UPDATElock on config row (id=1) prevents double-reservation races.OrderMode::as_str/from_db+OrderStatus::from_dbmappers inorders.rs. BigDecimal conversions need bothFromPrimitive+ToPrimitivein scope. Transaction executor pattern:&mut *tx(sqlx 0.7+). 249/87 tests, gate exit 0. - autotrade-reserve-outcome-enum — ENGINEERING DECISION —
Persistence::try_reservenow returnsReserveOutcomeenum (Reserved(i64)|CapExposure|CapDaily|CapMaxOpen|CapEvent|Dedup) instead ofResult<Option<i64>>(2026-06-19, branchautotrade-phase2-firstmover, GATE_EXIT=0). PreviouslyOk(None)collapsed both anON CONFLICTdedup-skip AND any cap-breach into the sameIngestOutcome::Skipped("dedup")— a double-trade during Phase-2 staging would look identical to benign noise. Now each variant maps to a distinctcap:exposure/cap:daily/cap:max_open/cap:event/dedupskip reason; aSkipped("dedup")unambiguously means an ON CONFLICT hit. Pre-lock cap strings unified to the samecap:*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-hoist — REFACTOR Phase 2 (2026-06-19, branch
autotrade-phase2-firstmover, GATE_EXIT=0) — publish consts hoisted tonats.rsaspub consts;PublishOutcomeenum +build_and_publish_first_mover_entryasync fn infirstmover.rsis the single canonical emit path for both cron and future watcher (identical id+gating+retry by construction);run_autotrade_entryinpositions.rsbecomes 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-identicalsignal_ids to the cron, otherwise the DBON CONFLICT(dedup_key)backstop doesn’t fire and the same logical entry can place two live orders. - autotrade-sources-canonical-signal-id — Phase 2 B1 (2026-06-19, branch
autotrade-phase2-firstmover) —cohort_signal_idnow lowercasesorigin_ref,condition_id, andinstancebefore hashing;enter_signalusescondition_idasinstance(replacing the unstableevent_slug.unwrap_or(condition_id)). These two fixes makesignal_idbyte-identical across the cron and future watcher producers, ensuring the DBON CONFLICT (dedup_key) DO NOTHINGdedup fires correctly and the same logical entry never places two live orders. Two new parity tests; gate=0. - autotrade-sources-signal-emitter — Task 6 —
sources.rsfirst_mover_core signal emitter (2026-06-15, branchautotrade-foundation).Tier::from_label(s: &str) -> Option<Tier>case-insensitive parser added tomodel.rs.CohortTradergainedtier: Option<Tier>inpositions.rs.latest_scored_cohort_with_scoresinrepo.rsSELECTst.tierTEXT and maps viafrom_label. Newsources.rs:SOURCE_ID = "first_mover_core",autotrade_active(mode, enabled) -> bool(const fn),enter_signal(freezesreference_price = p.avg_price, NOTcur_price— critical for the chase gate),format_dry_run_order. 3 new tests; total 352 tests, 0 failures, clippy clean. - autotrade-max-copies-per-event — Bug fix —
max_copies_per_eventloaded+validated but never enforced (2026-06-16, branchautotrade-foundation). Added field toEngineConfig+ReserveCaps;engine_config()maps i32→usize withmax(0, val)(zero=unlimited, diverges frommax_open_positionswhere 0 blocks all, by design); in-tx per-event-slug COUNT query intry_reserveLIVE branch under the existing configFOR UPDATElock;caps_from_cfg()const fn +pre_lock_cap_skip()private async helper extracted to keepingest_entryunder the 100-line Clippy limit; 2 new persistence tests (zero→unlimited, negative→zero). Gate: 327 tests, 0 failures. - autotrade-onchain-balanceof — Task 6 — ERC-1155
balanceOfon-chain primitive (2026-06-15, branchautotrade-foundation).ctf_balance_of(proxy_wallet, token_id, rpc_url)encodes ERC-1155 calldata, fireseth_call, decodes theuint256balance. Key facts:sol!generatesbalanceOfCall;SolCall::abi_encode();alloy_primitives::hexre-exported (no separate crate); selector0x00fdd58e; spike doc had hex-decode error (0x2c641ab6=744_757_942not744_144_054); binary-only crate needs--bin polymarket-fetchfor tests;polygon_rpc_urlinAppConfig(#[allow(dead_code)]until T7); 3 pure unit tests, 256 total pass. - autotrade-repo-exit-helpers — Task 7 — autotrade repo exit helpers (
repo.rslines 1270–1359, 2026-06-15, branchautotrade-foundation).AutotradeExitEventRow(sqlx::FromRow overpmv2_position_eventsexit join);autotrade_exits_cursor(reads cursor key'autotrade_exits', returns 0 if absent);advance_autotrade_exits_cursor(upsert viaEXCLUDED.last_check_at);autotrade_exit_events(kind=‘EXITED’, detected_ts > cursor, joined topmv2_autotrade_orders);autotrade_open_orders_for_origin(status=‘open’ orders by condition_id/outcome_index/origin_ref);autotrade_source_enabled(fail-closed bool frompmv2_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-hash — EIP-712 order hash implementation in
polymarket-clob(Task 3, 2026-06-15, branchautotrade-foundation).OrderV2is#[non_exhaustive](sol! macro) — FRU syntax blocked from external crates; use field-by-field assignment on aDefaultinstance. alloy 1.6 import paths:alloy_sol_types::{Eip712Domain, eip712_domain!, SolStruct as _}(NOTalloy::dyn_abi::Eip712Domain). Polymarket V2 domain: name="Polymarket CTF Exchange", version="2", chain_id=137, standard exchange0xE111180000…, neg-risk0xe2222d….LazyLock<Address>with scoped#[allow(clippy::unwrap_used)]for const address parsing (workspace forbidsexpect_used).verifying_contractmust beconst fnormissing_const_for_fnnursery lint fires.alloy-sol-typesneeds explicit workspace dep even when vendored crate uses the alloy umbrella. - autotrade-signing-client-order-ops — Plan 3a Task 1 —
place_order/cancel/get_order/open_orders/order_tradesonSigningClient(2026-06-15, branchautotrade-foundation). KEY FINDING:SignedOrder(vendored) has noDeserialize— the spec’sserde_json::from_value::<SignedOrder>(...)cannot compile. Fix: dropSignedPayloadwrapper, holdsigned_order: SignedOrdertyped value directly inBuiltOrder. API shapes confirmed:OrdersRequest/TradesRequestboth deriveDefault(no-filter =::default());Page<T>.data: Vec<T>;OpenOrderResponse:.id,.status(Display),.size_matched;TradeResponse:.taker_order_idis the CLOB order id (NOT an EIP-712 hash —TradeView.order_hashalwaysNone),.transaction_hash: B256;PostOrderResponse:.order_id(alias"orderID"),.making_amount/.taking_amount,.transaction_hashes Vec<B256>,.trade_ids Vec<String>;cancel_orderreturns(). 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_copysizing (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-16 — Four behavior-preserving DRY refactors (2026-06-16, branch
autotrade-foundation, EXIT 0 / 333 tests). (1)collecting_notify()factory incommands.rsdeduplicatesArc<Mutex<Vec<String>>>+ closure fromrun_reconcile/run_sell. (2)AutotradeDeps::market(pool)method replaces 3 verbatimClobMarketDatastruct literals acrossrunner.rs/positions.rs/commands.rs;build_autotrade_depspromoted topub. (3)FillOutcomeenum +record_fill_then_openasync fn inexecute.rs— shared byrecord_placed_fill+heal_filled. (4) Dead functions removed:repo::mark_dry_run+repo::load_autotrade_order;AutotradeOrderRowretained. - autotrade-halt-command —
/autotrade halt— full-stop command (2026-06-16, branchautotrade-foundation).config::halt(pool)executes a single atomic UPDATE settingmode='off'ANDexit_mode='notify'together; no intermediate state.Haltvariant +run_halt+ parse arm added tocommands.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-fix — Bug fix — exit-cursor 404 WEDGE (2026-06-16,
b5d5986, branchautotrade-foundation, NOT merged).run_exitsbroke on the FIRST exit-event whose CLOB/book404’d and parked the cursor atfirst_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()skipsbook_topforresolved||closed(emptyBookTop, no 404); pure helpersis_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_gatealready skips terminal). Gate 345+87 (+10); trade-safety 7/7. See the Gotchas topic for theresolved==closedvendored-field aliasing caveat.
CI / Build
- autotrade-clob-crate-bindings — enabling
features = ["clob"]lints vendored code: four fixes required —#[expect(clippy::derivable_impls)]on vendoredConfig::default(), add"tracing"to four vendored examplerequired-features, suppressclippy::single_option_mapwith#[expect](rename alone not enough), convertfn foo(&self)that ignoresselfto an associated function. Also: existing#[allow]suppressions must become#[expect(..., reason = "...")]per workspace rules. See full list in the note. - autotrade-market-data-trait — gate gotchas (2026-06-14):
clippy::unwrap_usedfires onunwrap()in test bodies (--all-targetsincludes tests); rustfmt reorders imports alphabetically (e.g.{bail, Result}not{Result, bail});async-traitmust be listed in the crate’s own Cargo.toml even when it’s in the workspace lock. - correctness-pass-six-fix-commits — PROCESS GOTCHA: the rtk shell wrapper MANGLES
cargo clippy ... -- -D warnings(it passes-Das 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. - ci — local 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 manglescargo … -- -D warningsat 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: pinrust-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 bumpingtestcontainersrather than ignoring them. - autotrade-onchain-balanceof — binary-only crate gotcha:
polymarket-fetchhas nosrc/lib.rssocargo test --libfails; usecargo test --bin polymarket-fetchor justcargo test -p polymarket-fetch. - polymarket-fetch-deploy —
Dockerfile.polymarket-fetchmulti-stage (rust:1-bookworm → debian:bookworm-slim) and.github/workflows/release.ymlcalling the reusablewowjeeez/terraform/.github/workflows/build-push.ymlwith WIF auth; pushes:latest+:<sha>to Artifact Registryeurope-west3-docker.pkg.dev/aerobic-tesla-490112-r3/apps/polymarket-fetch.
Claude Code / Workflow
- ci — rtk’s Claude-Code hook mangles
cargo … -- -D warnings(rewrites-Dinto a filename) at the top level, but the mangling does NOT happen when cargo runs inside a shell script.scripts/gate.shwraps 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 --resumeand can’t be cleared from the CLI; cause is a bloated, tool-block-unbalanced transcript (over token ceiling ANDtool_use/tool_resultmismatch). You can’t/compactout (400 fires pre-load). Recovery: reconstruct fromAGENT_HANDOFF.md+historian, continue in a fresh session. Includes the.jsonldiagnostic command set.
CLI
- polymarket-fetch —
run/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-scoring — pmv2 commands:
pmv2-positions(snapshot the cohort’s open event positions intopmv2_positions; default--concurrency 4) andpmv2-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-bets —
pmv2-trendingCLI +/trendingbot command surface the velocity query (distinct high-score cohort wallets entering a bet in a 6h window vs prior). - pmv2-topbets-categories-filters —
/topbetsfilter syntax:eq:geo(single bucket) /neq:sport,esport(exclude buckets, comma-separated); buckets are esport/sport/geo/crypto/other. - pmv2-exit-alerts —
pmv2-exitsCLI: 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-extraction — the cohort-mirror’s on-chain detection substrate (
OrderFilleddecoder + resilient WSS feed) is now a SHARED reusable git cratepmv2-onchain-watch(@5142d98, 2026-06-23, babylon #402). Rather thanposition_managercopying 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, likepmv2-contracts); cohort dogfoods it — itswatch/collapsed to a thinrun_feedconsumer (watch::Sendercohort set @ 60s refresh →run_feed→mpscofWatchedFills →handle_cohort), behavior byte-identical frommarkets.resolveonward (only the input typeOrderFilledEvent/CohortAction→WatchedFillchanged). The mirror’s detection logic is unchanged — this is library extraction, not a strategy change. - cohort-pre-live-signal-gating — the 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 livebest_askvs 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_gaterepointed 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-onlyClobReadClient(/bookbest-ask +/markets/{cond}status) — a deliberate CLOB re-coupling for best-ask accuracy over the freecur_price/gamma proxy. The mirror is still mode OFF. - cohort-algo-component — the 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 NATScohort.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 thepolymarket_fetchcarve (preservedcrate::pmv2::paths + thepolymarket-datacrate 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-signals — the copy lane is now a two-sided producer over NATS: first-mover ENTRY (
action:Buy) AND cohort EXIT (action:Sell) both publish tocohort.signalsfor 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 sharepublish_cohort_with_retry; publish-before-delete. GOTCHA: exitsignal_idcarries anexitdiscriminator distinct from the entry’s so the consumer’sON CONFLICT(dedup_key)doesn’t swallow the Sell as a dup Buy (capital trap). Exit gate stays the score-basedeligible_sell+ExitDebounce, NOT narrowed to Core-tier (you exit any cohort sell you’re mirroring). - bot-pmv2-native-v1-consensus-deleted — operator-overlap view in the bot/CLI: per operator position, how many edge-cohort traders hold the same
(condition_id, outcome_index)+ conviction-weightedbet_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-watcher — the 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 frompmv2_copy_config.enabled); the actual paper-copy ACTION remains independentlyenabled=false. Realizes the xref-consensus-rejected-fresh-copy-wins first-mover framing on the pmv2 pipeline as an operator-overlap signal. - pmv2-pipeline-bug-audit — pre-release hardening of the pmv2 first-mover watcher (
pmv2/positions.rs, default-OFF, unreleased): (1)load_copy_configfetch_one→fetch_optional+CopyConfig::disabled()so a missing singleton no longer errors the whole positions snapshot; (2)insert_position_eventnowwarns on error (was.ok()) + table getsUNIQUE(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_usedread only committed rows) → fixed with an in-flightHashMap<event_id,f64>accumulator + purewithin_event_caphelper +AND status='open'(cap = live exposure). Deferred sizing-formula tuning (score_weight=score·10+1makesbase_usda near-no-op above score~0.4) for enable-time. - xref-consensus-rejected-fresh-copy-wins — the verified mandate for the copy lane: copy on ONE fill, fast — do NOT add a consensus/quorum gate. A purged
--xrefbacktest (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’scohort_price/detect_latency_ms/up_mid_30s_ago/up_mid_60s_agocolumns 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-design — the 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 AROUNDdecide_copy, never inside it.reference_priceis frozen at first detection (not the later blendedavg_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-maker — worked example of a high-volume/high-PnL wallet that is explicitly NOT copyable.
swisstonyis 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-cleanup — babylon #333 IMPLEMENTED (
a3329dc, 2026-06-23): cohort squashed its 44 inherited sqlx migrations → ONE idempotentCREATE TABLE IF NOT EXISTSbaseline (20260624000000_baseline.sql) for ONLY the 12 tables it owns/reads (9pmv2_*+ 3 keptpm_*:pm_scheduled_runs/pm_category_consensus_rows/pm_category_consensus_runs); cumulative deployed schema (ALTERs folded), RLS preserved; dropped thepmv2_autotrade_*DDL (PM’s migrator4712de9recreates byte-identical so cohort’s READS survive) + ~25 deadpm_*.set_ignore_missing(true)on the Migrator (sqlx 0.8.6) indb.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+instaschema_snapshotgolden (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_snapshotvalidates columns only (not constraints/indexes/RLS) + omits 2 kept tables = coverage gap. Prod surgery (PM migrator first →pm_*DROPs →_sqlx_migrationsreset) is @deploy’s (#380→#397). - cohort-algo-shared-db-migrator-ownership — babylon #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 allpmv2_autotrade_*DDL → read-only precondition (cohort READS config+sources, never touches orders). Shared-DB sqlx safety = each repo uses a DISTINCT migration-history table (customMigratortable 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 onpmv2_autotrade_*schema until PM’s migrator ships. Cohort cleanup (NOT yet done): prune ~28 dead legacypm_*tables + drop the autotrade DDL + set a distinct history table. - position-manager-interface-design — SCHEMA SPLIT, REVISED post-audit (2026-06-20) — CODE ownership only, NO second migrator. PM owns the ledger CODE (
ledger.rsruns sqlx queries againstpmv2_autotrade_orders); the table DDL stays with the single existing upstreamsqlx::migrate!(it already exists in prod). PM runs NO migrator — nomigrations/dir,sqlxmigratefeature omitted, the applied upstream migration untouched. The earlier plan (PM owns an ADOPT-IF-EXISTSCREATE TABLE IF NOT EXISTSmigration) 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_configis 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-onlyid=1 FOR UPDATElock); strategy filters +modestay 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-trap — REFERENCE/LESSON: sqlx 0.8 tracks ALL applied migrations in ONE hardcoded
_sqlx_migrationstable keyed byversion(BIGINT PK) — so you cannot run two separatesqlx::migrate!sets against the same DB. Three failure modes: (1) two migrator sets → each throwsMigrateError::VersionMissingon the OTHER’s applied versions (validate_applied_migrationsdefaultsignore_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 EXISTSdoes 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_migrationstable; 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-consumer — two 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')), thenUPDATE ... SET mode='live' WHERE source_id='first_mover_core'(cohort backfill soeffective_mode==global, behavior preserved).20260617010000_pmv2_weather_source.sql: seeds theweathersource 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)onoff<dry_run<live. - correctness-pass-six-fix-commits — always filter
redeemable=falsewhen aggregating holdings (2026-06-03).pmv2_positions/pm_positionskeep 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_positionsgot the same filter (it was raising false overlap alerts on resolved markets). Also: display rank must use the MONTH window —COALESCE(rank_month, rank_week, rank_day), NOTrank_all— because the Polymarket leaderboard UI defaults to the month tab (fixed at all 5 query sites); anduser_namesubqueries needORDER BYwith theirLIMIT 1or the name is nondeterministic across the 4 leaderboard intervals. - first-mover-overlap-watcher — migration
20260603000000_pmv2_overlap(applied to prod): forward ALTERs on the LIVEpmv2_position_events— dropped the per-key UNIQUE (table is now an append-only enter→exit→re-enter lifecycle log),kindCHECK →('ENTERED','EXITED'), addedrun_id/trader_score/avg_won_bet_size/avg_lost_bet_size; createdpm_overlap_alerts(UNIQUE(event_id, self_wallet)for dedup). Overlap watcher cursor lives inpm_detection_cursors(key'overlap_watcher'). GOTCHA: the auto-generated constraint name was TRUNCATED to..._outcome_inde_key(not..._index_key) — look up real names viapg_constraintbeforeDROP CONSTRAINT. - pmv2-phase4-chain-cohort-sell-fast-exit — CONSEQUENCE of the dropped
pmv2_position_eventsUNIQUE (above): exit dedup CANNOT live in a DB constraint.insert_position_eventis a PLAIN INSERT with NOON CONFLICT, so duplicateEXITEDrows are possible at the table level. The three EXITED-row consumers dedup via their own cursors + themark_sellingopen→selling CAS, NOT the schema: cohort EXIT alert = thepmv2_exit_alerttimestamp cursor; auto_sell = theautotrade_exitscursor +mark_selling. Phase 4’s chain fast-exit path safely rides those by LOGGING the EXITED row + DELETING the priorpmv2_positionssnapshot (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.sqlgainsUNIQUE(proxy_wallet, condition_id, outcome_index)on the position-events table (paired withON CONFLICT DO NOTHINGto stop duplicate ENTERED rows). Edited but NOT applied — apply before enabling the watcher. - purged-backtest-gate-no-edge — covering index for the backtest’s slowest query:
idx_pm_pos_condition_enddate ON pm_positions (condition_id, end_date)turnedfetch_market_end_datesfrom ~459s → ~8s (index-only scan, no heap access). Migration20260601120000_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 indexesCONCURRENTLY. - 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’swallet_count). WS2’s daily heavy-sync gate keys offmax(computed_at)inpm_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-leakage — schema gap blocking honest validation:
pm_market_resolutionshas onlyfetched_at(when we learned the outcome), no trueresolved_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 realresolved_atis remedy step 2. - copy-trade-lane —
pm_copy_config(singleton:enabledkill-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 aslane='copy'rows inpm_paper_trades. Applied manually via Supabase MCP becausepm-arbdoes not runsqlx migrate. - pmv2-topbets-categories-filters —
pmv2_event_categorycache table (event_slug → category bucket), populated byrun_category_syncfrom gamma/eventstags (best-effort, bounded per-chunktokio::timeout). Filters are inert until a scheduled run primes it. - pmv2-exit-alerts — migration
20260604000000_pmv2_event_pnl.sql: addedcash_pnl/initial_value/current_value/cur_price/outcome/titlecolumns topmv2_position_events(captured ininsert_position_eventfrom the priorPositionRow— no 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-design — three 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_idPK +enabled, seededfirst_mover_core), andpmv2_autotrade_orders(the order/position ledger:dedup_keywith 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_pendingflags,error_class). Terminal statesdry_run|closed|canceled|failed; reservable/non-terminalpending|submitted|filled|partial|open|selling. DB is the source of truth for accounting; reservations countpending|submitted|selling. The exit source uses a newpm_detection_cursorsrowautotrade_exits(NOT the shared exits cursor). - category-consensus — migration
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-fix — migration
20260608000000addspmv2_event_category.end_date(the gamma EVENT end date, returned byevent_tags, stored byupsert_event_category). The 3 cohort-holder queriesCOALESCE(p.end_date, ec.end_date)so positions whose marketendDateis the1970-01-01epoch placeholder fall back to the real event date. - pmv2-trending-bets —
pm_trending_alertsdedup table for the acceleration auto-alert (12h cooldown key). - polymarket-fetch —
pm_runs(now withselector/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-deleted — v1 tables
pm_selected_wallets/pm_safe_bets/pm_trader_metricsare now fully DORMANT in code (no reader remains afterec75491), 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-25 — 2026-06-25 ~20:19Z:
apps-pmv2-cohort-algo-firstmoverpin8977b5b→d851d44, ONLY thefirstmovercomponent (query-responder + scheduled-run untouched). On-chain feed migrated Alchemy WSS log-subscription →eth_getLogsHTTP polling via the 150-proxy rotating pool (boot logpolygon 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 thepmv2_autotrade_*family it now owns,4712de9); (2) ~25 deadpm_*DROPs via psql —BEGIN/COMMIT, transcript-first, PM confirmed zero refs; (3) reset_sqlx_migrationsto 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-conventions — FLEET 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:4317gRPC /4318HTTP; Dockerhost.docker.internal:4318), NEVER SigNoz Cloud directly. Containerized apps deployed via the ansibleappsrole 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_OUTBOUND1d,OPS7d,PMV2_ORDERS, legacyWEATHER_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 infow/ pmv2 admin seed sparingly. SigNoz dashboards: “polymarket-infra — Host & Tailscale” / ”— App services (APM)” / ”— NATS + JetStream”. - polygon-wss-provider-drpc — Polygon 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 logsstream → 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 neweth_getLogsbackfill calls also benefit. Swap = a SINGLE env varPOLYGON_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-consumer — NEW long-running service unit pending (2026-06-17, branch
autotrade-foundation, NOT deployed). The NATS consumer runs as a dedicatedpmv2-consumersubcommand process — NOT part ofscheduled-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)@deployto add the service unit + the env (NATS_URLtailnetnats.taild4189d.ts.net:4222+NATS_NKEY_SEED, pmv2 identity pubkeyUDYV…ZEDD, inPOLYMARKET_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 modeliveANDweather.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.ymlonpolymarket-infra:polymarket-fetch-scheduled-runjobschedule:"*:0/30"(scheduled-run --dedup-seconds 300 --gap-warn-seconds 3600) +polymarket-fetch-botservice;wallet-monitor+pm-arb-validatenow absent (decommissioned);weather-bet-run+ dailyweather-bet-resolvepresent. systemdType=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@deployon babylon. - bot-pmv2-native-v1-consensus-deleted — deploy target now
ec75491(Round 52b); 135 tests pass, clippy clean (3 pre-existing only), fmt clean. OPS INCIDENT (2026-06-03) — zombie bot container: twopolymarket-fetchbot containers ran concurrently on the GCP VM — one on new code (overlap_watcher cursor advancing) + one on OLD pre-4794a76code (participant_changescursor STILL advancing every minute, a detector DELETED in4794a76), spamming a v1 “watched wallets position dump”. Diagnosis viapm_detection_cursors: an old-code-only detector advancing = old container still live (same class as the Round-50 Telegram 409getUpdatestwo-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-watcher — GO-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_thresholdwhich defaults to 0 → ALL scored cohort traders qualify → very noisy. Setscore_threshold > 0(e.g. 0.03–0.05) before/after deploy. The paper-copy lane stays independentlyenabled=false. Loose end carried into prod: self-status CLI + bot/walletsstill render v1 consensus alignment from the frozenpm_selected_wallets(safe-but-stale, no runtime error) — the consensus chain was intentionally kept because deleting it breaks self-status. - pmv2-two-tier-trader-scoring — pmv2 hard-cut deploy (handed to @deploy, AGENT_HANDOFF.md Round 49, STATUS pending): deploy
5ffc330, decommissionapps-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=trueto start). Two manual prerequisites: migrations applied via Supabase MCP (pm-arb doesn’t runsqlx migrate), and new env varPOLYGON_WSS_URLin thePOLYMARKET_FETCH_ENVGH secret (until set, the lane idles even if enabled — logs a warning, doesn’t error). Handed to @deploy inAGENT_HANDOFF.mdRound 42. - polymarket-fetch-deploy — three units (
bot/scheduled-run/wallet-monitor) live onops-vmineurope-west3-asince 2026-05-27 11:08 UTC. Docker via Artifact Registry, pulled by the genericappsAnsible 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-22 — DURABLE 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 on0xd189664c(77.8% ROI / $9.4K PnL).
Fees / Cost Model
- edge-discovery-2026-06-22 — P0 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-drpc — RPC-provider cost model for the 24/7 Polygon WSS subscription (2026-06-20): Alchemy meters
eth_subscribe logsat 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-arb — 2026 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::spawnpanic RESOLVES the parent’s&mut JoinHandleselect arm (withJoinError) — it is NOT only a clean-return signal; this is what turned a rustlsCryptoProviderpanic into a silent zero-events crash-loop in PM’s self-watch feed (cratepmv2-onchain-watchpin719dc32; crate-side fix NOT committed, 2026-06-24). PM’s self-watch feed connected to DRPCogwsthen 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 rustlsClientConfig::builder()→ resolves the process-defaultCryptoProvider; rustls 0.23’sget_default_or_install_from_crate_features()only auto-picks when EXACTLY ONE ofring/aws_lc_rsis enabled, but PM’sCargo.lockcompiles rustls 0.23.40 with BOTHaws-lc-rs(viaquinn-proto/reqwest http3) ANDringand never callsinstall_default→ the firstwss://handshake panics. The panic unwinds (defaultpanic=unwind) killing only the spawned feed task →run_feed’s_ = &mut handle =>arm RESOLVES with aJoinError→ 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 alltracing). 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:7installringas the first line ofmain(); PM was missing it). FIX = make the crate robust: idempotentensure_crypto_provider()(viastd::sync::Once, install-if-absent) at the top ofconnect_once, +rustlssingle-provider (ring,std) in the crateCargo.toml. Durable lessons: (1) a panicked spawned task makes the parent’s&mut JoinHandleselect 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-rsboth compiled with none installed → panic on first handshake; check the CONSUMER’sCargo.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’smain()via an idempotent install-if-absent on the connect path rather than relying on every binary to callinstall_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). Inpmv2-onchain-watch’sfeed, 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 15sPING_INTERVALnever fired and liveness drifted to ~90s. FIX: a dedicatedtokio::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) theeth_getLogs/eth_block_numberreqwest client now sets BOTHtimeoutANDconnect_timeoutand isOnceLock-reused (the standing proxied-hang trap — see theconnect_timeoutgotcha below); (b) a failing booteth_block_numberused to silently seed the backfill watermark to0(disabling backfill until the first live fill) — now retries 3×+warns. Two durable review lessons: (1) verbatim module extraction carries dead weight — movingonchain.rs→crate brought TWO whole entry points (run_detectorblock-latency path + theSubCommandresubscribe path;run_log_feed_statichardwired a dummy cmd channel so its arm was dead) + unused helpers that the new single consumerrun_feednever 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 tracingconnect_onceshowed 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 intopmv2-onchain-watch, the newrun_feedrunner first advancedlast_seen_blockonly on a delivered (watchlist-matched) fill. Combined with the 2000-block backfill cap (max_backfill_blocks), a long quiet stretch containing only non-deliverable watchedOrderFilledevents 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_onceadvances the watermark to the scannedtoUNCONDITIONALLY after each successful scan, via aconst 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 cohortbackfill.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 overHashSet<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 becausetransaction_hashwas always""(crate fix re-pinned1b8a0f9→719dc32, babylon #430, 2026-06-24). PM’s self-wallet (Balu/Bandi) instant-alert watch persists a row inpm_self_trade_alertskeyed by(wallet, transaction_hash, outcome_index)so it won’t re-alert across restarts. But thepmv2-onchain-watchWatchedFillstruct carried notx_hash, forcing PM to settransaction_hash=""+outcome_index=0for EVERY fill — so after the FIRST fill of a wallet was recorded,is_alertedreturned 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, andnow()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 onWatchedFill(tx_hash: B256,log_index: u64,block_number: u64; already on the in-scopeOrderFilledEventatdecode.rs:53-64, plumbed throughdecode_watched_fill→run_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-chaintx_hash/log_index), expose it from the producer rather than fabricating a weak key downstream. - cohort-pre-live-signal-gating — the ported
ev_floor_gatewas DEGENERATE —size_usdcancels out, reducing a “real EV floor” to a price-blind constantbps>200(caught + DROPPED, 2026-06-23).edge = size_usd·(bps/10_000)vscost = size_usd·0.02→size_usddivides out → the gate is justbps > 200, completely ignoring the price. It was a placeholder shaped like an EV floor. FIX = remove it, don’t rebuild it: operators setmax_entry_premium_bps(the chase gate) net of round-trip cost — one price-AWARE knob.min_round_trip_edge_bpsis left loaded-but-unused (NOT deleted) because thepmv2_autotrade_configschema is FROZEN (PM-owned, babylon #333). Also:GET /bookreturns asks DESCENDING, so.min()is load-bearing for the best ask (first-element = worst ask). Also: an emptyevent_slugcan never resolve a category —resolve_categoryshort-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-gotcha — a 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_laneand 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 (onlyfirst_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-id — Phase 2 B1:
signal_idwas NOT byte-identical across producers due to two independent bugs insources.rs(2026-06-19, branchautotrade-phase2-firstmover). (1)cohort_signal_idpassedorigin_ref(wallet address),condition_id, andinstanceverbatim toorders::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_signalusedp.event_slug.unwrap_or(condition_id)asinstance—event_slugisOption<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 DBON CONFLICT (dedup_key) DO NOTHINGdedup, meaning the same logical position could result in TWO live orders. FIX: lowercase all three inputs before hashing + usecondition_iddirectly asinstance(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; usecondition_idas the canonical market discriminator, notevent_slugwhich 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 usedasync_nats::Client::publish_with_headers(...)+client.flush()(the core client). Core NATS publish returns NO JetStreamPubAckand does NOT error when no stream’s subject filter captures the subject — so ifcohort.signalswas never bound into theWEATHER_SIGNALSstream (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 context —async_nats::jetstream::new(client.clone())→publish_with_headers(...).await→PublishAckFuture→.awaitthe ack, under a 5stokio::time::timeout. Compiler-pinned against vendored async-nats 0.49.1: a missing/misfiltered subject yieldsNO_RESPONDERS → StreamNotFound= a loudErrthe 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 throughasync_nats::jetstream::Context::publish*and.awaitthePublishAckFuture(ideally under a timeout). Companion operator-visibility fixes in the same commit: an “armed-but-unconfigured” lane (cohort active but watcherNATS_URL/NATS_COHORT_NKEY_SEEDunset) now warns + telegrams instead of dropping entries atdebug(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-testedpublisher_intent(at_active, has_url, has_seed) -> {Dormant, Unconfigured, Ready}classifier drives the distinction. T5 addendum: the cohort-publisher NKey must permit JetStream publish tocohort.signals(publish + receive PubAck on its inbox), not merely core publish. Gate green 429 polymarket-fetch + 89 polymarket-data, trade-safety reviewed SAFE; branchautotrade-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-tickreconcile_entries. It is safe TODAY only by coincidence (cohort lane dormant / global modeoff, no live orders exist,/autotrade reconcilestill 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: thecohort_roundtrip_paritytest (serialize ∘ parsebyte-equal to the legacy emitter, withevent_slug≠slugfixtures) is how a transport swap is proven not to change engine inputs. - autotrade-resolution-proposed-entry-guard — a 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 entryprice_bounds_gatechecked the COPIED TRADER’s frozenreference_price(~0.44, in-band), not the livebest_ask(0.001), and (2)liveness_gateonly rejected FORMALLY resolved/closed/non-accepting markets — a UMA-proposedone passes. Fix = two complementary ENTRY-ONLY guards: an ask-band gate (engine.rs::plan_context, reusesprice_bounds_gateon 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_proposedreadsumaResolutionStatuses→MarketStatusView.resolution_proposed, FAIL-OPEN with the ask-band as backstop →liveness_gateskip). TWO durable gotchas: (a)umaResolutionStatusesis a JSON-ENCODED STRING ("[]"/"[\"proposed\"]"), NOT a native JSON array — same encoding asoutcomes/outcomePrices; parse viaparse_json_string_array— aVec<String>deserialize silently fails. (b) in the vendoredpolymarket-clob,MarketStatus.resolvedis setresolved: m.closed— a MISNOMER (just a copy ofclosed), so the CLOB meta carries NO real UMA-resolution signal; only Gamma does (same family as theis_terminal_market = resolved||closedaliasing 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-closedstate that only Gamma’sumaResolutionStatusesexposes — the CLOB cannot tell you. Keep the guard entry-only so an already-held position stays exitable. Gate green 503 workspace tests; branchautotrade-foundation, NOT deployed. - autotrade-exit-cursor-404-wedge-fix — a RESOLVED/CLOSED Polymarket market returns CLOB
/book404"No orderbook exists for the requested token id"PERMANENTLY — so a cursor-driven loop that treats the first/bookfailure 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 andresume_cursorparked theautotrade_exitscursor atfirst_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 emptyBookTopforresolved||closed(never callsbook_top→ no 404); pureexit_decision_for(market)returnsExitDecision::{Skip (terminal),Exit (open+bid),Retry (open+no-bid or transient resolve error)}withadvances_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:251ClobClient::status_from) BOTHMarketStatus.resolvedAND.closedare mapped from the single/marketsfieldm.closed— sois_terminal_market = resolved||closedeffectively reduces to “closed” today; if a future change populatesresolvedindependently, 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 (VMbf4b628) — 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-v2Amount::shares(value)rejects any share amount whosevalue.normalize().scale() > LOT_SIZE_SCALE (=2)(types/mod.rs:193-200, re-appliedorder_builder.rs:25).auto_sellpassed the RAW on-chain CTF fill (full precision — locked test vector 7.6219 shares, scale 4) straight intobuild_order → Amount::shares→ build error →auto_sellreverted the row toopen→ EVERY 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 ownassert_sell_amountsbut not the vendored lot-size guard one layer deeper, so the assertion passed while the constructor it fed rejected. FIX: quantize once —sell_shares = filled_size.trunc_with_scale(2)— and use that single value forbuild_order+quantize_sell(expected_shares)+assert_sell_amounts; sub-0.01-share dust is held to resolution (return beforemark_selling, so it never entersselling). BUY is unaffected (Amount::usdcallows scale ≤ 6; the market-order builder has no lot gate). Durable lesson: a Polymarket SELL size must betrunc_with_scale(2)(0.01 lot); the full-precision on-chainbalanceOffill 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 viaArc::try_unwrapwhile the notify closure still held a 2nd Arc clone → always empty →/autotrade sellswallowed its SOLD/REJECTED/TIMEOUT reply (visibility only, CI green because nothing asserted output); fixed tolock()-drain + a regression test (0f06ec5). - autotrade-max-copies-per-event —
max_copies_per_eventwas wired (DB load, validation, conversion) but never consulted at placement (2026-06-16). The cap existed inAutotradeConfigand was mapped byengine_config(), but neitherEngineConfignorReserveCapscarried the field, and no query checked in-flight event counts before INSERT. Zero-means-unlimited (distinct frommax_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-ops —
SignedOrderhas noDeserialize— serde round-trip viaserde_json::from_value::<SignedOrder>(...)does not compile (2026-06-15). The vendored type derives onlyDebug + Builder + PartialEq. Do NOT attempt to serialize/deserializeSignedOrder. Fix: hold the typed value directly inBuiltOrder.signed_order: SignedOrderand drop theSignedPayload(Value)wrapper. Also:TradeResponsecarries NOorder_hash—TradeView.order_hashis alwaysNone; correlate trades to an order viataker_order_id == clob_order_id, not the EIP-712 hash.PostOrderResponse.order_idmaps from JSON"orderID"(camelCase alias in serde). - pmv2-bet-link-analyzer-flow — four cohort/category gotchas + category-cohort fallback (shipped
22d7697, verifiedmain2026-06-15): (1) the/consensuscache only warms onstatus='done'—recent_category_consensusselectsWHERE status='done'(repo.rs:1601) and a run is only readable aftercomplete_category_run(..,'done')(repo.rs:1542); computing + inserting rows without completing the run leaves it'running'and INVISIBLE (the next/consensusrecomputes from scratch). (2) no scheduler warms that cache —schedule.rs/scheduled_run.rshave no category-consensus job, so it is usually cold and the first/consensusper 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 (testscoring.rs:993-995);gamma.event_tags()(client.rs:505) takes EVENT slugs → deriving an event’s category fromparse_selectoroutput silently fails on deep links — preferBetScore.event_slug(DB truth) or the segment after/event/. (4) TWO empty-states in the bet reply mean different things —scores.is_empty()(nobody holds it) vs all-sides-below-GAUGE_FLOOR=0.005(held but no high-edge side);consolidate_gaugeis empty-safe so the latter renders cleanly, not a panic. Durable lesson: the edge-scored bet-link cohort and the monthly-PNL/consensuscohort are different populations on different category taxonomies — don’t cross-feed slugs or buckets without thecategory_tags/GEO_TAGS/CRYPTO_TAGSbridge. - autotrade-eip712-order-hash —
#[non_exhaustive]on sol!-generatedOrderV2blocks FRU from external crates (2026-06-15). BothOrderV2 { field, .. }andOrderV2 { 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::Eip712Domainis the correct type for EIP-712 work —alloy::dyn_abi::Eip712Domainis a different internal type; mixing them causes type-mismatch errors.missing_const_for_fnfires on any function returning only'static strliterals — addconst.expect_used/unwrap_usedare forbidden workspace-wide; useLazyLockstatics with a tight#[allow(clippy::unwrap_used)]per static for hardcoded-constant parsing. - topbets-enddate-1970-fix —
/topbetsshowedends 1970-01-01(2026-06-08). ROOT CAUSE: the data-api positions feed returns the epoch placeholder1970-01-01as the marketendDatefor markets whose end date lives on the gamma EVENT (market-levelendDateis null → serialized as epoch). FIX: capture the gamma eventend_dateintopmv2_event_category.end_date(migration20260608000000);normalize_end_dateinmap_positiondrops empty/pre-2000 → NULL; cohort-holder queriesCOALESCE(p.end_date, ec.end_date);/topbetsshowsends {date} (unresolved)for past-dated-but-held. Verified: kansas-governor→2026-11-03, openai-ipo→2026-12-31. Durable lesson: a Polymarket positionendDateof1970-01-01is 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 —
/walletsshowed 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 wasredeemableOR 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 withis_position_redeemed(position)=position.redeemableONLY; both the storedupsert_positions_for_walletand the live/walletsfetch_self_overlap_sections_livenow 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 isredeemable=false, NOT “before end-date” — a past end-date does not mean the position closed. - category-leaderboard-api — FALSE ALARM: the UPPERCASE
categoryquery param is NOT a “lowercase bug” (2026-06-05). A reviewer flaggedpolymarket_data’sCategory::as_query_value()renderingOVERALL/WEATHER(uppercase) as a bug. The data-apicategoryparam is case-insensitive (category=WEATHERandOVERALLboth work;timePeriod=month/MONTHboth work), and prod already runs with uppercaseOVERALLsuccessfully. 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-commits — codebase-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,…)notrank_all); (2) filterredeemable=falsewhen aggregating holdings or resolved dust shows as$0“held”; (3)consolidate_gaugemust group by(event_merge_key, outcome_index)notoutcome_indexalone, 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_winnersrandom winner on cancelled/0-payout markets corrupting SCORING → requirepayout>0+deterministic tiebreak (SHARED winner path, see pmv2-pipeline-bug-audit); leaderboard names unescaped HTML (Telegram<>&parse fail);clamp_to_telegram_limitbyte-vs-char; pmv2 collect/positions made non-fatal inexecute_pipeline(transient API error was aborting the whole scheduled run); non-pmv2flatten_resolution_rowsmissing dedup (#371-class crash). Scoring-math (operator-approved, shifts ranking):conviction()default0.5→0.0;consistency_factorreturns 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 prunedelete_positions_not_in_walletsDOUBLE-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_resolvedidempotent; 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-deleted — zombie deploy container signature (2026-06-03): two bot containers ran at once on the VM (new + old pre-
4794a76code); the OLD one’sparticipant_changescursor — a detector DELETED in4794a76— was STILL advancing inpm_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 409getUpdatestwo-poller-one-token conflict. After any bot deploy: confirm exactly ONE container runs AND that old-code-only detector cursors FREEZE. - first-mover-overlap-watcher — Postgres auto-generated constraint names can be TRUNCATED (2026-06-02). The overlap migration’s
DROP CONSTRAINThad 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 — querypg_constraintfor 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-audit — any chunked
ON CONFLICT (...) DO UPDATEupsert 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 chunkedON CONFLICT (condition_id, outcome_index) DO UPDATE— a duplicate key in one chunk crashes withON CONFLICT DO UPDATE command cannot affect row a second time, the exact crash that killed scheduled-run #371 (positions table). Fixed withdedup_resolution_rows(keep-last, order-preserving — behavior-identical to what DO UPDATE converges to). Durable lesson:INSERT ... ON CONFLICT DO UPDATEcan’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-scoring — don’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-2026to...by-may-3— a different, wrong identifier. Durable lesson: match bet selectors on the full slug /condition_id, never on aleft()/terminal-width-truncated display render. - pmv2-two-tier-trader-scoring — proxied reqwest clients need
connect_timeout, not justtimeout(2026-06-02).pmv2-positionshung ~75 min silently at--concurrency 8(zero rotation/retry/eviction warns, no completion) because a proxy accepting the TCP connection then stalling the HTTPSCONNECTtunnel hangs past the total.timeout(30s)— endpoint/proxies/data were all fine. Fix (defense-in-depth):.connect_timeout(10s)on the sharedbuild_http_client(DataApiClient+GammaClient) andbuild_proxy_client, a per-wallettokio::time::timeout(90s)infetch_positions, and default--concurrency8→4. No deterministic repro, so the two timeouts are belt-and-braces. Durable lesson: always set BOTHtimeoutandconnect_timeouton 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-probe — capture tests are flaky under parallel workspace load:
load_grids_buckets_events_onto_gridandload_grids_skips_blank_and_malformed_lines(crates/lag-probe/src/capture.rs) PASS in isolation but FAIL under full-workspacecargo test— the temp file is namedprocess_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 thetempfilecrate / unique dirs. Durable lesson: don’t rely onpid+nanosfor temp-file uniqueness under a parallel test runner — usetempfile. - phase1-consensus-rebuild-implemented — consensus
wallet_countcould be inflated by opposite-asset positions: keying buckets only on(condition_id, outcome_index)let negative-risk / opposite-asset holdings aggregate with longs. Fix = addassetto 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_dateparse fails open — unparseable kept +warn!— to avoid silently shrinking consensus on a formatting quirk.) - consensus-validation-leakage — three lookahead/survivorship leaks silently inflating the edge evidence (verified by reading source 2026-06-01): (1)
crates/polymarket-data/src/consensus.rs:30compute_consensus_groupsuses LIVE position fields (current_value/avg_price) → post-entry + survivorship, not as-of-trade; (2)crates/polymarket-fetch/src/backtest.rsbuild_wallet_data+crates/polymarket-data/src/metrics.rs:132pair_trades_to_resolvedpair trades to resolution payouts across the train/test cutoff → label leakage; (3)crates/polymarket-fetch/src/repo.rs:1429fetch_short_term_bet_rowshas NO time filter → survivorship + lookahead. Durable lesson: build features point-in-time frompm_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_msalways 0 becausecopy_lane.rstruncated the ms-precise upstreamDetection.latencyto whole seconds (block_ts) andcopy.rsrecomputed from those — sub-second latency floored to 0 (0 not NULL =block_timewas populated). Lesson: carry a precisely-computed upstream value through; don’t truncate-then-recompute downstream. (2) negativechase_spreadbecause the entry ask fell back to the MID. Durable no-arbitrage rule: on a binary up/down marketask_up = 1 − bid_downandask_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-arbdoes NOT runsqlx migrate(onlypolymarket-fetchdoes) — copy migrations had to be applied manually via Supabase MCP, and any future copy-schema change must too; (2) realOrderFilledtopic0 is0xd543adfd…, NOT the canonical-ABI0xd0a08e8c— filtering on the canonical hash returns zero logs; (3) missingPOLYGON_WSS_URLmakes 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 onlytry_longshot;try_momentumruns 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 testmomentum_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’spm_self_trade_alertsfeature was silently broken in prod for hours from this; (2)main.rs:2376-2380swallows sqlxMigrateErrorso failures degrade silently — task#15to fix (log at ERROR + non-zero exit so systemd restarts); (3) Supabase MCP direct prod surgery (CREATE TABLE IF NOT EXISTS + insert into_sqlx_migrationswith 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_pnlnumeric 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),
.dockerignorekillingsqlx::migrate!()(commit834bc66), gcloud GPG key.ascextension, Tailscale name collision after VM reprovision. - recovering-claude-session-400-on-resume — Claude Code session 400s on
--resumefrom a 26 MB / 14k-message transcript with unbalanced tool blocks; recover viaAGENT_HANDOFF.md+historianin 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-gating — startup-announcement dedup via a version-stamped
Nats-Msg-Id(e444af0, 2026-06-23): the boot announcement totelegram.outboundsetsNats-Msg-Id = startup|first_mover_core|<build-hash>, so the stream’s 5-minduplicate_windowswallows 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-cutover — first producer to emit on the v2
pmv2.order.<source>.<entry|exit>subjects (0986c4d, 2026-06-23): cohort publishes schema_version 2 onpmv2.order.first_mover_core.entry+…exit(source_id="first_mover_core"), replacing the now-DELETEDcohort.signalsschema_v1 lane (signals/wire.rs+signals/signal.rsremoved). Types come from the vendored sharedpmv2-contractscrate (single source of truth — don’t hand-roll). The executor (position_manager) consumes these directly. (sourceMUST be the EXACTsource_idkey or it silently routes toMode::Off; the exit’sorigin_refis load-bearing or the position can’t be auto-closed.) - pmv2-fleet-infra-observability-nats-conventions — FLEET NATS/JetStream CONVENTIONS (@deploy, babylon pmv2 #313, 2026-06-22). Always set
Nats-Msg-Idon publish (in-stream dedup withinduplicate_window, currently 5 min on all 3 streams). Streams (all @deploy-provisioned — producers NEVER create streams):TELEGRAM_OUTBOUND(subjecttelegram.outbound, 1d),OPS(ops.commands+ops.results, 7d — the control plane needs audit),PMV2_ORDERS(pmv2.order.<source>.<entry|exit>, pending PM consumer), legacyWEATHER_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 vianats stream info/nats consumer infowith the pmv2 admin seed (sparingly). NKey identity is logged as PUBKEY, never the seed. - telegram-connector-outbound-design —
telegram_connectoris a durable JetStream PULL consumer (tg_outbound) ontelegram.outbound/streamTELEGRAM_OUTBOUND(2026-06-22). Dedup is the STREAM’s job (Nats-Msg-Id= the envelopemsg_idwithin the 5-minduplicate_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 bymax_deliver, exhaustion → dead-letter + ERROR. Connects via nkey auth. Concretely applies the fleet conventions in pmv2-fleet-infra-observability-nats-conventions. - cohort-firstmover-nats-migration — async-nats CORE publish is FIRE-AND-FORGET (no PubAck, no error on a missing/misfiltered subject) — publish through the JetStream
Contextand.awaitthePublishAckFuture(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 tocohort.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-consumer — the dedicated
pmv2-consumerlong-running process (NOT part ofscheduled-run, which is a one-shot per-tick batch) consuming the unified signal lane, routed by the envelopesourcefield; dormant-safe (exits 0 whenNATS_URL/NATS_NKEY_SEEDunset) (2026-06-17, branchautotrade-foundation). The consumer half that cohort-firstmover-nats-migration publishes into. Tailnet NATS hostnats.taild4189d.ts.net:4222, pmv2 identity pubkeyUDYV…ZEDD. - query-commands-restoration — the
query.request/query.resultsrequest/reply channel (cohort’s first INBOUND NATS surface, 2026-06-24): connector publishesQueryRequest{request_id,actor,kind}onquery.request; cohort’squery-responderruns the ported pipeline and JetStream-publishesQueryReply{request_id,rendered}onquery.resultswithNats-Msg-Id={request_id}-result. Carries/topbets,/consensus,/filters. - bet-link-inspector-restored-pmv2 — additive
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-gating — cohort_algo now publishes a STARTUP ANNOUNCEMENT on watcher boot (operator directive #354,
e444af0, 2026-06-23): onetelegram.outboundmessagecomponent: first_mover_core started with version: <hash> <title>— version captured at BUILD time viabuild.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 streamduplicate_windowswallows a flapping restart on the same build; a new build → new hash → fresh announcement). Rides theNotifier::Outboundplane the v2 cutover flipped on → relayed by telegram_connector. - cohort-algo-v2-wire-contract-cutover — cohort_algo FLIPPED its Telegram routing to the outbound NATS plane (
0986c4d, 2026-06-23):Notifier::for_lanenow constructs theOutboundvariant (publishesTelegramOutboundtotelegram.outbound, consumed by telegram_connector) when NATS is configured; Direct-send is fallback only (NATS absent). This completes thetelegram.outboundseam that was prepared-but-Direct-defaulted in cohort-algo-component. - telegram-connector-outbound-design — the 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 publishesTelegramOutbound{msg_id,chat?,text,parse_mode?,kind?}to NATStelegram.outboundinstead of calling the Bot API; the connector durably consumes (tg_outboundon streamTELEGRAM_OUTBOUND) and relays tosendMessage. A DUMB faithful relay —text+parse_modepassed verbatim, NEVER re-escaped (escaping is the publisher’s job, e.g. cohort_algo’snotifier). At-least-once, ack-AFTER-send; transient (429+retry_after/5xx/net) → retry then JetStream redeliver bounded bymax_deliver; terminal (400 bad-format / 403 blocked) → drop+ack+ERROR + a best-effort loop-guarded plain-text meta-alert toTELEGRAM_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; inboundops.*kill-switch relay is a later spec gated on @positionmanager (#311). Supersedes the per-service fire-and-forget Telegram clients (e.g. polymarket-fetchtelegram.rs/notifier.rs) as the fleet consolidates onto the spine. - cohort-signals-service-split-and-exit-signals — cohort EXIT now sends a Telegram alert alongside the
action:Sellpublish (2026-06-22,e762f5d). Previouslyon_cohort_sellneither alerted nor published — it only logged anEXITEDrow + 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 sharedpublish_cohort_with_retrycarries a drop-alert on give-up so an exhausted exit publish is never silent. - pmv2-bet-link-analyzer-flow — the bet-link AUTO-REPLY (no command — fires on any posted Polymarket URL via
bot.rs::classify_message→MessageAction::BetLink): replies with a per-side skill-weighted cohort gauge built frompmv2_positions⋈pmv2_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 belowGAUGE_FLOOR=0.005,scoring.rs:472). Contrast with the explicit/consensus <category>command (a different cohort entirely). Category-cohort fallback shipped22d7697: when no signal, bot appends a category-consensus section to the SAME reply (25s-bounded, cache-first). - bet-link-inspector-restored-pmv2 — the 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}→ cohortrender_bet_inspect(score_bet(url)+ gauge;!has_high_edge_signal→resolve_fallback_category+category_consensus_section) →QueryReply{rendered}→ connector posts HTML. 1:1 verbatim port of the monolith pmv2-bet-link-analyzer-flow; pinspmv2-contractsc6b3a15+cohort_algo1d573b8. Connector non-slash arm pending (@pmv2_telegram, babylon #517). - correctness-pass-six-fix-commits — two 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); andclamp_to_telegram_limithad 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-bets — acceleration 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_eventsEXITEDfeed). 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 unrealizedcash_pnl, honestly labeled(last seen). Top-trader gated, cursor-deduped (pmv2_exit_alert), dust-filtered (≥$50), Telegram-budget capped. - pmv2-bot-startup-ping — bot boot ping (2026-06-04): on
polymarket-fetch botboot,notifier::startup_messagesends a best-effortNA CSUMI CSUMI CSUMI, bot STARTED, REF: <short-sha>(theREF:segment is omitted when no hash). The short SHA is baked at compile time bycrates/polymarket-fetch/build.rs(envBUILD_GIT_HASH→GIT_HASH→GITHUB_SHA→git rev-parse --short HEAD→empty).build_startup_messageis the pure builder. Bot-only. Deploy gotcha: a Docker image built without.gitand without one of those build-args reports NO sha — passGIT_HASH/GITHUB_SHAat image build to populate REF. - polymarket-fetch —
telegram.rs+notifier.rsfire-and-forget, lifecycle/milestone/error notifications,--silentglobal, BotFather setup flow, rich per-wallet messages - pmv2-autotrade-engine-design — owner-only
/autotradecommand surface + a reusableauthz.rsseam (design).Scope::{Public,Owner}+OwnerSet+authorize()/scope_for(); owner ids fromTELEGRAM_OWNER_IDSintoBotConfig.owner_ids(mirrorsallowed_chats, which is preserved — owner gate is additive); authorize once at the sharedrun_commandchokepoint (message + callback both converge there),Denied → ⛔ not authorized. Commands (allScope::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 asell:<order_id>inline-button callback (owner-revalidated, re-loads under lock). Subcommand routing is hand-written (parse_commandonly splits the leading token).
Observability / Telemetry
- pmv2-fleet-infra-observability-nats-conventions — FLEET 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 gRPC127.0.0.1:4317, HTTP127.0.0.1:4318; from Dockerhttp://host.docker.internal:4318. Std envOTEL_SERVICE_NAME/OTEL_EXPORTER_OTLP_ENDPOINT/OTEL_EXPORTER_OTLP_PROTOCOL/OTEL_RESOURCE_ATTRIBUTES=service.namespace=pmv2,deployment.environment=prod(auto-injected for ansibleapps-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 behindRUST_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-design —
telegram_connectortelemetry init follows the fleet contract (2026-06-22): onetelemetrymodule call frommainwirestracing+tracing-opentelemetry+opentelemetry-otlpto 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 behindRUST_LOG; NEVER logs the bot token / nkey seed. See pmv2-fleet-infra-observability-nats-conventions. - cohort-firstmover-lane-mode-restart-gotcha — GAP: the firstmover service (
pmv2-cohort-algo-firstmover, nsapps, prod hostpolymarket-infra.taild4189d.ts.net) emits NO startup INFO line announcing its effective lane mode (off/dry/live) — onlyfirst_mover_core armed(targetpmv2_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: thecohort publisher dormant (lane inactive)log is DEBUG (usually un-exported), so an inactive lane is effectively invisible in SigNoz. (2026-06-25) - tracked-alerts-consumer — end-to-end signal latency confirmed SUB-SECOND via SigNoz (the
pmv2-tracked-detector→tracked.signals→pmv2-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-crashloop — the WSS feed itself can crash-loop with ZERO events while backfill keeps working — a rustls
CryptoProviderpanic on the FIRSTwss://handshake in PM’s self-watch (cratepmv2-onchain-watchsrc/feed.rs, pin719dc32, 2026-06-24). The on-chain feed connects to DRPCogwsthen 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 BOTHring+aws-lc-rscompiled with none installed → panic on the first handshake → the spawned feed task dies →run_feed’s&mut JoinHandlearm resolves (JoinError) → respawn. Theeth_getLogsbackfill rides a separate reqwest path and is unaffected — so “backfill fine, live WSS dead” is the diagnostic split. Crate-side fix = idempotentensure_crypto_provider()on the connect path + single-providerrustls; same class as the cohort_algo/telegram_connectorinstall_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-extraction — the on-chain
OrderFilleddecoder + the resilient WSS feed are now a STANDALONE reusable cratepmv2-onchain-watch(extracted from cohort, canonical pin1b8a0f9after a 2026-06-23 review-pass cleanup, was5142d98, 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 + theSubCommandresubscribe path were unreachable fromrun_feed) + DRY (topic_hex,watchlist_topic_slots) + 3 robustness fixes (proxiedconnect_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 cohort0fdfe8a(295 tests green).decode(moved out ofpolymarket-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 viaSeenLogs.feedexposesrun_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 awatch::Receiver+ anmpscsink); cohort + PM-self-wallet are two callers of one feed. The cohortwatch/backfill.rs+polymarket-dataonchain primitives in watcher-onchain-gap-backfill are now historical for cohort (substrate moved to the crate). - pmv2-onchain-watch-crate-extraction —
WatchedFillnow carries the on-chain IDENTITY TRIPLEtx_hash/log_index/block_number(crate re-pinned1b8a0f9→719dc32, cohort re-vendored0fdfe8a→4a3f8b2, babylon #430, 2026-06-24). Added to fix a permanent one-alert-per-wallet suppression in PM’s self-wallet (Balu/Bandi) watch:WatchedFillhad notx_hash, so PM was forced to settransaction_hash=""+outcome_index=0for every fill, collapsing its(wallet,tx,outcome)dedup inpm_self_trade_alertsto per-wallet (see Gotchas). The fields were already on theOrderFilledEventin scope (decode.rs:53-64), sodecode_watched_filljust plumbs them through andrun_feedemits them with no extra change; regression testwatched_fill_carries_event_identity_fields. PM mapstransaction_hash=format!("{:#x}",fill.tx_hash)+outcome_index=fill.log_index. The cohort consumer needed NO change (it usesdecode_watched_filland has zeroWatchedFill { … }literals — additive fields are call-site-safe). DEFERRED out of the lean crate:block_time(block→timestamp RPC; derive fromblock_number) + a TRUEcondition_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-signals — the cohort on-chain detection substrate is now a SELF-CONTAINED signal producer (2026-06-22, branch
worktree-cohort-signals,8cd6f58). Thepmv2-watchbinary 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-chainConsensusTrackerand theOrderFilledcohort detection feed stay; what they DROVE in-process (fills→reconcile→settle, self-trade alerts, theonchain_booktriggers) is deleted — detection now emitsTradeSignals to NATS instead of triggering execution locally. - watcher-onchain-gap-backfill — the WSS
eth_subscribe logsfeed NEVER replays logs emitted during a disconnect —run_log_feedreconnects with backoff but only resubscribes for NEW logs, so cohort/self/our-fillOrderFilledlogs in the gap are lost permanently (2026-06-20,main43023ba). Fix = a 30s periodiceth_getLogsreconciliation sweep over[last_seen_block, latest]that re-fetches the cohort (both maker+taker topic slots, mirroringbuild_logs_payload) and feeds each recovered log through the IDENTICAL worker path as aWatchEvent. 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 atON CONFLICT(dedup_key), self-trade alerts dedup by(wallet,tx,outcome), our-fill reconcile shares the worker’sseenset keyed by(tx_hash,log_index)— live/backfill overlap is harmless, structurally equivalent to the existing first-mover match-cron backstop. Durable pattern: aneth_subscribe(or any push-subscription) feed needs a periodiceth_getLogsreconciliation 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 primitiveseth_block_number/eth_get_order_filled_logs/cohort_address_topics/build_get_logs_filter(modeled oneth_call_u128); watermarkArc<AtomicU64>viafetch_max;fromINCLUSIVE (intra-block partial-delivery recovery);MAX_BACKFILL_BLOCKS=2000;try_sendnon-blocking; reuses HTTPPOLYGON_RPC_URL. - tracked-alerts-consumer — the on-chain detector substrate now drives a LIVE Telegram alert lane (Phase 2):
pmv2-tracked-detectorpublishes per-filltracked.signals→ consumerpmv2-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 (current0f71f89). See the Notifications/Telegram topic for the full filter journey. - copy-trade-lane — Polygon on-chain
OrderFilledlog 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: realOrderFilledtopic0 is0xd543adfd…(NOT canonical-ABI0xd0a08e8c); cohort settles on CTF Exchange V20xE111180000d2663C0091e4f400237545B87B996B; on-chain maker/taker address ==pm_trades.proxy_walletfor 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 gammaclobTokenIds. Detector keeps anewHeadsblock-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) inonchain.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 thenewHeadsblock-time cache works. - pmv2-autotrade-engine-design — on-chain reads are the ground truth for the live trade lane (design).
pmv2/autotrade/onchain.rsdoes Polygon RPC reads — CTFbalanceOf(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 CLOBexpected_order_hash→clob_order_idcorrelation 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-gating — the read-only
ClobReadClient(re-added topolymarket-data2026-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 /bookreturns 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_statusviaGET /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’sresolvedis justclosedre-aliased — see autotrade-resolution-proposed-entry-guard), so the liveness gate fetchesumaResolutionStatusesfrom gamma separately. - 2026-06-20-fastest-polymarket-trade-submission — CLOB 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 dropstaker/expiration/nonce/feeRateBps, addstimestamp/metadata/builder; collateral is pUSD (ERC-20 backed by USDC, wrap USDC.e→pUSD via Collateral Onramp0x93070a847efEf7F70739046A929D47a521F5B8ee), NOT USDC.e; V2 exchange addrs (re-verify byte-for-byte vsctf-exchange-v2): Standard CTF0xE111180000d2663C0091e4f400237545B87B996B, Neg Risk CTF0xe2222d279d744050d28e00520010520000310F59;neg_riskis 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 carriesfeeRateBps). The CLOB WebSocketmarketchannel (wss://ws-subscriptions-clob.polymarket.com/ws/market, subscribe byassets_ids) streams abooksnapshot +price_change/tick_size_changedeltas — read-only, so you can mirror top-of-book locally but placement stays REST.tick_sizechanges 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 /ordersbatch max 15/request; rate limitsPOST /order5,000/10s burst. Polymarket origin region is undocumented (Cloudflare anycast; community-inferred AWS Londoneu-west-2, NOT us-east-1). - autotrade-resolution-proposed-entry-guard — a Gamma market record has a pre-
closed“result is in” state:umaResolutionStatuses=["proposed"]whileclosed=false+acceptingOrders=true(andoutcomePricesalready 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.umaResolutionStatusesis returned as a JSON-ENCODED STRING ("[]"/"[\"proposed\"]"), NOT a native JSON array — same stringified encoding asoutcomes/outcomePrices; parse withparse_json_string_array, never a plainVec<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 here —polymarket-clob’sMarketStatus.resolvedis justm.closedre-aliased, so resolution-proposed-ness is a Gamma-only fact. - category-leaderboard-api — data-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 (+OVERALLglobal); arbitrary gamma tags (iran/elections/geopolitics) return empty/global.categoryandtimePeriodare case-insensitive (WEATHER/OVERALL,MONTHall work).timePeriod∈{day,week,month,all},orderBy∈{PNL,VOL}. The 9 leaderboard categories are a separate namespace from gammaevent_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_dataCategoryenum already has all 9 +Overall;as_query_value()sends UPPERCASE (works — see the false-alarm under Gotchas). - topbets-enddate-1970-fix — a position
endDateof1970-01-01is an EPOCH PLACEHOLDER: the data-api positions feed returns it when the market-levelendDateis null and the real end date lives on the parent gamma event. Get the real date from gamma/events(event_tagsend_date), and normalize the1970-01-01placeholder to NULL before display. - pmv2-bet-link-analyzer-flow —
gamma.event_tags()(polymarket-data/src/client.rs:505) takes EVENT slugs, butparse_selector(scoring.rs:91) hands you the MARKET slug on deep links — for/event/<event>/<market>URLsparse_selectorreturns the last path segment (the market), which is NOT a valid event slug, so categorizing offparse_selectoroutput silently returns no tags (testscoring.rs:993-995). Extract the segment after/event/or use the DB-truthBetScore.event_slug. Also captures the two coexisting category namespaces (9 leaderboard categoriescategory_consensus.rs:13vs the coarsepmv2_event_category.categorybucket esport/sport/geo/crypto/other) and their bridge (category_tags+GEO_TAGS/CRYPTO_TAGS). - pmv2-gamma-tags-categorization — bet categorization comes ONLY from gamma
/events?slug=<event_slug>(atagsarray of{label, slug}); the/marketsendpoint returnscategory=null+ no tags and is useless for it (verified live 2026-06-03). Multi-slug batching?slug=a&slug=bworks. GOTCHA: esports events are ALSO taggedsports→ 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,
volvsvolume, stringifiedrank, OVERALL category sports bias, trades endpointoffsethard-capped at 3000 - pmv2-two-tier-trader-scoring — the 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-pagesto 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-pagesto lift the per-wallet ceiling. - cross-venue-pm-kalshi-arb — Kalshi 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
filtersmust 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 pagingclosed=false
Reverse-Engineering Top Traders
- category-leaderboard-api — per-category top-trader discovery: the data-api
/v1/leaderboard?category=<one of 9>&orderBy=PNLlets 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-wins — the cohort’s exploitable edge is EARLINESS, not agreement. A purged
--xrefbacktest (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-strategy — 2026-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);0x75cc3bis the sell-rotating outlier (28.1% SELLs). Universal cohort sweet spot is 12–16¢ (not 8–9¢ as previously documented). Three candidate tunes from a0xb27bc932deep-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:0x75cc3bwas framed as “the top trader” but they are actually wallet #2 by 30-day ROI in our band+window;0xb27bc932is #1. RE captured real edge, but specific parameters were biased to0x75cc3b’s personal pattern. - swisstony-hft-sports-market-maker — profile of
swisstony(proxy0x204f72…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-implemented — WS2 daily heavy-sync gate. The 30-min scheduler (
scheduled_run.rs, systemd timerOnCalendar=*: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 readsmax(computed_at)frompm_short_term_trader_accuracyrather 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-minconsensus_refreshpath 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-flow —
parse_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 (testscoring.rs:993-995). Fine forscore_bet/load_bet_holders(they resolve to the event’s sibling markets downstream), but a TRAP if you feed it straight togamma.event_tags()(which wants event slugs). - polymarket-fetch — TopN, Manual, MinTenure wrapper, ConsistentlyProfitable +
scoreJSON shapes,--require-windows 1..3loosening - 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 consultspm_short_term_trader_accuracy. Proposed: an edge-based selector (n_markets>=40,edge_lower>0, optionaledge_zfloor).
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-22 — the 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 inpm_copy_wallets(seeded with the 5 cohort wallets). Resolver applies the PM crypto taker feeshares×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, commita2de509),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 inmain.rs`.
Trades Pipeline
- polymarket-fetch —
pm_tradestable,trades-syncCLI with--all-watched/--since/--concurrency, per-wallet incremental resume, 3000-offset cap handling. Live: 53k trades / 24 wallets.
TODO / Backlog
- phase1-consensus-rebuild-implemented — commit + 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 bothconsensus.rsandrepo.rsintopolymarket-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-runis the systemd-timer unit that picks up the WS2 gate. - copytrade-paper-lane-latency-spread-fixes — commit + deploy the two copy-lane fixes (still in the working tree). Post-deploy: confirm
detect_latency_msshows 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-vmnode, mergespec2a-app-deployto 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:
#3forward-validate longshot tunes (in progress);#9bound scheduled-run’spm_trades WHERE proxy_wallet=$1query (24k–112k rows/cycle/wallet);#13trend-filter backtest (waiting on 2–3 days of mid-history data);#15stop swallowing sqlxMigrateErrorinmain.rs:2376-2380;#16cohort-validate the 30xb27bc932candidate 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_URLand the kill-switch is flipped (UPDATE pm_copy_config SET enabled=true), confirmlane='copy'rows actually appear inpm_paper_trades(definitive end-to-end proof). Then close the analytics niggles (up_mid_at_signalstores entry ask not up-mid;detect_latency_msmeasures 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.