Self-hosted Polymarket copy-trading dashboard. Follow a wallet address, set a sizing rule, and PolyCopy mirrors their fills onto your own funded Polymarket account in near real time.
This is a personal project, not an audited or officially supported piece of software. It places real orders with real money against a third-party exchange whose API isn't fully documented, using an SDK that's still actively changing.
Use this at your own risk. No warranty of any kind is provided or implied - nothing here guarantees correct sizing, correct risk-limit enforcement, or that an order does what you expect it to. The author(s) take no responsibility for financial losses, bugs, downtime, missed trades, incorrect trades, or any other outcome from running this software. Read the code before trusting it with money, start small, and keep the kill switch within reach.
- Real-time wallet following via Polymarket's public trade feed - one shared connection regardless of how many wallets you follow
- Configurable sizing per follow: a fixed USDC amount per trade, or a ratio of the source trade's size
- Risk controls: per-follow position caps, live slippage and spread checks before every order, a daily loss limit with automatic pause, and a manual kill switch
- Market orders, not resting limits - each mirrored trade fills immediately against available liquidity or doesn't happen, rather than sitting open on the order book
- Balance and sell-position verification before every order, so you never try to spend more than you have or sell a position you don't hold
- Live dashboard with cash/portfolio value, an equity chart with hover detail, open positions, and a filterable trade activity log with latency tracking
- Auto-refreshing UI - the dashboard polls for updates every few seconds, no manual refresh needed
- Single admin account, created automatically on first boot
PolyCopy holds one persistent WebSocket connection to Polymarket's public real-time trade feed. Every trade on the platform flows through this connection; the app matches each one against your active follows and, on a match, applies your configured risk checks before mirroring it with your own funded wallet.
Because that feed carries the platform's entire trade volume rather than a per-wallet stream, matching happens client-side. Trade processing runs concurrently so a slow risk check on one trade never blocks the next incoming message, while order placement itself is serialized to avoid two near-simultaneous trades racing each other over the same account balance.
Before submitting an order, PolyCopy checks:
- Whether trading is currently paused (kill switch or daily loss limit)
- Whether the market matches this follow's allow/block rules
- How far the live price has moved from the source trade's price (skipped if it exceeds your slippage limit)
- How wide the current bid/ask spread is (skipped if it exceeds your spread limit)
- For a sell, whether you actually hold the position (and if so, how much)
- Whether you have enough balance for the order
Only after all of that does it submit a Fill-And-Kill market order and record the outcome - placed, skipped, or failed, each with the reason - in the activity log.
- Copy
.env.exampleto.envand fill in:SESSION_SECRET- required. Generate with:python -c "import secrets; print(secrets.token_hex(32))"ADMIN_USERNAME/ADMIN_PASSWORD- optional. Set your own before first boot, or leaveADMIN_PASSWORDunset and a random one is generated and printed once to the container logs on first startup.POLYMARKET_PK/POLYMARKET_FUNDER- your trading wallet's private key and funder (proxy wallet) address. Get the key from your Polymarket account (the Magic export link if you signed up via email/Google, or your wallet's own export if you connected an external wallet directly). The funder address is your Polymarket proxy wallet, found on your public profile page - not the "API use only" address shown in account settings. This key controls real funds. Treat.envlike a password: don't commit it, restrict file permissions on the host.
- Build and run:
If you didn't set
docker compose up -d --buildADMIN_PASSWORD, retrieve the generated one from the logs:docker compose logs | grep -A5 "First run" - Visit the app, log in, and set Starting balance and a Daily loss limit on the dashboard before turning any follow on - those values drive the auto-pause logic.
- Add a wallet to follow: its Polygon address (found on its Polymarket profile page, not its display username), a sizing rule, and whatever risk limits you want, then hit Start.
Getting the app in front of a browser securely (TLS, network exposure, etc.) is left to your own deployment setup.
Polymarket usernames don't map directly to what you need. Every profile page (polymarket.com/profile/<address>) uses the wallet address in the URL - that's what goes in the "wallet address" field, not the display username.
A few things worth knowing if you're deploying this yourself:
.envholds your private key and session secret. It's excluded via.gitignore, but double-check before your first push that it was never previously committed - a.gitignoreentry doesn't scrub existing git history.SESSION_SECRETis required. The app refuses to start without a properly set one, since a predictable session secret would let anyone forge a valid login.- No rate limiting on the login page. Bcrypt's cost factor slows brute-force attempts somewhat, but there's no explicit lockout. Don't expose this port directly to the public internet.
- The Docker container runs as a non-root user.
polymarket-clientis intentionally unpinned inrequirements.txtsince it's an actively evolving SDK. For a fully reproducible build, pin it to whatever's actually installed and working (pip freeze | grep polymarketinside the container).- This has had a thorough code-level review but not live integration testing against Polymarket's production API in an automated way - real-money testing is what actually exercises the trading logic in practice.
- Balance/P&L tracking uses two different sources. "Cash" comes from an authenticated collateral check (reliable). "Portfolio value" comes from Polymarket's public portfolio-value endpoint, which has a known indexing bug on some accounts - treat it as a best-effort estimate, not authoritative. The equity chart's historical shape is reconstructed from your own trade history and anchored to the live Cash figure at its most recent point.
- A followed wallet's P&L figure is an approximation, computed from their closed positions over the last 24 hours. It won't necessarily match what Polymarket's own profile page shows for them, since that may include reward income or other components this doesn't account for.
- Open positions display depends on SDK field names that aren't all independently confirmed.
token_idandsizeare solid; avg price, current value, and P&L show as unavailable if the underlying field isn't present under the expected name. - Position and closed-position lookups paginate up to a bounded number of pages (500 items), which covers any realistic account but could undercount for an extremely high-frequency trader.
- Trade latency figures compare when a source trade happened (per the feed's own timestamp) to when PolyCopy started processing it - a reasonable relative signal, not a guaranteed precise wall-clock measurement, since the exact semantics of that timestamp field aren't independently confirmed.
- No CSRF tokens on state-changing forms; partially mitigated by
SameSite=Laxon the session cookie, not a complete guarantee.
app/
main.py FastAPI routes: auth, dashboard, config CRUD, risk settings
auth.py Password hashing + session cookie (JWT)
risk.py Balance/P&L ledger, daily loss auto-pause, market rule matching
models.py User, CopyConfig, TradeLog, AccountSettings (SQLModel/SQLite)
database.py SQLite engine + lightweight auto-migration on startup
copytrader.py Real-time trade feed, risk checks, order placement
templates/ Jinja2 pages (dashboard, login)
static/ style.css