Skip to content

Repository files navigation

Polymarket Market Making Bot — Airdrop Farming Edition

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.

How It Works

Strategy: Two-Sided Passive Market Making

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)

Inventory Management

When one side fills but the other doesn't, you accumulate directional exposure. The bot handles this by:

  1. Skewing quotes — shifting prices to attract offsetting flow
  2. Position limits — capping exposure per market and portfolio-wide
  3. Reduce-only mode — when at limit, only placing orders that reduce position

Risk Controls

  • 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

Setup Guide

Prerequisites

  • 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)

Step 1: Clone and Set Up

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.txt

Step 2: Configure Environment

cp .env.example .env
chmod 600 .env   # Restrict permissions — only you should read this file

Edit .env and fill in secrets only:

  1. PRIVATE_KEY — Your wallet's private key

  2. 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
  3. SIGNATURE_TYPE — How you log in to Polymarket:

    • 1 = Email / Magic wallet
    • 2 = 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.

Step 3: Derive API Credentials

python setup_keys.py

This will print your API_KEY, API_SECRET, and API_PASSPHRASE. Copy them into .env.

Step 4: Test with Dry Run

python main.py --dry-run

This 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.

Step 5: Start the Bot

python main.py

To stop gracefully: press CTRL+C. The bot will cancel all orders and save state.

Emergency: Cancel All Orders

python main.py --cancel-all

One-Time Cleanup: Flatten Legacy Non-Manual Bags

python main.py --flatten-leftovers

This market-sells all live positions except those matched by DONT_TOUCH_POSITION_CONDITION_IDS / DONT_TOUCH_POSITION_KEYWORDS.

Check Status

python main.py --status

Configuration Tuning

All settings are in config.py and load in this order:

  1. .env.shared (tracked shared defaults)
  2. .env (local secrets)
  3. .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.

Conservative Settings (recommended to start)

HALF_SPREAD=0.03
ORDER_SIZE=10
MAX_MARKETS=5
MAX_POSITION_PER_MARKET=100
MAX_TOTAL_EXPOSURE_USD=800
MAX_DAILY_LOSS=100

Cleanup Mode For A Large Legacy Position

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.

Aggressive Settings (more volume, more risk)

HALF_SPREAD=0.015
ORDER_SIZE=25
MAX_MARKETS=12
MAX_POSITION_PER_MARKET=300
MAX_TOTAL_EXPOSURE_USD=2500
MAX_DAILY_LOSS=250

Telegram Alerts (Optional)

  1. Create a bot via @BotFather on Telegram
  2. Get your chat ID by sending a message to the bot and visiting: https://api.telegram.org/bot<TOKEN>/getUpdates
  3. 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.

Deploying to a Cloud Server

For 24/7 operation, deploy to a cheap cloud server:

DigitalOcean ($6/month)

# 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 reattach

Using systemd (auto-restart)

Create /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.target

Then:

sudo systemctl daemon-reload
sudo systemctl enable polymarket-mm
sudo systemctl start polymarket-mm
sudo journalctl -u polymarket-mm -f  # view logs

Optional: auto-resync guard for stale SELL collateral loops

If 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.

  1. Copy scripts/resync_guard.sh to your server and make it executable:

    chmod +x /path/to/polymarket-mm/scripts/resync_guard.sh
  2. 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
  3. 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.

File Structure

├── .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)

Known Issues

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.

Troubleshooting

Common 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

Bot won't start

  1. Verify .env exists: ls -la .env
  2. Check all required fields are set: PRIVATE_KEY, API_KEY, API_SECRET, API_PASSPHRASE
  3. Verify Python version: python3 --version (need 3.9+)
  4. Ensure virtualenv is active: source venv/bin/activate
  5. Check connectivity: curl -s https://clob.polymarket.com/ | head -20

Bot runs but no fills

  1. Run python main.py --dry-run to check market selection
  2. Check spread mode — adaptive is recommended for fill rate
  3. Increase ORDER_SIZE or reduce MIN_EDGE_PER_PAIR
  4. Check if markets are active and liquid (look at volume)
  5. Review mm_bot.log for post-only rejections or risk pauses
  6. If using correlation controls, avoid overly tight caps. Recommended floor: CORRELATION_MAX_MARKETS_PER_KEYWORD >= 5 for better order flow.
  7. Generate a structured report for the last 30 minutes: python scripts/diagnose_no_fills.py --minutes 30

State recovery

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

Security Notes

  • 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.py to 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.

Expected Performance

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

Disclaimer

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.

About

Delta-neutral market making bot for Polymarket - airdrop farming, adaptive spreads, inventory management, risk controls, and Telegram alerts

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages