For Agents

Reverse-chronological session log. Newest entries at top, grouped by date (## YYYY-MM-DD). Each bullet: one piece of work, short summary, wikilinks to docs touched. Updated by obsidian-documenter on every project doc write. Read by historian at bootstrap.

2026-07-04

  • Cohort CONSENSUS signal counterfactual backtest — flat €10, 2-week window, point-in-time (NOT validated). Replayed production ConsensusTracker::record_buy (alerts_consumer.rs:100-116: per (condition_id,outcome_index), 48h window CONSENSUS_WINDOW=172800s, fire on the 4th DISTINCT cohort BUY holders.len()==4/CONSENSUS_MIN_HOLDERS=4, BUY-side only) over the 14-trader score>0 cohort (run 1991, pmv2_traders) for 2026-06-20 18:33→07-04 18:33 UTC, entering at the 4th buyer’s fill price, holding to resolution, settling via gamma /markets (closed==true, winner outcomePrices[i]>=0.5, mirrors parse_market_resolution). 98 fires (all distinct markets, 0 re-fires) → 93 resolved → 53W/40L = 56.99% hit-rate → +€113.44 realized / +12.20% ROI on €930; +€144.74 / +14.77% incl. 5 open MtM. Biggest winners Detroit Tigers ML @0.19 (+€42.63) / Germany “No” @0.25 / Netherlands “No” @0.26 / Brazil @0.29; median entry 0.49; mix 87 soccer/WC · 9 Wimbledon · 2 baseball. Reads as genuine edge OVER THIS WINDOW but NOT statistically robust — n=93, 2wk, World-Cup-clustered, and NOT a purged/embargoed harness (does not clear the purged-backtest-gate-no-edge bar). No repo backtest harness — standalone Python replay vs LIVE APIs; artifacts (consensus_backtest.py/consensus_signals.json/consensus_summary.json) ephemeral in scratchpad. Caveats: /trades ignores startTime + caps offset ~3–4k → 4 hyper-active wallets (>4000 buys) truncated ⇒ 98 fires is a LOWER BOUND (all P&L accurate); entry-at-cohort-fill-price is an upper-bound vs live best-ask; exit hold-to-resolution assumed (follow/notify deferred); flat €10 ≠ prod conviction sizing (signals/sizing.rs). Does NOT overturn xref-consensus-rejected-fresh-copy-wins (consensus structurally late) — same upper-bound entry bias, tiny correlated sample. €≈USDC 1:1 (no FX in codebase). — cohort-consensus-backtest-2026-07-04

2026-06-25

  • NEW cohort /mirror copy-mirroring signal source — built + merged to main (count-gate commit f385fbb), DORMANT. /mirror <user> <size> <limit> mirrors a single Polymarket user’s every trade: copies entries (sized by conviction band — VLOW 0.2 / LOW 0.5 / NEUTRAL 1.0 / HIGH 1.5 / VHIGH 2.5 ×base, UNKNOWN→NEUTRAL), mirrors exits, caps concurrent open copies per user (skip at cap, no backfill). Third signal source source="mirror" alongside first_mover_core, reusing the producer/PM split (cohort produces EntryOrder/ExitSignal, PM executes). Second on-chain feed lane in the firstmover binary uses Alchemy WSS run_feed (low latency) — count-gated by a supervisor task (opens WSS only when ≥1 mirror active, aborts when empty → zero mirrors = zero CU; but per-block CU while ≥1 active, the same Alchemy eth_subscribe(logs) billing that drove the cohort lane to eth_getLogs polling). Stable origin_ref = mirror|{wallet}|{condition_id}|{outcome_index}; PM matches exits by origin_ref+condition+outcome, NOT source. Persists to pmv2_mirrors + pmv2_mirror_positions; a 15-min cron resolves closed markets (closed=true proxy) to free slots + refresh avg-bet. New pmv2-contracts QueryKinds mirror/mirrorstop/mirrorlist + responder renderers. Code: crates/cohort_algo/src/pmv2/mirror/{mod,repo,target,sizing,signal,handler,query}.rs; spec+plan docs/superpowers/{specs,plans}/2026-06-25-cohort-mirror-feature{-design,}.md. DORMANT until POLYGON_WSS_URL set on firstmover AND PM adds a mirror row to pmv2_autotrade_sources (else SkipAck-drop) AND contracts query-kind bump AND telegram connector parses /mirror …. Two deferred design decisions: (a) sizes off a single fill’s USD (fill.notional_usd(), spec §3) → under-sizes traders who scale in over multiple fills; (b) /mirror stop leaves open copies to ride until resolution. — cohort-mirror-feature
  • Cohort firstmover redeploy: apps-pmv2-cohort-algo-firstmover pin 8977b5bd851d44 (image sha256:716ee0d2..., container restartCount=0, ~20:19Z). ONLY the firstmover component rolled — query-responder + scheduled-run untouched per cohort’s confirm. Headline change: on-chain feed migrated Alchemy WSS log-subscription → eth_getLogs HTTP polling via the 150-proxy rotating pool. Boot log proof: INFO cohort_algo::pmv2::watch::poll_feed: polygon poll feed armed interval_secs=5 max_blocks=2000. Kills the Alchemy WSS CU bleed on the cohort lane (5s poll cadence + max 2000 blocks/pull, bounded spend). Same pin also bumps consensus-alert threshold 2 → 4, and resizes first_mover_core armed cohort=18 (was 20 — minor cohort window drift). Important architecture note for future debugging: PM self-watch (pmv2-onchain-watch crate) is on a SEPARATE WSS path and STILL uses Alchemy WSS (swapped from DRPC ogws on 6/25 to escape flapping per pmv2-self-watch-rustls-cryptoprovider-crashloop). This redeploy only kills cohort’s WSS dep, NOT PM’s. Standard cohort-producer per-task go-ahead + Andras #480 standing-auth carve-out applies; no producer-DB mutation. NATS publish path (cohort.signals) unchanged — only the on-chain feed transport changed (this layer ≠ cohort-firstmover-nats-migration layer) — cohort-firstmover-roll-d851d44-2026-06-25, cohort-firstmover-nats-migration
  • OPERATIONAL GOTCHA — first-mover lane mode (off/dry/live) is FIXED AT PROCESS BOOT; flipping it in the DB requires a firstmover RESTART to take effect. The publish-lane decision is computed ONCE in watch/mod.rs run_watch()build_first_mover_lane() (~L85-101): lane_active = signal_lane_active(effective_mode(global_mode, source_mode), source_enabled) where global_mode=pmv2_autotrade_config.mode, (source_mode,source_enabled)=the first_mover_core pmv2_autotrade_sources row, effective_mode (signals/config.rs:44) = MORE conservative of the two, and signal_lane_active (signals/sources.rs:11) = source_enabled && mode != Off. Both enabled=true AND effective mode≠Off needed to build the NATS publisher (build_cohort_publisher, positions.rs:224). NOT hot-reloaded. Inactive→publisher None→DEBUG "cohort publisher dormant (lane inactive)" (usually un-exported). Armed-but-NATS-env-unset→WARN "cohort lane armed but NATS publisher env … unset; entries dropped this run". What DOES hot-reload: cohort watchlist (spawn_cohort_refresh) + breaker_tripped (breaker_tripped_live, watch/cohort.rs:106); mode does NOT. Observability gaps recorded: (a) NO startup INFO announces effective off/dry/live — only "first_mover_core armed" (target pmv2_watch, watch/mod.rs:192) w/ cohort count, so dry-vs-live is unverifiable from logs alone (must read DB rows); (b) entry signals are rare + mostly SILENTLY filtered (tier/position-select/candidate-skip/gates) before any log → absence of "first-mover published" logs is NOT evidence the lane is broken. Service: pmv2-cohort-algo-firstmover (ns apps, prod host polymarket-infra.taild4189d.ts.net). All claims verified against disk 2026-06-25. — cohort-firstmover-lane-mode-restart-gotcha
  • Cross-repo MATH PARITY AUDIT — NEW algo (/coding/pmv2, cohort_algo+pmv2-contracts) vs PREVIOUS algo (/coding/polymarket_fetch, polymarket-fetch/src/pmv2). Verdict: core decision math is BYTE-FOR-BYTE identical on disk. Trader scoring (model.rs: CONF_* 0.06/0.035/0.02/0.008, consistency/long_term factors, trader_score=e·(0.5·long_term+0.5), bet_confidence=((entry−lost)/(won−lost)).clamp(0,1)), consensus (category_consensus.rs/alerts_consumer.rs: fires at exactly holders.len()==2, MIN_COHORT_SCORE=0.0, CONSENSUS_WINDOW=48h, EXIT_DEBOUNCE_SECS=60, EXIT_FANOUT_PERMITS=4, eligible_buy <=/eligible_sell strict >, position_kept initial_value>1.0, truncate(25)), and sizing (old decide_copy→new size_usd: score_weight=clamp(score,0,1)·10+1; raw=base·score_weight·conviction; clamp(min,max); old inline score-threshold→score_threshold_gate, same strict >) all verified identical. Arch insight: old executor-side gate battery (chase/liveness/entry-band/category/min_conviction/ev_floor/stale) pulled UPSTREAM into the producer (watch/cohort.rs signal_gate_decision+apply_io_gates) with identical formulas. Genuine divergences: (1) EV-floor gate DROPPED (old ev_floor_gate at engine.rs:154 has no new equiv; min_round_trip_edge_bps loaded-but-inert — the one possible accidental loss; operator chose “report, don’t change”, likely delegated to PM downstream; note cohort-pre-live-signal-gating judged the old gate degenerate anyway); (2) reference_price entry-band check dropped (new checks best_ask only); (3) wire v1→v2 redesign (deliberate — subject cohort.signalspmv2.order.{source}.{entry,exit}, schema_version 1→2, flat f64→nested market+serde-tagged Pricing/Decimal, +size_usd/kind/produced_at/valid_until_ts entry TTL 300s, exit reshaped to minimal ExitSignal; v1/v2 mutually unparseable); (4) safety hardening — breaker read-error default flipped fail-open→fail-closed, new exit guards snapshot_trustworthy (skip on prior-nonempty/fresh-empty) + should_record_exit (delete row only when Sell published); (5) operational constants BET_LINK_TIMEOUT_SECS 25→90, CONSENSUS_CACHE_WINDOW_SECS 600→3600 (formula unchanged). Method = 4 parallel comparison agents (sizing/gates, firstmover/sources/config, consensus/exit timing, wire/envelope), all load-bearing math re-verified against disk by the main agent (decide_copy vs size_usd, ev_floor grep, engine.rs:154 call site). — cohort-algo-vs-polymarket-fetch-math-parity-audit

2026-06-24

  • ROOT-CAUSE — position_manager’s self-watch feed (crate pmv2-onchain-watch src/feed.rs, prod pin 719dc32) CRASH-LOOPS with ZERO events = a rustls CryptoProvider panic on the FIRST wss:// handshake; crate made robust this session via ensure_crypto_provider() (NOT committed). Symptom: PM connects to DRPC ogws (wss://lb.drpc.org/ogws?...), ~200ms later logs feed task ended; backing off before respawn, respawns every ~5s (= FeedConfig::respawn_backoff), delivers nothing — while backfill over POLYGON_RPC_URL (separate reqwest path) keeps delivering fills in ~5s batches. ROOT CAUSE: tokio-tungstenite 0.29 (tls.rs:126) builds its TLS via rustls ClientConfig::builder() → resolves the process-default CryptoProvider; rustls 0.23’s get_default_or_install_from_crate_features() only auto-picks when EXACTLY ONE of ring/aws_lc_rs is enabled — PM’s Cargo.lock compiles rustls 0.23.40 with BOTH aws-lc-rs (via quinn-proto/reqwest http3) AND ring, and PM never calls install_default, so the first handshake panics. THE MISSING MECHANISM (key gotcha): the panic unwinds (default panic=unwind) killing ONLY the spawned feed task → run_feed’s _ = &mut handle => select arm RESOLVES with a JoinError (a panicked spawned task makes the parent’s &mut JoinHandle arm fire — it is NOT only a clean-return signal) → logs the generic respawn line and respawns after respawn_backoff. This explains the 200ms timing, the 5s cadence, AND the zero crate logs (the panic bypasses all tracing). Why probes looked fine: python-websockets uses OpenSSL (different TLS stack), never exercises rustls provider resolution — the server is HEALTHY, the bug is client-side; and a single-provider standalone build (ring only) does NOT panic, so a standalone-crate repro is misleadingly green — the dual-provider condition only exists in the full consumer graph. SAME CLASS as the earlier cohort_algo “Detector rustls CryptoProvider fix”cohort_algo/src/main.rs:57 + telegram_connector/src/main.rs:7 both do let _ = rustls::crypto::ring::default_provider().install_default(); as the first line of main(); PM was missing it. FIX (crate-side, robust, NOT committed): added idempotent ensure_crypto_provider() in src/feed.rs (via std::sync::Once, installs ring only if none installed) called at top of connect_once before connect_async, + rustls = { version = "0.23", default-features=false, features=["ring","std"] } in the crate Cargo.toml (single-provider, keeps the crate panic-free). TDD: unit connect_path_installs_process_default_crypto_provider (red→green) + integration tests/feed_teardown_repro.rs (mock ogws: healthy ACK holds a connection; server-Close respawns). Gate green: cargo test/clippy --all-targets/fmt --check. OPERATOR ACTION: crate fix helps PM only after PM bumps its pmv2-onchain-watch git pin (rev=719dc32 in position_manager/crates/position_manager/Cargo.toml) + redeploys, OR applies the one-line install_default() hotfix in PM’s src/bin/position-manager.rs main(). Caveat: end-to-end “panic gone” is only observable in PM’s DUAL-provider graph — the crate is single-provider and never panicked, so crate tests pass either way and don’t prove PM is fixed. Durable lessons: (a) a tokio::spawn panic RESOLVES the parent’s &mut JoinHandle select arm (JoinError) — a respawn loop spinning with no error logs = suspect a swallowed panic in the spawned task; (b) rustls dual-provider panics are an AGGREGATE-graph property (check the consumer’s Cargo.lock, not the crate’s); (c) a different TLS stack (OpenSSL) is not a valid client repro for a rustls provider bug; (d) make libraries robust to the consumer’s main() via an idempotent install-if-absent on the connect path.pmv2-self-watch-rustls-cryptoprovider-crashloop
  • /topbets /consensus /filters RESTORED 1:1 from the monolith via a new cohort_algo query-responder over NATS (pmv2-contracts 1f05fff + cohort_algo 5d5bb48, both gated green; awaiting @connector build #466 + @deploy infra #467). These read-only reporting commands were dropped in the pmv2 split (the carve deleted bet/market scoring + the bot; the connector only ported the 5 ops commands) → /topbets = “unknown command”. Operator wants them back 1:1 EXACT. Option A: cohort owns data+rendering, exposed over a new query.request/query.results channel (cohort’s first inbound NATS surface); the connector owns the commands + sends the rendered HTML. Ported verbatim (parity-locked by porting the monolith’s own unit tests unchanged): topbets (repo all_cohort_holders SQL + a topbets_filter eq:/neq: module namespaced to avoid the gating CategoryFilter collision + scoring/format_top_bets_telegram + ~17 helpers + confidence_emoji, 10 tests); consensus (compute+format ALREADY byte-identical in cohort — it’s leaderboard-API based; only added CONSENSUS_CACHE_WINDOW_SECS, 4 tests); filters (category_counts/tag_counts + filters_body). Contract pmv2_contracts::query = QueryRequest{request_id,actor,kind=TopBets|Consensus|Filters} + QueryReply{request_id,rendered}; reply JetStream-published with Nats-Msg-Id={request_id}-result (dedup-safe per PM #427). Durable lessons: (a) an explicit user constraint (verbatim/1:1) outranks a secondary gate (zero-warnings) — a subagent DELETED 2 verbatim items to silence dead_code; RESTORED + narrow #[allow(dead_code)] instead. (b) one-shot request/reply can’t mirror the monolith’s async second-message push → cache-miss spawns recompute-to-cache + a “re-run” reply (identical final output). (c) a shared-crate push hit non-fast-forward (connector’s ops/telegram landed first) → rebase on top, no force-push, re-vendor full canonical. — query-commands-restoration
  • BET-LINK INSPECTOR RESTORED into pmv2 over the same NATS query channel — the monolith paste-a-polymarket.com/-link → cohort-holders-for-that-bet reply (+ category-consensus fallback when no high-edge signal) was never ported; now restored 1:1. Connector detects a non-slash message with polymarket.com/ → publishes additive QueryKind::BetInspect{url} (serde back-compat) → cohort query-responder’s render_bet_inspect does score_bet(url) (bet-scoped holder lookup) + gauge formatter; !has_high_edge_signalresolve_fallback_category + category_consensus_sectionQueryReply{rendered} → connector posts HTML. NO new subjects/permits. Pins: pmv2-contracts c6b3a15, cohort_algo 1d573b8 (= 299cf1a + handler, carries rustls fix). Reviewed (fidelity + 4 adversarial lenses) + gated green. Engineering: DRY-extracted shared BET_HOLDER_SELECT SQL const (byte-identity guard test) for load_bet_holders+all_cohort_holders; re-added a dropped backpressure semaphore (cap 4) to the query-responder. Deferred: advisory-lock dedup in run_and_persist for duplicate same-category consensus recompute (also hits /consensus). Pending: connector non-slash arm = @pmv2_telegram (babylon #517); deploy cohort 1d573b8 before/with connector (unknown betinspect to old cohort = fail-safe skip). — bet-link-inspector-restored-pmv2
  • pmv2-onchain-watch WatchedFill IDENTITY FIELDS added — fixes a PERMANENT one-alert-per-wallet suppression in PM’s self-wallet watch (babylon task #430 RESOLVED; crate re-pinned 1b8a0f9719dc32, cohort re-vendored 0fdfe8a4a3f8b2, both gates green). PM’s self-wallet watch (the Balu/Bandi instant-alert feed) dedups on (wallet, transaction_hash, outcome_index), persisting a row in pm_self_trade_alerts so it doesn’t re-alert across restarts. But WatchedFill did NOT carry tx_hash, so PM was forced to set transaction_hash=""+outcome_index=0 for EVERY fill → after the FIRST fill of a wallet was recorded, is_alerted returned true for ALL subsequent fills of that wallet = permanent one-alert-per-wallet suppression (PM-side adversarial review confirmed). No sound PM-side interim key existed (token_id+amounts isn’t unique; now() breaks idempotency), so the fix had to be in the crate. FIX: added 3 public fields to WatchedFillpub tx_hash: B256, pub log_index: u64, pub block_number: u64 (block_number a bonus — PM derives trade_time from it); decode_watched_fill sets all three from the OrderFilledEvent already in scope (event carried them at decode, decode.rs:53-64); run_feed emits the decoded WatchedFill straight through so the emit path carries them with no extra change; regression test watched_fill_carries_event_identity_fields asserts non-zero tx_hash/log_index/block_number survive decode. The cohort consumer needed NO change — it uses decode_watched_fill and has ZERO WatchedFill { … } struct literals (additive fields can’t break a call site — same single-entry-surface property that made the review-pass cleanup re-vendor cleanly). PM mapping (their side): transaction_hash = format!("{:#x}", fill.tx_hash); outcome_index = fill.log_index. Deferred (out of the lean crate’s scope): block_time (would need a block→timestamp RPC — PM derives from block_number or uses ingest time) and condition_id/a TRUE outcome_index (gamma token→market enrichment stays consumer-side per the crate’s out-of-scope boundary; PM’s outcome_index=log_index dedup key is unaffected). Gates: crate = 37 tests + clippy --all-targets + fmt; cohort = full suite (no failures) + clippy + fmt. babylon: #430 resolved, status #435 posted to @positionmanager. Durable lesson (7): a shared library type produced SOLELY by the library’s own constructors (no WatchedFill { … } literals at call sites) can grow its fields ADDITIVELY with zero downstream churn — inverting the I/O edges so callers RECEIVE the type rather than build it is what buys this.pmv2-onchain-watch-crate-extraction

2026-06-23

  • pmv2-onchain-watch REVIEW-PASS CLEANUP — DRY/SOLID/bug pass after publish; new canonical pin wowjeeez/pmv2-onchain-watch @ 1b8a0f9 (was 5142d98), re-vendored at cohort 0fdfe8a (295 tests/clippy/fmt green). Two parallel adversarial reviewers, all findings VERIFIED AGAINST DISK before acting. −378 lines (2079→1701) with the CONSUMER SURFACE UNCHANGED (run_feed sig + decode keep-list byte-stable) → a safe IN-PLACE cleanup. Dead code deleted (24 items, all carried VERBATIM from cohort’s onchain.rs + unreachable from the one real entry run_feed): the entire newHeads/Detection/DetectionSink/run_detector block-latency path; the SubCommand/dynamic-resubscribe path (run_log_feed_static hardwired a DUMMY command channel → the cmd select-arm was dead; live watchlist updates already flow via the watch::Receiver); a write-only ack registry; unused pub helpers order_filled_topic0/CONDITION_RESOLUTION_TOPIC0/watchlist_address_topics. DRY extraction: one topic_hex(B256) (the 0x+hex-encode pattern was dup’d ~5×) + one watchlist_topic_slots(...) (single source for the maker=slot2/taker=slot3 invariant, feeding BOTH the subscribe builder and the getLogs builder which had each duplicated it). Robustness fixes: (a) the eth_getLogs/eth_block_number reqwest client now sets BOTH timeout AND connect_timeout + is OnceLock-reused (the documented proxied-hang trap, reference_proxied-client-connect-timeout); (b) the WSS keepalive ping was COUPLED to the 30s read-timeout select-arm → the 15s PING_INTERVAL was DEAD under load (busy socket never hits the read-timeout arm) + stall detection drifted to ~90s → moved to a DEDICATED tokio::time::interval(PING_INTERVAL) arm (restores ~45s); (c) boot eth_block_number failure now retries 3×+warns instead of SILENTLY seeding the watermark to 0 (which disabled backfill until the first live fill). NOT changed (behavior-preserved): the decode_watched_fill price≥1 (usdc≥token) reject + the both-watched maker-first pick are inherited cohort semantics — only added debug! observability, NO logic change. 3 reusable lessons: (1) verbatim module extraction carries dead weight — moving a module wholesale brought along TWO entire entry points + helpers the new single consumer never touches; a post-extraction dead-code sweep against the ACTUAL consumer surface shed ~⅓ of the crate. (2) a reviewer’s SEVERITY can be wrong — verify against disk: the ping issue was flagged “Critical (600s detection + frame loss)” but tracing connect_once showed the read-timeout arm DOES fire on silence so stalls were detected (~90s) → downgraded Critical→Important (cadence fix, not emergency); reinforces the standing “verify subagent output against disk” rule (applies to a finding’s severity/impact, not just its existence). (3) keepalive ping cadence must live on its own timer, not gated behind the read-timeout, or the configured interval is silently ineffective. Documentation-only; crate canonical @ 1b8a0f9, cohort dogfooding @ 0fdfe8a, mode unchanged (OFF), not deployed. — pmv2-onchain-watch-crate-extraction
  • pmv2-onchain-watch CRATE EXTRACTION + cohort DOGFOOD — new lean reusable git crate so position_manager can run its OWN self-wallet watch (operator-directed, babylon #402). cohort_algo’s battle-tested on-chain OrderFilled decoder + resilient WSS feed extracted into private github.com/wowjeeez/pmv2-onchain-watch @ pin 5142d98 so PM gets instant self-wallet (Balu/Bandi) entry/exit alerts by depending on it (git dep, like pmv2-contracts) instead of copying the code. Crate is lean: alloy-primitives + serde_json + tokio + reqwest(rustls) + tokio-tungstenite(rustls) + anyhow/tracing/futures — NO sqlx/gamma/data-api/NATS; #![forbid(unsafe_code)], edition 2024. Two modules: (a) decode (moved out of polymarket-data/src/onchain.rs, renamed watchlist-neutral Cohort*Watched*): constants (POLYGON_WSS_ENV, EXCHANGE_ADDRESSES, ORDER_FILLED_TOPIC0, CONDITION_RESOLUTION_TOPIC0), OrderFilledEvent/decode_order_filled, classify, WatchedFill{party,side,token_id,price()/notional_usd()/shares()}+Side, decode_watched_fill(event, watchlist:&HashSet<Address,S>) (generic over the hasher — yields the watched maker OR taker leg, None if neither watched or no USDC collateral), SeenLogs (tx_hash+log_index) dedup, subscription builders; (b) feed (raw WSS loop moved + NEW generalized runner): run_feed(cfg:FeedConfig, watchlist:tokio::sync::watch::Receiver<HashSet<Address>>, sink:tokio::sync::mpsc::Sender<WatchedFill>) — subscribes OrderFilled filtered to the CALLER’s watchlist (maker+taker), decodes+dedups, delivers to caller’s sink; internalizes reconnect+backoff, stall watchdog (600s), live watchlist updates (resubscribe on watch change), backfill-on-reconnect (eth_getLogs from a last_seen_block watermark), bounded sink (drop-with-warn when full). FeedConfig(Default): wss_url/rpc_url/exchange_addresses/backfill_interval(30s)/max_backfill_blocks(2000)/watchdog(600s)/respawn_backoff(5s)/channel_capacity(4096). Commit story: 33c87e7(decode moved)→b6910e2(feed+run_feed)→d94d7c5(watermark fix)→5142d98(README, pin). cohort DOGFOODS it (cohort main @ 69a293a): dropped polymarket-data/src/onchain.rs+re-exports (REMOVED on disk) + deleted watch/backfill.rs; vendored crate as workspace path-dep (crates/pmv2-onchain-watch in members + [workspace.dependencies], cohort_algo takes {workspace=true} — exactly the pmv2-contracts pattern); watch/ is now a thin run_feed consumer (spawn_cohort_refresh(pool, wl_tx) re-derives cohort set from pmv2_traders on a 60s REFRESH_INTERVALwatch::Sender<HashSet<Address>>run_feedmpsc sink drains WatchedFills → handle_cohort). Behavior-preservinghandle_cohort/first-mover/exit/gating byte-identical from markets.resolve onward (cohort.rs: use pmv2_onchain_watch::decode::{Side,WatchedFill} + handle_cohort(action:&WatchedFill,…)markets.resolve(&deps.gamma, action.token_id)); ONLY the input type (OrderFilledEvent/CohortActionWatchedFill) changed. Gate: 306 tests, clippy -D warnings, fmt clean, dormant-safe. Built subagent-driven w/ per-task reviews. 3 reusable lessons: (1) gap-recovery watermark BUG caught in review, fixed before publish (d94d7c5): the new runner first advanced last_seen_block only on delivered (watchlist-matched) fills — combined with the 2000-block backfill cap, a long quiet stretch of only non-deliverable watched events could stall the watermark and PERMANENTLY miss a later real fill on reconnect; fix = backfill_once advances watermark to the scanned to unconditionally via const fn next_watermark(current,scanned_to)->u64 (feed.rs:638, monotonic max — also fixed the inherited flaw in the original cohort backfill.rs); a gap-recovery watermark tracks what you EXAMINED, not what you ACTED on. (2) watchlist-parameterization turned a cohort-specific watcher into a generic lib with near-zero logic change — the decode was already generic over HashSet<Address,S>, so the whole generalization = Cohort*Watched* rename + caller-supplied watch::Receiver watchlist + mpsc sink (no decode-logic change). (3) gamma resolver kept OUT of the lean crate (PM enriches from its own ledger + markets_by_clob_token; PM has no GammaClient) — a shared substrate stops where callers diverge. Documentation-only (PM’s self-wallet consumer = position-manager-interface-design Plan 2, NOT built here); mode unchanged (OFF), not deployed. Supersedes the watcher internals in watcher-onchain-gap-backfill + Layout (now historical for cohort). — pmv2-onchain-watch-crate-extraction
  • cohort_algo MIGRATION SQUASH CLEANUP — babylon #333 IMPLEMENTED (github.com/wowjeeez/pmv2-cohort-algo main @ a3329dc, subagent-driven w/ review). Closes cohort’s half of the shared-DB migration-ownership decision. Problem: cohort inherited polymarket_fetch’s 44 sqlx migrations incl. the pmv2_autotrade_* tables PM now owns + ~28 dead legacy pm_*; PM uses Supabase (not sqlx) so cohort is the SOLE sqlx owner of the shared DB — editing migration files in place would drift the default _sqlx_migrations checksums (VersionMismatch → bricks deployed boot). Solution = squash to idempotent baseline: all 44 → ONE supabase/migrations/20260624000000_baseline.sql = CREATE TABLE IF NOT EXISTS for ONLY the 12 tables cohort owns/reads (9 pmv2_* + 3 kept pm_*: pm_scheduled_runs, pm_category_consensus_rows, pm_category_consensus_runs); cumulative deployed schema (ALTERs folded in); RLS preserved. Dropped: pmv2_autotrade_config/sources/orders (PM’s migrator 4712de9 recreates byte-identical so cohort’s READS survive) + ~25 dead pm_*. set_ignore_missing(true) on the Migrator (sqlx 0.8.6) in db.rs + the schema test → the deployed DB’s 44 now-orphaned applied versions are tolerated (fresh DB: baseline creates all; deployed DB: safe no-op). KEY DIVERGENCE from the plan: the original #333 ask was a distinct sqlx history tableunnecessary because cohort is the sole sqlx owner; squash + ignore_missing is cleaner and supersedes that approach + the sqlx-shared-migrations-table-trap “distinct history table” framing for this DB. Validated via testcontainers+insta schema_snapshot golden match on a fresh PG (independently re-confirmed); gate green (310 tests, clippy, fmt); reviewed clean (no DROP-set leak, idempotent, RLS confined to kept tables). Out of scope = @deploy’s prod ops: PM migrator FIRST → ~25 pm_* DROPs via psql (BEGIN/COMMIT, transcript-first; PM confirmed zero refs) → reset _sqlx_migrations to the baseline. Coordinated babylon #380 (resolved)→#397. Lessons: (1) “sole sqlx owner” makes a separate sqlx history table unnecessary — verify the actual count of sqlx migrators before isolating history tables (a Supabase/Diesel/psql peer is not a sqlx migrator); (2) set_ignore_missing(true) is THE technique to edit a deployed sqlx set (squash/prune/re-baseline) without checksum/version errors; (3) schema_snapshot validates columns only (not constraints/indexes/RLS) + omits 2 kept tables — a coverage gap. Mode unchanged (OFF). — cohort-algo-migration-squash-cleanup
  • cohort_algo PRE-LIVE SIGNAL-GATING COMPLETE — the 4 first-mover gates that were blocking arming are CLOSED (github.com/wowjeeez/pmv2-cohort-algo main @ e444af0, commits 0a31f45e444af0, branch feat/pre-live-gating ff-merged). Closes the gaps cohort-algo-v2-wire-contract-cutover flagged. All 4 gates sit in the first-mover ENTRY decision path and are FAIL-CLOSED (any fetch failure/timeout/missing ⇒ Skip, never emit blind — PM can’t catch these, it has no GammaClient): (1) category_filterCategoryFilter{include,exclude}+passes_filter (comma-sep buckets, !x=exclude, bare=include, empty=pass-all, over esport/sport/geo/crypto/other) + resolve_category (cache-first pmv2_event_category→gamma event_tagscategory_bucket; empty event_slug short-circuits to None, skipping a doomed gamma RTT on the hot path); (2) chase/max_entry_premium_bpschase_gate: skip if best_ask > avg_price·(1+premium/10_000) (entry-decay vs the cohort trader’s fill); (3) market livenessliveness_gate: skip unless active && accepting_orders && !closed && !resolved && !resolution_proposed and secs_to_resolution≥min (status from the new CLOB read client; resolution-proposed from gamma umaResolutionStatuses); (4) EV-floor DROPPED — old ev_floor_gate was degenerate (size cancels → just bps>200, price-blind); removed, operators set max_entry_premium_bps net of round-trip cost (min_round_trip_edge_bps stays loaded-but-unused — pmv2_autotrade_config schema FROZEN per babylon #333). KEY DECISION — re-added a read-only ClobReadClient to vendored polymarket-data (best_ask via /book, market_status via /markets/{cond}) — public endpoints, NO signing/auth/exec (explicitly NOT the deleted execution client); reuses HttpExecutor/build_http_client (sets connect_timeout — the proxied-hang trap) + proxy pool; response structs grounded in REAL captured fixtures (CLOB not geoblocked; wire is snake_case, /book asks returned DESCENDING so .min() is load-bearing). This DELIBERATELY re-introduces a CLOB dep the carve removed (operator-chosen: best-ask accuracy over the free cur_price/gamma proxy). entry_price_band_gate repointed to gate best_ask (our real entry) not the trader’s fill. signal_gate_decision made async + staged cheapest-first: pure trust gates (score/conviction) → category (cache) → concurrent bounded tokio::join! (best_ask+market_status+resolution_proposed) → pure apply_io_gates (liveness→chase→band). Startup announcement (operator directive #354): watcher boot publishes one telegram.outbound component: first_mover_core started with version: <hash> <title> (version via build.rs BUILD_GIT_HASH/BUILD_GIT_TITLE; Nats-Msg-Id=startup|first_mover_core|<hash> dedups same-version restarts). Subagent-driven (per-task spec+quality reviews + final whole-branch review); 312 tests pass, clippy --all-targets+fmt clean, dormant-safe. Closes deploy precondition (c) “4 pre-live gaps closed” (others: PM live on PMV2_ORDERS=DONE; PM migrator inserts the first_mover_core pmv2_autotrade_sources row=PENDING). cohort still mode OFF; live-arming gated on those + operator go-ahead. Deferred Minors (non-blocking): no test asserts trust-gates-before-fetches (grep-guard only); resolve_category/I-O glue untested (no mock harness); exclude-only filter + unresolved category proceeds (intended exclude semantic). — cohort-pre-live-signal-gating
  • cohort_algo v2 WIRE-CONTRACT CUTOVER COMPLETE (github.com/wowjeeez/pmv2-cohort-algo main @ 0986c4d, 6 commits 131b0d50986c4d). cohort stopped emitting its old cohort.signals schema_v1 and now emits the v2 trade wire-contract, schema_version 2, on pmv2.order.first_mover_core.{entry,exit} (source_id="first_mover_core"). Vendored the shared pmv2-contracts crate (crates/pmv2-contracts, path dep; canonical github.com/wowjeeez/pmv2-contracts; serde + rust_decimal only) — the long-standing blocker (contract didn’t exist) is resolved. Entry payload = EntryOrder with the Pricing::Derived arm {reference_price:Decimal, conviction:f64, source_score:Option<f64>, size_usd:Decimal} (the Pricing::Limit{limit_price,size_shares} arm is for weather/crypto, NOT cohort); PM derives the limit from the live book + $→shares + lot-size. Exit payload = lean ExitSignal{schema_version,kind,id,source,produced_at,valid_until_ts,market{condition_id,outcome_index,…},origin_ref} → PM resolves OUR row + full-exits. DELETED the dead cohort.signals subject + signals/wire.rs + signals/signal.rs (clean replacement, no dual-emit). Producer now OWNS sizing (signals/sizing.rs, formula PORTED from the retired executor: score_weight=clamp(score,0,1)*10+1; raw=base_usd*score_weight*conviction; clamp [min_size_usd,max_size_usd]; portfolio caps deliberately EXCLUDED — those stay with PM, it has the ledger) and signal-level gating (signals/gates.rs: score_threshold/min_conviction/entry_price_band/time_to_resolution/ev_floor, knobs from SignalConfigpmv2_autotrade_config). Gating split per babylon #292: PRODUCER owns signal-level gates, PM owns mode/breaker/portfolio-caps (PM has no GammaClient). valid_until_ts=Some(now+300s) on entries (first-mover edge decays), None on exits. Telegram flipped: Notifier::for_lane now builds the Outbound variant (→ telegram.outbound, consumed by telegram_connector) when NATS configured; Direct-send is fallback only. Gate green: build clean, 307 tests, clippy+fmt clean, dormant-safe. Mode still OFF. 4 PRE-LIVE gating gaps flagged (NOT blocking — mode OFF — but REQUIRED before live, PM can’t catch them): (1) category_filter gate not implemented (needs per-event category fetch + CategoryFilter/passes_filter port); (2) chase/max_entry_premium_bps gate not implemented (needs live best-ask); (3) market open/active/accepting-orders status flags not implemented (needs market-status fetch); (4) the ported ev_floor_gate is the OLD DEGENERATE logic — size_usd cancels out so it reduces to min_round_trip_edge_bps>200 vs a fixed 2% cost, NOT a true EV calc, must be replaced. — cohort-algo-v2-wire-contract-cutover
  • pmv2 SHARED-DB MIGRATOR OWNERSHIP DECISION (babylon #331, resolved 2026-06-23 by @positionmanager; option (b) per data-ownership principle D3). position_manager owns the FULL pmv2_autotrade_* migrator (config/sources/orders tables + any v2 columns on them). Producers (cohort, etc.) DROP all pmv2_autotrade_* DDL from their own migrations → these become a read-only precondition (cohort READS pmv2_autotrade_config + pmv2_autotrade_sources, never touches the orders table). Shared-DB sqlx safety: each repo that migrates the shared Postgres DB MUST use a DISTINCT migration-history table (custom sqlx Migrator table name) so PM’s history + cohort’s own-table history can’t collide — this REFINES the sqlx-shared-migrations-table-trap “one migrator per DB” rule to “one migrator per (distinct) history table, with disjoint physical tables still required.” Producer-private tables (scoring/consensus/positions) stay in the producer’s own migrator + its own history table. RESOLVES the “physical relocation DEFERRED” hold from position-manager-interface-design. FREEZE: no pmv2_autotrade_* schema changes from anyone until PM’s migrator ships (PM prioritizes the migrator spec before v2 autotrade migrations land). Cohort action items (NOT yet done, pending operator go-ahead): prune ~28 dead legacy pm_* tables, drop the pmv2_autotrade_* DDL, set a distinct sqlx history table. (Tension with the legacy “don’t drop pm_* prod tables” rule is scope-resolved: that rule is about the retiring polymarket_fetch prod DB; this prune is cohort_algo’s own DB surface.) — cohort-algo-shared-db-migrator-ownership
  • Updated cohort-algo-component to mark the v2 cutover DONE (was “planned, BLOCKED”) + cross-link both #331 and the cutover note. — cohort-algo-component

2026-06-22

  • EDGE-DISCOVERY WORKFLOW SYNTHESIS (26-agent Claude run: 9 scouts → 53 raw → 18 canonical → 14 adversarially verified → 12 set aside). Net: NO fat new edge. P0 = a LIVE-CAPITAL fee correction — Polymarket WEATHER is NOT fee-free: under Fee Structure V2 (eff. 2026-03-30) Weather=Theta 0.05 (2.25/5M+ Apr-2026; quadratic closeness-to-mid S(v,s)=((v−s)/v)²·b sampled 1/min, 10,080/epoch); (b) UMA resolves discretionary geopolitics to HEADLINE/SPIRIT, whale-voted (95K=0.24% of ALL 2024-election arb in the zero-fee era; cold-start = empty book set by snipe-bots; news-latency free-RSS ~15min vs 150ms bots; ESPN is the SLOW feed; Pinnacle-PM already productized). The 3 viable NEW lanes (pilot-small, default-OFF, must clear the purged harness): C3’ Sports/Esports LR maker farming ($5-20k, AS-measurement make-or-break), C4-weather maker-flip of the live signal only (+10-20% over taker, drop crypto-1b/copy sub-variants), C-Tech resolution-source on objective oracles only (Theta 0.04). +2 research-more: C7 UMA dispute-window fade (gated on conditional-on-DVM backtest, ~0-3 events/mo), C15 on-chain MM cancel-event signal (reuses WSS+cohort, orthogonal to the FILLS copy lane). Infra: position_manager is FAK/taker-only (SDK has limit_order()+GTC/GTD+is_order_scoring(); cancel() is #[allow(dead_code)], must test sub-minute reliability); Data API trade-history capped ~3000 (deep history via Gamma dumps / on-chain reconstruction); portfolio correlated-resolution-timing risk un-modeled. Full report /Users/levander/coding/pmv2/research/edge-discovery-2026-06-22.md.edge-discovery-2026-06-22
  • DESIGN (approved) — telegram_connector OUTBOUND relay v1: the pmv2 fleet’s single Telegram I/O plane (sole bot-token holder + sole sender = single rate-limit authority), stateless, no trading logic, no DB. v1 = OUTBOUND ONLY: durable JetStream PULL consumer tg_outbound on telegram.outbound (stream TELEGRAM_OUTBOUND, 1d, 5-min dedup) → Bot API sendMessage. Dumb faithful relay (passes text + publisher parse_mode verbatim, never re-escapes). At-least-once, ack-AFTER-send; transient (429/5xx/net) → retry then redeliver bounded by max_deliver; terminal (400/403) → drop+ack+ERROR+best-effort loop-guarded plain-text meta-alert. Sequential (one in-flight), reactive rate-limit only (honor retry_after, no token bucket = YAGNI). Relay loop behind a MessageSource trait for broker-free unit tests; wiremock for Bot API; optional NATS testcontainer. Envelope TelegramOutbound{msg_id,chat:Option,text,parse_mode:Option,kind:Option} field-matched to cohort_algo/.../telegram_outbound.rs until pmv2-contracts publishes. Inbound (getUpdates→auth→ops.*) = separate later spec, gated on @positionmanager locking ops.* (#311).telegram-connector-outbound-design
  • FLEET REFERENCE — pmv2 infra / observability / NATS conventions (from @deploy, babylon pmv2 #313). Applies to ALL pmv2 services. Observability: traces+logs+metrics → SigNoz Cloud VIA LOCAL OTel collector on polymarket-infra (NEVER direct); OTLP gRPC 127.0.0.1:4317 / HTTP 4318 (Docker host.docker.internal:4318); std env OTEL_SERVICE_NAME/OTEL_EXPORTER_OTLP_ENDPOINT/OTEL_EXPORTER_OTLP_PROTOCOL/OTEL_RESOURCE_ATTRIBUTES=service.namespace=pmv2,deployment.environment=prod (auto-injected for ansible apps containers; native opts in). Logging: ERROR=operator failures, WARN=anomalies (rate-limit/retry/dedup), INFO=state transitions only (metered, never per-msg >~1/s), DEBUG=per-msg behind RUST_LOG; key fields as ATTRIBUTES not strings; NEVER log secrets/PII (keys, NKey seeds, bot tokens, DB URLs — log pubkey not seed). NATS: always set Nats-Msg-Id (5-min dedup window all 3 streams); streams TELEGRAM_OUTBOUND(1d)/OPS(7d audit)/PMV2_ORDERS(pending PM)/legacy WEATHER_SIGNALS; consumers create their OWN durable (stable name), producers don’t create streams. Tracing: one span tree per signal lifecycle, span names=role, data=attrs. Health: docker/systemctl=ground truth, journald=fallback, SigNoz=second look, nats stream/consumer info w/ admin seed sparingly. Coordination: pmv2-contracts confirmed standalone repo (#310); inbound ops. still open w/ @positionmanager (#311).* — pmv2-fleet-infra-observability-nats-conventions
  • COMPONENT DOC — cohort_algo as a STANDALONE repo/component (/Users/levander/coding/pmv2/cohort_algo, private github.com/wowjeeez/pmv2-cohort-algo, main @ 2e150b1). The pmv2 fleet’s cohort-intelligence SIGNAL PRODUCER — does NOT place trades (zero CLOB dep). Five responsibilities: (1) cohort scoring cron → pmv2_traders/leaderboard, gated by pmv2_runs; (2) cohort consensus alerts (Pmv2Consensus cron + on-chain ConsensusTracker); (3) first-mover ENTRY signals (Core-tier first BUY → NATS action:Buy); (4) cohort EXIT signals (tracked trader sells → NATS action:Sell); (5) on-chain WSS watcher + 30s eth_getLogs gap-backfill. Own cargo workspace (edition 2024, resolver 3, unsafe_code=forbid, sqlx/tokio/anyhow/tracing/async-nats 0.49+nkeys; members crates/cohort_algo+crates/polymarket-data); vendors polymarket-data as an internal crate. 278 tests pass, clippy+fmt green, dormant-safe (no panic when POLYGON_WSS_URL/NATS_URL unset). PORT: behavior-preserving from the retiring polymarket_fetch carve (483350c) — preserved internal crate::pmv2:: paths + the polymarket-data crate name ⇒ near-zero import churn, ZERO logic edits. Pre-port multi-lens review fixed 4 real bugs: (a) empty-Data-API-snapshot 200 [] mass-false-exit (fabricated Sells for a wallet’s whole book → guarded); (b) dropped-publish-still-deletes-the-row lost-signal (now gated on publish outcome); (c) entry double-publish removed (watcher is sole producer); (d) “autotrade purge” rename autotrade/signals/ (execution is the migrated-away PM’s concern; kept only the shared pmv2_autotrade_* table names). ARCH CONTEXT (babylon #286, Andras’s Miro): polymarket_fetch RETIRES → independent component repos in /coding/pmv2 on a NATS spine; producers (cohort/weather US+AS/crypto) own strategy+sizing → publish READY entry-orders + exit-SIGNALS; position_manager CONSUMES NATS DIRECTLY (no runner) → gate(off/dry/live, caps, breaker) → sign+POST+record; PM also owns telegram.outbound pub, an ops.commands/ops.results consumer (confirmable kill-switch), and the pmv2_autotrade_* migrator. telegram_connector = separate repo (other agent). v2 WIRE-CONTRACT (babylon 292): subjects pmv2.order.<source>.<entry|exit>; ONE entry envelope w/ pricing discriminator — limit (weather/crypto: limit_price+size_shares, PM uses) | derived (cohort: reference_price+conviction+size_usd, PM derives limit from live book + $→shares + lot-size); lean exit-signal (condition_id+outcome_index+origin_ref, PM resolves OUR row + full-exits); shared pmv2-contracts crate = source of truth (vendor it). Cohort gotchas: source MUST be the EXACT source_id DB key (first_mover_core, NOT a label — wrong value silently routes to Mode::Off); exit MUST carry origin_ref; producers OWN ev-floor/chase/category gating (PM has no GammaClient). CURRENT vs NEXT: today emits the OLD cohort.signals schema_v1 (COHORT_SUBJECT/SCHEMA_VERSION_MAX=1); telegram.outbound seam prepared but Direct-send still default. v2 cutover (publish to pmv2.order.cohort.{entry,exit}, schema_v2, vendor pmv2-contracts, ADD producer-side size_usd+valid_until_ts+ev/category gating) = planned, BLOCKED on @positionmanager publishing the pmv2-contracts crate. — cohort-algo-component
  • pmv2 MODE-CONTROL (off/dry/live kill switch) IMPLEMENTED — all 12 tasks via subagent-driven dev + per-task opus reviews on logic tasks + final whole-branch opus review (READY TO MERGE, no Critical/Important). position_manager is now the SINGLE execution authority: place_order resolves the authoritative mode fresh per call (global × per-source (mode,enabled) + breaker) and gates at the POST boundary — Off→Skipped (zero work), Dry→reserve dry_run row + build+sign rehearsal (no POST/caps/breaker), Live→reserve(live)+caps→place. Reserve+caps+breaker MOVED out of fetch’s engine/runner into PM (SQL byte-identical); runner now mode-agnostic (builds EntryIntent/ExitIntent, calls pm.place_order, maps typed ExecOutcome→NATS ack). Correctness: entries gate on effective_mode(global,source); EXITS gate on the row’s recorded mode (live position stays sellable when global=Off — E1 fix); enabled=false⇒Off now enforced (was dangling); pre-POST reserve/caps DB error→TransientNak(30s); recorded-failure/benign-race→Ack (no double-trade); fail-closed-to-Off two paths; cap:event literal. State: committed on main BOTH repos (PM 351a755, fetch 4fde695), NOT pushed, mode OFF. Gate: 66 PM offline + 532 fetch + 7 db-tests-vs-real-Postgres pass. OPEN: (1) commit contamination — 4fde695 swept in user’s unrelated in-progress exit_signal/cohort_exit_signal_id work (called by uncommitted firstmover/alerts code) — pending user split/keep; (2) follow-ups: delete dead orders.rs/EngineConfig cap helpers, add host-injectable signer seam for live-POST e2e; (3) pre-arm: live-test PM notify→Telegram before arming.position-manager-mode-control-implemented
  • ARCHITECTURE MILESTONE — pmv2 monorepo → independent services BY CONCERN (execution = @deploy’s position_manager; signal producers; a notification service). This session carved one producer: cohort-signals (branch worktree-cohort-signals, from main @ 43023ba). (1) THE CARVE (8cd6f58, gate green): repo reduced to ONLY cohort scoring (Pmv2Collect, kept so the slice is self-contained), cohort consensus (Pmv2Consensus cron + on-chain ConsensusTracker in pmv2-watch), first-mover ENTRY production, and cohort EXIT detection. ~13,400 lines DELETED: the whole autotrade execution engine (autotrade/{engine,execute,exits,runner,reconcile,settle,preflight,sizing,pricing,orders,…}), the onchain_book triggers (fill/settle/self-trade), the Telegram bot, self-trade & overlap watchers, resolution-detection, trending, bet/market scoring, the independent tracked-detector lane. 3 workspace crates dropped: polymarket-clob (CLOB signing/exec — notably NOT a dependency of a signal producer at all), pm-ws, lag-probe. The watcher (pmv2-watch) was ONE binary mixing cohort-signal lanes (consensus/first-mover/exit) WITH execution-trigger lanes (self-trade/fill-reconcile/settlement) — exec lanes stripped; consensus+first-mover+exit+gap-backfill sweep preserved BYTE-FOR-BYTE. Method = compile-driven (delete migrated callers, follow compiler errors); KEEP-WHOLE files verified diff-clean vs base. (2) COHORT EXIT SIGNALS (e762f5d, gate green polymarket-fetch 153 / polymarket-data 107): new requirement “watch trade exits the same way as first-mover entries.” Prior on_cohort_sell did DB-only bookkeeping (logged EXITED + deleted tracked rows) — no alert, no publish. Now it PUBLISHES an action:Sell TradeSignal to NATS cohort.signals AND telegrams, so the out-of-process executor can close the copied position. sources::exit_signal(PositionRow) builds the Sell (Execution::Context, reference_price=position.cur_price). KEY CORRECTNESS GOTCHA — the exit signal_id carries an exit discriminator DISTINCT from the entry signal_id for the same (condition,outcome); without it the consumer’s ON CONFLICT(dedup_key) would silently swallow the Sell as a dup of the entry Buy = capital trap. Test-locked exit_signal_id_differs_from_enter_signal_for_same_market. DRY: entry+exit publishers share publish_cohort_with_retry (retry/backoff + drop-alert on give-up). Ordering: publish-BEFORE-delete (a publish failure can’t silently lose the signal). The score-based eligible_sell + ExitDebounce gate kept unchanged (NOT narrowed to Core-tier). PROVISIONAL cross-service contract — @deploy’s position_manager must consume action:Sell on cohort.signals + treat repeated Sells as idempotent; still to be coordinated. NOT merged, NOT deployed.** — cohort-signals-service-split-and-exit-signals

2026-06-21

  • pmv2 EXECUTION MIGRATION (Plan 1) IMPLEMENTED — all 11 tasks via subagent-driven dev + per-task opus reviews + final whole-branch opus review (READY TO MERGE, no Critical/Important). New position_manager crate owns: polymarket-clob signing client (use_server_time OFF, ~20–100ms/order saved), place_order buy+sell (place_buysubmit_live, place_sellauto_sell), ledger code (mark_*/record_*/bump_breaker_count), onchain eth_call helpers (+connect_timeout), caps/breaker readers (load_caps/load_breaker), and FillSink/FillEventSink watcher seams (declared, unimplemented — Plans 2/3). polymarket_fetch rewired one-way onto PM; submit_live/auto_sell/execute.rs deleted. Behavior-preserving: 11 ledger SQL byte-identical, notify strings byte-identical, locked clob hash tests intact. Gate green: PM 43+5, polymarket-clob 11, fetch 529/0-failed. State: on main BOTH repos (PM cbd0f4c, fetch afc785d), NOT pushed, mode OFF, nothing armed/deployed. OPEN DECISIONS: (1) caps expose-only (DoD at API level only, fetch read path unchanged); (2) pre-arm live-test PM notify Arc→autotrade Telegram channel; (3) accept benign breaker-vs-no-signer check-order flip (mark_canceled vs old mark_failed, no POST either way). FOLLOW-UPS: make SigningClient Clone (kill startup double-build); host-injectable signer seam for end-to-end pm.place_order db-test.position-manager-execution-migration-implemented
  • pmv2 Task 2 — polymarket-clob crate physically moved from polymarket_fetch into position_manager workspace (cdfcca8); use_server_time flipped to false; self-referential include_str! test solved with split-needle; all 11 tests green.pmv2-polymarket-clob-crate-migration

2026-06-20

  • DESIGN REVISION (post-audit) of the position_manager (PM) interface design — TWO walk-backs + 2 folded-in wins. (1) Migration ownership WALKED BACK: PM does NOT run a second sqlx::migrate!. The pmv2_autotrade_orders table stays created by the single existing upstream migrator; PM owns the ledger CODE only (ledger.rs queries). No PM migrations/ dir, sqlx migrate feature omitted, upstream applied migration untouched; physical relocation DEFERRED to a coordinated single-migrator change. The earlier “adopt-if-exists CREATE TABLE IF NOT EXISTS migration owned by PM” plan was a sqlx trap — see sqlx-shared-migrations-table-trap. (2) place_order now spans BOTH buy and sell: the auto-sell/exit path (mark_selling/record_partial_sell/mark_sold/settle_placed_fill) folds into PM so the whole lifecycle buy→submitted→filled/partial→open→selling→sold lives in PM; OrderRequest carries reserve_id (acts on an already-inserted reserve row) and a buy/sell field split, NO provenance block. (3) Latency win folded in: disable the clob client’s use_server_time (hard-coded true at polymarket-clob/src/lib.rs:329) → drops a serial GET /time before every order POST (~20–100ms); signed-order bytes unchanged. The 3 per-trade GETs are upstream/out of scope; HTTP/2 + colocation deferred pending p50/p99. (4) Config table pmv2_autotrade_config is CO-OWNED: PM reads caps, writes breaker counters. — position-manager-interface-design
  • REFERENCE/LESSON — sqlx 0.8 shared _sqlx_migrations table trap (the reusable gotcha behind the PM migration walk-back). sqlx tracks ALL applied migrations in ONE hardcoded _sqlx_migrations table keyed by version (BIGINT PK). Three failure modes: (1) TWO separate sqlx::migrate! sets vs the same DB → each migrator throws VersionMissing on the OTHER’s applied versions (validate_applied_migrations defaults ignore_missing=false) → boot fails; (2) reusing a version integer across two dirs → VersionMismatch (different checksum/description for the same version); (3) editing an ALREADY-APPLIED migration file changes its stored checksum → VersionMismatch → bricks boot on deployed DBs. CREATE TABLE IF NOT EXISTS does NOT save you — sqlx tracks by version ROW not table existence, so it still inserts the version row and collides. Takeaways: ONE migrator per DB/_sqlx_migrations table; never reuse version numbers; never edit applied files; owning a table’s CODE is separate from owning its DDL/migrator. — sqlx-shared-migrations-table-trap
  • DESIGN DECISION — new position_manager (PM) crate: a focused Polymarket EXECUTION+WATCH crate (NOT a full engine rehome) that the deferred autotrade→position_manager rename lands into. PM OWNS place_order (build/sign/POST+record), the position ledger, portfolio caps + circuit-breaker, self-wallet reconciliation, watched-tx detection; STRATEGY stays upstream in polymarket_fetch (detection, gates, EV/conviction/sizing, first-mover gating, NATS Pmv2Consumer, mode). Dependency one-way in-process polymarket_fetch→position_manager, single host process to start; polymarket-clob (vendor rs-clob-client-v2) MOVES INTO PM (leaf crate). API: place_order(OrderRequest)->PlacedOutcome (≈ today’s submit_live; FAK, proxy sigType 1, V2 contracts preserved), watch_self_wallet (CLOB /user WS + periodic on-chain balanceOf truth check, chain-authoritative reconcile emitted never silent), watch_transactions (UNBUILT Phase-2 OrderFilled WSS detector over a configurable address set → raw FillEvent). Sinks are TRAITS (FillSink/FillEventSink) so a later separate-watcher-process split is cheap. Schema: pmv2_autotrade_orders ledger OWNED by PM via an adopt-if-exists (IF NOT EXISTS) migration so the shared Postgres table is never double-created; pmv2_autotrade_config stays upstream but PM gets read accessors for the caps/breaker subset and is the SOLE counter of exposure/open positions. WARNING — editing the already-applied upstream migration risks sqlx CHECKSUM DRIFT on a deployed DB; coordinate cutover (PM adopts first as a no-op). Behavior-preserving, proven by GOLDEN-PARITY tests (same OrderRequest → same maker/taker/contract triple + same ledger transitions pending→submitted→filled|partial→open). Split into 3 sequenced plans: (1) execution migration [written], (2) self-wallet watcher, (3) watched-tx watcher. Preserves V2 hard-cutover correctness (V2 verifyingContract addrs, pUSD collateral) + the connect_timeout rule (BOTH timeout AND connect_timeout on every reqwest client — the 75-min silent-hang lesson). Source: spec + Plan 1 in pmv2/position_manager/docs/superpowers/.position-manager-interface-design
  • RESEARCH — “fastest way to put trades into Polymarket.” Punchline: submission is ALREADY sub-second; the real latency is upstream DETECTION (~15-min REST poll), so the highest-leverage fix is Phase 2 first-mover (on-chain OrderFilled WSS → cohort.signals), NOT submission tuning. Biggest submission-path win = kill the 3 uncached per-trade GETs (market metadata + book + gamma resolution, ~150–600 ms) via CLOB WS market-channel book mirror (vendored rs-clob-client-v2 has a DISABLED ws feature) + cached static metadata; WS read-only, placement stays REST. Second win = disable use_server_time (~20–100 ms). Cannot go faster on-chain (off-chain matching). WARNINGS: V2 hard cutover 2026-04-28 makes stale-V1 orders REJECTED not slow (verify domain version “2”, new Order struct, pUSD collateral, V2 exchange addrs); proxied reqwest MUST set BOTH timeout AND connect_timeout (75-min silent-hang lesson). Don’t-bother: sigType Proxy→EOA for speed, hand-rolling EIP-712, pre-signing. Colocation = GCP Frankfurt now; origin region undocumented (inferred AWS London, NOT us-east-1) — verify before spend. Gaps: no p50/p99 measurement, HTTP/1.1 (no HTTP/2 benchmark). Priority: ① Phase 2 first-mover ② WS book mirror + metadata cache ③ disable use_server_time ④ verify V2 ⑤ measure p50/p99 ⑥ HTTP/2 + region-verified colocation if justified.2026-06-20-fastest-polymarket-trade-submission
  • NEW FEATURE — pmv2 watcher on-chain GAP-BACKFILL sweep: a 30s periodic eth_getLogs reconciliation over [last_seen_block, latest] that recovers cohort/self/our-fill OrderFilled logs the Polygon WSS feed (run_log_feed, polymarket-data/onchain.rs) drops during disconnects — eth_subscribe auto-reconnects with backoff but NEVER replays logs emitted in the gap, so they were lost permanently. Shipped to main 43023ba, gate green (563 polymarket-fetch + 107 polymarket-data), NOT yet redeployed. New file crates/polymarket-fetch/src/pmv2/watch/backfill.rs; new public primitives in onchain.rs (eth_block_number, eth_get_order_filled_logs, cohort_address_topics, build_get_logs_filter, modeled on eth_call_u128). Re-fetches OrderFilled for the cohort across BOTH maker+taker topic slots (mirrors live build_logs_payload) and feeds each recovered log through the IDENTICAL worker path as a WatchEvent. WHY a periodic sweep, not a literal on-reconnect hook: catches ALL gap causes (disconnect, silent frame drop, provider hiccup) not just full disconnects; lives entirely in the watcher (no surgery on the shared polymarket-data feed crate); simpler to test. KEY SAFETY INSIGHT — feeding backfilled logs is PROVABLY safe because every downstream consumer is ALREADY idempotent: consensus fires on the 2nd distinct holder by WALLET, first-mover dedups at the engine via ON CONFLICT(dedup_key), self-trade alerts dedup in DB by (wallet,tx,outcome), our-fill reconcile shares the worker’s seen set keyed by (tx_hash,log_index) — so live/backfill overlap is harmless, structurally equivalent to the pre-existing first-mover match-cron backstop. Mechanics: watermark last_seen_block: Arc<AtomicU64> advanced via fetch_max in handle_order_filled right after decode (fetch_max so out-of-order delivery can’t rewind); from = watermark is INCLUSIVE (recovers intra-block partial-delivery gaps, dedup absorbs the re-fetch — intentional, NOT an off-by-one); MAX_BACKFILL_BLOCKS = 2000 cap with a log line on hit (no silent truncation; outage > ~2000 blocks recovers across subsequent sweeps); try_send so the sweep never blocks the worker queue; reuses the existing POLYGON_RPC_URL HTTP endpoint (NOT the WSS feed). Deferred (noted): universal (tx_hash,log_index) front-dedup (unnecessary since consumers are idempotent + would alter the subtle both-parties-in-cohort double-delivery); instant on-reconnect replay (sweep recovers ≤30s); deep reorg handling (out of scope — detection signals, not settlement). Plan docs/superpowers/plans/2026-06-20-pmv2-watcher-gap-backfill.md. — watcher-onchain-gap-backfill
  • ENGINEERING DECISION — Polygon WSS provider = dRPC (recommended over Alchemy, 2026-06-20). TRIGGER: Alchemy creds drained; ROOT CAUSE = Alchemy bills 0.04 CU PER BYTE on the 24/7 eth_subscribe logs stream, so byte/event-metered billing structurally drains an always-on subscription (scales with traffic volume, not request count — a property of the billing model, not a misconfig). PICK = dRPC: flat per-request billing (20 CU/req), 210M CU/mo free tier, $6/1M CU PAYG with NO monthly floor, multi-node routing for redundancy; the new eth_getLogs backfill calls also benefit from flat-per-request. Swap = a SINGLE env var (POLYGON_WSS_URL), zero code change (same var the copy lane/watcher already read; unset = lane idles silently). Verify before cutover: dRPC per-notification CU cost (confirm a pushed logs notification is charged like a 20 CU request, not re-metered) + WSS concurrency limits on their dashboard. Runner-up = Alchemy PAYG (no re-integration, but the per-byte stream bill is exactly what drained us — fallback only). Self-host NOT worth it at a single connection. — polygon-wss-provider-drpc

2026-06-19

  • OBSERVABILITY — dry-skip visibility added to ingest_for_consumer in runner.rs (branch fix-dry-skip-visibility, GATE_EXIT=0, 334+87 tests): two previously silent skip paths (stale-gate + Dispatched::Skipped) now surface notify + tracing::info when eff == Mode::Dry; per_message_mode() hoisted above stale_gate so eff is available at both sites; dry_skip_msg(source_id, condition_id, reason) helper + unit test added; Live paths unchanged.autotrade-dry-skip-visibility
  • ENGINEERING DECISION — Persistence::try_reserve now returns ReserveOutcome enum (Reserved(i64)|CapExposure|CapDaily|CapMaxOpen|CapEvent|Dedup) instead of Result<Option<i64>>; cap-breach and real dedup are now distinct variants; pre-lock cap strings unified to cap:* namespace; MockStore gains with_outcome constructor; new test try_reserve_cap_breach_distinct_from_dedup asserts the variants cannot be confused; branch autotrade-phase2-firstmover, GATE_EXIT=0.autotrade-reserve-outcome-enum
  • REFACTOR — Phase 2 autotrade first-mover EMIT hoist (branch autotrade-phase2-firstmover, GATE_EXIT=0): publish consts (PUBLISH_RETRIES=3, PUBLISH_RETRY_BACKOFF=200ms, COHORT_SUBJECT) moved from positions.rs to nats.rs as pub consts; new PublishOutcome enum + build_and_publish_first_mover_entry async fn in firstmover.rs is now the single shared emit path for both cron and future watcher; run_autotrade_entry in positions.rs becomes a 1-call delegation; 3 new unit tests in firstmover.rs (skips_non_core, skips_when_breaker_tripped, skips_when_publisher_none); regression pin cron_run_autotrade_entry_emits_unchanged_signal_id in sources.rs guards the dedup key identity.autotrade-firstmover-emit-hoist
  • Bug fix / Phase 2 B1 — sources.rs canonical signal_id across producers (branch autotrade-phase2-firstmover): cohort_signal_id now lowercases origin_ref, condition_id, and instance before hashing; enter_signal uses condition_id as instance instead of the unstable event_slug.unwrap_or(condition_id). Two new parity tests (lowercase-canonicalization + byte-identical cron-vs-watcher output). One existing test updated ("evt""0xcond"). Gate=0.autotrade-sources-canonical-signal-id
  • ARCHITECTURE/OPERATIONAL FACTS (durable, branch autotrade-foundation) — autotrade source extensibility + the global-vs-per-source caps split. (1) Adding a Directive-shaped source is GENERIC — zero engine/runner/sizing changes: a new source that BUYs a token up to max_price valid until valid_until_ts needs ONLY (a) a parse branch in wire.rs::parse_signal producing a TradeSignal with the new source_id, and (b) a row in pmv2_autotrade_sources(source_id, enabled, mode). Mode is resolved generically in runner.rs::per_message_mode (repo::autotrade_sourceconfig::effective_mode(global, source), the const fn MORE-CONSERVATIVE floor on off(0)<dry_run(1)<live(2)); a missing source row + a failed config load both FAIL-CLOSED to Mode::Off. DRY: weather and crypto_shortterm_latency_test share one parse_directive(p, action, source_id) helper (generalized from parse_weather, 2026-06-19). DRY-PIN (generic capability, NOT what crypto uses): a row pinned mode='dry_run' CANNOT reach live regardless of global mode (effective_mode floor); available to ANY source as an opt-in, the live flip being a deliberate explicit row change (Andras’s call). The crypto_shortterm_latency_test source is NOT dry-pinned — it is seeded mode='off' at rest but LIVE-capable; dry can’t measure fill latency (no order is submitted in dry, so no round-trip to time), so the test runs LIVE. (2) Caps/sizing are GLOBAL, not per-source — and this is directly the crypto test’s safety surface: AutotradeConfig is a single global row (SELECT … FROM pmv2_autotrade_config WHERE id = 1) — base_usd, min/max_size_usd, per_event_cap_usd, total_exposure_cap_usd, daily_spend_cap_usd, max_open_positions, max_copies_per_event, slippage/premium bps all global. Per-source granularity is ONLY mode + enabled; NO per-source cap columns. Arming the crypto latency test = a 3-part flip for the window: (a) crypto source row → mode='live', (b) global mode → live, (c) TIGHT global caps; because caps are global, those tight caps are the ONLY thing bounding the live test (no per-source caps to scope them). LOW-IMPACT today (global mode off; the first-mover live trade trigger is Phase 2, not built/armed, so no cohort trade signals flow — during the test window nothing else produces live trades) but a REAL conflict once Phase 2 arms first-mover live — a tiny-caps latency-test window and the real first-mover strategy can’t have different caps simultaneously. Future fix = per-source cap columns if both must run live concurrently. — autotrade-source-extensibility-and-global-caps
  • 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 instead of waiting up to ~30 min for the cron. Gate GATE_EXIT=0 (508 binary [507 pass/1 ignored] + 99 lib, clippy -D + fmt clean, 6 new unit tests); only 2 files changed (alerts_consumer.rs + the main.rs call-site). New symbols: eligible_sell(sig,score) (gate: side=="SELL" + cohort + score>MIN_COHORT_SCORE=0.0), ExitDebounce (per-wallet 60s coalesce), on_cohort_sell(wallet). run_tracked_alerts gained cfg:&AppConfig and injects a DataApiClient (via cfg.build_data_client, which already sets connect_timeout), a tokio::Semaphore (cap 4) bounding fan-out, and ExitDebounce. THE CRUX — dedup vs cron (no double alert / no double sell): the exit pipeline already has THREE consumers of an EXITED row, each with its OWN dedup — (1) cohort EXIT alert (exits::run_check, dedup = the pmv2_exit_alert timestamp cursor), (2) auto_sell (runner::run_exits, dedup = autotrade_exits cursor + the mark_selling open→selling CAS that serializes ALL sell triggers), (3) insert_position_event = a PLAIN INSERT, NO ON CONFLICT (the UNIQUE(proxy_wallet,condition_id,outcome_index) on pmv2_position_events was DROPPED in 20260603000000_pmv2_overlap.sql for enter→exit→re-enter, so duplicate EXITED rows ARE possible at the table level — you CANNOT dedup exits via a DB constraint). THE SAFETY DECISION: the chain SELL path does detect_exited scoped to {wallet} (live data-API fetch vs prior pmv2_positions) and for a confirmed exit LOGS the EXITED row (repo::insert_position_event) AND DELETES the prior pmv2_positions rows (repo::delete_positions) — mirroring exactly what the cron’s run_pmv2_positions does; it does NOT send a direct alert and does NOT call run_exits/auto_sell itself. Deleting the prior snapshot row means the cron’s next detect_exited won’t re-see it ⇒ exactly ONE EXITED row exists ⇒ the existing pmv2_exit_alert cursor alerts once and run_exits sells once (and even a stray duplicate is safe via the mark_selling CAS). The chain path adds NO new alert source and NO new sell path — it just makes the SAME row appear seconds early, then the existing cursor-driven consumers act on their normal fast cadence. REJECTED ALTERNATIVE: firing a DIRECT alert from the chain path — it would need its own cross-process dedup against the cron’s pmv2_exit_alert cursor (impossible to guarantee; the two rows carry different detected_ts which defeats 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 from the live snapshot is never re-exited); cold-start (no prior snapshot for the wallet) produces NO exit (tested). ACCEPTED LIMIT (spec, NOT a code comment — repo no-comments rule): intra-gap round-trips (cohort enters AND exits within one cron gap, never snapshotted) get no fast-path exit — matches today’s cron coverage; the fast path only accelerates exits for ALREADY-snapshotted positions. Complements (supersedes nothing) the autotrade engine spec + the project_autotrade-engine-spec auto-memory; it’s the SELL/fast-exit counterpart to the BUY consensus work in tracked-alerts-consumer (same file) and changes only WHEN the EXITED row appears, not the sell mechanism. — pmv2-phase4-chain-cohort-sell-fast-exit
  • LIVE + FILTER EVOLUTION — the pmv2 tracked-alerts lane (Phase 2) went LIVE in prod and the round-trip works: detector pmv2-tracked-detectortracked.signals → consumer pmv2-tracked-alerts → Telegram, verified via SigNoz at SUB-SECOND detection latency. Post-launch the alert FILTER evolved through 3 iterations as Andras tuned signal vs noise, landing on a CONSENSUS filter. (1) per-fill, no filter → ~1 alert/sec FLOOD — the new lane is per on-chain fill (every OrderFilled, buy+sell) whereas the OLD batch lane structurally hid churn (≈30-min cadence + position-snapshot-diff + entry-only); a few high-frequency cohort wallets churning sub-second dominated. (2) conviction filter (merge 36a4264/e6a9041) — alert only when notional ≥ 2× the trader's own avg_bet_size (scored-only, reused conviction_multiple, CONVICTION_MULT_FLOOR=2.0); selective+correct but a touch noisy. KEY DATA INSIGHT: the “spamming” wallets were WHALES (avg bet 7.1K; cohort avg-bets 80K) ⇒ an absolute-dollar floor would have been WRONG — conviction RELATIVE to each trader’s own avg is the right shape for a whale cohort (the literal first attempt 9817110 WAS an absolute NOTIONAL_FLOOR_USD=25+6h dedup, superseded for exactly this reason). Ops gotcha: a pause→redeploy must use --deliver new or the consumer replays the whole JetStream backlog through the new filter. (3) consensus filter (CURRENT, merge 0f71f89/6c03528) — alert ONLY when a 2nd positive-score (“sharp”) cohort trader independently BUYS into the same (condition_id, outcome_index) within a 48h window (“the sharps are converging here”); single-trader bets no longer alert. In-memory ConsensusTracker = HashMap<MarketKey{condition_id,outcome_index}, HashMap<wallet,Instant>>, 48h window, pruned on the 60s cohort-refresh; eligible_buy gate (side==BUY + cohort score>MIN_TRADER_SCORE(0.0), drops sells+non-sharps); record_buy fires multi-trader format_consensus_alert when a buy makes the wallet the 2nd+ distinct sharp holder. Consts: CONSENSUS_WINDOW=48h, MIN_TRADER_SCORE=0.0, 2-trader threshold. The consensus REWRITE replaced the stateless decide_alert(...)->AlertDecision core with eligible_buy+stateful ConsensusTracker (cross-message state). Observability fix shipped alongside: per-alert tracing::info!(target:"tracked_alerts", n=…, "consensus alert sent") — the live alert rate is finally visible in SigNoz (was a blind spot); SigNoz service name polymarket-fetch-pmv2-tracked-alerts. Current state: LIVE on 0f71f89, cohort=21, correctly SILENT (0 sends so far — rare BY DESIGN; first alert is event-gated on a real 2-sharp convergence); window/score/2-trader knobs await first real alerts to calibrate. Distinction (don’t conflate): this “2nd sharp converges” is a human-ALERT gate, NOT the backtested “≥K agree” copy-trade consensus thesis that xref-consensus-rejected-fresh-copy-wins REJECTED. Known later-patch: Telegram 429 drops the send with no retry ⇒ a genuine rare conviction/consensus alert can be lost in a rate window; a send-retry/queue is a worthwhile future patch. Files crates/polymarket-fetch/src/pmv2/tracked/{alerts_consumer.rs, alert.rs}. — tracked-alerts-consumer
  • TRADER PROFILE (verified, CORRECTS prior framing) — Polymarket trader swisstony (proxy 0x204f72f35326db932158cba6adff0b9a1da95e14, pseudonym “Frail-Possible”) is an ALGORITHMIC HFT SPORTS MARKET-MAKER BOT, NOT a human “favorite-lay / draw-fade sportsbook” bettor (that earlier characterization is RETIRED — no prior Obsidian note existed; the stale framing lived in session memory). Re-investigated 2026-06-19 via a Claude workflow over the live Polymarket Data API + pmv2 Supabase, every figure recomputed from raw trade/position files, all claims adversarially verified. Standing: #1 by lifetime VOLUME (~990M), #5 by PnL (9.93M), ~100% SPORTS (not top-1000 in crypto/politics/culture); month #24 (+672,804), week #10 (+991,897); margin (PnL/volume) ~1.00% all-time / 0.45% mo / 1.13% wk. Bot evidence (decisive): 3,497 trades in a 3.3h window (~1,055/hr), up to 13 fills in one 1-second timestamp, 56.5% share a timestamp, ALL 3,497 with UNIQUE tx hashes (rules out one human click splitting maker fills), 100% BUY in-window (exits by resolution or offsetting BUY), breadth 64 events / 325 outcomes in 3.3h. Strategy = full-board liquidity provision on a few live matches (saturates moneyline/draw/BTTS/totals/spread/halves); snapshot World-Cup-soccer-dominated (97.5% notional); two-sided 74.4% of notional; 72.5% of two-sided open markets have combined Yes+No entry <0.992) = mechanical locked spread only the resting MAKER earns; price barbell (most trades <0.50 but most dollars ≥0.90 — big size into near-certain favorite legs). Profit source = volume×spread + favorite-premium harvesting, NOT directional skill — our favorite-stripped edge metric oscillates around ZERO (−0.0064 / +0.0036 / −0.0071, consistency 0, W/L 40/37 on n_resolved=77); that is WHY our engine reads edge≈0 while his ledger prints ~1% (scoring strips the favorite premium that IS his income — do NOT “fix” the engine to reward him). Risk = single-match prop concentration: Canada–Qatar was 46.1% of his open book + the entire −42.7k, goals/spread props −61, max $219k) tracking liquidity/flow not forecast. VERDICT — NOT copyable: his edge accrues to the resting maker, a follower mirroring fills crosses as a TAKER and pays away the ~1% that IS the strategy then loses on gas/fees; also 0% crypto so no fit with our crypto-updown first-mover lane (multi-leg two-sided vs single-leg directional), and our ~30-min REST trigger lag is fatal vs 13 fills/sec — only use = treat as a FLOW / liquidity-concentration signal, never a trade to mirror. CAVEATS: single 3.3h ~3,500-trade window (Data API offset cap) so “100% BUY”/World-Cup-mix are snapshot-specific not lifetime; open PnL is unrealized MtM; n_resolved small; model fee=0; NO explicit maker/taker flag in the data (market-making INFERRED, medium confidence). — swisstony-hft-sports-market-maker

2026-06-18

  • NEW FEATURE — Phase 2 of the pmv2 real-time tracked-detection lane: the pmv2-tracked-alerts consumer turns the Phase-1 on-chain detector’s tracked.signals stream into INSTANT first-mover buy AND sell Telegram alerts (recovering the first-mover edge lost to the ~15-min REST batch cadence). Branch autotrade-foundation, 2 commits 806ba78+e1d1f24, gate green (471 polymarket-fetch + 90 polymarket-data, clippy -D warnings + fmt clean, GATE_EXIT=0); NOT merged, NOT deployed. SEPARATE from the autotrade cohort.signalsWEATHER_SIGNALS lane (cohort-firstmover-nats-migration) — this is alert-only (subject tracked.signals, stream TRACKED_SIGNALS, does NOT execute). Phase 1 (already shipped) = always-on detector pmv2-tracked-detector publishing a TrackedEvent JSON per cohort buy/sell fill. Service: new long-running CLI pmv2-tracked-alerts = a NATS JetStream pull consumer (durable alerts on stream TRACKED_SIGNALS); dormant-safe (exits 0, env-first no-DB-touch, if NATS_URL/NATS_TRACKED_ALERTS_NKEY_SEED unset) so it’s deploy-safe before provisioning. Architecture: pure core decide_alert(payload,cohort)->AlertDecision{Send|Skip|Reject} (parse→lowercase-keyed cohort lookup→format) wrapped by a thin NATS loop run_tracked_alerts; files crates/polymarket-fetch/src/pmv2/tracked/{event.rs (owned TrackedSignal mirror + fail-closed parse_tracked), alert.rs (format_tracked_alert), alerts_consumer.rs (decide_alert + run loop)}. Two locked product decisions: (a) alert = trader-SCORED (consumer resolves wallet→name/rank/confidence-band from a 60s-refreshed cached scored cohort) + measured LATENCY + market/side/price/notional/link; intentionally DROPS the batch-only “N cohort holders” count and the paper-copy line; per-fill granularity (one alert per cohort order via order_hash dedup → alerts on ADDS, not just first entries). (b) alerts on BOTH buys and sells — today’s batch alert is buy-only, so SELL alerts are NET-NEW. Isolation: frozen scoring/paper lane and batch format_entry_alert/send_entry_alert UNTOUCHED — batch alert stays as the safety net; retiring it deferred until prod parity (no zero-alert window). Review triage (deliberate keeps, don’t “fix”): (1) #[allow(dead_code)] on TrackedSignal is CORRECT — canonical owned wire mirror; 6 fields (shares/fee_usd/block_number/tx_hash/order_hash/detected_at) unused by the alert but forward-looking for Phase-4; in a BINARY crate pub does NOT exempt never-read fields from dead_code, so removing the allow re-fails clippy. (2) cohort-cache staleness is a non-issue — it refreshes lazily at the loop top BEFORE processing each message (≥60s), so the first message after a lull gets a freshly-loaded cohort; cohort changes slowly. (3) Reject→Telegram-notify KEPT (parity with autotrade nats::run; near-zero from our own publisher but real diagnostic value). DEPLOY (pending, folds into the Phase-1 detector deploy): provision NATS_TRACKED_ALERTS_NKEY_SEED (consume-only on tracked.signals); a JetStream stream TRACKED_SIGNALS capturing tracked.signals with a durable pull consumer alerts; run pmv2-tracked-alerts as a long-lived service (alongside pmv2-consumer). Spec docs/superpowers/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
  • ARCHITECTURE CHANGE — cohort first-mover ENTRY signal migrated from in-process ingestion to NATS publish on subject cohort.signals, consumed by the SAME Pmv2Consumer that handles @weather (one unified lane routed by envelope source); in-process runner::run_entry DELETED. Branch autotrade-foundation (off main, NOT deployed), 4 commits 1166f51bdafd7f, gate green as reported (428 polymarket-fetch + 89 polymarket-data); each task spec+code-quality reviewed, T4 also trade-safety reviewed = SAFE. Sits on top of pmv2-position-manager-nats-consumer — that note’s wire::parse_signal, engine Context arm, and the single submit_live site are UNTOUCHED; only cohort transport changed. T1 (1166f51): parse_signal routes source=="first_mover_core"Execution::Context, FAIL-CLOSED (Reject) on any capital-critical missing/empty field — origin_ref empty⇒cohort exits never fire (capital trap), outcome_label empty⇒token_for fails, + reference_price/conviction/outcome_index; max_price/valid_until_ts became Option (cohort omits them; valid_until_ts=None is EXACT parity + avoids clock-skew wrongful drops). T2 (3c589c1): serialize_cohort + headline cohort_roundtrip_parity test — serialize_cohort ∘ parse_signal byte-equal to sources::enter_signal over the full persisted/exit-read field set (fixtures use event_slug≠slug so a field-collapse is caught); SUBTLETY — watcher has no CLOB at detection so enter_signal leaves tick/min/neg_risk=None, serialize emits 0.0/0.0/false, parse rebuilds Some(0.0)/Some(false), IMMATERIAL because the Context engine arm re-resolves these from the live CLOB and never reads the signal’s values. T3 (9505840): nats::publish_cohort — serialize then publish_with_headers w/ Nats-Msg-Id=signal_id (JetStream server-side dedup), then flush; reuses nats::connect. T4 cutover (bdafd7f, trade-safety reviewed): run_autotrade_entry (positions.rs) PUBLISHES instead of ingesting in-process — gated on active + Core tier + !breaker (the old !ready preflight clause DROPPED — preflight is now a consume-time concern); bounded retry PUBLISH_RETRIES=3/200ms, a dropped publish after retries = logged MISSED entry = under-trade NEVER a double (no telegram/breaker); publisher built once/run from env NATS_URL+NATS_COHORT_NKEY_SEED (absent⇒dormant⇒nothing published); DELETED runner::run_entry, dead safety::ready, unused AutotradeDeps.mode. SAFE: no double-LIVE order — dedup on signal_id at BOTH the NATS header AND the DB ON CONFLICT(dedup_key) backstop; money-path untouched; dormant cohort (off) publishes nothing. ⚠️ LOAD-BEARING GOTCHA: deleting watcher-side autotrade_ready() removed in-process entry-preflight AND per-tick reconcile_entries — safe TODAY (cohort dormant/off, no live orders, /autotrade reconcile works manually) BUT the Pre-arm consumer-preflight gate (the blocker shared with weather) must now ALSO restore entry-reconcile + preflight before arming ANY lane live, else the first live lane has stuck pending/submitted orders nothing auto-reconciles. DEPLOY (NOT done, T5): stream stays WEATHER_SIGNALS (ADD cohort.signals subject, do NOT rename); pmv2 durable must consume BOTH subjects; provision a cohort-publisher NKey; put NATS_URL+NATS_COHORT_NKEY_SEED in the watcher env; @weather — cohort is a 2nd source, no schema change (fields Option-default at v1). Spec docs/superpowers/specs/2026-06-17-cohort-firstmover-nats-migration-design.md; plan …/plans/2026-06-17-cohort-firstmover-nats-migration.md. — cohort-firstmover-nats-migration
  • REVIEW HARDENING (post-cutover 3-lens review: bugs/gaps/DRY) on the cohort→NATS migration — found ONE real silent-failure LANDMINE + operator-visibility gaps; fixed in 39c01dd (gate green 429 polymarket-fetch + 89 polymarket-data, trade-safety reviewed SAFE; branch autotrade-foundation, NOT deployed). LANDMINE: T3 nats::publish_cohort originally published via the CORE async-nats client (client.publish_with_headers+client.flush) — core NATS publish is FIRE-AND-FORGET: NO JetStream PubAck and does NOT error when no stream’s subject filter captures the subject. So if @deploy didn’t bind cohort.signals into stream WEATHER_SIGNALS (or set a filter_subject excluding it), EVERY cohort entry would publish “successfully” into NOTHING — consumer never sees it, ZERO error logged anywhere, operator thinks it’s live; exactly the T5 deploy footgun, undetectable by the old code. FIX (39c01dd): publish through the JetStream context (async_nats::jetstream::new(client.clone())publish_with_headers(...).awaitPublishAckFuture.await the ack), wrapped in a 5s tokio::time::timeout; compiler-pinned vs vendored async-nats 0.49.1 — a missing/misfiltered subject yields NO_RESPONDERS→StreamNotFound = a LOUD Err the bounded retry surfaces; persistence now CONFIRMED (PubAck). Single-order safety unchanged (Nats-Msg-Id=signal_id stream-dedup + authoritative DB ON CONFLICT(dedup_key) backstop). OPERATOR-VISIBILITY: the existing telegram notify closure threaded into the entry path so (1) an “armed-but-unconfigured” lane (cohort active but watcher NATS_URL/NATS_COHORT_NKEY_SEED unset) now WARNS+telegrams instead of dropping entries silently at debug (was indistinguishable from intentional dormancy); (2) retry-exhaustion telegrams a “first-mover entry LOST” alarm. WHY: re-detection next tick is IMPOSSIBLE (end-of-run position snapshot upserts unconditionally) so a dropped publish is a PERMANENT miss — benign for capital (never a double) but NOT self-healing, so must be visible. A pure publisher_intent(at_active,has_url,has_seed)->{Dormant,Unconfigured,Ready} classifier (unit-tested) drives the Dormant-vs-Unconfigured split. Also named two literals: COHORT_SUBJECT + wire::SCHEMA_VERSION_MAX (replacing a hardcoded 1, so the producer tracks the parser’s max). T5 ADDENDUM: the cohort-publisher NKey must permit JetStream publish to cohort.signals (publish + receive PubAck on its inbox), NOT merely core publish. VERIFIED CLEAN (don’t re-litigate): end-to-end field fidelity; dedup identity (Nats-Msg-Id==DB dedup_key==legacy orders::dedup_key(..,"enter",..)); no double-order; no worthwhile duplication; zero comments/dead-code. REUSABLE LESSON: async-nats core Client::publish* is fire-and-forget (Msg-Id stream-dedup still applies but you get NO ack and NO error on a missing subject) — use jetstream::Context::publish* + await the PublishAckFuture whenever you need delivery confirmation or loud-on-missing-subject behavior. — cohort-firstmover-nats-migration

2026-06-17

  • NEW FEATURE — pmv2 Position-Manager NATS consumer: generalizes the autotrade engine to ingest MULTIPLE signal sources over NATS JetStream (first consumer = @weather “buy the LOCKED winning temp bucket”); the reviewed money-path is preserved byte-for-byte, only ingestion + the order-derivation front-half generalize. Branch autotrade-foundation, 6 commits (4c0b7d4→74faf7d), gate-green, NOT deployed. Module STAYS autotrade (spec’s →position_manager rename deferred). T1 (4c0b7d4) generalize TradeSignal: SignalAction::{Enter,Exit}Execution enum — Directive{max_price} (weather: explicit cap + market constraints) vs Context{reference_price,conviction,source_score} (cohort); signal_id is the dedup key, dedup_key() returns it directly; LOAD-BEARING — cohort signal_id == legacy orders::dedup_key(..,"enter",..) so dedup is byte-identical mid-migration (old in-process trigger + new NATS path can’t double-order). T2 (1bc8989) wire.rs trust boundary: parses @weather’s flat JSON envelope (SignalPayload) → ParseOutcome{Accept,Skip,Reject}, NEVER panics; fail-CLOSED (Reject) on malformed/missing-required-common-field, fail-OPEN (Skip, never execute) on unknown source / SELL / future schema_version. T3 (9abc27b) plan_order seam (engine.rs): ingest_entry dispatches Executionplan_context (cohort, byte-for-byte unchanged) vs plan_directive (weather: token_id-direct, outcome_index from clob_token_ids, neg_risk verified vs CLOB, directive_limit_price=floor_to_tick(min(max_price,ask)), no-conviction size, NO cohort price gates — legit buys to 0.95); shared back-half untouched; stale_gate added. T4 (eb6ca9a) per-source mode: pmv2_autotrade_sources.mode col (20260617000000, CHECK off/dry_run/live), effective_mode=min(global,source) on off<dry<live, cohort backfilled to live (eff==global, preserved), /autotrade source <id> mode …; LOAD-BEARING fix = build_autotrade_deps overrides engine_cfg.mode=eff so a dry source under live global can’t POST. T5 (b7f8437) consumer seam + NATS adapter: ConsumerOutcome{Persisted,Skipped,Stale,Transient,Terminal} + ingest_for_consumer (staleness-FIRST before any I/O, then ingest_entry, reuses dry_rehearsal/submit_live, breaker fail-safe map_or(true,..)); nats.rs ack discipline (ack_action: Persisted|Skipped→Ack, Transient→Nak, Stale|Terminal→Term), async-nats 0.49 raw NKey seed (SU…, no TLS), binds EXISTING pmv2 durable on WEATHER_SIGNALS/weather.signals; ack-only-after-persist; ON CONFLICT(dedup_key) DO NOTHING makes at-least-once safe. T6 (74faf7d) Pmv2Consumer subcommand: dedicated long-running process (engine is a one-shot per-tick batch, a spawned loop would die); dormant-safe (exits 0 if NATS env absent); mode = weather effective (defaults off ⇒ skips all); migration 20260617010000 seeds weather source at mode=‘off’. GOTCHAS: async-nats 0.49 JetStream calls return Box<dyn StdError> (no anyhow::From) → need nats_err map_err helper, not bare ?; going live needs TWO operator actions (global=live AND weather.mode=live); env = NATS_URL(tailnet)+NATS_NKEY_SEED (pubkey UDYV…ZEDD), container DNS-resolution to the tailnet host was a pending @deploy fix. Spec docs/superpowers/specs/2026-06-16-pmv2-position-manager-nats-design.md; plan …/plans/2026-06-17-pmv2-position-manager-nats-consumer.md; wire SoT = weather_bet docs/signal-schema.md @ 3187585. REMAINING: deploy + @deploy launcher for the subcommand + manual dry-validate (publish a real signal → confirm DRY-RUN rehearsal) before arming weather live. — pmv2-position-manager-nats-consumer
  • Bug fix — cohort first-mover auto-trade was DRY-RUN “buying” already-decided markets (e.g. ITF tennis loser “Yidi Yang” at ~$0.002); fixed with TWO complementary ENTRY-ONLY guards on autotrade-foundation (NOT deployed; gate green, 503 workspace tests). Live Gamma showed ITF Zhengzhou: Yidi Yang vs Junhan Zhang closed=false/acceptingOrders=true but umaResolutionStatuses=["proposed"] + outcomePrices=["0.0005","0.9995"] — match decided (result proposed to UMA), just not formally closed. TWO holes: (1) the cohort entry price_bounds_gate validates the COPIED TRADER’s frozen reference_price=p.avg_price (~0.44, in-band), NEVER the live best_ask (0.001) we’d execute at → a market that collapsed AFTER the trader entered slips through; (2) liveness_gate only skips FORMALLY resolved/closed/non-accepting markets — a UMA-proposed market is still acceptingOrders=true so it passes. Part A — ask-band gate (824a22d, engine.rs::plan_context): after best_ask, skip when ask outside [min_entry_price,max_entry_price], reusing price_bounds_gate on the ASK; reason "ask … outside entry band"; Cohort/Context-only, NOT the weather Directive path (which legitimately buys up to max_price=0.95); this is the data-source-independent primary safety net. Part B — resolution-proposed entry guard (8b3c451): new GammaClient::market_resolution_proposed(condition_id) reads umaResolutionStatuses; adapter (market_data.rs) sets new MarketStatusView.resolution_proposed FAIL-OPEN (30s timeout, warn, default false — a Gamma hiccup never breaks resolve(), Part A backstops); liveness_gate skips entries when set, reason "market in resolution". ENTRY-ONLYis_terminal_market/exit_decision_for UNCHANGED so a resolution-proposed position stays sellable. GOTCHAS: umaResolutionStatuses is a JSON-ENCODED STRING ("[]"/"[\"proposed\"]"), NOT a native array — same as outcomes/outcomePrices; parse via parse_json_string_array (a Vec<String> deserialize silently fails); in polymarket-clob, MarketStatus.resolved = m.closed is a MISNOMER (copy of closed) so CLOB meta carries NO UMA signal — only Gamma does; query = GET gamma-api.polymarket.com/markets?limit=1&condition_ids=<id>. Also updated autotrade-gates (liveness_gate rejects resolution_proposed; price_bounds_gate reused on ask) + autotrade-market-data-trait (MarketStatusView.resolution_proposed). — autotrade-resolution-proposed-entry-guard
  • OPS finding (point-in-time, from journalctl on polymarket-infra) — autotrade alert latency decomposed + NEW intermittent scheduled-run 15-min timeouts. Current manifest (~/levandor/terraform/deploys/apps.yml): polymarket-fetch-scheduled-run job schedule:"*:0/30" cmd scheduled-run --dedup-seconds 300 --gap-warn-seconds 3600; polymarket-fetch-bot service present; wallet-monitor + pm-arb-validate now absent (decommissioned — supersedes the 2026-05-27 polymarket-fetch-deploy table); weather-bet-run service + weather-bet-resolve daily-14:00 job present. systemd Type=oneshot TimeoutStartUSec=15min. NEW: last 24h = 45 runs OK + 3 KILLED at the 15-min cap (status=130/SIGINT) at Jun16 20:00 / Jun17 08:30 / 09:00 UTC — last two consecutive (possible worsening). Typical OK run ~6–9 min; autotrade entry step runs ~7.5–8 min INTO the run (collect+resolutions+leaderboard precede run_pmv2_positions in execute_pipeline), exit/sell alerts run later still → a kill drops that cycle’s later alerts (incl. in-flight fire-and-forget tokio::spawn Telegram sends). LATENCY (buy alerts ~20–25 min behind source, up to ~38, sometimes a cycle skipped): (1) REST-poll detection on the 30-min timer mean ~15min/up-to-30; (2) ~8min in-run before autotrade step; (3) intermittent whole-cycle loss on a 15-min kill. Telegram delivery is sub-second (not the cause). Autotrade is live in DRY-RUN (mode=off/dry — observed 🧪 DRY-RUN — would BUY … w/ bet links + via 0x…·0x…). Slow query: EXITED exit-cursor read returned 37,741 rows in 2.87s (backlog growing; cursor may be near-NULL/not advancing). Timeout cause (hypotheses, unconfirmed): most likely an occasional silently-hanging proxied reqwest w/o connect_timeout (intermittent, not data-size-deterministic — reference_proxied-client-connect-timeout); the 09:00 run got past autotrade (09:08) then died at 09:15 → hang is in later stages (trending/exit alert/category sync/paper copies/settlement/self sync/resolution check/overlap watcher/self-trade-changes); secondary = growing EXITED backlog. PROCESS NOTE: facts gathered via direct read-only SSH which operator has since disallowed — future verification routes through @deploy on babylon. — autotrade-alert-latency-and-run-timeouts

2026-06-16

  • Bug fix — autotrade exit-cursor 404 WEDGE: run_exits broke on the FIRST exit-event whose CLOB /book 404’d and parked the cursor at first_failed−1µs; a RESOLVED/CLOSED market returns "No orderbook exists for the requested token id" PERMANENTLY → cursor wedged forever on that event (re-failing + Telegram-spamming every pass) AND blocked every exit-event queued behind it (once live = blocks cohort-mirror exits for positions behind the wedge). FIX b5d5986 = status-based classification: market_data.rs::resolve() now skips book_top for resolved||closed markets (returns empty BookTop, no 404). New pure helpers in market.rs: is_terminal_market(status)=resolved||closed, ExitDecision{Skip,Exit,Retry} with advances_cursor()=Skip|Exit, exit_decision_for(market). Both exit paths (process_exit_event, drain_pending_exits in runner.rs) classify: terminal→Skip (advance cursor / clear_exit_pending, quiet debug, NO alert), open+bid→Exit (unchanged), open+no-bid OR transient resolve error→Retry (halt + “will retry” alert). commands.rs::run_sell returns an informational msg for terminal markets. Terminal positions close via the separate SETTLEMENT sweep; entry path unaffected (liveness_gate already skips resolved/closed before best_ask). GOTCHA: vendored CLOB client (polymarket-clob/src/lib.rs:251 status_from) aliases BOTH resolved and closed to the single field m.closedis_terminal_market effectively reduces to “closed”; re-verify if a future change populates resolved independently. Gate green 345 polymarket-fetch + 87 polymarket-data (baseline 335, +10 tests); trade-safety review 7/7 (terminal predicate=resolved||closed only; transient still halts; breaker untouched by read errors; zero entry/reconcile blast radius). NOT on main/VM (VM runs bf4b628) — live engine still wedged until merge+redeploy. — autotrade-exit-cursor-404-wedge-fix
  • /autotrade halt command — atomic mode='off' + exit_mode='notify' in a single SQL UPDATE; stops all automated order flow (entries AND auto-exits) while keeping exit visibility as Telegram alerts; breaker state intentionally untouched; resume = /autotrade live + /autotrade exits auto_sell.autotrade-halt-command
  • 3c-2 ROUND-2 validation pass (bug/DRY/redundancy/gap lenses — NOT the round-1 trade-safety lens) — found real issues the safety reviews missed; final re-review CONFIRMED SAFE FOR STAGED ARM (334 polymarket-fetch + 87 polymarket-data). LESSON: a flow can be structurally trade-safe yet BROKEN at a vendored value-validation boundary the safety lens never checks. B1 (BLOCKER, capital-trapping): vendored rs-clob-client-v2 Amount::shares(value) rejects any share amount with value.normalize().scale() > LOT_SIZE_SCALE(=2) — shares trade in 0.01 lots (types/mod.rs:193-200, re-applied order_builder.rs:25). auto_sell passed the RAW on-chain fill (locked vector 7.6219 shares, scale 4) straight to build_order → Amount::shares → build error → revert row to open → EVERY real fractional-share position unsellable (auto-exit AND /autotrade sell), capital trapped until resolution; round-1 checked assert_sell_amounts but not the deeper vendored lot guard. FIX e93dde1: sell_shares = filled_size.trunc_with_scale(2) used consistently for build_order + quantize_sell(expected_shares) + assert_sell_amounts; sub-0.01 dust held to resolution (return before mark_selling); BUY unaffected (Amount::usdc scale ≤6, market builder has no lot gate). 2E (Important, false risk-limit, 84e3e34): max_copies_per_event loaded/validated/shown in status but NEVER enforced — cohort members entering the same event each copied up to GLOBAL max_open; FIX = per-event_slug COUNT of live non-terminal rows in the reserve config-lock tx, reject past cap (0=unlimited). 2C/I3/1D (Important gaps, d4a2a85): settle now alarms when closing an open row still carrying exit_pending (cohort exit never completed → rode to resolution); exit cursor advanced to last-success detected_ts and permanently DROPPED an event sharing that timestamp → forward-only resume_cursor (halt resumes at first_failed−1µs); reconcile’s unbounded per-row CTF balanceOf sweep ran before entries each tick → LIMIT 25 oldest + visibility notify. DRY (efc69bc) + self-inflicted regression (0f06ec5): extracted collecting_notify/AutotradeDeps::market()/record_fill_then_open(FillOutcome), removed dead mark_dry_run/load_autotrade_order; the collecting_notify extraction regressed /autotrade sell’s reply — run_sell drained via Arc::try_unwrap while the notify closure held a 2nd Arc clone → always empty → SOLD/REJECTED/TIMEOUT swallowed (visibility only; sell executed; CI green bc nothing asserted output); final re-review caught it → lock()-drain + regression test. Deferrals unchanged (fee=0, cap-breach-reads-as-dedup, is_resolution_only unwired, timed-out-but-landed sell books pnl=0+alarm, ~30-min REST trigger latency). — autotrade-3c2-round2-validation
  • 3c-2 COMPLETE — operator handoff/consolidation note. LIVE submit + lifecycle + safety DONE on autotrade-foundation (NOT merged); 17 commits 3c2.1→3c2.10; gate green (320 polymarket-fetch + 87 polymarket-data); DEFAULT mode=off (3-layer enforced — prod no-op until armed). Links Task 5 breaker, Task 6 reconcile (CTF balanceOf primary truth, decide_entry pure table, derived ready gate, per-tick), Task 7 selling-state exits (auto_sell FAK, mark_selling open→selling CAS serializes triggers, per-order-mode authority — live exits even with kill-switch off, exit_pending missed-exit queue, 1-click Sell), Task 8 settlement (gamma winners → mark_resolved, idempotent no-POST), Task 9 exposure-cap TOCTOU re-check + operator alarms + arm-time cursor seed + dead_code cleanup. Final adversarial review (3 reviewers) — found+FIXED: (A BLOCKER) live BUY never POSTed (build_order book-priced vs quantized taker → assert bailed; FIX pin .price(plan.limit) on BUY / floor on SELL); (B) partial FAK orphaned residual+mis-booked PnL (FIX prorate_partial+record_partial_sell revert selling→open w/ reduced filled/cost + accumulated pnl); (C) stuck selling/filled never reconciled (FIX load_reconcilable_entries widened — selling recovers via balance, filled/partial mark_open retry). Re-review: SAFE FOR STAGED ARM. Deferrals: fee_usd=0 (fail-safe bias); cap-breach reported as benign dedup Skip (visibility gap); balance-0 stuck-selling books pnl=0 + alarm; STRATEGIC: first-mover trigger is REST-polled ~30min (mean ~15min lag) — likely erodes the edge, real-time on-chain OrderFilled WSS recommended before scaling past tiny caps. Includes staged-arm runbook (keys+RPC → dryrun → small caps → live → observe full lifecycle; /autotrade off stops entries but live positions still exit). — autotrade-3c2-complete-handoff
  • 3c2.9 validation carry-forwards (tasks b/c/d/e): task b SKIPPED (MarketData::resolve already fail-closed via token_for); task c SKIPPED (neg_risk stays a caller arg — async round-trip YAGNI); task d DONE (operator Telegram alarms at 3 sites in runner.rs + positions.rs::init_autotrade sig updated to accept &CopyConfig); task e DONE (exits cursor seeded to Utc::now() at first arm in arm_live). 315+87 tests pass.autotrade-3c2-validation-carryforwards
  • Task 7a — selling-state exit foundations: assert_sell_amounts (5 TDD tests) + shared assert_contract DRY extraction in execute.rs; 5 CAS order-status transitions (mark_selling/mark_sold/revert_selling/set_exit_pending/clear_exit_pending) + SellableOrderRow + 2 query functions (autotrade_sellable_for_origin, autotrade_exit_pending_open) in repo.rs. Zero-submit. Gate: 306+87 tests, 0 failures.autotrade-selling-state-foundations
  • Bug fix — breaker.rs lock-order scanner false-negatives from {/} inside string/char/raw-string literals; added neutralize_literal_braces char state machine (preserves byte length); removed spurious #[allow(dead_code)] from repo::bump_breaker_count (fully reachable via live submit chain); 3 regression tests; 285 tests pass.autotrade-breaker-literal-brace-fix
  • Bug fix — max_copies_per_event was loaded + validated but never enforced at order placement. Added field to EngineConfig + ReserveCaps; mapped in engine_config() (0=unlimited, negative clamps to 0); in-tx per-event-slug count query in try_reserve LIVE branch under existing config FOR UPDATE lock; caps_from_cfg() const fn + pre_lock_cap_skip() helper extracted to keep ingest_entry under 100-line Clippy limit. 2 new persistence tests. Gate: 327 tests, 0 failures. — autotrade-max-copies-per-event
  • Four behavior-preserving DRY refactors on autotrade-foundation (333 tests, EXIT 0). (1) collecting_notify() helper extracted in commands.rs — deduplicates Arc<Mutex<Vec<String>>> + closure from run_reconcile + run_sell. (2) AutotradeDeps::market(pool) method + build_autotrade_deps promoted to pub — replaces 3 verbatim ClobMarketData struct literals in runner.rs, positions.rs, commands.rs. (3) FillOutcome enum + record_fill_then_open async fn in execute.rs — shared by record_placed_fill + heal_filled in reconcile.rs. (4) Dead repo functions removed: repo::mark_dry_run (dry path inserts direct in try_reserve) + repo::load_autotrade_order (superseded by autotrade_sellable_by_id/load_reconcilable_entries); AutotradeOrderRow struct retained. — autotrade-dry-refactors-2026-06-16

2026-06-15

  • Task 5 — circuit breaker consolidation: bump_breaker + BREAKER_THRESHOLD extracted from execute.rs into new breaker.rs; SQL moved to repo.rs::bump_breaker_count (config-row-only FOR UPDATE tx); §3.4 lock-order regression test added (structural scanner reads all autotrade *.rs files on every cargo test; flags any fn where order-row lock precedes config-row lock). Pure refactor, zero behavior change. 282 tests pass, gate green.autotrade-breaker-module
  • Plan 3a Task 1 — place_order/cancel/get_order/open_orders/order_trades on SigningClient; KEY FINDING: SignedOrder has no Deserialize — spec’s serde round-trip cannot compile; fix = drop SignedPayload wrapper, hold signed_order: SignedOrder typed value directly in BuiltOrder; vendored API shapes verified (OrdersRequest/TradesRequest default constructors, Page.data, OpenOrderResponse/TradeResponse/PostOrderResponse field names+types, taker_order_id as CLOB order id, no order_hash on TradeResponse, cancel returns ()). Gate: 265 tests, EXIT:0.autotrade-signing-client-order-ops
  • Adversarial-review validation pass on category-cohort fallback merged as 11d4e9a. Six gaps hardened: fallback_semaphore (MAX=3) in BotState decouples the ~25s fallback from the 4-slot handler pool (prevents /consensus starvation); run_and_persist gains Option<Duration> timeout param and commits status='error' on timeout (no more 'running' row leaks); recent_category_consensus filters by top_n+min_overlap (param-matched caching, 3 call sites updated); derive_leaderboard_category checks Politics/GEO before Crypto (matches category_bucket precedence); HTML-safe reply truncation cuts at last newline within 4000-char limit (prevents split <a> tag rejection); DRY: polymarket_path+tags_contain_any extracted, CONSENSUS_CACHE_WINDOW_SECS shared. Gate: build+clippy -D warnings+fmt+tests green. — pmv2-bet-link-analyzer-flow
  • Category-cohort fallback IMPLEMENTED and merged (commit 22d7697, 2026-06-15). Updated pmv2-bet-link-analyzer-flow to reflect shipped state: trigger = !has_high_edge_signal; orchestration via resolve_fallback_category + category_consensus_section + run_and_persist (cache-warm seam) in pmv2/category_consensus.rs; wired in bot.rs::score_bet_link_body (now accepts chat_id/loading_id/reply_to for interim spinner); constants TOP_N=20 MIN_OVERLAP=3 TIMEOUT=25s DISPLAY_ROWS=8 CACHE_WINDOW=600s; cold-cache live path not yet unit-tested; spec §14 deferred items noted. — pmv2-bet-link-analyzer-flow
  • Bet-link analyzer → category-consensus flow + gotchas (verified architecture, branch main). Documented the posted-link auto-reply path (bot.rs::classify_messageMessageAction::BetLinkscore_bet_link_body bot.rs:611pmv2::scoring::score_bet scoring.rs:414 reads pmv2_positions cohort via load_bet_holdersformat_bet_scores_telegram scoring.rs:593), its TWO distinct no-signal states (scores.is_empty() “No edge-cohort positions…” scoring.rs:514 vs all-below-GAUGE_FLOOR=0.005 empty consolidate_gauge.sides “no high-edge cohort holders…” scoring.rs:472/379, consolidate_gauge empty-safe), and how that EDGE-scored cohort differs from /consensus <category>’s freshly-fetched monthly-PNL cohort (handle_consensus bot.rs:866, 9 leaderboard categories category_consensus.rs:13). Four gotchas captured: consensus cache only warms on status='done' (repo.rs:1601/1542); no scheduler warms it (usually cold); parse_selector scoring.rs:91 returns the LAST path segment = MARKET slug on deep /event/<event>/<market> links, so feeding it to gamma.event_tags() (EVENT slugs, client.rs:505) silently fails (prefer BetScore.event_slug); and the TWO category taxonomies (9 leaderboard cats vs coarse pmv2_event_category.category esport/sport/geo/crypto/other) bridged only by category_tags + GEO_TAGS/CRYPTO_TAGS. Draft fallback spec docs/superpowers/specs/2026-06-15-pmv2-bet-link-category-fallback-design.md noted as NOT implemented. — pmv2-bet-link-analyzer-flow
  • Task 6 — ERC-1155 balanceOf on-chain primitive (autotrade/onchain.rs, branch autotrade-foundation). alloy sol! generates balanceOfCall (camelCase + Call suffix); SolCall::abi_encode() for calldata; alloy_primitives::hex::encode (no separate hex crate); selector 0x00fdd58e (ERC-1155, distinct from ERC-20 0x70a08231); spike doc hex-decode error corrected (0x2c641ab6 = 744_757_942, not 744_144_054); binary-only crate requires --bin polymarket-fetch not --lib; polygon_rpc_url added to AppConfig; 3 pure unit tests, zero network calls; 256 total tests pass. — autotrade-onchain-balanceof
  • Task 3 — EIP-712 order hash implementation in polymarket-clob (lib.rs + Cargo.toml). OrderV2 non-exhaustive gotcha (sol! macro, FRU blocked — use field-by-field assignment on Default); alloy 1.6 import paths (alloy_sol_types::{Eip712Domain, eip712_domain, SolStruct}); Polymarket V2 domain params (name/version/chain_id=137/two exchange addresses); LazyLock<Address> with scoped #[allow(clippy::unwrap_used)] for const address parsing; verifying_contract must be const fn; alloy-sol-types must be an explicit workspace dep. — autotrade-eip712-order-hash
  • Task 7 — autotrade repo exit helpers (repo.rs lines 1270–1359, branch autotrade-foundation). 4 new async functions + 1 struct added to support T8 exit-detection: AutotradeExitEventRow (sqlx::FromRow, maps pmv2_position_events exit join result); autotrade_exits_cursor (reads 'autotrade_exits' from pm_detection_cursors, returns 0 if absent); advance_autotrade_exits_cursor (upsert via EXCLUDED.last_check_at, matches codebase style); autotrade_exit_events (kind=‘EXITED’, detected_ts > cursor, joined to pmv2_autotrade_orders); autotrade_open_orders_for_origin (fetches status=‘open’ orders by condition_id/outcome_index/origin_ref); autotrade_source_enabled (fail-closed bool from pmv2_autotrade_sources). All carry #[allow(dead_code)] until T8 wires them. Cursor key 'autotrade_exits' is separate from 'pmv2_exit_alert' (notifier) and 'overlap_watcher'. 253+87 tests, 0 failures, 0 clippy errors. — autotrade-repo-exit-helpers
  • Task 6 — sources.rs first_mover_core emitter + Tier::from_label + tier on CohortTrader (branch autotrade-foundation). Tier::from_label(s: &str) -> Option<Tier> case-insensitive parser in model.rs (Core/Proven/Emerging; does NOT change classification logic). CohortTrader gained tier: Option<Tier> field in positions.rs. latest_scored_cohort_with_scores in repo.rs now SELECTs t.tier TEXT and maps it via from_label (tuple gained Option<String> between avg_lost_bet_size and rank). New sources.rs: SOURCE_ID = "first_mover_core", autotrade_active(mode, enabled) -> bool (const fn — false if Off or not enabled), enter_signal(trader, position, conviction) -> TradeSignal (freezes reference_price = p.avg_price, NOT cur_price), format_dry_run_order. pub mod sources; added to autotrade/mod.rs. 3 new tests pass; total 352 tests, 0 failures, clippy clean (fixed missing semicolon in test match arm). — autotrade-sources-signal-emitter
  • T5 — PgPersistence + engine_config + order mappers (persistence.rs, branch autotrade-foundation). engine_config(cfg: &AutotradeConfig) -> EngineConfig converts BigDecimal/i32 DB types to f64/i64/usize; builds CopyConfig from autotrade’s OWN sizing fields, category_filter via parse_topbets_filter. PgPersistence<'a> implements Persistence: dry-run reservations insert with terminal status dry_run (excluded from dedup partial index + exposure queries); live inserts as pending. Dedup via ON CONFLICT (dedup_key) WHERE status NOT IN ('dry_run','closed','canceled','failed') DO NOTHING RETURNING id. FOR UPDATE lock on config row id=1 prevents double-reservation races. spend_today_usd counts only mode='live' rows. OrderMode::as_str/from_db + OrderStatus::from_db mappers added to orders.rs. BigDecimal conversion requires BOTH use bigdecimal::{FromPrimitive, ToPrimitive}. Transaction executor uses &mut *tx (sqlx 0.7+, same as repo.rs:392). TDD: tests written first, 249/87 tests pass, gate exit 0. — autotrade-persistence-engine-config autotrade-engine-impl autotrade-config-repo
  • Plan 3b Task 1 — binding vendored rs-clob-client-v2 into polymarket-clob crate. Import paths for U256/Unauthenticated/Client/Config; U256::from_str (decimal display, no from_str_radix); Client::new has no chain ID arg; bon builder returns struct directly; MarketResponse field types; four vendored example required-features fixes; clippy landmines from enabling "clob" feature (derivable_impls, single_option_map, unused_self, allowexpect). — autotrade-clob-crate-bindings pmv2-autotrade-engine-design
  • /autotrade Telegram command wired (commands.rs + bot.rs, branch autotrade-foundation). apply_set (pure validate+column resolver), format_status (BigDecimal fields display via {} directly), run_autotrade (async dispatcher → config::{load,update_numeric,set_text,set_source_enabled}), set_text_reply (private helper shared by SetMode/SetExit/Filter/Maker arms); "autotrade" arm added to run_command; owner-auth gate from authz.rs not duplicated (already present at top of match). apply_set_ok_and_errors test added → 3 tests pass; gate exit 0 (244 tests). Key gotcha: #[allow(clippy::unwrap_used)] required on test bodies (project has -D clippy::unwrap_used). — autotrade-commands-bot-wiring autotrade-config-repo autotrade-authz-module
  • autotrade config repo layer (config.rs, branch autotrade-foundation). AutotradeConfig (sqlx::FromRow, BigDecimal for NUMERIC, i32 for INTEGER) + load(pool) (fetch singleton id=1) + update_numeric (f64→BigDecimal via numeric() helper, same pattern as repo.rs) + set_text (nullable text) + set_source_enabled (updates pmv2_autotrade_sources) + update_field_sql (pure SQL-builder, #[must_use]) + validate_set (domain-bound check for Telegram command inputs). 5 unit tests pass, gate exit 0 (243 tests). Key: runtime sqlx::query_as::<_, T> (NOT compile-time macros — no live DB needed at build). — autotrade-config-repo pmv2-autotrade-engine-design
  • On-demand category resolver T3 (branch autotrade-foundation). read_event_category(pool, event_slug) in repo.rs (runtime sqlx, returns Option<String> from cache); resolve_category(pool, gamma, event_slug) in category.rs (cache-hit-or-fetch: reads cache → falls back to gamma.event_tags(&[slug])category_bucketupsert_event_category; returns Ok(None) if gamma doesn’t know the slug). Both carry #[allow(dead_code)] until T4/T8 wiring. Key gotcha: event_tags takes &[String] — bind let slugs = [event_slug.to_string()] before passing &slugs to avoid temporary lifetime errors. GammaClient imported from polymarket_data crate. No staleness logic — batch run_category_sync owns refresh. — autotrade-category-resolver autotrade-gates pmv2-autotrade-engine-design

2026-06-14

  • autotrade Plan 2 capstone — engine.rs implemented; all 8 tests pass (branch autotrade-foundation). ingest_entry wires the full gate sequence via injected traits: mode-check → liveness_gateprice_bounds_gateev_floor_gate → min-conviction → category_gatechase_gate → exposure-cap → try_reserve (single mutation point). Key decisions: EngineConfig drops Clone (CategoryFilter holds a HashSet); min_round_trip_edge_bps must be >200 in tests (ev floor: size × bps/10_000 > size × 0.02; bps=1 can never pass — test helper uses 300); ingest_entry takes &(dyn Trait + Send + Sync) for tokio multi-thread Send bound; category_gate passes None for now (Plan 3 wires real category resolution; eq: fails-closed, neq:/no-filter proceeds). Tests cover all skip cases + DryRunPlanned + LiveReserved. — autotrade-engine-impl pmv2-autotrade-engine-design
  • autotrade P2.4 — entry gate chain gates.rs implemented (branch autotrade-foundation). GateOutcome::{Proceed, Skip(String)} + 5 pure gate functions: liveness_gate (rejects resolved/closed/inactive/non-accepting/unknown-end-time markets and markets too close to resolution), category_gate (delegates to crate::pmv2::category::passes_filter; None filter = Proceed), price_bounds_gate (rejects outside [min_entry, max_entry]), ev_floor_gate (skip unless size × bps/10_000 > est_fee; #[allow(clippy::cast_precision_loss)] for i64 as f64), chase_gate (skip if ask exceeds reference_price × (1 + bps/10_000) — uses the price frozen at signal detection). Helper est_round_trip_cost_usd(size_usd) = size × ROUND_TRIP_COST_RATE (0.02). 9/9 tests pass, clippy clean. Gate script passes — prior exit=1 was rtk intercepting -- -D warnings (not a real clippy error). — autotrade-gates pmv2-autotrade-engine-design
  • autotrade P2.3 — MarketData trait + ResolvedMarket + token_for() outcome→token validation (market.rs, branch autotrade-foundation). MarketStatusView (copy-struct: active/accepting_orders/resolved/closed/secs_to_resolution) + ResolvedMarket (full snapshot: condition_id, outcomes, clob_token_ids, tick_size, neg_risk, status, bid/ask) + ResolvedMarket::token_for(outcome_index, outcome_label) (3-guard validator: length-match, in-range, label-match → returns clob token id). MarketData async trait (single resolve(condition_id, outcome_index) -> Result<ResolvedMarket> method). 4 tests pass. Gate gotchas: clippy::unwrap_used fires in test bodies (add #[allow]); cargo fmt --check sorts imports alphabetically; async-trait must be in the crate’s own Cargo.toml even if present in the workspace lock. — autotrade-market-data-trait pmv2-autotrade-engine-design
  • autotrade P2.1 — TradeSignal + SignalAction types implemented (signal.rs, branch autotrade-foundation). Canonical signal type that all autotrade sources emit. SignalAction enum: Enter { reference_price: f64 } (price frozen at detection, feeds the engine’s chase guard) + Exit. TradeSignal struct: source_id, action, condition_id, outcome_index, outcome_label, event_slug, slug, origin_ref, conviction, source_score, tier, instance. dedup_key() delegates to orders::dedup_key() (canonical 6-discriminator function stays in orders.rs). Tier re-exported from pmv2::model. 2 tests pass. Clippy fix: label() promoted to const fn, match arms use Self:: (use_self lint). — autotrade-signal-type pmv2-autotrade-engine-design
  • autotrade Task 2 — authz.rs module implemented (branch autotrade-foundation). Pure authorization layer: Scope (Public/Owner), Authz (Allowed/Denied), OwnerSet (parses TELEGRAM_OWNER_IDS CSV), authorize(), scope_for() ("autotrade" → Owner, else Public). Zero I/O, 6 passing unit tests, clippy pedantic+nursery clean. #![allow(dead_code)] is a temporary suppression until Task 3 wires the module into run_command. — autotrade-authz-module pmv2-autotrade-engine-design
  • ARCHITECTURE — pmv2 Auto-Trade Engine designed + spec LOCKED (docs/superpowers/specs/2026-06-14-pmv2-autotrade-design.md). The first system that writes REAL trades to Polymarket’s CLOB. A source-agnostic engine (seam = engine::ingest(TradeSignal)); first source = first-mover copy of Core-tier cohort traders. Execution: vendor+pin the official Polymarket/rs-clob-client-v2 Rust crate (alloy) into crates/polymarket-clob/ — the only place that signs; NO Python sidecar, NO hand-rolled signing. Wallet: Bandi PROXY 0xdb0d23c2c3468da13c6e8abfe96edfecc72d9efe, signatureType=1 (maker=proxy, signer=EOA 0xfa77…84f7); EOA key from terraform secrets, in-memory only; gasless off-chain order signing → NO relayer/gas (proxy allowances pre-set). V1/V2 LANDMINE: two live PM contract generations (different exchange addresses, EIP-712 domain version 1 vs 2, collateral USDC.e vs pUSD) — resolved at RUNTIME via preflight + a non-deferrable verifyingContract-vs-neg_risk assert, never hardcoded. Control: owner-only Telegram via reusable authz.rs (Scope::Owner, TELEGRAM_OWNER_IDS); OFF/DRY_RUN/LIVE kill-switch gating ENTRIES; exits governed by the per-order mode, not the live kill-switch. Safety: marketable-limit FAK + slippage/chase/EV/price-bound/liveness gates; hard caps (exposure/daily-spend/per-event/max-copies) computed with in-flight reservations under a config-row (id=1) lock; a selling reserve state + global lock order (config-before-order) preventing double-sell + ABBA deadlock; on-chain CTF-balanceOf reconciliation as ground truth; circuit breaker (entries-only). Phase-0 SPIKE mandatory before engine — pin amount-quantization (verbatim from the client’s roundDown/roundNormal, NOT “GCD of price”), order_hashorderID, and V1/V2 collateral; can’t be settled on paper. Isolation: frozen scoring/topbets/consensus/decide_copy math UNCHANGED — engine only consumes outputs (additive read-path touches: tier into cohort read, bet_confidence unconditional, new pmv2_position_events exit read). 3 adversarial review passes → LOCKED. — pmv2-autotrade-engine-design copy-trade-lane first-mover-overlap-watcher pmv2-two-tier-trader-scoring

2026-06-08

  • /topbets end-date 1970-01-01 fix. Symptom: /topbets showed ends 1970-01-01 for some markets. ROOT CAUSE: the data-api positions feed returns the epoch placeholder 1970-01-01 as the market endDate for markets whose end date lives on the gamma EVENT, not the market (market-level endDate is null → serialized as epoch). FIX (4 parts): (a) the category sync captures the gamma event endDate into a new pmv2_event_category.end_date column (migration 20260608000000; event_tags returns end_date; upsert_event_category stores it); (b) map_position runs normalize_end_date → drops empty/pre-2000 dates so 1970-01-01 becomes NULL; (c) the 3 cohort-holder queries COALESCE(p.end_date, ec.end_date) so epoch markets fall back to the real event date; (d) /topbets marks past-dated-but-held markets ends {date} (unresolved) (via end_date_seg using snapshot_ts). VERIFIED vs prod: kansas-governor → 2026-11-03, openai-ipo → 2026-12-31. DISTINCT from the prior correctness-pass end_date issue (date-only-as-UTC-midnight countdown). Scoring/ranking untouched (only end_date metadata + display). — topbets-enddate-1970-fix self-sync-held-only-fix correctness-pass-six-fix-commits pmv2-topbets-categories-filters
  • Self-sync “/wallets showed 0 positions” fix. Operator’s self-sync showed 0 positions despite holding several. ROOT CAUSE (SAME past-date class as the 1970 fix): repo::is_position_inactive(position, today) dropped any position that was redeemable OR past its end-date, so still-HELD bets on past-but-unresolved markets (elections) were filtered out. FIX: replaced with is_position_redeemed(position) = position.redeemable only; the self-sync now keeps currently-held positions (redeemable=false, size>0) regardless of end-date, dropping only resolved + dust — applied at BOTH the stored upsert_positions_for_wallet AND the live /wallets fetch_self_overlap_sections_live. DECISION: “currently-held only” (resolved bets stay hidden; widening to show settled trades is a one-liner). Cohort/scoring untouched. — self-sync-held-only-fix topbets-enddate-1970-fix correctness-pass-six-fix-commits
  • Reference pointers: scripts/gate.sh remains the canonical gate (build + test + clippy --workspace --all-targets -- -D warnings + fmt; bypasses the rtk -D-as-filename mangling) — already documented in ci. The bounce-target SHA history (deploy round-by-round) lives in AGENT_HANDOFF.md, not the vault. — ci

2026-06-05

  • HIGH-VALUE API DISCOVERY — Polymarket data-api has a per-CATEGORY leaderboard. 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 — arbitrary gamma tags (iran, elections, geopolitics) return empty/global. Param is case-insensitive (category=WEATHER/OVERALL both work; timePeriod=month/MONTH both work). timePeriod∈{day,week,month,all}, orderBy∈{PNL,VOL}. VERIFIED LIVE 2026-06-05: weather→ShyGuy1/HighTempTation ~467k, global(OVERALL)→Inaccuratestake $1.36M. Found by inspecting the Polymarket UI /leaderboard “All Categories” dropdown (route /leaderboard/{category}/{window}/{metric}). IN CODE: the polymarket_data Category enum (types.rs) ALREADY has all 9 + Overall, and leaderboard() already sends category=params.category.as_query_value() (UPPERCASE, works); pmv2’s cohort fetch uses Category::Overall. FALSE ALARM: a reviewer flagged the uppercase as a “lowercase bug” — the API is case-insensitive and prod already uses uppercase OVERALL, do not “fix” it. — category-leaderboard-api category-consensus pmv2-gamma-tags-categorization
  • Category consensus — fetch-based async /consensus <category> (replaced tag-based). /consensus <category> [N] [K] (defaults N=50, K=3). Validates one of the 9 leaderboard categories; serves a 10-min cached run if present; else instantly replies “running consensus algo for {category}…” and tokio::spawns a background job: (1) fetch top-N category leaderboard traders by monthly PNL, (2) fan out their positions (bounded buffer_unordered(16) + 30s per-wallet tokio::timeout), (3) filter to in-category bets via gamma event_tags, (4) consensus = group by (condition_id, outcome_index), ≥K distinct cohort wallets agree, rank by agreement then stake, top 25, (5) persist, (6) push. Tables (migration 20260605010000_pm_category_consensus.sql): pm_category_consensus_runs (status running→done/error, cohort_size, top_n) + pm_category_consensus_rows. Module crates/polymarket-fetch/src/pmv2/category_consensus.rs. NO edge/band — these fetched traders aren’t in pmv2_traders; signal = agreement-count + stake + leaderboard PNL. category→gamma-tag mapping: identity for most, but culturepop-culture, economics→{economy,economics}, mentions→NO clean tag (skip-filter fallback). REPLACED the earlier short-lived tag-based tag_consensus SQL query (consensus over the existing pmv2 cohort filtered by tag — removed). CAUTION: distinct from v1 wallet-consensus AND pmv2 cohort-overlap — a freshly-fetched, category-specific PNL cohort. Fan-out was 8/90s initially (a weather run took ~9.4 min, looked stuck) → tuned to 16/30s. Does NOT touch topbets/scoring. — category-consensus category-leaderboard-api pmv2-gamma-tags-categorization

2026-06-04

  • EXIT alerts — ”🔴 TRADE EXITED”. A Telegram alert when top cohort traders EXIT a bet. Reads the existing pmv2_position_events EXITED feed (logged each cycle by the position watcher); runs as a non-fatal scheduled step after the trending alert. Top-trader gated (live score > copy threshold), cursor-deduped via pm_detection_cursors detector pmv2_exit_alert, dust-filtered (≥$50 stake), Telegram-budget capped. Shows trader name, score/band/rank, market+outcome, and P/L (last seen) = the position’s last-snapshot cash_pnl. CAVEAT: exits are snapshot-detected so the real fill price is unknown — P/L is the last-seen unrealized cash_pnl, labeled “(last seen)“. Migration 20260604000000_pmv2_event_pnl.sql added cash_pnl/initial_value/current_value/cur_price/outcome/title columns to pmv2_position_events (captured in insert_position_event from the prior PositionRow, no call-site change). pmv2-exits CLI = read-only 24h preview. Module crates/polymarket-fetch/src/pmv2/exits.rs. — pmv2-exit-alerts first-mover-overlap-watcher pmv2-trending-bets
  • Bot startup ping — “NA CSUMI CSUMI CSUMI, bot STARTED, REF: “. On polymarket-fetch bot boot, sends a best-effort Telegram message NA CSUMI CSUMI CSUMI, bot STARTED, REF: <short-sha> (omits REF if no hash). A new crates/polymarket-fetch/build.rs captures the short commit hash at build time, trying env BUILD_GIT_HASHGIT_HASHGITHUB_SHAgit rev-parse --short HEAD→else empty (REF omitted). notifier::startup_message / build_startup_message (pure). Bot-only (scheduled-run untouched). GOTCHA for deploy: if the Docker image is built without .git and without one of those env/build-args, the deployed bot reports no SHA — pass GIT_HASH/GITHUB_SHA at image build to populate REF. — pmv2-bot-startup-ping polymarket-fetch-deploy

2026-06-03

  • MILESTONE — codebase-wide CORRECTNESS PASS + 6 fix commits (HEAD f42c7f4, all pushed), revalidated clean. Triggered by the operator spotting data inaccuracies in the Telegram bot → a 6-domain audit (scoring/confidence, gauge/display, rank, pipeline/watcher math, all SQL, bot/notifier/self-wallet) found ~50 issues; operator-visible + correctness-critical fixed across c9a4e12(Fix-A) 9b60e7f(Fix-B) e27e952(Fix-C1) d271cb5(Fix-C2) f42c7f4(Fix-D); follow-up revalidation confirmed all correct, no regressions/data-loss. OPERATOR-REPORTED (all fixed): (1) rank 9-vs-12 — display used COALESCE(rank_all,…) but the PM leaderboard UI defaults to the MONTH tabCOALESCE(rank_month,rank_week,rank_day) at all 5 query sites; (2) $0 dust lines — holder queries read pmv2_positions with no redeemable=false filter so resolved positions lingered as “held” with near-zero stakes rounding to $0redeemable=false AND initial_value>1 (GOTCHA: pmv2_positions/pm_positions keep resolved rows until the next prune — always filter redeemable when aggregating holdings); (3) “No — for what?”consolidate_gauge grouped by outcome_index alone, merging distinct candidates’ Yes/No → group by (event_merge_key, outcome_index) (strips date/year/integer tokens so date-variants merge but distinct names stay separate; KNOWN EDGE: strips the token “may” so a month-word candidate slug could mis-key); (4) **yungstalin 220k = NEUTRAL** — band is **SKILL** (`edge×consistency`) and ignored conviction → show `· N× avg` (stake/avg_bet_size); **KEY SEMANTIC: band=skill, N×avg=conviction, orthogonal.** **OTHER FIXES**: `copy_pnl` ÷0 (entry_price 0→NaN→copy stuck 'open'); `load_event_winners` random winner on cancelled/0-payout markets corrupting SCORING → require `payout>0`+deterministic tiebreak (SHARED winner path — see [[pmv2-pipeline-bug-audit]]); leaderboard names sent as unescaped HTML (Telegram `<>&` parse fail); `clamp_to_telegram_limit` byte-vs-char; pmv2 collect/positions made non-fatal in `execute_pipeline` (transient API error was aborting the whole scheduled run); `self_positions` redeemable filter (false overlap alerts on resolved); `user_name` subqueries `LIMIT 1` without `ORDER BY` (nondeterministic across 4 intervals); non-pmv2 `flatten_resolution_rows` missing dedup (#371-class `ON CONFLICT` crash). **SCORING-MATH (operator-approved, shifts ranking)**: `conviction()` default `0.5→0.0` (no history = no weight); `consistency_factor` returns 0 when overall edge ≤0 + dropped the 0.01 floor; `avg_won/lost/bet_size` now per-POSITION not per trade-fill. **MOP-UP (Fix-D)**: anonymous-sender rate-limit now denies; ex-cohort positions pruned via `delete_positions_not_in_wallets` DOUBLE-guarded against empty-set whole-table wipe (`<> ALL('{}')` is TRUE for all rows → empty-slice early-return is load-bearing); `event_id=None` copy-cap bucketed under condition_id; `mark_copy_resolved` idempotent; partial-fetch min-success floor (collect bails if >25% of fetches fail; `fetch_candidate_trades` distinguishes failure from genuine-zero). **DEFERRED**: ~3500-trade API offset cap truncates prolific traders' history (structural); date-only end_date parsed as UTC midnight (countdown ≤24h off). **PROCESS GOTCHA**: the rtk wrapper MANGLES `cargo clippy ... -- -D warnings` (passes `-D` as a filename) — run CI gate via full path `CARGO=(which cargo); “$CARGO” clippy —workspace —all-targets — -D warnings. @deploykeeping bot on:latest`; CRM asked to reconcile its dashboard against the same data points. — correctness-pass-six-fix-commits pmv2-pipeline-bug-audit pmv2-two-tier-trader-scoring first-mover-overlap-watcher bot-pmv2-native-v1-consensus-deleted ci
  • MILESTONE — bot fully pmv2-native; LAST v1 consensus code DELETED (commit ec75491 on main, net −156 lines). /wallets (bot) + CLI self-status previously showed a v1 “consensus alignment” per operator position from the frozen v1 tables (pm_selected_wallets/pm_safe_bets/pm_trader_metrics); replaced with a pmv2 cohort-overlap view — per position, how many edge-cohort traders hold the same (condition_id, outcome_index) + conviction-weighted bet_score, reusing the bet-link scoring path. New: CohortOverlap + overlap_by_position() in pmv2/scoring.rs; cohort_holders_for_conditions() in pmv2/repo.rs (mirrors load_bet_holders, batched WHERE condition_id = ANY($1)); a single shared fetch_self_overlap_sections helper feeds BOTH bot and CLI (DRY), best-effort (lookup error degrades to “no overlap”, never fails the command); rendered via render_overlap_cell (Telegram) + plain-text CLI cell. DELETED the entire v1 consensus chain: ConsensusAlignment, fetch_consensus_alignment, classify_consensus, fetch_safe_bets_by_market, fetch_raw_watched_consensus, alignment render helpers — grep confirms ZERO live consensus refs remain. Code-only: pm_* v1 tables left dormant (not dropped). v1 DECOMMISSION OVERALL: with this commit v1 is FULLY removed from CODE (earlier hard-cut removed pipeline/pm-arb/consensus engine; the overlap-watcher removed change_detection/participant_changes; this removes the last consensus display) — only dormant pm_* tables remain in DB. OPS INCIDENT (separate, 2026-06-03): two bot containers found running concurrently on the GCP VM — one on new code (overlap_watcher cursor advancing) + one on OLD pre-4794a76 code (participant_changes cursor STILL advancing every minute, a detector DELETED in 4794a76), spamming the operator with a v1 “watched wallets position dump”. Diagnosis via pm_detection_cursors: a detector that only exists in old code advancing = old container still live (same root-cause class as the Round-50 Telegram 409 getUpdates two-poller conflict). Fix is VM-side (kill stale container, leave one on HEAD), escalated to @deploy via AGENT_HANDOFF Round 52/52b. GOTCHA for future: after a bot deploy, verify only ONE container runs AND check old-code-only detector cursors FREEZE — a still-advancing deleted-detector cursor is the signature of a zombie container. Deploy target now ec75491 (Round 52b); 135 tests pass, clippy clean (3 pre-existing only), fmt clean. — bot-pmv2-native-v1-consensus-deleted first-mover-overlap-watcher pmv2-two-tier-trader-scoring xref-consensus-rejected-fresh-copy-wins polymarket-fetch-deploy
  • Gamma tags = the ONLY bet-categorization source (verified live 2026-06-03). Polymarket categorization comes from gamma /events?slug=<event_slug> → a tags array of {label, slug}; the /markets endpoint (GammaClient::market_metadata) returns category=null + no tags and is useless for this. Multi-slug batching (?slug=a&slug=b) works. Verified examples: MLB→sports/games/mlb/baseball; LA mayoral→politics/elections/…; IEM CS2→iem-cologne/sports/counter-strike-2/esports/major; crypto→crypto. GOTCHA: esports events are ALSO tagged sports → bucket precedence must be esport→sport→geo(politics/elections/geopolitics/world)→crypto→other or every esport mis-buckets as sport. — pmv2-gamma-tags-categorization
  • TRENDING feature shipped (2c05c5a..4906eea on main). Joiner-seed fix in run_pmv2_positions (is_new_entry: only emit ENTERED for wallets already in the prior snapshot, so cohort reshuffles can’t fabricate entry bursts); trending_bets velocity query (distinct high-score cohort wallets entering in a 6h window vs prior); /trending bot command + pmv2-trending CLI; acceleration auto-alert with 12h cooldown dedup (pm_trending_alerts), Telegram-size-guarded to avoid an infinite re-alert loop (an over-limit unsent alert never records its dedup row → re-detects forever). — pmv2-trending-bets
  • /topbets categories+filters+price+multi-date shipped (5c933a5..908824f on main). pmv2_event_category cache populated by run_category_sync (gamma /events tags, best-effort, bounded per-chunk tokio::timeout); eq:geo / neq:sport,esport filters; entry→current price display (52¢ → 58¢); multi-date fix in load_bet_holders (resolve selector → event_slug set, return all sibling markets). CAVEATS: broadens multi-candidate events (e.g. a nomination race) to the whole event; category filters are inert until the cache is primed by a scheduled run. — pmv2-topbets-categories-filters
  • Dev-workflow: added scripts/gate.sh (build + test + clippy --workspace --all-targets -- -D warnings + fmt). rtk’s Claude-Code hook mangles cargo … -- -D warnings at the top level (passes -D as a filename) but NOT when cargo runs inside a shell script — the script bypasses the mangling. Allow-listed locally so it stops prompting every run. — ci

2026-06-02

  • FIRST-MOVER OVERLAP WATCHER SHIPPED (commit 4794a76 on main, +932/−2381, migration 20260603000000_pmv2_overlap applied to prod). Reframed the v1 self-wallet consensus watchers as first-mover OVERLAP detection on the pmv2 pipeline. Architecture (Approach A — event-log join): run_pmv2_positions is now the source of truth for cohort ENTER/EXIT, writing an append-only pmv2_position_events feed; detection + event logging are DECOUPLED from the copy-enabled gate (new should_detect(prior_len)=prior_len>0), only the paper-copy ACTION stays gated on pmv2_copy_config.enabledevents populate even with the copy lane OFF. New detect_exited + the snapshot is now a true current-state mirror (each cycle DELETEs cohort positions absent from the fresh fetch) but SCOPED to wallets that successfully fetchedfetch_positions now returns (rows, succeeded: HashSet) so a timed-out wallet can’t fire false exits or mass-delete the snapshot. Events are denormalized at log time with run_id + trader_score + avg_won_bet_size + avg_lost_bet_size (events outlive cohort membership; avoids a fragile “which run’s score” join). New module overlap_watcher.rs: cursor-based (pm_detection_cursors key 'overlap_watcher') join 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 intentionally NOT alerted; dedup via pm_overlap_alerts UNIQUE(event_id,self_wallet); alerts carry the trader’s confidence score + avg won/lost bet sizes (user requirement). Operator side: new-position alert = prior-vs-current self-snapshot diff (repo::self_new_positions, scoped to pm_self_wallets + latest two self_snapshot runs, first-run suppressed) → ACTIVE notifier (self-sync start/complete summaries stay SILENT; run_self_sync_for_scheduler now takes both notifiers); self_trade_changes repointed off the stale pm_trades to the live DataApiClient::trades. Removals: change_detection.rs + consensus_participant_changes.rs deleted, consensus-only repo/notifier surface removed, pipeline + bot daemon repointed (overlap watcher replaces change-detection + participant-changes everywhere). Migration (forward ALTERs on the LIVE pmv2_position_events): dropped the per-key UNIQUE (append-only enter→exit→re-enter lifecycle), kind CHECK → ('ENTERED','EXITED'), added run_id/trader_score/avg_won_bet_size/avg_lost_bet_size, created pm_overlap_alerts. GOTCHA: the Postgres-generated unique constraint name was TRUNCATED to ..._outcome_inde_key (not ..._index_key) → always look up real constraint names via pg_constraint before DROP. GO-LIVE (important): unlike the copy lane, the overlap/new-position/self-trade alerts are NOT default-off — they go live the moment the binary deploys; the overlap gate uses pmv2_copy_config.score_threshold which defaults to 0 → ALL scored cohort traders qualify → noisy → set score_threshold>0 (e.g. 0.03–0.05) before/after deploy; the paper-copy lane stays independently enabled=false. LOOSE END: self-status CLI + bot /wallets still render v1 consensus alignment from the frozen pm_selected_wallets (safe-but-stale); the consensus chain (fetch_consensus_alignment/fetch_raw_watched_consensus/ConsensusAlignment/classify_consensus/fetch_safe_bets_by_market) was intentionally KEPT because deleting it breaks self-status — reframe/drop in a future pass. Verification: subagent-driven (12 tasks), per-task gates + spec/quality reviews + final integration review; 0 test failures, clippy only the 3 pre-existing scoring.rs warnings, fmt clean. — first-mover-overlap-watcher pmv2-two-tier-trader-scoring pmv2-pipeline-bug-audit xref-consensus-rejected-fresh-copy-wins copy-trade-lane
  • pmv2 PIPELINE BUG AUDIT — 4-reviewer pass before enabling the first-mover watcher; 1 real latent live bug + 3 watcher-hardening fixes + 1 sharp architectural constraint. 40 raw findings over collect/score → positions/watcher → resolve → orchestration; most “Critical/High” were over-claimed and triaged out. THE ONE REAL LATENT LIVE BUG: upsert_market_resolutions/flatten_resolution_rows (crates/polymarket-fetch/src/pmv2/repo.rs) had NO dedup before an ON CONFLICT (condition_id, outcome_index) DO UPDATE chunked insert → a duplicate key in one chunk crashes with ON CONFLICT DO UPDATE command cannot affect row a second time — the EXACT crash class that killed scheduled-run #371 (which was the positions table). Fixed with dedup_resolution_rows (keep-last, order-preserving) — behavior-identical to what DO UPDATE converges to. DURABLE GOTCHA: any chunked DO UPDATE upsert in this codebase MUST dedup its input by the conflict key first (2nd occurrence of this class). SHARP ARCHITECTURAL CONSTRAINT: repo::load_event_winners is SHARED — feeds BOTH the watcher resolution/PnL pass (positions.rs) AND the validated edge/consistency trader scoring (mod.rscompute_trader_metrics); the 50/50 payout-DESC tiebreak + NaN-drop guard MUST NOT be changed casually or every trader’s edge shifts and the validated scoring breaks (any winner-selection change ⇒ re-validate scoring). 3 WATCHER-HARDENING FIXES (watcher default-OFF, unreleased): (1) load_copy_config was an unconditional fetch_one before the enabled-check → a missing singleton row errored the ENTIRE positions snapshot; fixed → fetch_optional + CopyConfig::disabled() default; (2) insert_position_event swallowed errors via .ok() + no unique constraint → silent audit-log loss + duplicate ENTERED rows on overlapping runs; fixed → tracing::warn on error + UNIQUE(proxy_wallet,condition_id,outcome_index) + ON CONFLICT DO NOTHING; (3) per-event cap bypassable within one cycle (event_cap_used read only committed DB rows → N new same-event entries each saw used=0 and all inserted, blowing past per_event_cap_usd); fixed with an in-flight HashMap<event_id,f64> accumulator + a pure within_event_cap helper + AND status='open' so cap = live exposure (resolved copies no longer count). OVER-CLAIMED / NOT BUGS (don’t re-investigate): “n=1→consistency=0” is unreachable (min_resolved=50 floor); “consistency_factor deviates from min(early,recent)” is wrong — the clamp(min/max(overall,0.01),0,1) ratio form is correct BY DESIGN so long_term=consistency×span stays in [0,1] keeping the multiplier in [0.5,1.0]; negative-edge→negative-score is intentional (ranks losers last); NaN-in-sort impossible (scores are NUMERIC(6,5)). DEFERRED (real, out of scope): sizing-formula tuning (score_weight=score·10+1 makes base_usd a near-no-op above score0.4, all clamps to max_size_usd) → tune at enable-time; no per-wallet tokio timeout on fetch_candidate_trades (only reqwest-level) → resilience pass; no min-success floor (mass-timeout run completes with a silently degraded cohort); multiple DB pools per run (Supabase connection budget); overlapping 30-min runs (runs take 11–23 min, dedup guard prevents simultaneous starts only). STATE: all fixes behavior-neutral to scoring; gates green (164 tests, clippy clean except 3 pre-existing, fmt clean); NOT YET committed; watcher migration 20260602030000_pmv2_watcher.sql edited (added UNIQUE) but NOT applied. — pmv2-pipeline-bug-audit pmv2-two-tier-trader-scoring xref-consensus-rejected-fresh-copy-wins copytrade-shipped
  • MILESTONE — v1 → pmv2 HARD CUT COMPLETE. pmv2 is now the entire system; the legacy pm_* pipeline is fully decommissioned in code. ~27,000 lines removed across 4 commits on main: 96104b9 delete the pm-arb on-chain copy-lane crate (−12,087); 7b3132d backtest/short-term/wallet-monitor/persistence analysis (−6,296); 0befd3b consensus/ranked/safe-bets CLI + /safebets //ranked //snapshot bot commands (−1,297); 5ffc330 repoint scheduled-run to pmv2 + delete the consensus/safe-bets/trades pipeline + polymarket-data consensus.rs/safe_bets.rs + dead repo/output/notifier surface (−7,440). New binary surface (only these remain): migrate, resolutions-sync, telegram-, self-, scheduled-run, schedule-, bot/bot-, pmv2-collect / pmv2-positions / pmv2-score-bet. The bot lost /safebets //ranked //snapshot and gained the bet-link auto-reply (paste a Polymarket link → score_bet reply). scheduled-run is now pmv2: resolutions-sync → pmv2-collect → pmv2-positions → self-wallet/resolution/change-detection (self-wallet tracking retained); pmv2 is the scheduled algo. Code-only cut — pm_* prod tables left in place (dormant, NOT dropped). Build/clippy/fmt green; polymarket-fetch 151 tests + polymarket-data 99 tests pass (3 pre-existing pedantic clippy nits remain in pmv2/scoring.rs, non-blocking). GOTCHA avoided: a stray edit to the already-applied 20260530000100_pm_paper_trades_mid_history.sql was accidentally committed then reverted — would have tripped the sqlx checksum on startup migrate; never edit an applied migration. Deploy handed to @deploy via AGENT_HANDOFF.md Round 49: deploy 5ffc330, decommission apps-pm-arb-validate (ends the rounds-44–48 copy-lane validation) + apps-polymarket-fetch-wallet-monitor, redeploy the bot. STATUS: deploy + the bet-link live paste-test still PENDING.pmv2-two-tier-trader-scoring copytrade-shipped copy-trade-lane polymarket-fetch polymarket-fetch-deploy
  • pmv2 PHASE 2a SHIPPED — per-bet scoring engine (the Tier-2 read-side) built, reviewed & validated in prod (2026-06-02). New pmv2-score-bet --bet <condition_id | market-slug | Polymarket URL> CLI + pmv2::scoring module (score_bet, group_into_bet_scores, parse_selector, bet_confidence-based BetHolder::conviction, format_bet_scores) + repo::load_bet_holders / latest_position_snapshot_ts. Read-only (joins pmv2_positions ⋈ pmv2_traders), no migration. Built TDD (26 pmv2 tests incl. multi-version grouping fan-out) + reviewed. Same single-bet-input function the future Telegram link-check AND the Phase-2b poller will call — one scoring entry point, three callers. Scoring: per held bet bet_score = Σ trader_score × bet_confidence(stake vs the trader's own won/lost averages), grouped per (condition_id, outcome_index); event URLs/slugs fan out to all variant markets (multi-version, ≈3.3 markets/event). Validation (read-only prod run): all three selector forms returned identical output and matched the earlier ad-hoc demo (bet_score 0.19870). Example — MSTR “sells BTC by May 31”: NO side 16 edge holders (0.199) vs YES 4 (0.024); a $849k NO whale with a NEGATIVE trader_score correctly DRAGS the score downskill-weighted, not money-weighted, as Tier-1 intends. Output is honestly labeled “current cohort EXPOSURE (consensus), not a first-mover signal” (per the 2026-06-01 first-mover decision) + shows snapshot age so a stale snapshot can’t masquerade as live. GOTCHA: an ad-hoc left(slug,40) in an earlier diagnostic truncated the slug (...by-may-3 was really ...by-may-31-2026) — match on the full slug/condition_id, never a truncated display render. Status: Phase 2a done. Remaining = (1) the Telegram link-handler (wire score_bet to the bot for on-demand link checks); (2) Phase 2b (poller → delta detection on pmv2_positions → auto-score → alerts + paper copies + resolution PnL).pmv2-two-tier-trader-scoring
  • pmv2 copy-lane PHASE 1 SHIPPED — positions snapshot built & run in prod (2026-06-02). New pmv2-positions command + pmv2_positions current-state table (migration 20260602020000) snapshot the cohort’s open event positions. First prod run: 35 cohort wallets → ~2,546 open event positions across 32 traders and 757 distinct events (≈3.3 markets per event) — confirms the multi-version/variant reality the design accounts for (one event spans several related markets; positions aggregate at event level). Detection model = POLL the Polymarket Data API positions endpoint, NOT on-chain WSS (a deliberate divergence from the legacy on-chain OrderFilled copy lane). Built TDD, reviewed (spec + code-quality lenses), migration applied. Status: Phase 1 done. Next = Phase 0 (purged backtest) and/or Phase 2 (poller → Telegram alerts + paper copies + resolution PnL) still to build.pmv2-two-tier-trader-scoring
  • GOTCHA — proxied reqwest clients need connect_timeout, not just timeout. Symptom: pmv2-positions hung ~75 min silently at --concurrency 8zero rotation/retry/eviction warns, no completion. A proxy that accepts the TCP connection then stalls the HTTPS CONNECT tunnel hangs past the total .timeout(30s) (the total-request timeout doesn’t bound a stuck connect). Endpoint/proxies/data were all fine — red herrings ruled out one by one. Fix (defense-in-depth): .connect_timeout(10s) on the shared build_http_client (covers DataApiClient + GammaClient) and on build_proxy_client, plus a per-wallet tokio::time::timeout(90s) in fetch_positions, plus lowered the positions default --concurrency 8→4. Honest caveat: no deterministic repro (would risk another long hang), so the two timeouts are defense-in-depth — a silent hang is now impossible regardless of root cause. Durable lesson: always set BOTH timeout and connect_timeout on networked/proxied clients; bound fan-out fetches with a per-item timeout; watch for the “silent stall, zero warns” signature (it points at a stuck connect, not retries/rotation). — pmv2-two-tier-trader-scoring
  • pmv2 Tier-1 edge-scoring — FIRST PROD RUN validated (run 3, 2026-06-02). The edge ranking reshuffled the board exactly as designed: favorite-riders with high W/L but ~zero/negative edge SANK — ArmageddonRewardsBilly (0.962 W/L, 0.96 entry, edge +0.003) fell #3→#21; “The Spirit of Ukraine>UMA” (0.890 W/L, 0.90 entry) has NEGATIVE edge −0.007 → #27; kinderSman (0.935 W/L, 0.93 entry) edge ~0 → #25 — while edge-pickers ROSE: Dropper (just 0.723 W/L, 0.64 entry, edge +0.081, 453-day span, consistency 0.81) → #1, betwick (only 0.585 W/L, 0.54 entry, edge +0.042, 377-day span) → #3. KEY FINDING — the leaderboard is NOT a skill list: across the 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. Most top-PnL traders are favorite-riders / price-traders, not resolution-pickers — which directly validates the design premise AND the user’s buy-and-hold-to-resolution strategy (edge = result − entry_price measures exactly that). Implication: the Tier-2 copy-worthy cohort is small & sharp (~top 5–10), to be gated on an edge/trader_score threshold, not the whole board. Diagnostics (clean): 0×429 at concurrency 16 (proxy rotation clean); min-50 floor dropped 15 of 50 collected traders as noise (qualify gate working); lone error was the benign gamma empty-batch (25 markets skipped). CONFIRMED LIMITATION: trade history is hard-capped at 3500 trades/wallet by an API offset ceiling — NOT --trades-max-pages (bumping to 100 changed nothing); ~14 heavy traders scored on their most-recent 3500 trades only. Span/long-term integrity sound: 0 traders have exactly-zero span; short spans are real burst-bettors correctly given low long_term. OPEN ITEM (before Tier-2): wallet addresses with a numeric -<timestamp> suffix recur — 0xcF609D…-1771809916847 at #13 (n=1293), 0xE32F…possible sub-account vs a parse/join quirk affecting trader identity (and thus which entity Tier-2 copies); must resolve before wiring the edge-selected cohort into the copy lane. — pmv2-two-tier-trader-scoring copytrade-shipped purged-backtest-gate-no-edge

2026-06-01

  • DECISION — pmv2 Tier-2 = FIRST-MOVER, not consensus (user decided 2026-06-01, reversing their earlier consensus push). Resolves the prior “Honest open question” on pmv2-two-tier-trader-scoring in favor of the verified evidence. (1) Act on the FIRST high-edge (high-Tier-1) trader’s new event bet; do NOT wait for a quorum — aligns Tier-2 with xref-consensus-rejected-fresh-copy-wins (first cohort BUY K1 = +7.59%, K2/K3 progressively worse; consensus is structurally late). The summed-over-holders single_bet_score consensus framing is dropped as a trigger. (2) bet_confidence is repurposed as the position-SIZING input for the first-mover copy (this bet’s size vs the trader’s own won/lost averages → larger copy when a high-edge trader bets big for them), NOT a multi-trader trigger. (3) Likely execution path: reuse the existing on-chain copy paper lane (copytrade-shipped / copy-trade-lane) — already a first-mover detector on Polygon — by feeding pmv2’s edge-selected cohort into it and adding conviction sizing, rather than a from-scratch build (wiring to be verified at design time). (4) Still must clear the purged + embargoed harness vs first-mover/random before real money (purged-backtest-gate-no-edge); the now-open validation question is narrower — does the EDGE-selected cohort match or beat the old PnL-selected one under that harness. Note’s danger/open-question callout converted to a resolved-decision callout (original preserved foldable for the record). — pmv2-two-tier-trader-scoring xref-consensus-rejected-fresh-copy-wins purged-backtest-gate-no-edge copytrade-shipped copy-trade-lane
  • DESIGN — pmv2 two-tier trader-scoring engine (decided 2026-06-01). pmv2 is a from-scratch rebuild (pmv2_* tables); Step 1 (leaderboard collection across Day/Week/Month/All, intersection of traders in ≥2 intervals, self-contained resolutions) already shipped — this is Step 2: scoring. Why raw W/L is useless: it’s favorite-biased (0.96 W/L = bets heavy favorites, not skill) AND sample-blind (a 5-0 / even 1-0 trader ties an 800-bet pro at 1.000). Tier 1 — trader_score (per trader, stored on pmv2_traders): qualify gate resolved_bets >= min_resolved (default 50, else unscored/sinks); skill base edge = mean(result − entry_price) over resolved event bets (result∈{0,1}, entry_price = volume-weighted avg BUY price) — chosen over W/L to strip favorite-bias; long-term weighting = track-record CONSISTENCY (split bets early/recent halves by time, min-of-halves edge held vs overall, 0 if either half non-positive; span_factor=min(1,active_days/365); long_term=consistency×span_factor); trader_score = edge × (0.5 + 0.5·long_term) — long-term as a 0.5–1.0 trust multiplier (never zeroes a real edge, ranks durability above flashes). Tier 2 — single-bet score (per candidate bet, signal-time, NOT stored): Σ over qualifying holders of (trader_score × bet_confidence) — the consensus mechanism; bet_confidence = where the bet’s size sits between the trader’s avg LOST-bet and avg WON-bet size (clamped 0–1), a conviction proxy. GOTCHAS: (1) avg bet sizes are TRADE-based (each BUY = a bet, averaged per trader), NOT per-position, NOT first-buy-only — corrected after two mis-scopes; (2) bet_confidence measures conviction not profit (bigger bets correlate with favorites/price, edge is flat across size) → must be a multiplier on an edge-positive bet in Tier 2, never a standalone trigger; (3) long-term weighting must multiply the edge base, not raw W/L — else “long successful” decays into “long bet favorites”; (4) cross-run persistence is a future longevity signal that accrues for free since every run is tagged with run_id. OPEN QUESTION: Tier-2 is a soft consensus signal, and xref-consensus-rejected-fresh-copy-wins REJECTED a quorum gate (more agreement → worse ROI, first-mover K1 +7.59% wins) — but Tier-2 is edge+conviction-weighted ranking, not a raw headcount timing gate, and that rejection was EVENT-only; must validate against the purged harness (purged-backtest-gate-no-edge) and beat first-mover/random before using as a trigger (vs a ranking/sizing input). STATUS: Tier-1 being implemented now (migration 20260602010000_pmv2_scoring.sql adds edge/consistency/span/avg-bet columns); Tier-2 next (design pending: where candidate bets come from — likely current open positions of top-scored traders). — pmv2-two-tier-trader-scoring xref-consensus-rejected-fresh-copy-wins purged-backtest-gate-no-edge selector-vs-skill-gap best-consensus-algo-design copy-trade-algorithm
  • CENTRAL FINDING — cross-reference CONSENSUS is REJECTED, but FRESH single-wallet copying WINS (independently verified twice via the leakage-clean purged backtest; negative control ≈0 across all cells, max |ROI| 1.31% within ±2%). Same purged + embargoed walk-forward as purged-backtest-gate-no-edge, extended with a backtest --xref co-occurrence mode and run on EVENT markets only (updown excluded, 3 disjoint 15-day folds, 200bps slippage). Pooled ROI: xref-K1 (copy the FIRST cohort BUY per event-market outcome) = +7.59%, positive in ALL 3 folds (+6.7% / +9.4% / +8.4%), 34,957 fires; xref-K2 = +5.3% to +6.6% (window-dependent) — WORSE than K1; xref-K3 = ~−0.4% to −3.9%; xref-K4 = noise (~50 fires); event-only all-watched +2.39%, random +0.93%. Monotonic: more agreement (higher K) → WORSE ROI; tighter window doesn’t help. INTERPRETATION: requiring consensus is structurally LATE — by the time K top traders have all bought, the price has already moved, so you copy at a worse entry; the first informed entry is the well-priced one, agreement is the market catching up. So speed/earliness beats agreement. GOAL STATUS: the goal (copy skilled/insider traders) is VALIDATED (+7.59% vs +0.93% random); it’s the cross-reference MECHANISM that failed. BINDING CAVEAT: +7.59% is an UPPER BOUND — measured at the cohort’s own trade price with flat 200bps slippage; K1’s edge IS the early entry price (the hardest to fill — their informed buy moves the price before a copier can act), so tradeability is UNPROVEN until validated at the real ask price at detection + detection latency (the on-chain copy-trade-lane already captures cohort_price, detect_latency_ms, up_mid_30s_ago/up_mid_60s_ago for exactly this). PRODUCT IMPLICATION: vindicates the existing fresh single-wallet copy lane (copy on any one cohort fill, fast); refutes adding a consensus/quorum gate to it — the “best consensus algo” outcome is no consensus gate, copy the first mover, fast. IMPLEMENTATION (all uncommitted): backtest.rs::detect_xref_copies + xref strategies (K1..K4), cli.rs --xref, output.rs::render_xref, TDD on the co-occurrence detector. — xref-consensus-rejected-fresh-copy-wins best-consensus-algo-design purged-backtest-gate-no-edge consensus-validation-leakage copy-trade-lane
  • PURGED WALK-FORWARD VERDICT: the recommendation gate has NO edge — and is ANTI-SELECTIVE. The leakage-clean re-derivation that consensus-validation-leakage demanded is done, verified independently via the now leakage-clean truth-meter (negative-control ≈0). Purged + embargoed walk-forward (45d cutoff, 200bps slippage, 3 disjoint 15-day folds), pooled ROI: gate (recommended) −2.78% · all-watched +3.68% · random +2.75% · raw-pnl top-20 (purged) −3.24% · negative-control −0.04% (≈0 → leakage-clean). The gate loses to the baselines in 2 of 3 folds → the current PnL/copy_score recommendation is mildly anti-selective (picks worse-than-random trades). The old (leaky/unpurged) −43.37% gate was a LEAKAGE ARTIFACT, not a real loss — confirms the “−44% is provisional” caution in consensus-validation-leakage. CAVEAT: baseline ROIs use last-trade price as a mid proxy → UPPER BOUNDS (real fills worse); the robust finding is the RELATIVE ordering (gate < random < all-watched), not the absolute magnitudes. Phase 2 bar: any new signal (skill-gated selector, log-odds/independence aggregator) must BEAT ~+2.75% random under this same purged harness to be worth shipping — operationalizes the “required baseline arm” in best-consensus-algo-design. PERF FIX: covering index idx_pm_pos_condition_enddate ON pm_positions (condition_id, end_date) turned fetch_market_end_dates from ~459s → ~8s index-only scan (migration 20260601120000_pm_positions_condition_enddate_idx.sql; applied CONCURRENTLY to prod, reversible) → full purged+control backtest now runs ~2min. — purged-backtest-gate-no-edge consensus-validation-leakage best-consensus-algo-design selector-vs-skill-gap copy-trade-algorithm
  • Phase 1 consensus rebuild — implemented & verified (UNCOMMITTED working tree). Verified green this session: cargo fmt clean, cargo clippy no issues, WS1 13 tests pass, WS2 4 tests pass. WS1 consensus hygiene (crates/polymarket-data/src/consensus.rs): new compute_consensus_groups_with(members, min_wallets, now, dust_floor_usd); public compute_consensus_groups delegates with Utc::now() + DEFAULT_DUST_FLOOR_USD=5.0. New eligibility filter drops redeemable, past-end_date (parsed %Y-%m-%d then RFC3339; unparseable kept + tracing::warn!), and dust (current_value < 5.0). Bucket key (condition_id, outcome_index)(condition_id, outcome_index, asset) so negative-risk/opposite-asset positions can’t inflate a long outcome’s wallet_count. WS2 daily heavy-sync (crates/polymarket-fetch/src/scheduled_run.rs + main.rs + repo.rs): scheduler now runs short-term-markets-sync + short-term-accuracy in dependency order (trades→resolutions→short-term-markets→metrics→short-term-accuracy), gated ≤ once per 24h via a DB-backed gate (heavy_sync_is_due reads max(computed_at) from pm_short_term_trader_accuracy) — DB-backed because the scheduler is a fresh process each ~30-min tick so an in-memory timer wouldn’t persist; the 30-min consensus_refresh path is unchanged. Two follow-ups: (1) DRY — parse_end_date now duplicated in consensus.rs and repo.rs across crates → hoist into polymarket-data; (2) behavior change to watch — asset-in-key + $5 dust floor will reduce consensus group counts (correctly dropping dead/dust/synthetic-short). First concrete code response to consensus-validation-leakage / selector-vs-skill-gap. — phase1-consensus-rebuild-implemented consensus-validation-leakage selector-vs-skill-gap best-consensus-algo-design
  • GOTCHA: lag-probe capture tests are flaky under parallel load. crates/lag-probe/src/capture.rs tests load_grids_buckets_events_onto_grid and load_grids_skips_blank_and_malformed_lines PASS in isolation but FAIL under full-workspace cargo test — the temp file is named with process_id + timestamp_nanos (capture.rs:150) so concurrent tests race on the path (remove_file().unwrap() panics; parsed-count assert fails). Pre-existing — NOT from Phase 1 (lag-probe untouched this session). Fix later with the tempfile crate / unique dirs. Logged so a future full-workspace run doesn’t mistake it for a regression. — lag-probe phase1-consensus-rebuild-implemented
  • Documented the best-consensus-algo design (Phase 2+ target) — synthesis of 3 R&D lenses. Replace the count>=2 agreement rule with a skill-weighted LOGARITHMIC OPINION POOL (geometric mean of odds) over the FRESH (built from pm_trades trade_time, market-time decay) forecasts of INDEPENDENT skilled wallets. Independence = effective-N via Kish design-effect + eigenvalue participation ratio over a co-entry/Jaccard wallet-dependence matrix. Weights = EB-shrunk edge_lower with a hard per-wallet CAP (to neutralize the one +30σ wallet) + a bounded tanh stake-conviction term. Output calibrated p_agg; edge = p_agg − cur_price. Trade only when residual edge survives fees + √-impact slippage AND skilled money isn’t split (disagreement → abstain). Sports noise dissolves for free — sports wallets have ~0 edge_z, so EB-shrunk weighting zeros them (no keyword filter needed). Honest caveats: edge is thin/concentrated (maybe 1–2 wallets), and the forecast-combination puzzle means a simple equal-weight log-pool of edge_lower>0 wallets may beat the fancy version → that simple pool is a REQUIRED backtest baseline arm. Full spec: docs/superpowers/specs/2026-06-01-best-consensus-algo-design.md. — best-consensus-algo-design selector-vs-skill-gap consensus-validation-leakage short-term-accuracy
  • Flagged that consensus validation evidence is contaminated by lookahead/survivorship (multi-agent re-validation). The headline edge numbers are PROVISIONAL, not proven: the −44% copy-gate backtest ROI and the short-term skill persistence result (Spearman 0.53 / p=0.025 / 12 of 51 wallets edge_z>2) both rest on contaminated evidence. Three leaking code paths: (1) crates/polymarket-data/src/consensus.rs:30 (compute_consensus_groups) aggregates LIVE positions (current_value/avg_price) — post-entry + survivorship, not as-of-trade; (2) crates/polymarket-fetch/src/backtest.rs (build_wallet_data) + crates/polymarket-data/src/metrics.rs:132 (pair_trades_to_resolved) pair trades to resolution payouts across the train/test cutoff → label leakage; (3) crates/polymarket-fetch/src/repo.rs:1429 (fetch_short_term_bet_rows) has NO time filter → survivorship + lookahead. Remedy (López de Prado): reconstruct the signal point-in-time from pm_trades; add a real resolved_at (pm_market_resolutions only has fetched_at); purged+embargoed CPCV; a negative-control shuffled-label sentinel; deflate by Deflated Sharpe Ratio (many variants will be tried). The one sound piece to keep = the price-conditioned Monte-Carlo luck baseline at short_term_accuracy.rs:259. Implication: trust no current edge number until re-derived leakage-free.consensus-validation-leakage copy-trade-algorithm selector-vs-skill-gap short-term-accuracy
  • Documented the trader-selection vs skill-measurement gap (verified by reading the code directly). The production default selector is top_n_intersection (crates/polymarket-fetch/src/selector.rs:183polymarket_data::select_top_n_intersection, defined crates/polymarket-data/src/pipeline.rs:253): top-50 by PnL in each of WEEK/MONTH/ALL, keep wallets in >= min_windows (default 3), ranked/filtered purely by cumulative PnL. Only “consistency” control is the 3-window presence gate — no statistical luck control (no sample-size correction, no volume normalization, no edge-vs-price test). On fresh OVERALL run 803 (2026-06-01) this yielded intersection_size=2 — the consensus signal currently rests on 2 PnL-whales. Meanwhile a genuinely rigorous skill layer exists but is NOT wired into selection: crates/polymarket-fetch/src/short_term_accuracy.rspm_short_term_trader_accuracy table (Wilson lower bound Z=1.96 :63; small-sample penalty pinned by edge_lower_punishes_single_lucky_win :524; edge = hit_rate−avg_entry_price; conservative edge_lower = Wilson−price; edge_z; Brier; split-half persistence split_halves :216 gated at PERSISTENCE_MIN_MARKETS=40 :11/:294; Monte-Carlo luck baseline edge_luck_distribution LUCK_ITERATIONS=200 :12/:259luck_p_value+sigmas_above_null :319/:335) — surfaced only by the short-term-accuracy/ranked-traders/diagnose-persistence commands, not the selection path. consistently_profitable (selector.rs:342) is a partial improvement (rolling appearances + PnL/vol + ROI/composite) but still filters on PnL/volume not edge/Wilson/persistence, and isn’t the default. Fix: add a selector drawing from pm_short_term_trader_accuracy with n_markets>=40 AND edge_lower>0 (optional edge_z floor) instead of the PnL-leaderboard intersection. Why it matters: ties into copy-trade-algorithm’s finding that top-by-PnL persistence (~0.21 Spearman) did NOT beat the luck baseline (~0.23) — the default selector picks on the exact metric shown not to persist, which is why consensus was demoted to “information, not instructions.” — selector-vs-skill-gap short-term-accuracy copy-trade-algorithm

2026-05-31

  • Fixed two copy-lane analytics bugs (working tree, NOT committed/deployed). (1) detect_latency_ms was always 0: polymarket-data/onchain.rs computes a ms-precise Detection.latency, but copy_lane.rs discarded it — kept only block_time.timestamp() (whole SECONDS) on CohortFill.block_ts, then copy.rs::evaluate_copy recomputed latency as (now−b)*1000 from those seconds; real detection latency is sub-second so it floored to 0 (0 not NULL proved block_time WAS populated). Fix: carry detect_latency_ms: Option<i64> from detection.latency.as_millis() through evaluate_copy instead of truncate-then-recompute (expect ~200–900ms post-deploy). (2) implausibly negative chase_spread (entry cheaper than cohort fill): copy_lane.rs fell back to the MID as if it were an ask (last_up_ask.or(last_up_mid)); the mid sits below the ask → optimistic entry. A subagent’s “gamma token-order inversion” hypothesis was REFUTED (4 rows matched cohort price to the cent, so mapping is fine). Fix: new copy_ask(st, side) synthesizes a REAL ask by no-arbitrage from the opposite-side bid (down_ask=1−up_bid, up_ask=1−down_bid, never the mid; matches market_state.rs ask_history_down=1−bid). Durable insight: on a binary up/down market ask_up=1−bid_down, ask_down=1−bid_up; the mid is NOT a valid ask. Residual (deferred, not fixed): the −15¢ outlier is a stale/fast-moved CLOB book (book-age not stored); a staleness guard’s threshold interacts with the planned low-traffic <60s lane, needs data first. 282 tests pass, clippy pedantic+nursery clean. — copytrade-paper-lane-latency-spread-fixes copy-trade-lane
  • Shipped the on-chain copy-trade PAPER lane in pm-arb (commit b3f9e21, main). Strategic pivot: our autonomous longshot signal is negative-EV (live ~breakeven, t≈0.2 / 655 trades), while the top-5 cohort has a real cheap-outcome selection edge (+~18–30% flat ROI, t up to 9 over tens of thousands of trades, verified across all 5 wallets, crypto-updown-specific). So we copy which cheap outcome they buy instead of generating our own signal. Paper only — no on-chain execution, no keys/USDC. Detection is on-chain (OrderFilled logs on Polygon, ~1–3s + carries the wallet) because the Data API surfaces cohort trades a median ~5 min late (only ~2% within 30s) — useless for 300s markets. Verified decode facts: on-chain maker/taker == pm_trades.proxy_wallet for all 5 wallets; real OrderFilled topic0 is 0xd543adfd… (NOT canonical 0xd0a08e8c); cohort settles on CTF Exchange V2 0xE111180000d2663C0091e4f400237545B87B996B; per leg, got-token+gave-USDC=BUY / gave-token+got-USDC=SELL, price=USDC/token (both 6-dec), token→outcome via gamma clobTokenIds; reconciled against pm_trades. Architecture: detector (polymarket-data/onchain.rs, WSS eth_subscribe logs + newHeads block-ts cache) → copy_loop (pm-arb/copy_lane.rs, spawned in validate::run) → decode → token→market map (rotation poller) → gated evaluate_copy (pm-arb/copy.rs: cap/floor/window/dedup/daily-notional, rejects SELLs) → lane='copy' rows in pm_paper_trades, held to resolution; resolver subtracts the PM crypto taker fee (shares×0.072×p×(1−p), ≈6.4% of stake at ~11¢; no-op for existing lanes). DB-configured + hot-reloaded ~60s, no redeploy: pm_copy_config singleton (enabled kill-switch DEFAULT FALSE, cap_price, min_price, size_usd, min_window_left_secs, max_daily_notional_usd) + pm_copy_wallets (per-wallet enable, seeded with the 5 cohort wallets). Start = UPDATE pm_copy_config SET enabled=true. Deploy state: both migrations applied via Supabase MCP + verified, enabled=false (dormant, zero-risk); pm-arb does NOT run sqlx migrate so copy migrations had to be applied manually; needs new env var POLYGON_WSS_URL (added to POLYMARKET_FETCH_ENV GH secret by @deploy — until set the lane idles even if enabled, logs a warning, doesn’t error); handed to @deploy in AGENT_HANDOFF.md Round 42. Honest limits: assetId↔gamma clobTokenIds link verified on 2 fills (Gate-0 forward run = confirm copy rows actually appear = definitive proof); analytics niggles (up_mid_at_signal stores entry ask not up-mid; detect_latency_ms measures block→eval); no stale-book guard on copy ask; copy counters absent from hourly summary; real-money execution (EIP-712 signed CLOB orders) is a separate future project — PM matches off-chain, only settles on-chain, so you can’t trade on-chain instead of the API. — copy-trade-lane top-trader-edge longshot-strategy

2026-05-30

  • Longshot signal cap bumped 0.10 → 0.13 (commit a2de509) on the back of a fresh 5-wallet / 3,206-trade / 30-day cohort study. The previous “8–9¢ sweet spot” was an artifact of capping the analysis ≤10¢ — re-running across the full cheap tail shows the cohort universally peaks in 12–16¢ (17.5% avg win rate across 5 wallets, every wallet has 12–16¢ as its top band). With the existing 1.3× slippage guard, the new max accepted fill is ~16.9¢. — longshot-strategy
  • Daily notional cap bumped 700 (commit 86dbbdc). The rig was hitting $500 by ~02:00 UTC and idling for the rest of the day; +40% more trade-hours without changing per-trade risk. — longshot-strategy
  • Mid-history instrumentation shipped (commit b677ce2): four new columns on pm_paper_trades (up_mid_30s_ago, down_mid_30s_ago, up_mid_60s_ago, down_mid_60s_ago). Recording-only, no behavior change; populates as new trades land. Enables the trend-filter backtest (task #13) without re-deriving from pm_clob_book_snapshots. — longshot-strategy
  • Corrected the previous Obsidian note: 0x75cc3b was the cohort outlier (and is wallet #2 by 30-day ROI in our band+window, not the top), not the template. The actual #1 is 0xb27bc932 at +48.1% / 30d (24h spikes to +295.5%); they’re also one of the 4/5 cohort wallets that are buy-only / hold-to-resolution — which is what our paper model actually matches. 0x75cc3b sells 28.1% of the time. The reverse-engineering still captured real edge (the basic longshot template is universal across the cohort), but several specific parameters in yesterday’s note were biased to 0x75cc3b’s personal pattern. — longshot-strategy top-trader-edge
  • Per-asset rolling-ROI gate killed by retrospective backtest. Three variants tested (asset-only −25%, asset×side −25%, asset×side −50% aggressive) — all three worse than baseline. The trades the gate would have blocked carried +$320 cumulative profit. Generalizable lesson: in a reversion strategy, recent per-category performance is anti-correlated with future performance — filtering on recent losses removes the setup for the next win. Don’t add “skip recent losers” gates of any shape to this strategy. — longshot-strategy
  • Deep-dive on 0xb27bc932 (the actual #1) surfaced 3 candidate tunes pending cohort validation: (a) asset+side bias (BTC buy_up and ETH buy_down dominate, +285 cumulative respectively in 10–16¢; skips SOL+XRP); (b) sub-tiw goldmine [0.85–0.90] = 45.6% win / +284% ROI inside the broader [0.66, 0.93] window; (c) hour-of-day clustering 8–14 UTC. None ship until validated across 4+ of the cohort — same overfit risk as the killed directional filter. Tracked as task #16. — longshot-strategy
  • Operational gotcha: self-trade alerts feature was silently broken in prod for hours. Root cause was sqlx migration tracking drift — a prior migration (20260527203948) was missing its tracking row, sqlx tried to re-run it on every boot, failed, and never attempted today’s 20260530000000_pm_self_trade_alerts.sql, so the table never existed. Compounded by yesterday’s migration not being idempotent. Fixed via direct prod surgery through Supabase MCP: CREATE TABLE IF NOT EXISTS … + insert into _sqlx_migrations with computed SHA-384 checksums. Two durable lessons: (1) all sqlx migrations must be idempotent (IF NOT EXISTS, DROP POLICY IF EXISTS … ; CREATE POLICY …); (2) main.rs:2376-2380 swallows sqlx MigrateError so failures degrade silently — tracked as task #15. Also durable: auto-catch per @deploy Round 36 applies to pm-arb only — polymarket-fetch services need explicit redeploys.longshot-strategy
  • Structural ceiling clarified: top-tier 0xb27bc932-style returns (consistent +48% / 30d, 24h spikes to +295%) likely require HFT-grade execution + microstructure reads (order-book depth, flow imbalance, sub-second timing) — we don’t ingest any of that. Mid-tier (0xb55fa129 style: +4–20% ROI, 9–15% win) is what’s achievable with focused tuning of the existing rig. The deep-late 0.93–1.0 mechanism remains parked (requires maker/live execution, separate project). — longshot-strategy
  • Side-thread offloaded to its own project: contract-scanner. While in a polymarket-fetch session, scoped a separate workflow — auto-run smart-contract static analysis (Slither MVP, Aderyn/Mythril/Halmos later) across DeFi protocols on a chain, narrow output to a ranked CSV of likely-vulnerable contracts for white-hat / responsible-disclosure research. Discovery work done in-session (DeFiLlama BSC funnel: 696 → 175 → 55 unaudited → 26 high-priority → 154 total candidates; top-5 picks led by EA Finance as the pipeline validation target; pattern recognized: multi-chain reputable protocols with unaudited BSC ports are the sweet spot). Two cross-cutting DeFiLlama gotchas worth carrying (apply to ANY future DeFiLlama work in this project too): chain name is "Binance" not "BSC" in chains[]/chainTvls keys; audits field is stale (0 sometimes means “no audit,” sometimes “DeFiLlama doesn’t know” — Frax Swap is the canonical FP). The design is documented in the new contract-scanner project folder (sibling to this one) so it gets its own context window when implementation begins; this entry preserves the trail so future readers see where it came from. — contract-scanner

2026-05-29

  • Tuned the longshot lane in pm-arb and validated it against the top trader. Re-derived 0x75cc3b’s actual trades from pm_trades (5m updown, late-window, paid 0.04–0.10) → ~10.7% win / +50% ROI; our paper strategy in the same band → ~10.6% win / +60% ROI. The reverse-engineering genuinely captured the edge (corrects the earlier “only half the strength” inception claim). Our +15% headline ROI was a leaky rendering of the ~+50% core edge — three structural leaks, each fixed: (1) floor 5¢ DEFAULT_LONGSHOT_MIN_PRICE 0.04→0.05 (4–5¢ band is −22.7% over 97 trades; 0x75cc3b’s 4¢ is −91%); (2) slippage guard 1.3× DEFAULT_MAX_SLIPPAGE_MULT 2.0→1.3, enforced in market_state.rs::try_resolve_pending (fills >1.3× signal price are net-negative); (3) cap tiw≤0.93 DEFAULT_LONGSHOT_MAX_TIME_IN_WINDOW=0.93 (longshot is 0/41 / −1–2/trade vs our flat $10); edge concentrates in 8–9¢ (peak +195% ROI at 8¢), not 4–7¢ (that was small-sample noise). — longshot-strategy top-trader-edge
  • Shelved a cross-venue Polymarket↔Kalshi arb (“buy YES one venue + NO the other when combined < 54 on ~1.001–1.02). Recorded the PM/Kalshi fee schedules, Kalshi public market-data endpoints, and the resolution-rule-mismatch failure mode as durable facts. Reopen only with atomic-ish dual-venue execution + a broader set of rule-matched markets. — cross-venue-pm-kalshi-arb
  • Claude Code ops gotcha: a crashed session for this project 400’d on every claude --resume and couldn’t be cleared from the CLI. Root cause: 26 MB / 14,155-message transcript, auto-compacted 3×, ~3,000 messages live since the last isCompactSummary (line 11,118) — over the model token ceiling AND tool blocks left unbalanced by the crash (1,456 tool_use vs 1,481 tool_result). Can’t /compact out (400 fires before the session loads). Recovery: don’t revive the giant session — reconstruct state from AGENT_HANDOFF.md + historian, continue in a fresh session. Verified the uncommitted B1 supabase-upsert-retry fix in crates/pm-arb/src/validate.rs green against the full CI gate (116 tests pass). Diagnostic commands (wc/du, awk length sort, jq tool-block pairing, grep isCompactSummary, tail -n1 | jq -e) recorded. — recovering-claude-session-400-on-resume

2026-05-27

  • Production deploy live on GCP. All three services running on ops-vm in europe-west3-a (Frankfurt) since 11:08 UTC. Deploy mechanism: Docker images via Artifact Registry (europe-west3-docker.pkg.dev/aerobic-tesla-490112-r3/apps/polymarket-fetch), pulled by the generic Ansible apps role from ~/levandor/terraform/ — Spec 2a Phase 1+2 executed in this single deploy. VM: e2-small / 50GB pd-balanced / external IP 34.179.237.88 / Tailscale hostname ops-vm-1 (collision with offline old US node, manual admin cleanup still pending). Branch spec2a-app-deploy (16 commits, validated by the live deploy) not yet merged to main on the terraform repo. — polymarket-fetch-deploy polymarket-fetch
    • apps-polymarket-fetch-bot — Docker, restart=always, the Telegram bot
    • apps-polymarket-fetch-scheduled-run — systemd timer, OnCalendar=*:0/30, the 30-min snapshot job (replaces the staged launchd plist)
    • apps-polymarket-fetch-wallet-monitor — Docker, restart=always, top-edge wallet capture rig (--top-edge-z 9 --interval-secs 5 --out-dir /var/lib/wallet-monitor)
  • Live verification at ~14:10 UTCscheduled-run healthy (10 ticks since deploy, 6 in the last 90 min, latest pm_runs.started_at = 2026-05-27 13:57:06 UTC via Supabase MCP); wallet-monitor healthy (2,668 rows in last 30 min, latest pm_trades.trade_time = 2026-05-27 14:07:22 UTC, on-disk /var/lib/wallet-monitor/ = 1.4M across 2 JSONL files via Tailscale SSH); bot container active per systemd but end-to-end Telegram round-trip not yet smoke-tested. systemctl is-active returns active for all three units. — polymarket-fetch-deploy
  • Four GCP/Docker gotchas hit during the deploy, all captured in gcp-terraform-ansible-gotchas (#15-#18):
    • #18 Supabase IPv6 vs Docker IPv4 — the killer. Direct DSN db.<ref>.supabase.co is IPv6-only, Docker bridge is IPv4-only; all three containers crash-looped on first deploy. Fix: switch env file to session pooler URL aws-1-eu-west-1.pooler.supabase.com:5432. Pattern will apply to every future Supabase-using app on this rig.
    • #17 .dockerignore exclusionsupabase/ was excluded, broke sqlx::migrate!() at image build time. Fix: commit 834bc66 in this repo (fix(docker): include supabase/migrations/ in build context).
    • #16 gcloud GPG key extension — apt refused the key until renamed .gpg.asc.
    • #15 Tailscale name collision — destroyed US VM left a ghost node in tailnet; new EU VM got auto-suffixed to ops-vm-1. Inventory patched at runtime; admin-console cleanup pending to restore the canonical ops-vm hostname.
  • Build artifacts shipped this sessionDockerfile.polymarket-fetch (multi-stage rust:1-bookworm builder → debian:bookworm-slim runtime) and .github/workflows/release.yml (calls reusable wowjeeez/terraform/.github/workflows/build-push.yml, WIF-auth, pushes :latest + :<sha> tags). — polymarket-fetch-deploy ci
  • Open follow-ups carried forward: Telegram bot end-to-end test; clean up offline ops-vm Tailscale node so make ssh works without inventory patch; merge spec2a-app-deploy to main in ~/levandor/terraform/; (optional) backfill old pm_runs records pre-deploy (pm_runs currently 212 historical + 10 new from the 30-min timer). — polymarket-fetch-deploy
  • NOT deployed in this cycle (intentionally out of scope): pm-arb validate (6 known blocker bugs in a separate track — state HashMap leak, slug parse ghost state, config defaults mismatch with backtest, stale-book ghost fills, Supabase silent fallback); pm-arb live / pm-arb paper (not yet built per master spec docs/lag-arb-plan.md M1/M2); lag-probe (separate workload, separate deploy decision). — lag-arb-plan

2026-05-25

  • Reverse-engineered 0x75cc3b’s edge — it’s not latency arb, it’s late-window 5m longshot reversion + momentum-chase. Strategy v2 redesigned around book state + time-in-window, not BTC vs book lag. 0xd189664c shows the purest expression at 77.8% ROI on cheap-tail entries. Naive lag-arb model failed M0 at 46% win rate. — top-trader-edge short-term-accuracy lag-probe

2026-05-24

  • 14h lag-probe capture finished clean (5.9 GB, 38M+ events, real 171.7 bps BTC range). Headline: Polymarket CLOB book lags direct Binance p50 300 ms / mean 844 ms / p99 8 s — edge window is 300 ms to 2 s, sub-100 ms colo in eu-west-2 wins. RTDS Binance ~800 ms slow, RTDS Chainlink ~2.6 s slow; perp vs spot indistinguishable at 100 ms grid. Caveat: clob_book throughput was only ~10/sec vs prior 200–460/sec — possible back-pressure from the recent CLOB/price channel split, real book lag may be even tighter. Next: scope the actual bot. — lag-probe

2026-05-22

  • Fixed GitHub Actions CI, red on every push since 2026-05-19, now fully green. fmt run; cargo-deny moved off the Alpine Docker action onto a normal runner, wildcard path dep allowed via publish=false, two RUSTSEC advisories cleared by bumping testcontainers; clippy lint skew fixed by pinning rust-toolchain.toml to 1.95.0. Added a multi-stage Dockerfile + build workflow (push disabled until a registry is picked). — ci
  • Built lag-probe crate — read-only harness measuring Polymarket book lag vs true BTC price for the latency-arb strategy. Captures 4 feeds (direct Binance, RTDS Binance/Chainlink, CLOB book), JSONL + lag analysis. RTDS measured ~3.4s laggy (convenience feed, not tradeable); primary book-vs-Binance lag still unmeasured — needs an overnight capture during real volatility. — lag-probe

2026-05-21

  • Corrected short-term-accuracy: the slug timestamp is the market START, not end. Price curve (~0.74 at close) proves high-edge wallets trade in-window on live markets, not snipe post-resolution — the edge is a copyable Binance→Polymarket latency arb. Added off-chain-matching / AWS eu-west-2 colocation infra notes. Next step: measure repricing lag. — short-term-accuracy
  • Added short-term accuracy analysis (short-term-accuracy CLI, pm_short_term_trader_accuracy table) for crypto Up/Down markets. Fixed a survivorship-bias bug (outcomes were joined on traded side); rank by edge-over-price not hit_rate. League-wide persistence p=0.08 (unproven), but wallet 0x75cc3b shows edge_z +11.3. — short-term-accuracy
  • Backtested the copy-trade algo end to end — no demonstrated edge, ROI swings -23% to +62% across windows, doesn’t beat naive PnL ranking. Kept as an observation tool; removed copy_score magnitude ranking. — copy-trade-algorithm

2026-05-18

  • Shipped trades fetcher, copy-trade algo skeleton (gated copy_score, MM filter), Telegram adapter + lifecycle. 53k trades / 24 wallets / pushed to wowjeeez/polymarket-fetch. — polymarket-fetch copy-trade-algorithm

2026-05-17

  • Shipped Selector trait (4 impls), trades-since, auto-chain. 21 runs, 8-wallet watchlist, 9.1K positions. tubeyou newly entered intersection at $2.75M week+month PnL. — polymarket-fetch
  • Initialized project folder. Wrote main overview note covering pipeline, tables, CLI, current DB state, and TODOs — polymarket-fetch