/mirror <user> <size> <limit> is a new cohort_algo signal source that mirrors a single Polymarket user’s every trade — it copies their entries (sized by conviction band), mirrors their exits, and caps the number of concurrent open copies per mirrored user. It’s a third signal source (source="mirror") alongside first_mover_core and weather/crypto, reusing the established producer/PM split: cohort_algo produces EntryOrder/ExitSignal, position_manager executes. Built + merged to main, but DORMANT — inert until activation env + cross-service pieces land.
For Agents — state at a glance
- Repo:
/Users/levander/coding/pmv2/cohort_algo(github.com/wowjeeez/pmv2-cohort-algo), merged tomain. Count-gate commitf385fbb(“gate WSS feed on active-mirror count”).- Code:
crates/cohort_algo/src/pmv2/mirror/{mod,repo,target,sizing,signal,handler,query}.rs.- Spec + plan:
docs/superpowers/specs/2026-06-25-cohort-mirror-feature-design.mdanddocs/superpowers/plans/2026-06-25-cohort-mirror-feature.md.- New source id:
source="mirror". Stable origin:origin_ref = mirror|{wallet}|{condition_id}|{outcome_index}.- Tables:
pmv2_mirrors,pmv2_mirror_positions.- Status: DORMANT — see Status — DORMANT, gated on activation.
What it is
A /mirror command registers a watched Polymarket wallet. From then on, cohort_algo treats that wallet as a personal copy-trade target:
| Aspect | Behavior |
|---|---|
| Entries | Every detected BUY fill by the watched wallet → an EntryOrder copy, sized by the trader’s conviction band (see below). |
| Exits | The watched wallet selling → mirror an ExitSignal so PM closes the copy. |
| Concurrency cap | A per-user open-copy limit (<limit>). At cap, new entries are skipped with no backfill (the missed trade is gone, not queued). |
| Slot recycling | A 15-min cron resolves closed markets and frees slots when copies settle. |
The three command args: /mirror <user> <size> <limit> — the wallet/user to mirror, the base spend <size>, and the max concurrent open copies <limit>.
Architecture
Mirror is a second on-chain feed lane inside the firstmover binary, parallel to the cohort lane, plus a sizing/handler/persistence path and a command surface.
graph TD subgraph FM["firstmover binary"] SUP["mirror supervisor task<br/>(count-gated)"] FEED["mirror feed lane<br/>Alchemy WSS run_feed"] HAND["mirror handler<br/>sizing + cap enforcement"] SUP -->|"≥1 active mirror"| FEED FEED -->|"watched wallet fill"| HAND end HAND -->|"EntryOrder / ExitSignal<br/>source=mirror"| NATS["NATS pmv2.order.*"] HAND --> DB["pmv2_mirrors<br/>pmv2_mirror_positions"] NATS --> PM["position_manager (executor)"] CRON["15-min cron<br/>resolve closed markets"] --> DB
Feed lane (run_feed, Alchemy WSS)
The mirror feed uses Alchemy WSS run_feed (eth_subscribe(logs)) for low-latency detection of the watched wallet’s OrderFilled events — distinct from the cohort lane which now polls via eth_getLogs (see the CU gotcha below). A supervisor task count-gates the subscription: it starts run_feed only when the watchlist becomes non-empty and aborts it when empty.
Sizing (sizing.rs) — conviction bands
A detected fill is sized by the trader’s conviction band via per-band spend multipliers applied to the base <size>:
| Band | Multiplier |
|---|---|
| VLOW | 0.2 |
| LOW | 0.5 |
| NEUTRAL | 1.0 |
| HIGH | 1.5 |
| VHIGH | 2.5 |
| UNKNOWN | → treated as NEUTRAL (1.0) |
Conviction originates from cohort’s scoring layer (see pmv2-two-tier-trader-scoring).
Handler (handler.rs)
On a watched-wallet fill: size by band → enforce per-user open-copy limit (skip at cap, no backfill) → publish with a stable origin_ref.
Exit matching is by origin_ref, NOT source
The handler publishes a stable
origin_ref = mirror|{wallet}|{condition_id}|{outcome_index}. PM matches exits byorigin_ref+ condition + outcome — it does NOT key onsource. This is what lets a mirror exit find and close the exact copy that the mirror entry opened.
Persistence (repo.rs)
Two tables: pmv2_mirrors (the registered watch targets) and pmv2_mirror_positions (open copies). A 15-min cron resolves closed markets (uses the closed=true proxy from Gamma/data-api) to free concurrency slots and refresh the trader’s avg-bet.
Command surface (query.rs + pmv2-contracts)
New pmv2-contracts QueryKinds: mirror / mirrorstop / mirrorlist, with responder renderers for each:
/mirror <user> <size> <limit>— register a mirror./mirror stop(mirrorstop) — stop mirroring a user./mirror list(mirrorlist) — list active mirrors.
GOTCHA — Alchemy eth_subscribe(logs) bills per-block regardless of matches
WSS log subscriptions cost CU per block, even on zero matches
Alchemy’s
eth_subscribe(logs)is billed per block whether or not any log matches your filter. This is exactly why the cohort feed was migrated off WSS toeth_getLogspolling — the ~500k CU / 5h problem (see watcher-onchain-gap-backfill, polygon-wss-provider-drpc).The mirror feed deliberately uses WSS
run_feedfor low latency, so it incurs per-block CU whenever active. Mitigation built in: the feed is count-gated — the supervisor opens the WSS subscription only when ≥1 mirror is active and aborts it when the watchlist empties. So zero mirrors = zero CU. But while ≥1 mirror is active, expect per-block CU cost.Future option: switch the mirror feed to
eth_getLogspolling like cohort if the active-mirror CU cost matters.
Status — DORMANT, gated on activation
Merged to main but inert. Activation requires:
POLYGON_WSS_URLset on firstmover — without it the mirror feed never opens.- PM adds a
mirrorrow topmv2_autotrade_sources— otherwise mirror entries are SkipAck-dropped by position_manager’s fail-closed source gate (unknown source →Mode::Off). - pmv2-contracts query-kind bump — the
mirror/mirrorstop/mirrorlistQueryKindsmust ship in the pinned contracts. - Telegram connector parsing
/mirror …— the connector must route the new command surface.
Open design decisions (deferred)
Two known limitations, deferred
(a) Conviction sizing granularity. Sizing currently keys off a single fill’s USD (
fill.notional_usd(), mandated by spec §3) vs the trader’s avg total position. This under-sizes copies of traders who scale in over multiple fills (each partial fill is sized independently rather than against the intended full position).(b)
/mirror stopleaves open copies to ride. Stopping a mirror does NOT unwind already-open copies — they ride until natural market resolution rather than being force-closed.
Related
- cohort-algo-component — the cohort_algo signal-producer component this lane lives in
- cohort-signals-service-split-and-exit-signals — the producer/PM split this reuses
- cohort-algo-v2-wire-contract-cutover — the v2 wire contract (
EntryOrder/ExitSignal,Pricing::Derived) - position-manager-interface-design — the executor that consumes mirror signals
- pmv2-position-manager-nats-consumer — PM’s NATS consumer + source gate
- watcher-onchain-gap-backfill — the eth_getLogs migration / CU context
- polygon-wss-provider-drpc — Polygon WSS provider notes
- pmv2-two-tier-trader-scoring — where conviction / band scoring originates