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 byobsidian-documenteron every project doc write. Read byhistorianat 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 windowCONSENSUS_WINDOW=172800s, fire on the 4th DISTINCT cohort BUYholders.len()==4/CONSENSUS_MIN_HOLDERS=4, BUY-side only) over the 14-traderscore>0cohort (run 1991,pmv2_traders) for2026-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, winneroutcomePrices[i]>=0.5, mirrorsparse_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:/tradesignoresstartTime+ capsoffset~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
/mirrorcopy-mirroring signal source — built + merged to main (count-gate commitf385fbb), DORMANT./mirror <user> <size> <limit>mirrors a single Polymarket user’s every trade: copies entries (sized by conviction band — VLOW 0.2 / LOW 0.5 / NEUTRAL 1.0 / HIGH 1.5 / VHIGH 2.5 ×base, UNKNOWN→NEUTRAL), mirrors exits, caps concurrent open copies per user (skip at cap, no backfill). Third signal sourcesource="mirror"alongsidefirst_mover_core, reusing the producer/PM split (cohort produces EntryOrder/ExitSignal, PM executes). Second on-chain feed lane in the firstmover binary uses Alchemy WSSrun_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 Alchemyeth_subscribe(logs)billing that drove the cohort lane toeth_getLogspolling). Stableorigin_ref = mirror|{wallet}|{condition_id}|{outcome_index}; PM matches exits by origin_ref+condition+outcome, NOT source. Persists topmv2_mirrors+pmv2_mirror_positions; a 15-min cron resolves closed markets (closed=trueproxy) to free slots + refresh avg-bet. New pmv2-contracts QueryKindsmirror/mirrorstop/mirrorlist+ responder renderers. Code:crates/cohort_algo/src/pmv2/mirror/{mod,repo,target,sizing,signal,handler,query}.rs; spec+plandocs/superpowers/{specs,plans}/2026-06-25-cohort-mirror-feature{-design,}.md. DORMANT untilPOLYGON_WSS_URLset on firstmover AND PM adds amirrorrow topmv2_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 stopleaves open copies to ride until resolution. — cohort-mirror-feature - Cohort
firstmoverredeploy:apps-pmv2-cohort-algo-firstmoverpin8977b5b→d851d44(imagesha256:716ee0d2..., containerrestartCount=0, ~20:19Z). ONLY thefirstmovercomponent rolled —query-responder+scheduled-rununtouched per cohort’s confirm. Headline change: on-chain feed migrated Alchemy WSS log-subscription →eth_getLogsHTTP 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 resizesfirst_mover_core armed cohort=18(was 20 — minor cohort window drift). Important architecture note for future debugging: PM self-watch (pmv2-onchain-watchcrate) 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.rsrun_watch()→build_first_mover_lane()(~L85-101):lane_active = signal_lane_active(effective_mode(global_mode, source_mode), source_enabled)whereglobal_mode=pmv2_autotrade_config.mode,(source_mode,source_enabled)=thefirst_mover_corepmv2_autotrade_sourcesrow,effective_mode(signals/config.rs:44) = MORE conservative of the two, andsignal_lane_active(signals/sources.rs:11) =source_enabled && mode != Off. Bothenabled=trueAND 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"(targetpmv2_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(nsapps, prod hostpolymarket-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 exactlyholders.len()==2, MIN_COHORT_SCORE=0.0, CONSENSUS_WINDOW=48h, EXIT_DEBOUNCE_SECS=60, EXIT_FANOUT_PERMITS=4, eligible_buy<=/eligible_sell strict>, position_keptinitial_value>1.0, truncate(25)), and sizing (olddecide_copy→newsize_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.rssignal_gate_decision+apply_io_gates) with identical formulas. Genuine divergences: (1) EV-floor gate DROPPED (oldev_floor_gateatengine.rs:154has no new equiv;min_round_trip_edge_bpsloaded-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 — subjectcohort.signals→pmv2.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 guardssnapshot_trustworthy(skip on prior-nonempty/fresh-empty) +should_record_exit(delete row only when Sell published); (5) operational constantsBET_LINK_TIMEOUT_SECS25→90,CONSENSUS_CACHE_WINDOW_SECS600→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 (cratepmv2-onchain-watchsrc/feed.rs, prod pin719dc32) CRASH-LOOPS with ZERO events = a rustlsCryptoProviderpanic on the FIRSTwss://handshake; crate made robust this session viaensure_crypto_provider()(NOT committed). Symptom: PM connects to DRPCogws(wss://lb.drpc.org/ogws?...), ~200ms later logsfeed task ended; backing off before respawn, respawns every ~5s (=FeedConfig::respawn_backoff), delivers nothing — while backfill overPOLYGON_RPC_URL(separate reqwest path) keeps delivering fills in ~5s batches. ROOT CAUSE: tokio-tungstenite 0.29 (tls.rs:126) builds its TLS via rustlsClientConfig::builder()→ resolves the process-defaultCryptoProvider; rustls 0.23’sget_default_or_install_from_crate_features()only auto-picks when EXACTLY ONE ofring/aws_lc_rsis enabled — PM’sCargo.lockcompiles rustls 0.23.40 with BOTHaws-lc-rs(viaquinn-proto/reqwest http3) ANDring, and PM never callsinstall_default, so the first handshake panics. THE MISSING MECHANISM (key gotcha): the panic unwinds (defaultpanic=unwind) killing ONLY the spawned feed task →run_feed’s_ = &mut handle =>select arm RESOLVES with aJoinError(a panicked spawned task makes the parent’s&mut JoinHandlearm fire — it is NOT only a clean-return signal) → logs the generic respawn line and respawns afterrespawn_backoff. This explains the 200ms timing, the 5s cadence, AND the zero crate logs (the panic bypasses alltracing). 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:7both dolet _ = rustls::crypto::ring::default_provider().install_default();as the first line ofmain(); PM was missing it. FIX (crate-side, robust, NOT committed): added idempotentensure_crypto_provider()insrc/feed.rs(viastd::sync::Once, installsringonly if none installed) called at top ofconnect_oncebeforeconnect_async, +rustls = { version = "0.23", default-features=false, features=["ring","std"] }in the crateCargo.toml(single-provider, keeps the crate panic-free). TDD: unitconnect_path_installs_process_default_crypto_provider(red→green) + integrationtests/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 itspmv2-onchain-watchgit pin (rev=719dc32inposition_manager/crates/position_manager/Cargo.toml) + redeploys, OR applies the one-lineinstall_default()hotfix in PM’ssrc/bin/position-manager.rsmain(). 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) atokio::spawnpanic RESOLVES the parent’s&mut JoinHandleselect 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’sCargo.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’smain()via an idempotent install-if-absent on the connect path. — pmv2-self-watch-rustls-cryptoprovider-crashloop /topbets/consensus/filtersRESTORED 1:1 from the monolith via a new cohort_algoquery-responderover NATS (pmv2-contracts1f05fff+cohort_algo5d5bb48, 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 newquery.request/query.resultschannel (cohort’s first inbound NATS surface); the connector owns the commands + sends therenderedHTML. Ported verbatim (parity-locked by porting the monolith’s own unit tests unchanged): topbets (repoall_cohort_holdersSQL + atopbets_filtereq:/neq: module namespaced to avoid the gatingCategoryFiltercollision + 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 addedCONSENSUS_CACHE_WINDOW_SECS, 4 tests); filters (category_counts/tag_counts+filters_body). Contractpmv2_contracts::query=QueryRequest{request_id,actor,kind=TopBets|Consensus|Filters}+QueryReply{request_id,rendered}; reply JetStream-published withNats-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 withpolymarket.com/→ publishes additiveQueryKind::BetInspect{url}(serde back-compat) → cohortquery-responder’srender_bet_inspectdoesscore_bet(url)(bet-scoped holder lookup) + gauge formatter;!has_high_edge_signal→resolve_fallback_category+category_consensus_section→QueryReply{rendered}→ connector posts HTML. NO new subjects/permits. Pins:pmv2-contractsc6b3a15,cohort_algo1d573b8(=299cf1a+ handler, carries rustls fix). Reviewed (fidelity + 4 adversarial lenses) + gated green. Engineering: DRY-extracted sharedBET_HOLDER_SELECTSQL const (byte-identity guard test) forload_bet_holders+all_cohort_holders; re-added a dropped backpressure semaphore (cap 4) to the query-responder. Deferred: advisory-lock dedup inrun_and_persistfor duplicate same-category consensus recompute (also hits/consensus). Pending: connector non-slash arm = @pmv2_telegram (babylon #517); deploy cohort1d573b8before/with connector (unknownbetinspectto old cohort = fail-safe skip). — bet-link-inspector-restored-pmv2 pmv2-onchain-watchWatchedFillIDENTITY FIELDS added — fixes a PERMANENT one-alert-per-wallet suppression in PM’s self-wallet watch (babylon task #430 RESOLVED; crate re-pinned1b8a0f9→719dc32, cohort re-vendored0fdfe8a→4a3f8b2, both gates green). PM’s self-wallet watch (the Balu/Bandi instant-alert feed) dedups on(wallet, transaction_hash, outcome_index), persisting a row inpm_self_trade_alertsso it doesn’t re-alert across restarts. ButWatchedFilldid NOT carrytx_hash, so PM was forced to settransaction_hash=""+outcome_index=0for EVERY fill → after the FIRST fill of a wallet was recorded,is_alertedreturned true for ALL subsequent fills of that wallet = permanent one-alert-per-wallet suppression (PM-side adversarial review 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 toWatchedFill—pub tx_hash: B256,pub log_index: u64,pub block_number: u64(block_numbera bonus — PM derivestrade_timefrom it);decode_watched_fillsets all three from theOrderFilledEventalready in scope (event carried them at decode,decode.rs:53-64);run_feedemits the decodedWatchedFillstraight through so the emit path carries them with no extra change; regression testwatched_fill_carries_event_identity_fieldsasserts non-zero tx_hash/log_index/block_number survive decode. The cohort consumer needed NO change — it usesdecode_watched_filland has ZEROWatchedFill { … }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 fromblock_numberor uses ingest time) andcondition_id/a TRUEoutcome_index(gamma token→market enrichment stays consumer-side per the crate’s out-of-scope boundary; PM’soutcome_index=log_indexdedup 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 (noWatchedFill { … }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(was5142d98), re-vendored at cohort0fdfe8a(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_feedsig +decodekeep-list byte-stable) → a safe IN-PLACE cleanup. Dead code deleted (24 items, all carried VERBATIM from cohort’sonchain.rs+ unreachable from the one real entryrun_feed): the entire newHeads/Detection/DetectionSink/run_detectorblock-latency path; theSubCommand/dynamic-resubscribe path (run_log_feed_statichardwired a DUMMY command channel → the cmd select-arm was dead; live watchlist updates already flow via thewatch::Receiver); a write-only ack registry; unusedpubhelpersorder_filled_topic0/CONDITION_RESOLUTION_TOPIC0/watchlist_address_topics. DRY extraction: onetopic_hex(B256)(the0x+hex-encode pattern was dup’d ~5×) + onewatchlist_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) theeth_getLogs/eth_block_numberreqwest client now sets BOTHtimeoutANDconnect_timeout+ isOnceLock-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 15sPING_INTERVALwas DEAD under load (busy socket never hits the read-timeout arm) + stall detection drifted to ~90s → moved to a DEDICATEDtokio::time::interval(PING_INTERVAL)arm (restores ~45s); (c) booteth_block_numberfailure now retries 3×+warns instead of SILENTLY seeding the watermark to 0 (which disabled backfill until the first live fill). NOT changed (behavior-preserved): thedecode_watched_fillprice≥1 (usdc≥token) reject + the both-watched maker-first pick are inherited cohort semantics — only addeddebug!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 tracingconnect_onceshowed 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
OrderFilleddecoder + resilient WSS feed extracted into privategithub.com/wowjeeez/pmv2-onchain-watch@ pin5142d98so PM gets instant self-wallet (Balu/Bandi) entry/exit alerts by depending on it (git dep, likepmv2-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 ofpolymarket-data/src/onchain.rs, renamed watchlist-neutralCohort*→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,Noneif 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 onwatchchange), backfill-on-reconnect (eth_getLogs from alast_seen_blockwatermark), 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 (cohortmain@69a293a): droppedpolymarket-data/src/onchain.rs+re-exports (REMOVED on disk) + deletedwatch/backfill.rs; vendored crate as workspace path-dep (crates/pmv2-onchain-watchin members +[workspace.dependencies],cohort_algotakes{workspace=true}— exactly thepmv2-contractspattern);watch/is now a thinrun_feedconsumer (spawn_cohort_refresh(pool, wl_tx)re-derives cohort set frompmv2_traderson a 60sREFRESH_INTERVAL→watch::Sender<HashSet<Address>>→run_feed→mpscsink drainsWatchedFills →handle_cohort). Behavior-preserving —handle_cohort/first-mover/exit/gating byte-identical frommarkets.resolveonward (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/CohortAction→WatchedFill) 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 advancedlast_seen_blockonly 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_onceadvances watermark to the scannedtounconditionally viaconst fn next_watermark(current,scanned_to)->u64(feed.rs:638, monotonic max — also fixed the inherited flaw in the original cohortbackfill.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 overHashSet<Address,S>, so the whole generalization =Cohort*→Watched*rename + caller-suppliedwatch::Receiverwatchlist +mpscsink (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-algomain@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. thepmv2_autotrade_*tables PM now owns + ~28 dead legacypm_*; 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_migrationschecksums (VersionMismatch→ bricks deployed boot). Solution = squash to idempotent baseline: all 44 → ONEsupabase/migrations/20260624000000_baseline.sql=CREATE TABLE IF NOT EXISTSfor ONLY the 12 tables cohort owns/reads (9pmv2_*+ 3 keptpm_*: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 migrator4712de9recreates byte-identical so cohort’s READS survive) + ~25 deadpm_*.set_ignore_missing(true)on the Migrator (sqlx 0.8.6) indb.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 table — unnecessary 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+instaschema_snapshotgolden 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 → ~25pm_*DROPs via psql (BEGIN/COMMIT, transcript-first; PM confirmed zero refs) → reset_sqlx_migrationsto 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_snapshotvalidates 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-algomain@e444af0, commits0a31f45→e444af0, branchfeat/pre-live-gatingff-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_filter —CategoryFilter{include,exclude}+passes_filter(comma-sep buckets,!x=exclude, bare=include, empty=pass-all, over esport/sport/geo/crypto/other) +resolve_category(cache-firstpmv2_event_category→gammaevent_tags→category_bucket; empty event_slug short-circuits to None, skipping a doomed gamma RTT on the hot path); (2) chase/max_entry_premium_bps—chase_gate: skip ifbest_ask > avg_price·(1+premium/10_000)(entry-decay vs the cohort trader’s fill); (3) market liveness —liveness_gate: skip unlessactive && accepting_orders && !closed && !resolved && !resolution_proposedandsecs_to_resolution≥min(status from the new CLOB read client; resolution-proposed from gammaumaResolutionStatuses); (4) EV-floor DROPPED — oldev_floor_gatewas degenerate (size cancels → justbps>200, price-blind); removed, operators setmax_entry_premium_bpsnet of round-trip cost (min_round_trip_edge_bpsstays loaded-but-unused —pmv2_autotrade_configschema FROZEN per babylon #333). KEY DECISION — re-added a read-onlyClobReadClientto vendoredpolymarket-data(best_askvia/book,market_statusvia/markets/{cond}) — public endpoints, NO signing/auth/exec (explicitly NOT the deleted execution client); reusesHttpExecutor/build_http_client(setsconnect_timeout— the proxied-hang trap) + proxy pool; response structs grounded in REAL captured fixtures (CLOB not geoblocked; wire is snake_case,/bookasks returned DESCENDING so.min()is load-bearing). This DELIBERATELY re-introduces a CLOB dep the carve removed (operator-chosen: best-ask accuracy over the freecur_price/gamma proxy).entry_price_band_gaterepointed to gate best_ask (our real entry) not the trader’s fill.signal_gate_decisionmade async + staged cheapest-first: pure trust gates (score/conviction) → category (cache) → concurrent boundedtokio::join!(best_ask+market_status+resolution_proposed) → pureapply_io_gates(liveness→chase→band). Startup announcement (operator directive #354): watcher boot publishes onetelegram.outboundcomponent: first_mover_core started with version: <hash> <title>(version viabuild.rsBUILD_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 thefirst_mover_corepmv2_autotrade_sourcesrow=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-algomain@0986c4d, 6 commits131b0d5→0986c4d). cohort stopped emitting its oldcohort.signalsschema_v1 and now emits the v2 trade wire-contract, schema_version 2, onpmv2.order.first_mover_core.{entry,exit}(source_id="first_mover_core"). Vendored the sharedpmv2-contractscrate (crates/pmv2-contracts, path dep; canonicalgithub.com/wowjeeez/pmv2-contracts; serde + rust_decimal only) — the long-standing blocker (contract didn’t exist) is resolved. Entry payload =EntryOrderwith thePricing::Derivedarm{reference_price:Decimal, conviction:f64, source_score:Option<f64>, size_usd:Decimal}(thePricing::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 = leanExitSignal{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 deadcohort.signalssubject +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 fromSignalConfig←pmv2_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_lanenow builds theOutboundvariant (→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_filtergate not implemented (needs per-event category fetch +CategoryFilter/passes_filterport); (2) chase/max_entry_premium_bpsgate not implemented (needs live best-ask); (3) market open/active/accepting-orders status flags not implemented (needs market-status fetch); (4) the portedev_floor_gateis the OLD DEGENERATE logic —size_usdcancels out so it reduces tomin_round_trip_edge_bps>200vs 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_managerowns the FULLpmv2_autotrade_*migrator (config/sources/orders tables + any v2 columns on them). Producers (cohort, etc.) DROP allpmv2_autotrade_*DDL from their own migrations → these become a read-only precondition (cohort READSpmv2_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 sqlxMigratortable 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: nopmv2_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 legacypm_*tables, drop thepmv2_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 retiringpolymarket_fetchprod 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_connectorOUTBOUND 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 consumertg_outboundontelegram.outbound(streamTELEGRAM_OUTBOUND, 1d, 5-min dedup) → Bot APIsendMessage. Dumb faithful relay (passestext+ publisherparse_modeverbatim, never re-escapes). At-least-once, ack-AFTER-send; transient (429/5xx/net) → retry then redeliver bounded bymax_deliver; terminal (400/403) → drop+ack+ERROR+best-effort loop-guarded plain-text meta-alert. Sequential (one in-flight), reactive rate-limit only (honorretry_after, no token bucket = YAGNI). Relay loop behind aMessageSourcetrait for broker-free unit tests; wiremock for Bot API; optional NATS testcontainer. EnvelopeTelegramOutbound{msg_id,chat:Option,text,parse_mode:Option,kind:Option}field-matched tocohort_algo/.../telegram_outbound.rsuntilpmv2-contractspublishes. Inbound (getUpdates→auth→ops.*) = separate later spec, gated on @positionmanager lockingops.*(#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 gRPC127.0.0.1:4317/ HTTP4318(Dockerhost.docker.internal:4318); std envOTEL_SERVICE_NAME/OTEL_EXPORTER_OTLP_ENDPOINT/OTEL_EXPORTER_OTLP_PROTOCOL/OTEL_RESOURCE_ATTRIBUTES=service.namespace=pmv2,deployment.environment=prod(auto-injected for ansibleappscontainers; 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 setNats-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 infow/ 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_algoas a STANDALONE repo/component (/Users/levander/coding/pmv2/cohort_algo, privategithub.com/wowjeeez/pmv2-cohort-algo,main@2e150b1). The pmv2 fleet’s cohort-intelligence SIGNAL PRODUCER — does NOT place trades (zero CLOB dep). Five responsibilities: (1) cohort scoring cron →pmv2_traders/leaderboard, gated bypmv2_runs; (2) cohort consensus alerts (Pmv2Consensuscron + on-chainConsensusTracker); (3) first-mover ENTRY signals (Core-tier first BUY → NATSaction:Buy); (4) cohort EXIT signals (tracked trader sells → NATSaction:Sell); (5) on-chain WSS watcher + 30seth_getLogsgap-backfill. Own cargo workspace (edition 2024, resolver 3,unsafe_code=forbid, sqlx/tokio/anyhow/tracing/async-nats 0.49+nkeys; memberscrates/cohort_algo+crates/polymarket-data); vendorspolymarket-dataas an internal crate. 278 tests pass, clippy+fmt green, dormant-safe (no panic whenPOLYGON_WSS_URL/NATS_URLunset). PORT: behavior-preserving from the retiringpolymarket_fetchcarve (483350c) — preserved internalcrate::pmv2::paths + thepolymarket-datacrate name ⇒ near-zero import churn, ZERO logic edits. Pre-port multi-lens review fixed 4 real bugs: (a) empty-Data-API-snapshot200 []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” renameautotrade/→signals/(execution is the migrated-away PM’s concern; kept only the sharedpmv2_autotrade_*table names). ARCH CONTEXT (babylon #286, Andras’s Miro):polymarket_fetchRETIRES → independent component repos in/coding/pmv2on a NATS spine; producers (cohort/weather US+AS/crypto) own strategy+sizing → publish READY entry-orders + exit-SIGNALS;position_managerCONSUMES NATS DIRECTLY (no runner) → gate(off/dry/live, caps, breaker) → sign+POST+record; PM also ownstelegram.outboundpub, anops.commands/ops.resultsconsumer (confirmable kill-switch), and thepmv2_autotrade_*migrator. telegram_connector = separate repo (other agent). v2 WIRE-CONTRACT (babylon 292): subjectspmv2.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); sharedpmv2-contractscrate = source of truth (vendor it). Cohort gotchas:sourceMUST be the EXACTsource_idDB key (first_mover_core, NOT a label — wrong value silently routes toMode::Off); exit MUST carryorigin_ref; producers OWN ev-floor/chase/category gating (PM has no GammaClient). CURRENT vs NEXT: today emits the OLDcohort.signalsschema_v1 (COHORT_SUBJECT/SCHEMA_VERSION_MAX=1); telegram.outbound seam prepared butDirect-send still default. v2 cutover (publish topmv2.order.cohort.{entry,exit}, schema_v2, vendorpmv2-contracts, ADD producer-sidesize_usd+valid_until_ts+ev/category gating) = planned, BLOCKED on @positionmanager publishing thepmv2-contractscrate. — 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_manageris now the SINGLE execution authority:place_orderresolves the authoritative mode fresh per call (global × per-source(mode,enabled)+ breaker) and gates at the POST boundary — Off→Skipped(zero work), Dry→reservedry_runrow + build+sign rehearsal (no POST/caps/breaker), Live→reserve(live)+caps→place. Reserve+caps+breaker MOVED out of fetch’s engine/runner into PM (SQL byte-identical); runner now mode-agnostic (buildsEntryIntent/ExitIntent, callspm.place_order, maps typedExecOutcome→NATS ack). Correctness: entries gate oneffective_mode(global,source); EXITS gate on the row’s recordedmode(live position stays sellable when global=Off — E1 fix);enabled=false⇒Offnow enforced (was dangling); pre-POST reserve/caps DB error→Transient→Nak(30s); recorded-failure/benign-race→Ack(no double-trade); fail-closed-to-Off two paths;cap:eventliteral. State: committed onmainBOTH repos (PM351a755, fetch4fde695), NOT pushed, mode OFF. Gate: 66 PM offline + 532 fetch + 7 db-tests-vs-real-Postgres pass. OPEN: (1) commit contamination —4fde695swept in user’s unrelated in-progressexit_signal/cohort_exit_signal_idwork (called by uncommitted firstmover/alerts code) — pending user split/keep; (2) follow-ups: delete deadorders.rs/EngineConfigcap helpers, add host-injectable signer seam for live-POST e2e; (3) pre-arm: live-test PM notify→Telegram before arming. — position-manager-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(branchworktree-cohort-signals, frommain@43023ba). (1) THE CARVE (8cd6f58, gate green): repo reduced to ONLY cohort scoring (Pmv2Collect, kept so the slice is self-contained), cohort consensus (Pmv2Consensuscron + on-chainConsensusTrackerinpmv2-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,…}), theonchain_booktriggers (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.” Prioron_cohort_selldid DB-only bookkeeping (logged EXITED + deleted tracked rows) — no alert, no publish. Now it PUBLISHES anaction:SellTradeSignalto NATScohort.signalsAND 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 exitsignal_idcarries anexitdiscriminator DISTINCT from the entrysignal_idfor the same (condition,outcome); without it the consumer’sON CONFLICT(dedup_key)would silently swallow the Sell as a dup of the entry Buy = capital trap. Test-lockedexit_signal_id_differs_from_enter_signal_for_same_market. DRY: entry+exit publishers sharepublish_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-basedeligible_sell+ExitDebouncegate kept unchanged (NOT narrowed to Core-tier). PROVISIONAL cross-service contract — @deploy’sposition_managermust consumeaction:Selloncohort.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_managercrate owns: polymarket-clob signing client (use_server_timeOFF, ~20–100ms/order saved),place_orderbuy+sell (place_buy≡submit_live,place_sell≡auto_sell), ledger code (mark_*/record_*/bump_breaker_count), onchaineth_callhelpers (+connect_timeout), caps/breaker readers (load_caps/load_breaker), andFillSink/FillEventSinkwatcher seams (declared, unimplemented — Plans 2/3).polymarket_fetchrewired one-way onto PM;submit_live/auto_sell/execute.rsdeleted. 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: onmainBOTH repos (PMcbd0f4c, fetchafc785d), 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_canceledvs oldmark_failed, no POST either way). FOLLOW-UPS: makeSigningClientClone (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-clobcrate physically moved frompolymarket_fetchintoposition_managerworkspace (cdfcca8);use_server_timeflipped tofalse; self-referentialinclude_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 secondsqlx::migrate!. Thepmv2_autotrade_orderstable stays created by the single existing upstream migrator; PM owns the ledger CODE only (ledger.rsqueries). No PMmigrations/dir,sqlxmigratefeature omitted, upstream applied migration untouched; physical relocation DEFERRED to a coordinated single-migrator change. The earlier “adopt-if-existsCREATE TABLE IF NOT EXISTSmigration owned by PM” plan was a sqlx trap — see sqlx-shared-migrations-table-trap. (2)place_ordernow 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;OrderRequestcarriesreserve_id(acts on an already-inserted reserve row) and a buy/sell field split, NOprovenanceblock. (3) Latency win folded in: disable the clob client’suse_server_time(hard-codedtrueatpolymarket-clob/src/lib.rs:329) → drops a serialGET /timebefore 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 tablepmv2_autotrade_configis CO-OWNED: PM reads caps, writes breaker counters. — position-manager-interface-design - REFERENCE/LESSON — sqlx 0.8 shared
_sqlx_migrationstable trap (the reusable gotcha behind the PM migration walk-back). sqlx tracks ALL applied migrations in ONE hardcoded_sqlx_migrationstable keyed byversion(BIGINT PK). Three failure modes: (1) TWO separatesqlx::migrate!sets vs the same DB → each migrator throwsVersionMissingon the OTHER’s applied versions (validate_applied_migrationsdefaultsignore_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 EXISTSdoes NOT save you — sqlx tracks by version ROW not table existence, so it still inserts the version row and collides. Takeaways: ONE migrator per DB/_sqlx_migrationstable; never reuse version numbers; never edit applied files; owning a table’s CODE is separate from owning its DDL/migrator. — 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 deferredautotrade→position_managerrename lands into. PM OWNSplace_order(build/sign/POST+record), the position ledger, portfolio caps + circuit-breaker, self-wallet reconciliation, watched-tx detection; STRATEGY stays upstream in polymarket_fetch (detection, gates, EV/conviction/sizing, first-mover gating, NATSPmv2Consumer,mode). Dependency one-way in-processpolymarket_fetch→position_manager, single host process to start;polymarket-clob(vendorrs-clob-client-v2) MOVES INTO PM (leaf crate). API:place_order(OrderRequest)->PlacedOutcome(≈ today’ssubmit_live; FAK, proxy sigType 1, V2 contracts preserved),watch_self_wallet(CLOB/userWS + periodic on-chainbalanceOftruth check, chain-authoritative reconcile emitted never silent),watch_transactions(UNBUILT Phase-2OrderFilledWSS detector over a configurable address set → rawFillEvent). Sinks are TRAITS (FillSink/FillEventSink) so a later separate-watcher-process split is cheap. Schema:pmv2_autotrade_ordersledger OWNED by PM via an adopt-if-exists (IF NOT EXISTS) migration so the shared Postgres table is never double-created;pmv2_autotrade_configstays 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 transitionspending→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 (V2verifyingContractaddrs, pUSD collateral) + the connect_timeout rule (BOTHtimeoutANDconnect_timeouton every reqwest client — the 75-min silent-hang lesson). Source: spec + Plan 1 inpmv2/position_manager/docs/superpowers/. — 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
OrderFilledWSS →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 WSmarket-channel book mirror (vendoredrs-clob-client-v2has a DISABLEDwsfeature) + cached static metadata; WS read-only, placement stays REST. Second win = disableuse_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 BOTHtimeoutANDconnect_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_getLogsreconciliation over[last_seen_block, latest]that recovers cohort/self/our-fillOrderFilledlogs the Polygon WSS feed (run_log_feed, polymarket-data/onchain.rs) drops during disconnects —eth_subscribeauto-reconnects with backoff but NEVER replays logs emitted in the gap, so they were lost permanently. Shipped tomain43023ba, gate green (563 polymarket-fetch + 107 polymarket-data), NOT yet redeployed. New filecrates/polymarket-fetch/src/pmv2/watch/backfill.rs; new public primitives inonchain.rs(eth_block_number,eth_get_order_filled_logs,cohort_address_topics,build_get_logs_filter, modeled oneth_call_u128). Re-fetches OrderFilled for the cohort across BOTH maker+taker topic slots (mirrors livebuild_logs_payload) and feeds each recovered log through the IDENTICAL worker path as aWatchEvent. 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 viaON CONFLICT(dedup_key), self-trade alerts dedup in DB by(wallet,tx,outcome), our-fill reconcile shares the worker’sseenset keyed by(tx_hash,log_index)— so live/backfill overlap is harmless, structurally equivalent to the pre-existing first-mover match-cron backstop. Mechanics: watermarklast_seen_block: Arc<AtomicU64>advanced viafetch_maxinhandle_order_filledright after decode (fetch_max so out-of-order delivery can’t rewind);from = watermarkis INCLUSIVE (recovers intra-block partial-delivery gaps, dedup absorbs the re-fetch — intentional, NOT an off-by-one);MAX_BACKFILL_BLOCKS = 2000cap with a log line on hit (no silent truncation; outage > ~2000 blocks recovers across subsequent sweeps);try_sendso the sweep never blocks the worker queue; reuses the existingPOLYGON_RPC_URLHTTP 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). Plandocs/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 logsstream, 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 neweth_getLogsbackfill 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 pushedlogsnotification 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_consumerinrunner.rs(branchfix-dry-skip-visibility, GATE_EXIT=0, 334+87 tests): two previously silent skip paths (stale-gate +Dispatched::Skipped) now surfacenotify+tracing::infowheneff == Mode::Dry;per_message_mode()hoisted abovestale_gatesoeffis 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_reservenow returnsReserveOutcomeenum (Reserved(i64)|CapExposure|CapDaily|CapMaxOpen|CapEvent|Dedup) instead ofResult<Option<i64>>; cap-breach and real dedup are now distinct variants; pre-lock cap strings unified tocap:*namespace;MockStoregainswith_outcomeconstructor; new testtry_reserve_cap_breach_distinct_from_dedupasserts the variants cannot be confused; branchautotrade-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 frompositions.rstonats.rsaspub consts; newPublishOutcomeenum +build_and_publish_first_mover_entryasync fn infirstmover.rsis now the single shared emit path for both cron and future watcher;run_autotrade_entryinpositions.rsbecomes a 1-call delegation; 3 new unit tests infirstmover.rs(skips_non_core, skips_when_breaker_tripped, skips_when_publisher_none); regression pincron_run_autotrade_entry_emits_unchanged_signal_idinsources.rsguards the dedup key identity. — autotrade-firstmover-emit-hoist - Bug fix / Phase 2 B1 —
sources.rscanonicalsignal_idacross producers (branchautotrade-phase2-firstmover):cohort_signal_idnow lowercasesorigin_ref,condition_id, andinstancebefore hashing;enter_signalusescondition_idasinstanceinstead of the unstableevent_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 tomax_pricevalid untilvalid_until_tsneeds ONLY (a) a parse branch inwire.rs::parse_signalproducing aTradeSignalwith the newsource_id, and (b) a row inpmv2_autotrade_sources(source_id, enabled, mode). Mode is resolved generically inrunner.rs::per_message_mode(repo::autotrade_source→config::effective_mode(global, source), theconst fnMORE-CONSERVATIVE floor onoff(0)<dry_run(1)<live(2)); a missing source row + a failed config load both FAIL-CLOSED toMode::Off. DRY:weatherandcrypto_shortterm_latency_testshare oneparse_directive(p, action, source_id)helper (generalized fromparse_weather, 2026-06-19). DRY-PIN (generic capability, NOT what crypto uses): a row pinnedmode='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). Thecrypto_shortterm_latency_testsource is NOT dry-pinned — it is seededmode='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:AutotradeConfigis 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 ONLYmode+enabled; NO per-source cap columns. Arming the crypto latency test = a 3-part flip for the window: (a) crypto source row →mode='live', (b) global mode →live, (c) TIGHT global caps; because caps are global, those tight caps are the ONLY thing bounding the live test (no per-source caps to scope them). LOW-IMPACT today (global modeoff; 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. GateGATE_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+ themain.rscall-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_alertsgainedcfg:&AppConfigand injects aDataApiClient(viacfg.build_data_client, which already setsconnect_timeout), atokio::Semaphore(cap 4) bounding fan-out, andExitDebounce. THE CRUX — dedup vs cron (no double alert / no double sell): the exit pipeline already has THREE consumers of anEXITEDrow, each with its OWN dedup — (1) cohort EXIT alert (exits::run_check, dedup = thepmv2_exit_alerttimestamp cursor), (2) auto_sell (runner::run_exits, dedup =autotrade_exitscursor + themark_sellingopen→selling CAS that serializes ALL sell triggers), (3)insert_position_event= a PLAIN INSERT, NOON CONFLICT(theUNIQUE(proxy_wallet,condition_id,outcome_index)onpmv2_position_eventswas DROPPED in20260603000000_pmv2_overlap.sqlfor enter→exit→re-enter, so duplicate EXITED rows ARE possible at the table level — you CANNOT dedup exits via a DB constraint). THE SAFETY DECISION: the chain SELL path doesdetect_exitedscoped to{wallet}(live data-API fetch vs priorpmv2_positions) and for a confirmed exit LOGS the EXITED row (repo::insert_position_event) AND DELETES the priorpmv2_positionsrows (repo::delete_positions) — mirroring exactly what the cron’srun_pmv2_positionsdoes; it does NOT send a direct alert and does NOT callrun_exits/auto_sell itself. Deleting the prior snapshot row means the cron’s nextdetect_exitedwon’t re-see it ⇒ exactly ONE EXITED row exists ⇒ the existingpmv2_exit_alertcursor alerts once andrun_exitssells once (and even a stray duplicate is safe via themark_sellingCAS). 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’spmv2_exit_alertcursor (impossible to guarantee; the two rows carry differentdetected_tswhich 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 + theproject_autotrade-engine-specauto-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-detector→tracked.signals→ consumerpmv2-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 (merge36a4264/e6a9041) — alert only whennotional ≥ 2× the trader's own avg_bet_size(scored-only, reusedconviction_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 attempt9817110WAS an absoluteNOTIONAL_FLOOR_USD=25+6h dedup, superseded for exactly this reason). Ops gotcha: a pause→redeploy must use--deliver newor the consumer replays the whole JetStream backlog through the new filter. (3) consensus filter (CURRENT, merge0f71f89/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-memoryConsensusTracker=HashMap<MarketKey{condition_id,outcome_index}, HashMap<wallet,Instant>>, 48h window, pruned on the 60s cohort-refresh;eligible_buygate (side==BUY+ cohortscore>MIN_TRADER_SCORE(0.0), drops sells+non-sharps);record_buyfires multi-traderformat_consensus_alertwhen 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 statelessdecide_alert(...)->AlertDecisioncore witheligible_buy+statefulConsensusTracker(cross-message state). Observability fix shipped alongside: per-alerttracing::info!(target:"tracked_alerts", n=…, "consensus alert sent")— the live alert rate is finally visible in SigNoz (was a blind spot); SigNoz service namepolymarket-fetch-pmv2-tracked-alerts. Current state: LIVE on0f71f89, 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. Filescrates/polymarket-fetch/src/pmv2/tracked/{alerts_consumer.rs, alert.rs}. — tracked-alerts-consumer - TRADER PROFILE (verified, CORRECTS prior framing) — Polymarket trader
swisstony(proxy0x204f72f35326db932158cba6adff0b9a1da95e14, 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-alertsconsumer turns the Phase-1 on-chain detector’stracked.signalsstream into INSTANT first-mover buy AND sell Telegram alerts (recovering the first-mover edge lost to the ~15-min REST batch cadence). Branchautotrade-foundation, 2 commits806ba78+e1d1f24, gate green (471 polymarket-fetch + 90 polymarket-data, clippy -D warnings + fmt clean, GATE_EXIT=0); NOT merged, NOT deployed. SEPARATE from the autotradecohort.signals→WEATHER_SIGNALSlane (cohort-firstmover-nats-migration) — this is alert-only (subjecttracked.signals, streamTRACKED_SIGNALS, does NOT execute). Phase 1 (already shipped) = always-on detectorpmv2-tracked-detectorpublishing aTrackedEventJSON per cohort buy/sell fill. Service: new long-running CLIpmv2-tracked-alerts= a NATS JetStream pull consumer (durablealertson streamTRACKED_SIGNALS); dormant-safe (exits 0, env-first no-DB-touch, ifNATS_URL/NATS_TRACKED_ALERTS_NKEY_SEEDunset) so it’s deploy-safe before provisioning. Architecture: pure coredecide_alert(payload,cohort)->AlertDecision{Send|Skip|Reject}(parse→lowercase-keyed cohort lookup→format) wrapped by a thin NATS looprun_tracked_alerts; filescrates/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 viaorder_hashdedup → 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 batchformat_entry_alert/send_entry_alertUNTOUCHED — 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)]onTrackedSignalis CORRECT — canonical owned wire mirror; 6 fields (shares/fee_usd/block_number/tx_hash/order_hash/detected_at) unused by the alert but forward-looking for Phase-4; in a BINARY cratepubdoes NOT exempt never-read fields fromdead_code, so removing the allow re-fails clippy. (2) cohort-cache staleness is a non-issue — 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 autotradenats::run; near-zero from our own publisher but real diagnostic value). DEPLOY (pending, folds into the Phase-1 detector deploy): provisionNATS_TRACKED_ALERTS_NKEY_SEED(consume-only ontracked.signals); a JetStream streamTRACKED_SIGNALScapturingtracked.signalswith a durable pull consumeralerts; runpmv2-tracked-alertsas a long-lived service (alongsidepmv2-consumer). Specdocs/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 SAMEPmv2Consumerthat handles @weather (one unified lane routed by envelopesource); in-processrunner::run_entryDELETED. Branchautotrade-foundation(offmain, NOT deployed), 4 commits1166f51→bdafd7f, gate green as reported (428 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’swire::parse_signal, engineContextarm, and the singlesubmit_livesite are UNTOUCHED; only cohort transport changed. T1 (1166f51):parse_signalroutessource=="first_mover_core"→Execution::Context, FAIL-CLOSED (Reject) on any capital-critical missing/empty field —origin_refempty⇒cohort exits never fire (capital trap),outcome_labelempty⇒token_forfails, + reference_price/conviction/outcome_index;max_price/valid_until_tsbecameOption(cohort omits them;valid_until_ts=Noneis EXACT parity + avoids clock-skew wrongful drops). T2 (3c589c1):serialize_cohort+ headlinecohort_roundtrip_paritytest —serialize_cohort ∘ parse_signalbyte-equal tosources::enter_signalover the full persisted/exit-read field set (fixtures useevent_slug≠slugso a field-collapse is caught); SUBTLETY — watcher has no CLOB at detection soenter_signalleaves tick/min/neg_risk=None, serialize emits 0.0/0.0/false, parse rebuilds Some(0.0)/Some(false), IMMATERIAL because the Context engine arm re-resolves these from the live CLOB and never reads the signal’s values. T3 (9505840):nats::publish_cohort— serialize thenpublish_with_headersw/Nats-Msg-Id=signal_id(JetStream server-side dedup), then flush; reusesnats::connect. T4 cutover (bdafd7f, trade-safety reviewed):run_autotrade_entry(positions.rs) PUBLISHES instead of ingesting in-process — gated on active + Core tier + !breaker (the old!readypreflight clause DROPPED — preflight is now a consume-time concern); bounded retryPUBLISH_RETRIES=3/200ms, a dropped publish after retries = logged MISSED entry = under-trade NEVER a double (no telegram/breaker); publisher built once/run from envNATS_URL+NATS_COHORT_NKEY_SEED(absent⇒dormant⇒nothing published); DELETEDrunner::run_entry, deadsafety::ready, unusedAutotradeDeps.mode. SAFE: no double-LIVE order — dedup onsignal_idat BOTH the NATS header AND the DBON CONFLICT(dedup_key)backstop; money-path untouched; dormant cohort (off) publishes nothing. ⚠️ LOAD-BEARING GOTCHA: deleting watcher-sideautotrade_ready()removed in-process entry-preflight AND per-tickreconcile_entries— safe TODAY (cohort dormant/off, no live orders,/autotrade reconcileworks manually) BUT the Pre-arm consumer-preflight gate (the blocker shared with weather) must now ALSO restore entry-reconcile + preflight before arming ANY lane live, else the first live lane has stuck pending/submitted orders nothing auto-reconciles. DEPLOY (NOT done, T5): stream staysWEATHER_SIGNALS(ADDcohort.signalssubject, do NOT rename);pmv2durable must consume BOTH subjects; provision a cohort-publisher NKey; putNATS_URL+NATS_COHORT_NKEY_SEEDin the watcher env; @weather — cohort is a 2nd source, no schema change (fields Option-default at v1). Specdocs/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; branchautotrade-foundation, NOT deployed). LANDMINE: T3nats::publish_cohortoriginally 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 bindcohort.signalsinto streamWEATHER_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(...).await→PublishAckFuture→.awaitthe ack), wrapped in a 5stokio::time::timeout; compiler-pinned vs vendored async-nats 0.49.1 — a missing/misfiltered subject yieldsNO_RESPONDERS→StreamNotFound= a LOUD Err the bounded retry surfaces; persistence now CONFIRMED (PubAck). Single-order safety unchanged (Nats-Msg-Id=signal_idstream-dedup + authoritative DBON CONFLICT(dedup_key)backstop). OPERATOR-VISIBILITY: the existing telegramnotifyclosure threaded into the entry path so (1) an “armed-but-unconfigured” lane (cohort active but watcherNATS_URL/NATS_COHORT_NKEY_SEEDunset) 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 purepublisher_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 hardcoded1, so the producer tracks the parser’s max). T5 ADDENDUM: the cohort-publisher NKey must permit JetStream publish tocohort.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==DBdedup_key==legacyorders::dedup_key(..,"enter",..)); no double-order; no worthwhile duplication; zero comments/dead-code. REUSABLE LESSON: async-nats coreClient::publish*is fire-and-forget (Msg-Id stream-dedup still applies but you get NO ack and NO error on a missing subject) — usejetstream::Context::publish*+ await thePublishAckFuturewhenever 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 STAYSautotrade(spec’s→position_managerrename deferred). T1 (4c0b7d4) generalizeTradeSignal:SignalAction::{Enter,Exit}→Executionenum —Directive{max_price}(weather: explicit cap + market constraints) vsContext{reference_price,conviction,source_score}(cohort);signal_idis the dedup key,dedup_key()returns it directly; LOAD-BEARING — cohortsignal_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.rstrust 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_orderseam (engine.rs):ingest_entrydispatchesExecution→plan_context(cohort, byte-for-byte unchanged) vsplan_directive(weather: token_id-direct, outcome_index fromclob_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_gateadded. T4 (eb6ca9a) per-source mode:pmv2_autotrade_sources.modecol (20260617000000, CHECK off/dry_run/live),effective_mode=min(global,source)on off<dry<live, cohort backfilled tolive(eff==global, preserved),/autotrade source <id> mode …; LOAD-BEARING fix =build_autotrade_depsoverridesengine_cfg.mode=effso 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, theningest_entry, reusesdry_rehearsal/submit_live, breaker fail-safemap_or(true,..));nats.rsack discipline (ack_action: Persisted|Skipped→Ack, Transient→Nak, Stale|Terminal→Term), async-nats 0.49 raw NKey seed (SU…, no TLS), binds EXISTINGpmv2durable onWEATHER_SIGNALS/weather.signals; ack-only-after-persist;ON CONFLICT(dedup_key) DO NOTHINGmakes at-least-once safe. T6 (74faf7d)Pmv2Consumersubcommand: 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); migration20260617010000seedsweathersource at mode=‘off’. GOTCHAS: async-nats 0.49 JetStream calls returnBox<dyn StdError>(no anyhow::From) → neednats_errmap_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. Specdocs/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_betdocs/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 showedITF Zhengzhou: Yidi Yang vs Junhan Zhangclosed=false/acceptingOrders=truebutumaResolutionStatuses=["proposed"]+outcomePrices=["0.0005","0.9995"]— match decided (result proposed to UMA), just not formally closed. TWO holes: (1) the cohort entryprice_bounds_gatevalidates the COPIED TRADER’s frozenreference_price=p.avg_price(~0.44, in-band), NEVER the livebest_ask(0.001) we’d execute at → a market that collapsed AFTER the trader entered slips through; (2)liveness_gateonly skips FORMALLY resolved/closed/non-accepting markets — a UMA-proposedmarket is stillacceptingOrders=trueso it passes. Part A — ask-band gate (824a22d,engine.rs::plan_context): afterbest_ask, skip when ask outside[min_entry_price,max_entry_price], reusingprice_bounds_gateon the ASK; reason"ask … outside entry band"; Cohort/Context-only, NOT the weather Directive path (which legitimately buys up tomax_price=0.95); this is the data-source-independent primary safety net. Part B — resolution-proposed entry guard (8b3c451): newGammaClient::market_resolution_proposed(condition_id)readsumaResolutionStatuses; adapter (market_data.rs) sets newMarketStatusView.resolution_proposedFAIL-OPEN (30s timeout, warn, default false — a Gamma hiccup never breaksresolve(), Part A backstops);liveness_gateskips entries when set, reason"market in resolution". ENTRY-ONLY —is_terminal_market/exit_decision_forUNCHANGED so a resolution-proposed position stays sellable. GOTCHAS:umaResolutionStatusesis a JSON-ENCODED STRING ("[]"/"[\"proposed\"]"), NOT a native array — same asoutcomes/outcomePrices; parse viaparse_json_string_array(aVec<String>deserialize silently fails); inpolymarket-clob,MarketStatus.resolved = m.closedis a MISNOMER (copy ofclosed) 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_gaterejectsresolution_proposed;price_bounds_gatereused on ask) + autotrade-market-data-trait (MarketStatusView.resolution_proposed). — autotrade-resolution-proposed-entry-guard - OPS finding (point-in-time, from
journalctlonpolymarket-infra) — autotrade alert latency decomposed + NEW intermittentscheduled-run15-min timeouts. Current manifest (~/levandor/terraform/deploys/apps.yml):polymarket-fetch-scheduled-runjobschedule:"*:0/30"cmdscheduled-run --dedup-seconds 300 --gap-warn-seconds 3600;polymarket-fetch-botservice present;wallet-monitor+pm-arb-validatenow absent (decommissioned — supersedes the 2026-05-27 polymarket-fetch-deploy table);weather-bet-runservice +weather-bet-resolvedaily-14:00 job present. systemdType=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 precederun_pmv2_positionsinexecute_pipeline), exit/sell alerts run later still → a kill drops that cycle’s later alerts (incl. in-flight fire-and-forgettokio::spawnTelegram 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:EXITEDexit-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 proxiedreqwestw/oconnect_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@deployon babylon. — autotrade-alert-latency-and-run-timeouts
2026-06-16
- Bug fix — autotrade exit-cursor 404 WEDGE:
run_exitsbroke on the FIRST exit-event whose CLOB/book404’d and parked the cursor atfirst_failed−1µs; a RESOLVED/CLOSED market returns"No orderbook exists 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). FIXb5d5986= status-based classification:market_data.rs::resolve()now skipsbook_topforresolved||closedmarkets (returns emptyBookTop, no 404). New pure helpers inmarket.rs:is_terminal_market(status)=resolved||closed,ExitDecision{Skip,Exit,Retry}withadvances_cursor()=Skip|Exit,exit_decision_for(market). Both exit paths (process_exit_event,drain_pending_exitsinrunner.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_sellreturns an informational msg for terminal markets. Terminal positions close via the separate SETTLEMENT sweep; entry path unaffected (liveness_gatealready skips resolved/closed beforebest_ask). GOTCHA: vendored CLOB client (polymarket-clob/src/lib.rs:251status_from) aliases BOTHresolvedandclosedto the single fieldm.closed→is_terminal_marketeffectively reduces to “closed”; re-verify if a future change populatesresolvedindependently. Gate green 345 polymarket-fetch + 87 polymarket-data (baseline 335, +10 tests); trade-safety review 7/7 (terminal predicate=resolved||closedonly; transient still halts; breaker untouched by read errors; zero entry/reconcile blast radius). NOT on main/VM (VM runsbf4b628) — live engine still wedged until merge+redeploy. — autotrade-exit-cursor-404-wedge-fix /autotrade haltcommand — atomicmode='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-v2Amount::shares(value)rejects any share amount withvalue.normalize().scale() > LOT_SIZE_SCALE(=2)— shares trade in 0.01 lots (types/mod.rs:193-200, re-appliedorder_builder.rs:25).auto_sellpassed the RAW on-chain fill (locked vector 7.6219 shares, scale 4) straight tobuild_order → Amount::shares→ build error → revert row toopen→ EVERY real fractional-share position unsellable (auto-exit AND/autotrade sell), capital trapped until resolution; round-1 checkedassert_sell_amountsbut not the deeper vendored lot guard. FIXe93dde1:sell_shares = filled_size.trunc_with_scale(2)used consistently forbuild_order+quantize_sell(expected_shares)+assert_sell_amounts; sub-0.01 dust held to resolution (return beforemark_selling); BUY unaffected (Amount::usdcscale ≤6, market builder has no lot gate). 2E (Important, false risk-limit,84e3e34):max_copies_per_eventloaded/validated/shown in status but NEVER enforced — cohort members entering the same event each copied up to GLOBALmax_open; FIX = per-event_slugCOUNT 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 anopenrow still carryingexit_pending(cohort exit never completed → rode to resolution); exit cursor advanced to last-successdetected_tsand permanently DROPPED an event sharing that timestamp → forward-onlyresume_cursor(halt resumes atfirst_failed−1µs); reconcile’s unbounded per-row CTFbalanceOfsweep ran before entries each tick →LIMIT 25oldest + visibility notify. DRY (efc69bc) + self-inflicted regression (0f06ec5): extractedcollecting_notify/AutotradeDeps::market()/record_fill_then_open(FillOutcome), removed deadmark_dry_run/load_autotrade_order; thecollecting_notifyextraction regressed/autotrade sell’s reply —run_selldrained viaArc::try_unwrapwhile 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_onlyunwired, 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 offstops 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 inrunner.rs+positions.rs::init_autotradesig updated to accept&CopyConfig); task e DONE (exits cursor seeded toUtc::now()at first arm inarm_live). 315+87 tests pass. — autotrade-3c2-validation-carryforwards - Task 7a — selling-state exit foundations:
assert_sell_amounts(5 TDD tests) + sharedassert_contractDRY extraction inexecute.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) inrepo.rs. Zero-submit. Gate: 306+87 tests, 0 failures. — autotrade-selling-state-foundations - Bug fix —
breaker.rslock-order scanner false-negatives from{/}inside string/char/raw-string literals; addedneutralize_literal_braceschar state machine (preserves byte length); removed spurious#[allow(dead_code)]fromrepo::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_eventwas loaded + validated but never enforced at order placement. Added field toEngineConfig+ReserveCaps; mapped inengine_config()(0=unlimited, negative clamps to 0); in-tx per-event-slug count query intry_reserveLIVE branch under existing configFOR UPDATElock;caps_from_cfg()const fn +pre_lock_cap_skip()helper extracted to keepingest_entryunder 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 incommands.rs— deduplicatesArc<Mutex<Vec<String>>>+ closure fromrun_reconcile+run_sell. (2)AutotradeDeps::market(pool)method +build_autotrade_depspromoted topub— replaces 3 verbatimClobMarketDatastruct literals inrunner.rs,positions.rs,commands.rs. (3)FillOutcomeenum +record_fill_then_openasync fn inexecute.rs— shared byrecord_placed_fill+heal_filledinreconcile.rs. (4) Dead repo functions removed:repo::mark_dry_run(dry path inserts direct intry_reserve) +repo::load_autotrade_order(superseded byautotrade_sellable_by_id/load_reconcilable_entries);AutotradeOrderRowstruct retained. — autotrade-dry-refactors-2026-06-16
2026-06-15
- Task 5 — circuit breaker consolidation:
bump_breaker+BREAKER_THRESHOLDextracted fromexecute.rsinto newbreaker.rs; SQL moved torepo.rs::bump_breaker_count(config-row-only FOR UPDATE tx); §3.4 lock-order regression test added (structural scanner reads all autotrade*.rsfiles on everycargo 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_tradesonSigningClient; KEY FINDING:SignedOrderhas noDeserialize— spec’s serde round-trip cannot compile; fix = dropSignedPayloadwrapper, holdsigned_order: SignedOrdertyped value directly inBuiltOrder; 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) inBotStatedecouples the ~25s fallback from the 4-slot handler pool (prevents/consensusstarvation);run_and_persistgainsOption<Duration>timeout param and commitsstatus='error'on timeout (no more'running'row leaks);recent_category_consensusfilters bytop_n+min_overlap(param-matched caching, 3 call sites updated);derive_leaderboard_categorychecks Politics/GEO before Crypto (matchescategory_bucketprecedence); HTML-safe reply truncation cuts at last newline within 4000-char limit (prevents split<a>tag rejection); DRY:polymarket_path+tags_contain_anyextracted,CONSENSUS_CACHE_WINDOW_SECSshared. Gate: build+clippy -D warnings+fmt+tests green. — pmv2-bet-link-analyzer-flow - Category-cohort fallback IMPLEMENTED and merged (commit
22d7697, 2026-06-15). Updatedpmv2-bet-link-analyzer-flowto reflect shipped state: trigger =!has_high_edge_signal; orchestration viaresolve_fallback_category+category_consensus_section+run_and_persist(cache-warm seam) inpmv2/category_consensus.rs; wired inbot.rs::score_bet_link_body(now acceptschat_id/loading_id/reply_tofor interim spinner); constantsTOP_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_message→MessageAction::BetLink→score_bet_link_bodybot.rs:611→pmv2::scoring::score_betscoring.rs:414readspmv2_positionscohort viaload_bet_holders→format_bet_scores_telegramscoring.rs:593), its TWO distinct no-signal states (scores.is_empty()“No edge-cohort positions…”scoring.rs:514vs all-below-GAUGE_FLOOR=0.005emptyconsolidate_gauge.sides“no high-edge cohort holders…”scoring.rs:472/379,consolidate_gaugeempty-safe), and how that EDGE-scored cohort differs from/consensus <category>’s freshly-fetched monthly-PNL cohort (handle_consensusbot.rs:866, 9 leaderboard categoriescategory_consensus.rs:13). Four gotchas captured: consensus cache only warms onstatus='done'(repo.rs:1601/1542); no scheduler warms it (usually cold);parse_selectorscoring.rs:91returns the LAST path segment = MARKET slug on deep/event/<event>/<market>links, so feeding it togamma.event_tags()(EVENT slugs,client.rs:505) silently fails (preferBetScore.event_slug); and the TWO category taxonomies (9 leaderboard cats vs coarsepmv2_event_category.categoryesport/sport/geo/crypto/other) bridged only bycategory_tags+GEO_TAGS/CRYPTO_TAGS. Draft fallback specdocs/superpowers/specs/2026-06-15-pmv2-bet-link-category-fallback-design.mdnoted as NOT implemented. — pmv2-bet-link-analyzer-flow - Task 6 — ERC-1155
balanceOfon-chain primitive (autotrade/onchain.rs, branchautotrade-foundation).alloy sol!generatesbalanceOfCall(camelCase +Callsuffix);SolCall::abi_encode()for calldata;alloy_primitives::hex::encode(no separate hex crate); selector0x00fdd58e(ERC-1155, distinct from ERC-200x70a08231); spike doc hex-decode error corrected (0x2c641ab6=744_757_942, not744_144_054); binary-only crate requires--bin polymarket-fetchnot--lib;polygon_rpc_urladded toAppConfig; 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).OrderV2non-exhaustive gotcha (sol! macro, FRU blocked — use field-by-field assignment onDefault); 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_contractmust beconst fn;alloy-sol-typesmust be an explicit workspace dep. — autotrade-eip712-order-hash - Task 7 — autotrade repo exit helpers (
repo.rslines 1270–1359, branchautotrade-foundation). 4 new async functions + 1 struct added to support T8 exit-detection:AutotradeExitEventRow(sqlx::FromRow, mapspmv2_position_eventsexit join result);autotrade_exits_cursor(reads'autotrade_exits'frompm_detection_cursors, returns 0 if absent);advance_autotrade_exits_cursor(upsert viaEXCLUDED.last_check_at, matches codebase style);autotrade_exit_events(kind=‘EXITED’, detected_ts > cursor, joined topmv2_autotrade_orders);autotrade_open_orders_for_origin(fetches status=‘open’ orders by condition_id/outcome_index/origin_ref);autotrade_source_enabled(fail-closed bool frompmv2_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.rsfirst_mover_core emitter +Tier::from_label+tieronCohortTrader(branchautotrade-foundation).Tier::from_label(s: &str) -> Option<Tier>case-insensitive parser inmodel.rs(Core/Proven/Emerging; does NOT change classification logic).CohortTradergainedtier: Option<Tier>field inpositions.rs.latest_scored_cohort_with_scoresinrepo.rsnow SELECTst.tierTEXT and maps it viafrom_label(tuple gainedOption<String>betweenavg_lost_bet_sizeand rank). Newsources.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(freezesreference_price = p.avg_price, NOTcur_price),format_dry_run_order.pub mod sources;added toautotrade/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, branchautotrade-foundation).engine_config(cfg: &AutotradeConfig) -> EngineConfigconverts BigDecimal/i32 DB types to f64/i64/usize; buildsCopyConfigfrom autotrade’s OWN sizing fields,category_filterviaparse_topbets_filter.PgPersistence<'a>implementsPersistence: dry-run reservations insert with terminal statusdry_run(excluded from dedup partial index + exposure queries); live inserts aspending. Dedup viaON CONFLICT (dedup_key) WHERE status NOT IN ('dry_run','closed','canceled','failed') DO NOTHING RETURNING id.FOR UPDATElock on config row id=1 prevents double-reservation races.spend_today_usdcounts onlymode='live'rows.OrderMode::as_str/from_db+OrderStatus::from_dbmappers added toorders.rs. BigDecimal conversion requires BOTHuse bigdecimal::{FromPrimitive, ToPrimitive}. Transaction executor uses&mut *tx(sqlx 0.7+, same asrepo.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-v2intopolymarket-clobcrate. Import paths forU256/Unauthenticated/Client/Config;U256::from_str(decimal display, nofrom_str_radix);Client::newhas no chain ID arg; bon builder returns struct directly;MarketResponsefield types; four vendored examplerequired-featuresfixes; clippy landmines from enabling"clob"feature (derivable_impls,single_option_map,unused_self,allow→expect). — autotrade-clob-crate-bindings pmv2-autotrade-engine-design /autotradeTelegram command wired (commands.rs+bot.rs, branchautotrade-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 torun_command; owner-auth gate fromauthz.rsnot duplicated (already present at top of match).apply_set_ok_and_errorstest 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, branchautotrade-foundation).AutotradeConfig(sqlx::FromRow,BigDecimalfor NUMERIC,i32for INTEGER) +load(pool)(fetch singleton id=1) +update_numeric(f64→BigDecimal vianumeric()helper, same pattern asrepo.rs) +set_text(nullable text) +set_source_enabled(updatespmv2_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: runtimesqlx::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)inrepo.rs(runtime sqlx, returnsOption<String>from cache);resolve_category(pool, gamma, event_slug)incategory.rs(cache-hit-or-fetch: reads cache → falls back togamma.event_tags(&[slug])→category_bucket→upsert_event_category; returnsOk(None)if gamma doesn’t know the slug). Both carry#[allow(dead_code)]until T4/T8 wiring. Key gotcha:event_tagstakes&[String]— bindlet slugs = [event_slug.to_string()]before passing&slugsto avoid temporary lifetime errors.GammaClientimported frompolymarket_datacrate. No staleness logic — batchrun_category_syncowns refresh. — autotrade-category-resolver autotrade-gates pmv2-autotrade-engine-design
2026-06-14
- autotrade Plan 2 capstone —
engine.rsimplemented; all 8 tests pass (branchautotrade-foundation).ingest_entrywires the full gate sequence via injected traits: mode-check →liveness_gate→price_bounds_gate→ev_floor_gate→ min-conviction →category_gate→chase_gate→ exposure-cap →try_reserve(single mutation point). Key decisions:EngineConfigdropsClone(CategoryFilter holds a HashSet);min_round_trip_edge_bpsmust be >200 in tests (ev floor:size × bps/10_000 > size × 0.02; bps=1 can never pass — test helper uses 300);ingest_entrytakes&(dyn Trait + Send + Sync)for tokio multi-thread Send bound;category_gatepassesNonefor 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.rsimplemented (branchautotrade-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 tocrate::pmv2::category::passes_filter;Nonefilter = Proceed),price_bounds_gate(rejects outside[min_entry, max_entry]),ev_floor_gate(skip unlesssize × bps/10_000 > est_fee;#[allow(clippy::cast_precision_loss)]fori64 as f64),chase_gate(skip if ask exceedsreference_price × (1 + bps/10_000)— uses the price frozen at signal detection). Helperest_round_trip_cost_usd(size_usd) = size × ROUND_TRIP_COST_RATE (0.02). 9/9 tests pass, clippy clean. Gate script passes — priorexit=1was rtk intercepting-- -D warnings(not a real clippy error). — autotrade-gates pmv2-autotrade-engine-design - autotrade P2.3 —
MarketDatatrait +ResolvedMarket+token_for()outcome→token validation (market.rs, branchautotrade-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).MarketDataasync trait (singleresolve(condition_id, outcome_index) -> Result<ResolvedMarket>method). 4 tests pass. Gate gotchas:clippy::unwrap_usedfires in test bodies (add#[allow]);cargo fmt --checksorts imports alphabetically;async-traitmust 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+SignalActiontypes implemented (signal.rs, branchautotrade-foundation). Canonical signal type that all autotrade sources emit.SignalActionenum:Enter { reference_price: f64 }(price frozen at detection, feeds the engine’s chase guard) +Exit.TradeSignalstruct:source_id,action,condition_id,outcome_index,outcome_label,event_slug,slug,origin_ref,conviction,source_score,tier,instance.dedup_key()delegates toorders::dedup_key()(canonical 6-discriminator function stays inorders.rs).Tierre-exported frompmv2::model. 2 tests pass. Clippy fix:label()promoted toconst fn, match arms useSelf::(use_self lint). — autotrade-signal-type pmv2-autotrade-engine-design - autotrade Task 2 —
authz.rsmodule implemented (branchautotrade-foundation). Pure authorization layer:Scope(Public/Owner),Authz(Allowed/Denied),OwnerSet(parsesTELEGRAM_OWNER_IDSCSV),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 intorun_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 officialPolymarket/rs-clob-client-v2Rust crate (alloy) intocrates/polymarket-clob/— the only place that signs; NO Python sidecar, NO hand-rolled signing. Wallet: Bandi PROXY0xdb0d23c2c3468da13c6e8abfe96edfecc72d9efe,signatureType=1(maker=proxy, signer=EOA0xfa77…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-deferrableverifyingContract-vs-neg_riskassert, never hardcoded. Control: owner-only Telegram via reusableauthz.rs(Scope::Owner,TELEGRAM_OWNER_IDS);OFF/DRY_RUN/LIVEkill-switch gating ENTRIES; exits governed by the per-ordermode, 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; asellingreserve state + global lock order (config-before-order) preventing double-sell + ABBA deadlock; on-chain CTF-balanceOfreconciliation as ground truth; circuit breaker (entries-only). Phase-0 SPIKE mandatory before engine — pin amount-quantization (verbatim from the client’sroundDown/roundNormal, NOT “GCD of price”),order_hash↔orderID, and V1/V2 collateral; can’t be settled on paper. Isolation: frozen scoring/topbets/consensus/decide_copymath UNCHANGED — engine only consumes outputs (additive read-path touches:tierinto cohort read,bet_confidenceunconditional, newpmv2_position_eventsexit 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
/topbetsend-date1970-01-01fix. Symptom:/topbetsshowedends 1970-01-01for some markets. ROOT CAUSE: the data-api positions feed returns the epoch placeholder1970-01-01as the marketendDatefor 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 eventendDateinto a newpmv2_event_category.end_datecolumn (migration20260608000000;event_tagsreturnsend_date;upsert_event_categorystores it); (b)map_positionrunsnormalize_end_date→ drops empty/pre-2000 dates so1970-01-01becomes NULL; (c) the 3 cohort-holder queriesCOALESCE(p.end_date, ec.end_date)so epoch markets fall back to the real event date; (d)/topbetsmarks past-dated-but-held marketsends {date} (unresolved)(viaend_date_segusingsnapshot_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 wasredeemableOR past its end-date, so still-HELD bets on past-but-unresolved markets (elections) were filtered out. FIX: replaced withis_position_redeemed(position)=position.redeemableonly; the self-sync now keeps currently-held positions (redeemable=false,size>0) regardless of end-date, dropping only resolved + dust — applied at BOTH the storedupsert_positions_for_walletAND the live/walletsfetch_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.shremains 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 inAGENT_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/OVERALLboth work;timePeriod=month/MONTHboth 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: thepolymarket_dataCategoryenum (types.rs) ALREADY has all 9 +Overall, andleaderboard()already sendscategory=params.category.as_query_value()(UPPERCASE, works); pmv2’s cohort fetch usesCategory::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}…” andtokio::spawns a background job: (1) fetch top-N category leaderboard traders by monthly PNL, (2) fan out their positions (boundedbuffer_unordered(16)+ 30s per-wallettokio::timeout), (3) filter to in-category bets via gammaevent_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 (migration20260605010000_pm_category_consensus.sql):pm_category_consensus_runs(status running→done/error, cohort_size, top_n) +pm_category_consensus_rows. Modulecrates/polymarket-fetch/src/pmv2/category_consensus.rs. NO edge/band — these fetched traders aren’t inpmv2_traders; signal = agreement-count + stake + leaderboard PNL. category→gamma-tag mapping: identity for most, butculture→pop-culture,economics→{economy,economics},mentions→NO clean tag (skip-filter fallback). REPLACED the earlier short-lived tag-basedtag_consensusSQL 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_eventsEXITEDfeed (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 viapm_detection_cursorsdetectorpmv2_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-snapshotcash_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)“. Migration20260604000000_pmv2_event_pnl.sqladded cash_pnl/initial_value/current_value/cur_price/outcome/title columns topmv2_position_events(captured ininsert_position_eventfrom the prior PositionRow, no call-site change).pmv2-exitsCLI = read-only 24h preview. Modulecrates/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:
“. Onpolymarket-fetch botboot, sends a best-effort Telegram messageNA CSUMI CSUMI CSUMI, bot STARTED, REF: <short-sha>(omits REF if no hash). A newcrates/polymarket-fetch/build.rscaptures the short commit hash at build time, trying envBUILD_GIT_HASH→GIT_HASH→GITHUB_SHA→git 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.gitand without one of those env/build-args, the deployed bot reports no SHA — passGIT_HASH/GITHUB_SHAat 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 acrossc9a4e12(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 usedCOALESCE(rank_all,…)but the PM leaderboard UI defaults to the MONTH tab →COALESCE(rank_month,rank_week,rank_day)at all 5 query sites; (2)$0dust lines — holder queries readpmv2_positionswith noredeemable=falsefilter so resolved positions lingered as “held” with near-zero stakes rounding to$0→redeemable=false AND initial_value>1(GOTCHA:pmv2_positions/pm_positionskeep resolved rows until the next prune — always filter redeemable when aggregating holdings); (3) “No — for what?” —consolidate_gaugegrouped byoutcome_indexalone, 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
ec75491onmain, net −156 lines)./wallets(bot) + CLIself-statuspreviously 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-weightedbet_score, reusing the bet-link scoring path. New:CohortOverlap+overlap_by_position()inpmv2/scoring.rs;cohort_holders_for_conditions()inpmv2/repo.rs(mirrorsload_bet_holders, batchedWHERE condition_id = ANY($1)); a single sharedfetch_self_overlap_sectionshelper feeds BOTH bot and CLI (DRY), best-effort (lookup error degrades to “no overlap”, never fails the command); rendered viarender_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 dormantpm_*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-4794a76code (participant_changescursor STILL advancing every minute, a detector DELETED in4794a76), spamming the operator with a v1 “watched wallets position dump”. Diagnosis viapm_detection_cursors: a detector that only exists in old code advancing = old container still live (same root-cause class as the Round-50 Telegram 409getUpdatestwo-poller conflict). Fix is VM-side (kill stale container, leave one on HEAD), escalated to@deployvia 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 nowec75491(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>→ atagsarray of{label, slug}; the/marketsendpoint (GammaClient::market_metadata) returnscategory=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 taggedsports→ 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..4906eeaonmain). Joiner-seed fix inrun_pmv2_positions(is_new_entry: only emit ENTERED for wallets already in the prior snapshot, so cohort reshuffles can’t fabricate entry bursts);trending_betsvelocity query (distinct high-score cohort wallets entering in a 6h window vs prior);/trendingbot command +pmv2-trendingCLI; 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 /topbetscategories+filters+price+multi-date shipped (5c933a5..908824fonmain).pmv2_event_categorycache populated byrun_category_sync(gamma/eventstags, best-effort, bounded per-chunktokio::timeout);eq:geo/neq:sport,esportfilters; entry→current price display (52¢ → 58¢); multi-date fix inload_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 manglescargo … -- -D warningsat the top level (passes-Das 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
4794a76onmain, +932/−2381, migration20260603000000_pmv2_overlapapplied 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_positionsis now the source of truth for cohort ENTER/EXIT, writing an append-onlypmv2_position_eventsfeed; detection + event logging are DECOUPLED from the copy-enabled gate (newshould_detect(prior_len)=prior_len>0), only the paper-copy ACTION stays gated onpmv2_copy_config.enabled→ events populate even with the copy lane OFF. Newdetect_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 fetched —fetch_positionsnow 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 withrun_id+trader_score+avg_won_bet_size+avg_lost_bet_size(events outlive cohort membership; avoids a fragile “which run’s score” join). New moduleoverlap_watcher.rs: cursor-based (pm_detection_cursorskey'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 viapm_overlap_alertsUNIQUE(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 topm_self_wallets+ latest twoself_snapshotruns, first-run suppressed) → ACTIVE notifier (self-sync start/complete summaries stay SILENT;run_self_sync_for_schedulernow takes both notifiers);self_trade_changesrepointed off the stalepm_tradesto the liveDataApiClient::trades. Removals:change_detection.rs+consensus_participant_changes.rsdeleted, consensus-only repo/notifier surface removed, pipeline + bot daemon repointed (overlap watcher replaces change-detection + participant-changes everywhere). Migration (forward ALTERs on the LIVEpmv2_position_events): dropped the per-key UNIQUE (append-only enter→exit→re-enter lifecycle),kindCHECK →('ENTERED','EXITED'), addedrun_id/trader_score/avg_won_bet_size/avg_lost_bet_size, createdpm_overlap_alerts. GOTCHA: the Postgres-generated unique constraint name was TRUNCATED to..._outcome_inde_key(not..._index_key) → always look up real constraint names viapg_constraintbefore 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 usespmv2_copy_config.score_thresholdwhich defaults to 0 → ALL scored cohort traders qualify → noisy → setscore_threshold>0(e.g. 0.03–0.05) before/after deploy; the paper-copy lane stays independentlyenabled=false. LOOSE END: self-status CLI + bot/walletsstill render v1 consensus alignment from the frozenpm_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-existingscoring.rswarnings, 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:0.4, all clamps toupsert_market_resolutions/flatten_resolution_rows(crates/polymarket-fetch/src/pmv2/repo.rs) had NO dedup before anON CONFLICT (condition_id, outcome_index) DO UPDATEchunked insert → a duplicate key in one chunk crashes withON 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 withdedup_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_winnersis SHARED — feeds BOTH the watcher resolution/PnL pass (positions.rs) AND the validated edge/consistency trader scoring (mod.rs→compute_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_configwas an unconditionalfetch_onebefore the enabled-check → a missing singleton row errored the ENTIRE positions snapshot; fixed →fetch_optional+CopyConfig::disabled()default; (2)insert_position_eventswallowed errors via.ok()+ no unique constraint → silent audit-log loss + duplicate ENTERED rows on overlapping runs; fixed →tracing::warnon error +UNIQUE(proxy_wallet,condition_id,outcome_index)+ON CONFLICT DO NOTHING; (3) per-event cap bypassable within one cycle (event_cap_usedread only committed DB rows → N new same-event entries each saw used=0 and all inserted, blowing pastper_event_cap_usd); fixed with an in-flightHashMap<event_id,f64>accumulator + a purewithin_event_caphelper +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 — theclamp(min/max(overall,0.01),0,1)ratio form is correct BY DESIGN solong_term=consistency×spanstays 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 areNUMERIC(6,5)). DEFERRED (real, out of scope): sizing-formula tuning (score_weight=score·10+1makesbase_usda near-no-op above scoremax_size_usd) → tune at enable-time; no per-wallettokiotimeout onfetch_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 migration20260602030000_pmv2_watcher.sqledited (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 onmain:96104b9delete thepm-arbon-chain copy-lane crate (−12,087);7b3132dbacktest/short-term/wallet-monitor/persistence analysis (−6,296);0befd3bconsensus/ranked/safe-bets CLI +/safebets//ranked//snapshotbot commands (−1,297);5ffc330repointscheduled-runto pmv2 + delete the consensus/safe-bets/trades pipeline +polymarket-dataconsensus.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//snapshotand gained the bet-link auto-reply (paste a Polymarket link →score_betreply).scheduled-runis 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 inpmv2/scoring.rs, non-blocking). GOTCHA avoided: a stray edit to the already-applied20260530000100_pm_paper_trades_mid_history.sqlwas accidentally committed then reverted — would have tripped the sqlx checksum on startupmigrate; never edit an applied migration. Deploy handed to @deploy via AGENT_HANDOFF.md Round 49: deploy5ffc330, decommissionapps-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::scoringmodule (score_bet,group_into_bet_scores,parse_selector,bet_confidence-basedBetHolder::conviction,format_bet_scores) +repo::load_bet_holders/latest_position_snapshot_ts. Read-only (joinspmv2_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 betbet_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 NEGATIVEtrader_scorecorrectly DRAGS the score down — skill-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-hocleft(slug,40)in an earlier diagnostic truncated the slug (...by-may-3was 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 (wirescore_betto the bot for on-demand link checks); (2) Phase 2b (poller → delta detection onpmv2_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-positionscommand +pmv2_positionscurrent-state table (migration20260602020000) 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-chainOrderFilledcopy 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 justtimeout. Symptom:pmv2-positionshung ~75 min silently at--concurrency 8— zero rotation/retry/eviction warns, no completion. A proxy that accepts the TCP connection then stalls the HTTPSCONNECTtunnel 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 sharedbuild_http_client(coversDataApiClient+GammaClient) and onbuild_proxy_client, plus a per-wallettokio::time::timeout(90s)infetch_positions, plus lowered the positions default--concurrency8→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 BOTHtimeoutandconnect_timeouton 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_pricemeasures exactly that). Implication: the Tier-2 copy-worthy cohort is small & sharp (~top 5–10), to be gated on an edge/trader_scorethreshold, 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 lowlong_term. OPEN ITEM (before Tier-2): wallet addresses with a numeric-<timestamp>suffix recur —0xcF609D…-1771809916847at #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_scoreconsensus framing is dropped as a trigger. (2)bet_confidenceis 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 onpmv2_traders): qualify gateresolved_bets >= min_resolved(default 50, else unscored/sinks); skill baseedge= 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_confidencemeasures 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 withrun_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 (migration20260602010000_pmv2_scoring.sqladdsedge/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 --xrefco-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 capturescohort_price,detect_latency_ms,up_mid_30s_ago/up_mid_60s_agofor 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_scorerecommendation 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 indexidx_pm_pos_condition_enddate ON pm_positions (condition_id, end_date)turnedfetch_market_end_datesfrom ~459s → ~8s index-only scan (migration20260601120000_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 fmtclean,cargo clippyno issues, WS1 13 tests pass, WS2 4 tests pass. WS1 consensus hygiene (crates/polymarket-data/src/consensus.rs): newcompute_consensus_groups_with(members, min_wallets, now, dust_floor_usd); publiccompute_consensus_groupsdelegates withUtc::now()+DEFAULT_DUST_FLOOR_USD=5.0. New eligibility filter drops redeemable, past-end_date(parsed%Y-%m-%dthen 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’swallet_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_duereadsmax(computed_at)frompm_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-minconsensus_refreshpath is unchanged. Two follow-ups: (1) DRY —parse_end_datenow duplicated inconsensus.rsandrepo.rsacross crates → hoist intopolymarket-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.rstestsload_grids_buckets_events_onto_gridandload_grids_skips_blank_and_malformed_linesPASS in isolation but FAIL under full-workspacecargo test— the temp file is named withprocess_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 thetempfilecrate / 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>=2agreement rule with a skill-weighted LOGARITHMIC OPINION POOL (geometric mean of odds) over the FRESH (built frompm_tradestrade_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-shrunkedge_lowerwith a hard per-wallet CAP (to neutralize the one +30σ wallet) + a boundedtanhstake-conviction term. Output calibratedp_agg;edge = p_agg − cur_price. Trade only when residual edge survives fees + √-impact slippage AND skilled money isn’t split (disagreement → abstain). Sports noise dissolves for free — sports wallets have ~0edge_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 ofedge_lower>0wallets 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 frompm_trades; add a realresolved_at(pm_market_resolutionsonly hasfetched_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 atshort_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:183→polymarket_data::select_top_n_intersection, definedcrates/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 yieldedintersection_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.rs→pm_short_term_trader_accuracytable (Wilson lower boundZ=1.96:63; small-sample penalty pinned byedge_lower_punishes_single_lucky_win:524; edge = hit_rate−avg_entry_price; conservativeedge_lower= Wilson−price;edge_z; Brier; split-half persistencesplit_halves:216gated atPERSISTENCE_MIN_MARKETS=40:11/:294; Monte-Carlo luck baselineedge_luck_distributionLUCK_ITERATIONS=200:12/:259→luck_p_value+sigmas_above_null:319/:335) — surfaced only by theshort-term-accuracy/ranked-traders/diagnose-persistencecommands, 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 frompm_short_term_trader_accuracywithn_markets>=40ANDedge_lower>0(optionaledge_zfloor) 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_mswas always 0:polymarket-data/onchain.rscomputes a ms-preciseDetection.latency, butcopy_lane.rsdiscarded it — kept onlyblock_time.timestamp()(whole SECONDS) onCohortFill.block_ts, thencopy.rs::evaluate_copyrecomputed latency as(now−b)*1000from those seconds; real detection latency is sub-second so it floored to 0 (0 not NULL proved block_time WAS populated). Fix: carrydetect_latency_ms: Option<i64>fromdetection.latency.as_millis()throughevaluate_copyinstead of truncate-then-recompute (expect ~200–900ms post-deploy). (2) implausibly negativechase_spread(entry cheaper than cohort fill):copy_lane.rsfell 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: newcopy_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; matchesmarket_state.rsask_history_down=1−bid). Durable insight: on a binary up/down marketask_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(commitb3f9e21, 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 (OrderFilledlogs 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_walletfor all 5 wallets; realOrderFilledtopic0 is0xd543adfd…(NOT canonical0xd0a08e8c); cohort settles on CTF Exchange V20xE111180000d2663C0091e4f400237545B87B996B; per leg, got-token+gave-USDC=BUY / gave-token+got-USDC=SELL, price=USDC/token (both 6-dec), token→outcome via gammaclobTokenIds; reconciled againstpm_trades. Architecture: detector (polymarket-data/onchain.rs, WSSeth_subscribelogs +newHeadsblock-ts cache) →copy_loop(pm-arb/copy_lane.rs, spawned invalidate::run) → decode → token→market map (rotation poller) → gatedevaluate_copy(pm-arb/copy.rs: cap/floor/window/dedup/daily-notional, rejects SELLs) →lane='copy'rows inpm_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_configsingleton (enabledkill-switch DEFAULT FALSE, cap_price, min_price, size_usd, min_window_left_secs, max_daily_notional_usd) +pm_copy_wallets(per-wallet enable, seeded with the 5 cohort wallets). Start =UPDATE pm_copy_config SET enabled=true. Deploy state: both migrations applied via Supabase MCP + verified,enabled=false(dormant, zero-risk);pm-arbdoes NOT runsqlx migrateso copy migrations had to be applied manually; needs new env varPOLYGON_WSS_URL(added toPOLYMARKET_FETCH_ENVGH secret by @deploy — until set the lane idles even if enabled, logs a warning, doesn’t error); handed to @deploy inAGENT_HANDOFF.mdRound 42. Honest limits: assetId↔gammaclobTokenIdslink verified on 2 fills (Gate-0 forward run = confirm copy rows actually appear = definitive proof); analytics niggles (up_mid_at_signalstores entry ask not up-mid;detect_latency_msmeasures 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 onpm_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 frompm_clob_book_snapshots. — longshot-strategy - Corrected the previous Obsidian note:
0x75cc3bwas the cohort outlier (and is wallet #2 by 30-day ROI in our band+window, not the top), not the template. The actual #1 is0xb27bc932at +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.0x75cc3bsells 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 to0x75cc3b’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 (BTCbuy_upand ETHbuy_downdominate, +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’s20260530000000_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_migrationswith 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-2380swallows sqlxMigrateErrorso failures degrade silently — tracked as task#15. Also durable: auto-catch per @deploy Round 36 applies topm-arbonly — 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 (0xb55fa129style: +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"inchains[]/chainTvlskeys;auditsfield is stale (0sometimes means “no audit,” sometimes “DeFiLlama doesn’t know” — Frax Swap is the canonical FP). The design is documented in the newcontract-scannerproject 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-arband validated it against the top trader. Re-derived 0x75cc3b’s actual trades frompm_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_PRICE0.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_MULT2.0→1.3, enforced inmarket_state.rs::try_resolve_pending(fills >1.3× signal price are net-negative); (3) cap tiw≤0.93DEFAULT_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 --resumeand couldn’t be cleared from the CLI. Root cause: 26 MB / 14,155-message transcript, auto-compacted 3×, ~3,000 messages live since the lastisCompactSummary(line 11,118) — over the model token ceiling AND tool blocks left unbalanced by the crash (1,456tool_usevs 1,481tool_result). Can’t/compactout (400 fires before the session loads). Recovery: don’t revive the giant session — reconstruct state fromAGENT_HANDOFF.md+historian, continue in a fresh session. Verified the uncommitted B1 supabase-upsert-retry fix incrates/pm-arb/src/validate.rsgreen against the full CI gate (116 tests pass). Diagnostic commands (wc/du,awklength sort,jqtool-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-vmineurope-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 Ansibleappsrole 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 hostnameops-vm-1(collision with offline old US node, manual admin cleanup still pending). Branchspec2a-app-deploy(16 commits, validated by the live deploy) not yet merged to main on the terraform repo. — polymarket-fetch-deploy polymarket-fetchapps-polymarket-fetch-bot— Docker,restart=always, the Telegram botapps-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 UTC —
scheduled-runhealthy (10 ticks since deploy, 6 in the last 90 min, latestpm_runs.started_at = 2026-05-27 13:57:06 UTCvia Supabase MCP);wallet-monitorhealthy (2,668 rows in last 30 min, latestpm_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);botcontaineractiveper systemd but end-to-end Telegram round-trip not yet smoke-tested.systemctl is-activereturnsactivefor 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.cois IPv6-only, Docker bridge is IPv4-only; all three containers crash-looped on first deploy. Fix: switch env file to session pooler URLaws-1-eu-west-1.pooler.supabase.com:5432. Pattern will apply to every future Supabase-using app on this rig. - #17 .dockerignore exclusion —
supabase/was excluded, brokesqlx::migrate!()at image build time. Fix: commit834bc66in 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 canonicalops-vmhostname.
- #18 Supabase IPv6 vs Docker IPv4 — the killer. Direct DSN
- Build artifacts shipped this session —
Dockerfile.polymarket-fetch(multi-stagerust:1-bookwormbuilder →debian:bookworm-slimruntime) and.github/workflows/release.yml(calls reusablewowjeeez/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-vmTailscale node somake sshworks without inventory patch; mergespec2a-app-deployto main in~/levandor/terraform/; (optional) backfill oldpm_runsrecords pre-deploy (pm_runscurrently 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 specdocs/lag-arb-plan.mdM1/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.0xd189664cshows 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 bumpingtestcontainers; clippy lint skew fixed by pinningrust-toolchain.tomlto 1.95.0. Added a multi-stage Dockerfile + build workflow (push disabled until a registry is picked). — ci - Built
lag-probecrate — 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-accuracyCLI,pm_short_term_trader_accuracytable) 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 wallet0x75cc3bshows 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