On-chain copy-trade PAPER lane in the pm-arb crate (commit b3f9e21, main, 2026-05-31). It mirrors the top-trader cohort’s cheap-outcome BUYs on crypto up/down markets, detecting their fills via Polygon on-chain OrderFilled logs instead of the Data API. Paper trading only — no on-chain order execution, no keys, no USDC. Ships default-OFF (DB kill-switch enabled DEFAULT FALSE), so the deploy is dormant and zero-risk until flipped.
For Agents
This is the strategic pivot from the autonomous longshot signal in longshot-strategy. The bet is no longer “we predict longshots ourselves” — it’s “we copy the cohort’s outcome selection.” The edge being harvested is SELECTION, not our price/time signal. Read top-trader-edge for who the cohort is and why they have an edge; read this note for how the copy lane mirrors them.
Why this exists — the strategy basis
Two prior findings forced the pivot:
- Our autonomous longshot signal is negative-EV. Live it runs ~breakeven, t≈0.2 over 655 trades. The price×time-in-window reversion bet we built (see longshot-strategy) does not have a demonstrable edge on its own.
- The top-5 cohort wallets have a real, replicable cheap-outcome selection edge. +~18–30% flat ROI, t up to 9 over tens of thousands of trades, verified across all 5 cohort wallets and specific to crypto-updown markets.
So instead of generating our own signal, we copy which cheap outcome the cohort buys. This is distinct from copy-trade-algorithm (the all-category, lifetime-PnL-edge tracker that backtested to no demonstrated edge) — the copy lane is narrow (crypto-updown only), on-chain-detected, and rides a selection edge that survived cohort-wide significance testing.
Why on-chain detection (not the Data API)
The cohort trades 300-second markets. Detection latency is the whole game.
| source | latency | carries wallet? |
|---|---|---|
| Polymarket Data API | median ~5 min after the trade (only ~2% within 30s) | yes, but too slow |
Polygon on-chain OrderFilled logs | ~1–3 s | yes (maker/taker address) |
The Data API’s ~5-min surfacing lag is useless for 5-minute markets — by the time a cohort trade appears, the market is resolving. On-chain logs surface in seconds and carry the wallet address, so we can attribute a fill to a specific cohort wallet in near-real-time.
You cannot "trade on-chain instead of the API"
Polymarket matches off-chain (centralized CLOB) and only settles on-chain. On-chain detection works for reading the cohort’s fills, but real-money execution still requires EIP-712-signed CLOB orders against the off-chain matcher — a separate future project (see Colocation for the off-chain-matching constraint). There is no on-chain order placement path.
Verified on-chain decode facts
For Agents — load-bearing constants, all reconciled against
pm_tradesground truthThese are the facts that make the decoder correct. Several are counter-intuitive (the topic0 in particular is not the canonical-ABI value).
- Address match: the on-chain maker/taker address equals our
pm_trades.proxy_walletfor all 5 cohort wallets. (This is what lets us attribute a fill to a cohort wallet.) - Real
OrderFilledtopic0 is0xd543adfd...— NOT the canonical-ABI0xd0a08e8c. Filtering on the canonical hash yields zero logs. Use the real one. - Settlement contract: the cohort settles on CTF Exchange V2
0xE111180000d2663C0091e4f400237545B87B996B. - Both token and USDC are 6-decimals.
Decode rule (per OrderFilled leg)
For each leg where the cohort wallet is maker or taker, look at what the cohort gave vs got:
| cohort gave | cohort got | side | price |
|---|---|---|---|
| USDC | token | BUY | USDC / token |
| token | USDC | SELL | USDC / token |
got-token + gave-USDC⇒ BUY;gave-token + got-USDC⇒ SELL.price = USDC / token(both 6-decimals).token → outcomevia gammaclobTokenIds.
Reconciled against pm_trades (the Data-API-sourced ground truth) to confirm the decode produces the same trades.
Architecture
Copy lane data flow
graph TD WSS["Polygon WSS<br/>eth_subscribe logs + newHeads"] --> DET["detector<br/>polymarket-data/onchain.rs"] DET -->|"decoded fill + wallet"| LOOP["copy_loop<br/>pm-arb/copy_lane.rs<br/>(spawned in validate::run)"] LOOP --> DEC["decode leg → BUY/SELL + price"] DEC --> MAP["token → market map<br/>(populated by rotation poller)"] MAP --> EVAL["evaluate_copy<br/>pm-arb/copy.rs<br/>(gates)"] EVAL -->|"accepted BUY"| PAPER["pm_paper_trades<br/>lane='copy'<br/>hold to resolution"] EVAL -.->|"rejected: SELL / gates"| DROP["drop"] PAPER --> RES["resolver<br/>(subtracts PM crypto taker fee)"] style WSS fill:#264653,stroke:#2a9d8f,color:#fff style PAPER fill:#2d2d2d,stroke:#888,color:#fff style EVAL fill:#3d2020,stroke:#a55,color:#fff
Pipeline, end to end:
- Detector (
crates/polymarket-data/src/onchain.rs) — Polygon WSSeth_subscribeonlogsfor theOrderFilledtopic, plusnewHeadsto maintain a block-timestamp cache (logs don’t carry wall-clock time; block timestamp supplies it). - copy_loop (
crates/pm-arb/src/copy_lane.rs) — spawned insidevalidate::run. Consumes detector events. - Decode — apply the decode rule above to get side + price + token.
- token → market map — populated by the existing rotation poller (the same component that tracks the active crypto-updown markets). Maps the on-chain
assetIdto a market/outcome. - Gated
evaluate_copy(crates/pm-arb/src/copy.rs) — applies cap / floor / window / dedup / daily-notional gates and rejects cohort SELLs (we only mirror cheap-outcome BUYs). - Write — accepted BUYs write
lane='copy'rows topm_paper_trades, held to resolution. - Resolver — at resolution, subtracts the Polymarket crypto taker fee:
shares × 0.072 × p × (1−p), which is ≈6.4% of stake at ~11¢. This fee subtraction is a no-op for the existing (longshot) lanes — it only bites the copy lane.
The copy lane shares
pm_paper_tradeswith the longshot laneRows are distinguished by
lane='copy'vs the existing longshot lane. The resolver’s fee subtraction is gated so it doesn’t change longshot-lane accounting. See longshot-strategy for the longshot lane’s own columns/behavior on the same table.
Operational runbook — DB-configured, hot-reloaded
Runtime config lives in Supabase and hot-reloads ~every 60s — no redeploy needed
This is the key operational fact. You start, stop, and tune the copy lane entirely from the database.
Two tables drive it:
pm_copy_config (singleton)
| column | meaning |
|---|---|
enabled | kill-switch, DEFAULT FALSE |
cap_price | max outcome price to copy |
min_price | floor price |
size_usd | per-copy stake |
min_window_left_secs | skip if too little time left in the market |
max_daily_notional_usd | daily spend cap |
pm_copy_wallets
Per-wallet enable flags, seeded with the 5 cohort wallets. Toggle individual wallets on/off here.
Commands
-- START the copy lane
UPDATE pm_copy_config SET enabled = true;
-- STOP it
UPDATE pm_copy_config SET enabled = false;
-- tune live (example) — takes effect within ~60s, no redeploy
UPDATE pm_copy_config SET cap_price = 0.13, size_usd = 10;Config changes propagate within ~60s via the hot-reload loop.
Deploy state & gotchas (2026-05-31)
Two manual prerequisites before the lane can run
- Migrations were applied manually via Supabase MCP —
pm-arbdoes NOT runsqlx migrate(onlypolymarket-fetchdoes), so the two copy migrations could not ship through the normal pipeline. Both are applied and verified;enabled = false(dormant).- New env var
POLYGON_WSS_URLrequired — a Polygon PoS WSS endpoint (e.g. Alchemy), added to thePOLYMARKET_FETCH_ENVGitHub secret by @deploy. Until it is set, the lane idles even ifenabled = true— it logs a warning and does not error.
- Default-OFF = dormant, zero-risk deploy. The
enabledDEFAULT FALSE means the lane does nothing until the kill-switch is flipped. Shipping it does not change live behavior. - Handed to @deploy in
AGENT_HANDOFF.mdRound 42: addPOLYGON_WSS_URL+ confirm the auto-catch bounce. (Auto-catch per @deploy Round 36 applies topm-arb, so a push should redeploy — see Today’s operational learnings (gotchas worth durable docs).) pm-arbdoes not run migrations — remember this for any future copy-lane schema change; it has to go through Supabase MCP, not a code-shipped migration. (Contrast with polymarket-fetch, whosemigratecommand does run them.)
Honest limits & follow-ups
These do not block paper validation, but a future session should know them
- assetId ↔ gamma
clobTokenIdslink is verified on 2 real fills. The live forward run is the definitive proof — see Gate-0 below.- Analytics niggles (cosmetic, not behavioral):
up_mid_at_signalstores the entry ask, not the up-mid.detect_latency_msmeasures block → eval, not trade → eval.- No stale-book guard on the copy ask (the longshot lane’s stale-book concerns may apply here too).
- Copy counters are absent from the hourly summary.
- Real-money execution is a separate future project. It needs EIP-712-signed CLOB orders against Polymarket’s off-chain matcher — you cannot trade on-chain (see the callout above). On-chain detection does not get us closer to on-chain execution.
Gate-0 — the definitive next check
First validation step once
POLYGON_WSS_URLis set andenabled=trueConfirm that
lane='copy'rows actually appear inpm_paper_trades. Everything upstream (detector, decode, token→market map, gates) is verified in pieces; the live forward run that produces real copy rows is the end-to-end proof that the assetId↔outcome link and the whole pipeline work in production. No copy rows after enabling = something in the chain is broken (most likely thePOLYGON_WSS_URL, the topic0, or the token→market map).
Related
- longshot-strategy — the autonomous longshot lane this pivots away from (negative-EV on its own); shares the
pm_paper_tradestable andvalidate::runhost - top-trader-edge — who the cohort is and why their cheap-outcome selection has an edge; the selection edge this lane copies
- copy-trade-algorithm — the other, unrelated copy idea (all-category, lifetime-PnL-edge tracker, no demonstrated edge); do not confuse with this on-chain crypto-updown lane
- polymarket-fetch-deploy — deploy mechanism, GH secrets, auto-catch; where
POLYGON_WSS_URLlands - polymarket-fetch — workspace, schema, CLI,
pm_*tables overview