/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 to main. Count-gate commit f385fbb (“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.md and docs/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:

AspectBehavior
EntriesEvery detected BUY fill by the watched wallet → an EntryOrder copy, sized by the trader’s conviction band (see below).
ExitsThe watched wallet selling → mirror an ExitSignal so PM closes the copy.
Concurrency capA per-user open-copy limit (<limit>). At cap, new entries are skipped with no backfill (the missed trade is gone, not queued).
Slot recyclingA 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>:

BandMultiplier
VLOW0.2
LOW0.5
NEUTRAL1.0
HIGH1.5
VHIGH2.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 by origin_ref + condition + outcome — it does NOT key on source. 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 to eth_getLogs polling — the ~500k CU / 5h problem (see watcher-onchain-gap-backfill, polygon-wss-provider-drpc).

The mirror feed deliberately uses WSS run_feed for 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_getLogs polling like cohort if the active-mirror CU cost matters.

Status — DORMANT, gated on activation

Merged to main but inert. Activation requires:

  1. POLYGON_WSS_URL set on firstmover — without it the mirror feed never opens.
  2. PM adds a mirror row to pmv2_autotrade_sources — otherwise mirror entries are SkipAck-dropped by position_manager’s fail-closed source gate (unknown source → Mode::Off).
  3. pmv2-contracts query-kind bump — the mirror/mirrorstop/mirrorlist QueryKinds must ship in the pinned contracts.
  4. 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 stop leaves open copies to ride. Stopping a mirror does NOT unwind already-open copies — they ride until natural market resolution rather than being force-closed.