Simple arbitrage bot implementing Jeremy Whittaker's strategy for Bitcoin 15-minute markets on Polymarket.
Pure arbitrage: Buy both sides (UP + DOWN) when total cost < $1.00 to guarantee profit regardless of outcome.
BTC goes up (UP): $0.48
BTC goes down (DOWN): $0.51
─────────────────────────
Total: $0.99 ✅ < $1.00
Profit: $0.01 per share (1.01%)
Why does it work? - At close, ONE of the two sides pays $1.00 per share - If you paid $0.99 total, you earn $0.01 no matter which side wins - It's guaranteed profit (pure arbitrage)
git clone https://github.com/Jonmaa/btc-polymarket-bot.git
cd btc-polymarket-bot
python -m venv .venv
.\.venv\Scripts\activate # Windows
# or: source .venv/bin/activate # Linux/Mac
pip install -r requirements.txt
Copy .env.example to .env:
cp .env.example .env
Then configure each variable (see detailed explanation below).
Note:
.envis loaded without overriding existing environment variables. This means values you set in the terminal / CI will take precedence over.env.
| Variable | Description | How to Get It |
|---|---|---|
POLYMARKET_PRIVATE_KEY |
Your wallet's private key (starts with 0x) |
Export from your wallet (MetaMask, etc.) or use the one linked to your Polymarket account |
POLYMARKET_API_KEY |
API key for Polymarket CLOB | Run python -m src.generate_api_key |
POLYMARKET_API_SECRET |
API secret for Polymarket CLOB | Run python -m src.generate_api_key |
POLYMARKET_API_PASSPHRASE |
API passphrase for Polymarket CLOB | Run python -m src.generate_api_key |
| Variable | Description | Value |
|---|---|---|
POLYMARKET_SIGNATURE_TYPE |
Type of wallet signature | 0 = EOA (MetaMask, hardware wallet) |
1 = Magic.link (email login on Polymarket)
2 = Gnosis Safe |
| POLYMARKET_FUNDER | Proxy wallet address (only for Magic.link users) | Leave empty for EOA wallets. For Magic.link, see instructions below. |
If you use email login on Polymarket (Magic.link), you have two addresses:
To find your proxy wallet address:
1. Go to your Polymarket profile: https://polymarket.com/@YOUR_USERNAME
2. Click the "Copy address" button next to your balance
3. This is your POLYMARKET_FUNDER — it should look like 0x... and is different from your signer address
Common mistake: Setting POLYMARKET_FUNDER to your Polygon wallet address (where you might have USDC on-chain) instead of the Polymarket proxy address. This causes "invalid signature" errors.
How to verify: Run python -m src.test_balance:
- "Getting USDC balance" shows the balance via Polymarket API (should show your funds)
- "Balance on-chain" queries Polygon directly (may show $0 if your funds are in the proxy, which is normal)
| Variable | Description | Default | Recommended |
|---|---|---|---|
TARGET_PAIR_COST |
Maximum combined cost to trigger arbitrage | 0.991 |
0.99 - 0.995 |
ORDER_SIZE |
Number of shares per trade (minimum is 5) | 5 |
Start with 5, increase after testing |
ORDER_TYPE |
Order time-in-force (FOK, FAK, GTC) |
FOK |
Use FOK to avoid leaving one leg open |
DRY_RUN |
Simulation mode | true |
Start with true, change to false for live trading |
SIM_BALANCE |
Starting cash used in simulation mode (DRY_RUN=true) |
0 |
e.g. 100 |
COOLDOWN_SECONDS |
Minimum seconds between executions | 10 |
Increase if you see repeated triggers |
| Variable | Description |
|---|---|
POLYMARKET_MARKET_SLUG |
Force a specific market slug (leave empty for auto-discovery) |
USE_WSS |
Enable Polymarket Market WebSocket feed (true/false) |
POLYMARKET_WS_URL |
Base WSS URL (default: wss://ws-subscriptions-clob.polymarket.com) |
Before running the bot, you need to generate your Polymarket API credentials.
Edit .env and add your private key:
POLYMARKET_PRIVATE_KEY=0xYOUR_PRIVATE_KEY_HERE
python -m src.generate_api_key
This will output something like:
API Key: abc123...
Secret: xyz789...
Passphrase: mypassphrase
.envPOLYMARKET_API_KEY=abc123...
POLYMARKET_API_SECRET=xyz789...
POLYMARKET_API_PASSPHRASE=mypassphrase
⚠️ Important: The API credentials are derived from your private key. If you change the private key, you'll need to regenerate the API credentials.
If you get "invalid signature" errors, run the diagnostic tool:
python -m src.diagnose_config
This will check:
- Whether your POLYMARKET_FUNDER is correctly set (required for Magic.link accounts)
- Whether the signer and funder addresses are different (they should be for Magic.link)
- Whether the bot can detect neg_risk for BTC 15min markets
- Your current USDC balance via the Polymarket API
Common causes of "invalid signature":
1. POLYMARKET_FUNDER is empty for Magic.link accounts
2. POLYMARKET_FUNDER is set to your Polygon wallet address instead of your Polymarket proxy wallet
3. API credentials were generated with a different private key or configuration
4. The neg_risk flag is incorrectly detected (fixed in latest version - bot now forces neg_risk=True for BTC 15min markets)
About "Balance on-chain" showing $0: This is normal for Magic.link accounts. Your funds are held in a Polymarket proxy contract, not directly in your Polygon wallet. The "USDC balance" via API should show your correct balance.
Before trading, verify that your wallet is configured correctly and has funds:
python -m src.test_balance
This will show:
======================================================================
POLYMARKET BALANCE TEST
======================================================================
Host: https://clob.polymarket.com
Signature Type: 1
Private Key: ✓
API Key: ✓
API Secret: ✓
API Passphrase: ✓
======================================================================
1. Creating ClobClient...
✓ Client created
2. Deriving API credentials from private key...
✓ Credentials configured
3. Getting wallet address...
✓ Address: 0x52e78F6071719C...
4. Getting USDC balance (COLLATERAL)...
💰 BALANCE USDC: $25.123456
5. Verifying balance directly on Polygon...
🔗 Balance on-chain: $25.123456
======================================================================
TEST COMPLETED
======================================================================
⚠️ If balance shows
$0.00but you have funds on Polymarket, check yourPOLYMARKET_SIGNATURE_TYPEandPOLYMARKET_FUNDERsettings.
Make sure DRY_RUN=true in .env, then:
python -m src.simple_arb_bot
The bot will scan for opportunities but won't place real orders.
By default the bot polls the CLOB order book over HTTPS. You can optionally enable the Polymarket CLOB Market WebSocket feed to receive pushed order book updates and reduce per-scan latency.
Set the following in your .env:
USE_WSS=true
POLYMARKET_WS_URL=wss://ws-subscriptions-clob.polymarket.com
Notes on WSS mode:
- The Market channel can send either a single JSON object or a JSON array (batched events). The bot handles both.
- If the connection drops or a proxy/firewall blocks WSS, the bot will reconnect and print the error reason.
- Internally, WSS mode maintains an in-memory L2 book using book snapshots + price_change deltas.
Then run the bot the same way:
python -m src.simple_arb_bot
DRY_RUN=false in .envpython -m src.simple_arb_bot
In real trading, it’s possible for only one leg (UP or DOWN) to fill if the book moves. To reduce the risk of ending up with an imbalanced position, the bot now:
get_order.trades_executed when both legs are confirmed filled.SELL on the filled leg at the current best_bid using FAK (fill-and-kill).Recommendation:
- Keep ORDER_TYPE=FOK for entries (fill-or-kill) to avoid leaving open orders.
Important: - This is risk-reduction, not a perfect guarantee. In fast markets, unwind orders can also fail or partially fill. - Always monitor your positions on Polymarket, especially if you see a “Partial fill detected” warning.
✅ Auto-discovers active BTC 15min market
✅ Detects opportunities when price_up + price_down < threshold
✅ Execution-aware pricing: uses order book asks (not last trade price)
✅ Depth-aware sizing: walks the ask book to ensure ORDER_SIZE can fill (uses a conservative “worst fill” price)
✅ Continuous scanning with no delays (maximum speed)
✅ Lower latency polling: fetches UP/DOWN order books concurrently
✅ Auto-switches to next market when current one closes
✅ Final summary with total investment, profit and market result
✅ Simulation mode for risk-free testing
✅ Balance verification before executing trades
✅ Paired execution verification: confirms both legs filled (otherwise cancels + attempts to unwind)
🚀 BITCOIN 15MIN ARBITRAGE BOT STARTED
======================================================================
Market: btc-updown-15m-1765301400
Time remaining: 12m 34s
Mode: 🔸 SIMULATION
Cost threshold: $0.99
Order size: 5 shares
======================================================================
[Scan #1] 12:34:56
No arbitrage: UP=$0.48 + DOWN=$0.52 = $1.00 (needs < $0.99)
🎯 ARBITRAGE OPPORTUNITY DETECTED
======================================================================
UP price (goes up): $0.4800
DOWN price (goes down): $0.5100
Total cost: $0.9900
Profit per share: $0.0100
Profit %: 1.01%
----------------------------------------------------------------------
Order size: 5 shares each side
Total investment: $4.95
Expected payout: $5.00
EXPECTED PROFIT: $0.05
======================================================================
✅ ARBITRAGE EXECUTED SUCCESSFULLY
🏁 MARKET CLOSED - FINAL SUMMARY
======================================================================
Market: btc-updown-15m-1765301400
Result: UP (goes up) 📈
Mode: 🔴 REAL TRADING
----------------------------------------------------------------------
Total opportunities detected: 3
Total trades executed: 3
Total shares bought: 30
----------------------------------------------------------------------
Total invested: $14.85
Expected payout at close: $15.00
Expected profit: $0.15 (1.01%)
======================================================================
Bot/
├── src/
│ ├── simple_arb_bot.py # Main arbitrage bot
│ ├── config.py # Configuration loader
│ ├── lookup.py # Market ID fetcher
│ ├── trading.py # Order execution
│ ├── generate_api_key.py # API key generator utility
│ └── test_balance.py # Balance verification utility
├── tests/
│ └── test_state.py # Unit tests
├── .env # Environment variables (create from .env.example)
├── .env.example # Environment template
├── requirements.txt # Dependencies
└── README.md # This file
DRY_RUN=false without funds in your Polymarket walletPOLYMARKET_SIGNATURE_TYPE matches your wallet typepython -m src.generate_api_keypython -m src.test_balance to see your wallet address$ claude mcp add btc-polymarket-bot \
-- python -m otcore.mcp_server <graph>