A delta-neutral market-making bot for Polymarket that maximises trading volume while keeping directional risk minimal. Designed for airdrop farming: generate activity stats (volume, markets traded, days active) at the lowest possible cost.
On Polymarket, every market has YES and NO tokens where YES + NO = $1.00.
The bot places BUY orders on both sides simultaneously:
- BUY YES at
mid - spread(our bid) - BUY NO at
(1 - mid) - spread(our ask, expressed as a NO bid)
When both sides fill, you hold a matched YES+NO pair that's worth exactly $1.00, and you paid less than $1.00 for it (the spread is your profit).
Example at mid = 0.50, spread = 2¢ per side:
- BUY YES at 0.48, BUY NO at 0.48
- Total cost: $0.96 → Guaranteed payout: $1.00 → Profit: $0.04 per pair
- Volume generated: $0.96 (counted towards your airdrop stats)
When one side fills but the other doesn't, you accumulate directional exposure. The bot handles this by:
- Skewing quotes — shifting prices to attract offsetting flow
- Position limits — capping exposure per market and portfolio-wide
- Reduce-only mode — when at limit, only placing orders that reduce position
- Per-market position limit (default: 200 shares)
- Portfolio-wide USD exposure limit (default: $1,500)
- Daily loss limit with auto-pause (default: $150/day)
- Lifetime loss limit with full shutdown (default: $3,000)
- Post-only orders (never cross the spread accidentally)
- Stale order cleanup
- Graceful shutdown on CTRL+C
- Python 3.9+ (tested on 3.11)
- A funded Polymarket account (USDC on Polygon)
- Your wallet's private key (see Step 2)
- A Linux VPS or local machine for 24/7 operation (optional)
git clone <your-repo-url> polymarket-mm
cd polymarket-mm
# Create a virtual environment (recommended)
python3 -m venv venv
source venv/bin/activate
# Install dependencies
pip install -r requirements.txtcp .env.example .env
chmod 600 .env # Restrict permissions — only you should read this fileEdit .env and fill in secrets only:
-
PRIVATE_KEY — Your wallet's private key
- Go to https://reveal.polymarket.com to export it
- Or export from MetaMask / your wallet
-
POLYMARKET_PROXY_ADDRESS — The address that holds your funds on Polymarket
- Find this in the Polymarket UI profile dropdown
- This is the address you deposit USDC to
-
SIGNATURE_TYPE — How you log in to Polymarket:
1= Email / Magic wallet2= Browser wallet (MetaMask, Coinbase Wallet, etc.)0= Standalone EOA
Shared non-secret tuning lives in tracked .env.shared and is loaded
automatically on git pull.
Optional per-user non-secret overrides can go in .env.local.
python setup_keys.pyThis will print your API_KEY, API_SECRET, and API_PASSPHRASE. Copy them into .env.
python main.py --dry-runThis shows which markets the bot would trade and what quotes it would place, without placing any actual orders. Review the output to make sure everything looks reasonable.
python main.pyTo stop gracefully: press CTRL+C. The bot will cancel all orders and save state.
python main.py --cancel-allpython main.py --flatten-leftoversThis market-sells all live positions except those matched by
DONT_TOUCH_POSITION_CONDITION_IDS / DONT_TOUCH_POSITION_KEYWORDS.
python main.py --statusAll settings are in config.py and load in this order:
.env.shared(tracked shared defaults).env(local secrets).env.local(optional local overrides)
The table below shows code defaults from config.py.
| Setting | Code Default | Description |
|---|---|---|
SPREAD_MODE |
adaptive | adaptive (near top-of-book) or fixed (HALF_SPREAD offset) |
HALF_SPREAD |
0.02 | Fixed-mode spread per side in cents |
MIN_EDGE_PER_PAIR |
0.003 | Adaptive-mode minimum profit per matched pair |
ADAPTIVE_IMPROVE_TICKS |
1 | Ticks to improve best bid (0=match, 1=improve) |
ORDER_SIZE |
50 | Shares per order |
NUM_LEVELS |
1 | Price levels per side |
REFRESH_INTERVAL |
30 | Seconds between quote refreshes |
FORCE_REQUOTE_CYCLES |
12 | Re-quote periodically even if midpoint is unchanged |
MAX_MARKETS |
8 | Markets to trade simultaneously |
MAX_POSITION_PER_MARKET |
300 | Max directional shares per market |
MAX_TOTAL_EXPOSURE_USD |
2500 | Max total portfolio exposure in USD |
MAX_DAILY_LOSS |
125 | Daily loss limit (auto-pause) |
MAX_LIFETIME_LOSS |
500 | Lifetime loss limit (full shutdown) |
INVENTORY_SKEW_FACTOR |
0.4 | How aggressively to skew quotes for inventory |
POST_ONLY |
true | Reject orders that would match immediately |
MIN_MARKET_VOLUME |
5000 | Min 24h volume to consider a market |
MIN_DAYS_TO_EXPIRY |
14 | Skip markets expiring sooner than this |
ENABLE_DIRECTIONAL_UNWIND |
true | Maker SELLs on heavy side to reduce bags |
ALWAYS_ON_INVENTORY_UNWIND |
true | Keep heavy-side SELL ladder active |
ENABLE_CONTROLLED_LOSS_LADDER |
true | Relax loss caps for large/stale/near-expiry bags |
STALE_ORDER_TIMEOUT |
300 | Cancel orphaned orders after this many seconds |
See .env.example for the full list of tunable parameters and .env.shared
for the current shared runtime profile.
HALF_SPREAD=0.03
ORDER_SIZE=10
MAX_MARKETS=5
MAX_POSITION_PER_MARKET=100
MAX_TOTAL_EXPOSURE_USD=800
MAX_DAILY_LOSS=100
If one market has a large one-sided bag (for example Iran NO), pin it to reduce-only or exclude it entirely:
# Option A: reduce-only cleanup (keeps hedging toward balance)
FORCE_REDUCE_ONLY_CONDITION_IDS=0xYOUR_IRAN_CONDITION_ID
# Option B: hard exclude from trading/selection
# EXCLUDED_CONDITION_IDS=0xYOUR_IRAN_CONDITION_ID
# EXCLUDED_MARKET_KEYWORDS=iran
# Optional: allow controlled-loss unwind SELLs on heavy side
ENABLE_DIRECTIONAL_UNWIND=true
MAX_UNWIND_LOSS_PER_SHARE=0.02
UNWIND_MIN_IMBALANCE_SHARES=40
ENABLE_CONTROLLED_LOSS_LADDER=true
With directional unwind enabled, the bot can reduce a stuck one-sided bag
without flipping to the opposite 0/100 bias: it shares one reduction budget per
cycle and refuses unwind prices worse than MAX_UNWIND_LOSS_PER_SHARE.
With the controlled-loss ladder enabled, that loss cap is relaxed only when
imbalance gets large, a market is stale (no fills for long), or expiry gets
near — so trapped bags can bleed out instead of freezing indefinitely.
With always-on inventory unwind enabled, heavy-side inventory stays listed with
an adaptive sell ladder (small target profit first, then ratcheting down when
stale), and near-expiry mode stops adding fresh directional risk while forcing
faster de-risking.
HALF_SPREAD=0.015
ORDER_SIZE=25
MAX_MARKETS=12
MAX_POSITION_PER_MARKET=300
MAX_TOTAL_EXPOSURE_USD=2500
MAX_DAILY_LOSS=250
- Create a bot via @BotFather on Telegram
- Get your chat ID by sending a message to the bot and visiting:
https://api.telegram.org/bot<TOKEN>/getUpdates - Add to
.env:TELEGRAM_BOT_TOKEN=your_bot_token TELEGRAM_CHAT_ID=your_chat_id
You'll receive alerts for: bot start/stop, true fills, no-fill stalls/recovery, market auto-rotation, risk warnings, and hourly status updates.
For 24/7 operation, deploy to a cheap cloud server:
# 1. Create a droplet (Ubuntu, $6/month Basic)
# 2. SSH in and install Python
sudo apt update && sudo apt install -y python3 python3-pip python3-venv
# 3. Clone/upload your bot
mkdir ~/polymarket-mm && cd ~/polymarket-mm
# (upload files via scp, rsync, or git)
# 4. Set up virtual environment
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
# 5. Create .env and configure
cp .env.example .env
nano .env # fill in keys/secrets only
# 6. Run with screen/tmux for persistence
tmux new -s mm
python main.py
# Press CTRL+B then D to detach
# tmux attach -t mm to reattachCreate /etc/systemd/system/polymarket-mm.service:
[Unit]
Description=Polymarket Market Maker
After=network.target
[Service]
Type=simple
# Use a non-root user for security. Create one with:
# sudo useradd -m -s /bin/bash mmbot
# sudo -u mmbot cp -r ~/polymarket-mm /home/mmbot/polymarket-mm
User=mmbot
WorkingDirectory=/home/mmbot/polymarket-mm
ExecStart=/home/mmbot/polymarket-mm/venv/bin/python main.py
Restart=on-failure
RestartSec=30
[Install]
WantedBy=multi-user.targetThen:
sudo systemctl daemon-reload
sudo systemctl enable polymarket-mm
sudo systemctl start polymarket-mm
sudo journalctl -u polymarket-mm -f # view logsIf logs repeatedly show Insufficient collateral on SELL ... likely stale local inventory,
run a periodic guard that auto-resyncs live state and restarts the bot when this error
clusters.
-
Copy
scripts/resync_guard.shto your server and make it executable:chmod +x /path/to/polymarket-mm/scripts/resync_guard.sh
-
Create a oneshot service and timer (example):
# /etc/systemd/system/polymarket-resync-guard.service [Unit] Description=Polymarket collateral/inventory resync guard After=network-online.target Wants=network-online.target [Service] Type=oneshot Environment=BOT_DIR=/path/to/polymarket-mm Environment=BOT_SERVICE=polymarket-mm.service ExecStart=/path/to/polymarket-mm/scripts/resync_guard.sh
# /etc/systemd/system/polymarket-resync-guard.timer [Unit] Description=Run Polymarket resync guard every 10 minutes [Timer] OnBootSec=5min OnUnitActiveSec=10min RandomizedDelaySec=30s Persistent=true Unit=polymarket-resync-guard.service [Install] WantedBy=timers.target
-
Enable:
sudo systemctl daemon-reload sudo systemctl enable --now polymarket-resync-guard.timer
The guard is conservative by default and only triggers on repeated collateral/state drift patterns.
├── .env.example # Environment variable template (safe to commit)
├── .env.shared # Shared non-secret runtime profile (safe to commit)
├── .gitignore # Git ignore rules
├── requirements.txt # Python dependencies
├── config.py # Config loader (.env.shared -> .env -> .env.local)
├── setup_keys.py # One-time API key derivation
├── main.py # Entry point (run this)
├── client_wrapper.py # Polymarket API wrapper
├── market_selector.py # Market discovery and scoring
├── market_maker.py # Core market-making engine
├── inventory_manager.py # Position tracking and P&L
├── risk_manager.py # Risk limits and kill switches
├── telegram_alerts.py # Telegram notifications
├── REVIEW.md # Known issues and adversarial code review
├── tests/ # Test suite
├── bot_state.json # Saved state (auto-generated, gitignored)
└── mm_bot.log # Log file (auto-generated, gitignored)
See REVIEW.md for a detailed adversarial code review. Key items to be aware of:
- P0-1:
DONT_TOUCH_POSITION_*guards are only checked in the flatten command, not in the main trading loop. If you use these to protect manual positions, verify they work as expected in your workflow. - P0-2: Auto-merge can create temporary inventory imbalance if only one side fills.
- P1-4: No built-in API rate limiting — rapid market reselection can trigger 429 errors.
| Error | Cause | Fix |
|---|---|---|
PRIVATE_KEY is not set |
Missing .env or empty key |
Run cp .env.example .env and fill in credentials |
API credentials not set |
Didn't run setup_keys.py | Run python setup_keys.py and paste results into .env |
Health check failed |
Can't reach Polymarket API | Check internet, firewall, or if API is down |
Post-only rejection |
Spread too tight for post-only | Normal — bot will retry next cycle |
Insufficient balance |
Not enough USDC for order size | Reduce ORDER_SIZE or deposit more USDC |
429 / rate limit |
Too many API calls | Increase API_RATE_LIMIT_DELAY_SECONDS |
- Verify
.envexists:ls -la .env - Check all required fields are set:
PRIVATE_KEY,API_KEY,API_SECRET,API_PASSPHRASE - Verify Python version:
python3 --version(need 3.9+) - Ensure virtualenv is active:
source venv/bin/activate - Check connectivity:
curl -s https://clob.polymarket.com/ | head -20
- Run
python main.py --dry-runto check market selection - Check spread mode —
adaptiveis recommended for fill rate - Increase
ORDER_SIZEor reduceMIN_EDGE_PER_PAIR - Check if markets are active and liquid (look at volume)
- Review
mm_bot.logfor post-only rejections or risk pauses - If using correlation controls, avoid overly tight caps. Recommended floor:
CORRELATION_MAX_MARKETS_PER_KEYWORD >= 5for better order flow. - Generate a structured report for the last 30 minutes:
python scripts/diagnose_no_fills.py --minutes 30
If bot_state.json gets corrupted or you trade manually outside the bot:
python main.py --sync-live-state # Rebuild state from live wallet positions- Never commit
.env— it contains your private key and API credentials. - Set file permissions:
chmod 600 .env bot_state.json mm_bot.log - Rotate keys if you suspect any exposure (see
setup_keys.pyto re-derive API creds). - The private key signs on-chain transactions — treat it like a bank password.
- Telegram tokens: if compromised, revoke via @BotFather and create a new bot.
With $5K–$10K capital and default settings:
- Volume per day: ~$5K–$15K (depends on market activity)
- Expected cost: 0.3–0.5% of volume (adverse selection)
- Time to $500K volume: 1–3 months
- Estimated total cost: $1,500–$2,500
This bot is for educational and personal use. Prediction market trading involves financial risk. The bot may lose money due to adverse selection, market movements, API failures, or bugs. Never trade with more than you can afford to lose. This is not financial advice.