Personal Project — 2026

Mirror Trader

Reverse-engineering Polymarket's most profitable traders, in real-time.

Detection latency — mirror lag vs target fill Seconds · lower is better
25s 15s 10s 0s v0 baseline v1 chain listener v2 decode tuning v3 production public API · ~22s 22.4s · ws-public.polymarket 3.1s · on-chain WSS
Role
Designer & engineer
Duration
4 weeks
Stack
Python · Polygon WSS · Polymarket CLOB v2
Status
Active · v3 in production

Polymarket has thousands of traders. A handful consistently make money.

What if you could mirror them at a fraction of their bankroll?

Copying trades 1:1 sounds simple, but every second of delay between their fill and yours costs slippage. By the time the public data API surfaces a trade — about 22 seconds later — the orderbook has already moved. The price you see is the price they got. The price you'd actually fill at is somewhere worse.

The whole project sits on a single bet: if you can shave detection latency below the slippage window, an asymmetric copy of a profitable trader is mathematically tractable. If you can't, you are paying the trader for the privilege of being late to their own ideas.

Not every profitable trader is mirror-able.

Before writing a line of code, the question was who, not how. Polymarket leaderboards surface dozens of traders that look superficially profitable, but most of them are unmirror-able for structural reasons — they hedge aggressively, they trade in sizes that exceed a small bankroll, or their edge is concentrated in a narrow window.

I built a profile of what an "ideal" target looks like:

Target profile
Hedge rate
Bankroll fit
Edge horizon
Verdict
Tennis specialist
single sport · high freq
77%
capital-intensive
consistent · 90d
rejected
Multi-sport spreader
26 winning days streak
~30%
workable
collapsed mid-test
rejected
LoL / esports specialist
narrow market depth
<5%
5k bankroll viable
modest · consistent 60d
selected

The selected target — a quiet LoL specialist I'll call Target A — was the least flashy of the three. Lower headline returns. Smaller fills. But on the four dimensions that actually mattered for a mirror — hedge rate, sizing fit, market liquidity, and consistency — nobody else came close.

Skip the API. Listen to the chain directly.

Polymarket settles on Polygon. Every fill emits an OrderFilled event from the exchange contracts — both NegRisk and CTF. The public data API just reads those events and re-publishes them with caching delay baked in.

If you're willing to subscribe to the chain directly, you get the same information without the cache. That's the whole architecture.

Polygon block ~2s WSS listener eth_subscribe(logs) Trade decoder topic + ABI Bot logic filter + size CLOB v2 place on-chain source subscribed to OrderFilled our position public data API · ~22s cache · replaced
Latency breakdown
block time
~2.0s
decode
~1.0s
place order
1–3s
total · v3
~6s
public API
~22s
listener/subscribe.py Python
# both Polymarket exchange contracts emit OrderFilled
NEG_RISK_EXCHANGE = "0xC5d563A36AE78145C45a50134d48A1215220f80a"
CTF_EXCHANGE     = "0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E"
ORDER_FILLED     = "0xd543adfd945773f1a62f74f0ee55a5e3b9b1a28262980ba90b1a89f2ea84d8ee"

async def subscribe(ws, target_addr):
    await ws.send_json({
        "jsonrpc": "2.0",
        "id": 1,
        "method": "eth_subscribe",
        "params": [
            "logs",
            {
                "address": [NEG_RISK_EXCHANGE, CTF_EXCHANGE],
                "topics": [ORDER_FILLED, None, target_addr],
            },
        ],
    })

    async for raw in ws:
        log = json.loads(raw)["params"]["result"]
        fill = decode_order_filled(log)
        await trade_queue.put(fill)   # < 1s from block to queue

The engineering was the easy part.

The chain listener worked on day three. Most of the four weeks went into the much harder problem: figuring out who to copy, after the first two attempts proved that profitable-on-paper doesn't mean mirror-able-in-practice.

v1
Attempt 01 · failed

The tennis trader who hedged everything

First target: a high-frequency tennis specialist with 90 days of clean returns. We caught his initial position on the favourite, missed the in-play hedge on the underdog 40 minutes later, and lost on every match that didn't go straight to plan.

The lesson wasn't subtle. A 77% hedge rate means he wasn't betting on outcomes, he was running a structured book. Mirroring one leg of a hedged book isn't a copy — it's an unhedged version of someone else's hedged trade.

Hedge rate: 77% Coverage: ~50% of legs Outcome: −14% in 9 days
v1.5
Attempt 02 · failed

"26 winning days in a row" — selection bias

Pivoted to a multi-sport spreader who looked statistically incredible: 26 of 26 winning days going in. Within 72 hours of switching the listener over, he went 0 of 3 losing days — and his streak ended.

This is the trap with leaderboard targeting. Of 1,000 traders, a handful will look impossibly clean over 30 days by chance alone. By the time you notice them, you're watching the peak of a curve that almost certainly mean-reverts.

Days at peak: 26 Days post-mirror: 3 losing Outcome: −9% before pause
v2
Pivot · new target profile

Rewriting what "good" looks like

The two failures forced an explicit rewrite of the target spec: single-sport (no edge dilution), low hedge rate (mirrorable book), modest sizing (5k bankroll viable), edge sustained over 60+ days (not 30). That spec excluded almost everyone on the public leaderboard.

The trader who survived the filter was a quiet LoL specialist with smaller absolute returns but a much harder-to-fake consistency. Less impressive on paper. More copy-able in reality.

v3
Tuning · the parameters that actually matter

Slippage caps, thresholds, hedge logic

Final round was parameter work, not architecture. Slippage cap moved from 7% to 10% (a 7% cap was rejecting Target A's most profitable fills, where he was the one moving the price). Minimum trade threshold tuned to filter dust without missing real positions. Hedge follow-up logic disabled entirely — for this target, the hedge events were noise, not signal.

Each parameter was ratified through a 7-day clean test: deploy the change, then make zero engineering changes for the full window so the result wasn't polluted by mid-flight tweaks.

Four counter-intuitive calls.

01

Remove all bankroll caps

The original silent 5%-of-bankroll cap was clipping every position the trader sized big — precisely the trades that signalled conviction. Removing the cap turned a 70%-fidelity copy into a true 1:1 mirror, and the captured edge per dollar moved with it.

02

100% mirror, no hedges

Following hedges sounds prudent. In practice it captures the trader's losses while the original entry already absorbed the risk premium. We eat the asymmetry on purpose: copy the conviction trade, ignore the protective leg.

03

"Fresh-start" deploy mode

On every deploy the bot only follows positions opened after its start timestamp. A naive backfill would dump the bankroll into the trader's existing book — including markets already resolved but not yet redeemed. Fresh-start sidesteps the entire class of errors.

04

Wind-down, not kill-switch

When deprecating a target, the bot stops opening new positions but lets existing ones resolve naturally. A hard kill-switch would force-close at whatever the orderbook offered, paying maximum slippage to exit. Graceful deprecation costs nothing and protects realised P&L.

Where v3 landed.

Detection latency
22s3s
7× improvement vs the public data API. Measured wall-clock from target's on-chain fill to fill detected by listener.
Coverage rate (24h)
22%89%
Share of target's trades successfully captured and mirrored within the slippage window. Remaining 11% are sub-threshold or markets without depth.
Mirror accuracy · peak
99%
By traded volume during steady-state operation. Drift is dominated by the trader's last 1–2% of dust positions, not strategic divergence.
Bankroll · monitoring
$5,000
USDC on Polygon — intentionally small for proof-of-concept. 24/7 monitoring via Railway with Telegram alerts on every fill.
mirror · incident log · LoL DCA event
14:02:18  FILL   target opened YES @ 0.11  size $1,420
14:02:24  MIRROR placed   YES @ 0.11  size $142
14:18:51  FILL   target DCA'd YES @ 0.08  size $2,100
14:18:57  MIRROR placed   YES @ 0.08  size $210
14:41:09  FILL   target DCA'd YES @ 0.04  size $3,800
14:41:14  MIRROR placed   YES @ 0.04  size $380
15:24:02  RESOLVE market settled NO
15:24:02  PNL    realised −$174  (target −$7,320)

# a real loss, surfaced cleanly. the bot did exactly what it
# was supposed to: mirror conviction, including when conviction
# is wrong. selection of who to mirror is a separate problem.

What I'd build next.

What worked. The infrastructure. The chain listener has run for weeks without a missed event. Detection latency held at 3–6 seconds across thousands of fills. The bot, mechanically, does what it was built to do.

What didn't. Target selection — repeatedly. Both rejected targets looked correct on the metrics that public leaderboards expose, and both reverted within days of being mirrored. The pattern was so consistent that it stopped being bad luck and became the actual problem.

The lesson. The hardest problem in copy trading isn't the engineering. It's the selection bias. Anyone who looks profitable for 30 days might be having a hot streak indistinguishable from real edge. Real edge needs to be observed for months — ideally across regime changes, not just inside one.

The thing I want to build next is a target-vetting tool that tracks 100+ candidate traders over time, and only surfaces the small subset whose edge persists across multi-month windows. The listener was the prerequisite. The vetting layer is the actual product.