If you run a trading bot against Polymarket's CLOB, today's V2 cutover broke it. I migrated my live crash-recovery bot (308 closed trades, 80.2% WR) in 4 hours during the cutover window. This post is exactly what I observed — not what the announcement said, but what actually happened on the wire.
There's a companion repo with the working code, line-by-line diffs, and a smoke test you can run in 30 seconds: github.com/LuciferForge/polymarket-v2-migration
Reading this without checking the cookbook is fine. Running a live bot through the cutover without the cookbook is how you lose hours.
The headline you missed
Polymarket announced a 1-hour V2 cutover. The actual disruption was ~6 hours of mixed states, not 1. If your bot was running during the window and you were retrying every 30 seconds, you logged a few hundred 503s for nothing. Here's what those 6 hours looked like, hour by hour:
| Time (UTC) | State | What worked | What didn't |
|---|---|---|---|
| 11:00 | Cutover begins | Reads (orderbook, balance, positions) | New orders → HTTP 503 cancel-only |
| 11:00–13:00 | Pure cancel-only | Cancels of pre-existing V1 orders | Anything that posts a new order |
| 13:00–14:30 | V2 contracts active, V1 SDK confused | Migrated wallets via UI | V1 SDK paths still 503-ing |
| 14:30–16:00 | Mixed state | V2 SDK + migrated wallets | V1 SDK; un-migrated V2 wallets (allowance errors) |
| 16:00 → | Stable V2 | Everything | Anyone still on V1 imports |
The bot saw HTTP 503: Trading is currently cancel-only for the entire window. Nothing about that error tells you it'll last 6 hours. Polling the are-orders-scoring flag wouldn't have helped either — the right behavior is "pause, walk away, resume when V2 is up".
What actually changed (it's not a version bump)
The V2 migration is a new SDK package, not a pip install --upgrade. Polymarket shipped V2 as a parallel package so bots could be migrated incrementally rather than via big-bang upgrade.
pip install py-clob-client-v2 # V2 SDK
# Don't uninstall py-clob-client yet — keep it for rollback
Three changes in your code. That's it:
- from py_clob_client.client import ClobClient
- from py_clob_client.clob_types import (
+ from py_clob_client_v2.client import ClobClient
+ from py_clob_client_v2.clob_types import (
ApiCreds, BalanceAllowanceParams, AssetType, MarketOrderArgs,
)
- from py_clob_client.constants import POLYGON
+ from py_clob_client_v2.constants import POLYGON
...
- return client.post_order(signed_order, orderType="FOK")
+ return client.post_order(signed_order, order_type="FOK")
Two import rewrites and one kwarg rename (orderType → order_type, snake_case in V2). If you forget the kwarg rename, you get a loud TypeError: post_order() got an unexpected keyword argument 'orderType'. Loud errors are mercy.
Contract addresses (changed) and the only way to migrate your wallet
V2 ships with new contract addresses. The SDK has them baked in, but your wallet's allowances are not auto-migrated.
| Contract | V1 | V2 |
|---|---|---|
| CTF Exchange | 0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E |
0xE111180000d2663C0091e4f400237545B87B996B |
| NegRisk Exchange | 0xC5d563A36AE78145C45a50134d48A1215220f80a |
0xe2222d279d744050d28e00520010520000310F59 |
| Collateral | USDC.e | pUSD (1:1 backed by USDC.e) |
| CTF (token framework) | unchanged | unchanged |
Important: the only way to trigger your wallet's V2 migration is to manually trade ≥5 shares in the Polymarket UI. I tried each of the SDK methods first because the docs hinted they might work. They don't:
- ❌
update_balance_allowance()from the SDK — only refreshes the SDK's view of existing allowances. - ❌ Manually approving the V2 contracts on Polygonscan — Polymarket also requires the pUSD wrapper signature, which only happens in the UI flow.
- ❌ Logging into polymarket.com without trading — login alone does not trigger the migration prompt.
- ❌ Trading <5 shares — Polymarket gates the migration prompt behind a minimum.
The only thing that works: go to polymarket.com, click Trade on any market, set size to ≥5 shares, sign all 3 wallet signatures (USDC.e → pUSD wrap, V2 CTF allowance, V2 NegRisk allowance). Costs about 0.1 MATIC in gas. Takes 60 seconds.
After that, your wallet has V2 allowances and the V2 SDK works. Until then, you'll get HTTP 400: insufficient allowance for V2 CTF Exchange.
If you operate multiple proxy wallets, each one has to do its own UI flow. There is no batched migration.
What I actually did, in order
| Time (UTC) | Action | Result |
|---|---|---|
| 11:05 | Detected first 503. Paused bot via MAX_ENTRY_PRICE = 0.0 config flag. |
Bot stopped firing buys; existing positions kept being monitored. |
| 11:30 | Read V2 announcement + SDK release notes. | Confirmed import + kwarg changes. |
| 12:00 |
pip install py-clob-client-v2. Did NOT uninstall V1. |
Both SDKs available; rollback path preserved. |
| 12:15 | `sed -i '' 's\ | from py_clob_client.\ |
| 12:20 | {% raw %}`sed -i '' 's\ | orderType="FOK"\ |
| 12:30 | Smoke test: {% raw %}python -c "from py_clob_client_v2.client import ClobClient; print('ok')"
|
Pass. |
| 12:45 | Opened polymarket.com, traded 5 shares on a small market, signed migration approvals. | ~$0.01 in MATIC gas. Wallet now holds pUSD; V2 allowances appeared in get_balance_allowance response. |
| 13:00 | Manually posted a 1-share order via my updated bot to verify V2 path. | Order filled on-chain. |
| 14:00 | Resumed bot at half-size (POSITION_SIZE_USD = 5.0 instead of 10). |
First 3 orders went through cleanly. |
| 17:00 | Confirmed 6h of clean operation. | Returned to normal POSITION_SIZE. |
Total wall time: ~4 hours of focus, plus passive waiting for V2 to stabilize.
The 12 errors I hit (with fixes)
The cookbook's troubleshooting guide has all 12 with exact fixes. The most common ones:
ModuleNotFoundError: No module named 'py_clob_client_v2'→pip install py-clob-client-v2into the right venv. If you use launchd or systemd, double-check the interpreter path.TypeError: post_order() got an unexpected keyword argument 'orderType'→ grep fororderType=and replace withorder_type=.HTTP 503: Trading is currently cancel-only→ Polymarket-side maintenance. Pause your bot, wait. Don't retry-storm.HTTP 400: insufficient allowance for V2 CTF Exchange→ wallet not migrated. Go to polymarket.com and trade 5 shares.Decimal precision mismatch on amount→ V2 is stricter. Wrap your amount inround(amount_usd, 2).Order accepted but never settles on-chain → your wallet has no pUSD balance. The migration trade wraps USDC.e to pUSD; if you skipped that step, accepts will fail at settlement.
The full list with code is in docs/troubleshooting.md of the cookbook repo.
The slippage problem V2 doesn't fix
If your bot used a price-discount ladder (e.g., 5%, 15%, 25% below ref price) for TIMEOUT exits on thin markets, V2 doesn't fix that. V2 fixes nonce-related failed-fill bugs in the matching layer — it doesn't change order-book depth.
I ran a separate audit on my bot's lifetime P&L vs. on-chain fills using a tool I open-sourced today: pnl-truthteller. Result:
Lifetime theoretical P&L: +$33.49
Lifetime actual P&L: -$89.01
Total slippage cost: -$122.50 (-365.8% of theoretical)
That's 302 closed trades, 4-5 months of operation, and the difference between "I'm slightly profitable" and "I'm clearly not, and I should change the exit ladder." If you've never run that reconciliation against your bot's records, you don't know what you're really making.
pip install pnl-truthteller
pnl-truthteller --wallet 0xYourProxyAddress
Read-only, wallet address only, no API key. Outputs a Markdown report with by-exit-reason breakdown, worst-10 trades, and dust shares stranded on-chain.
The deeper lesson: V2 wasn't really about V2
Every DeFi protocol upgrade exposes which bots are well-coupled to the protocol's own SDK abstractions and which ones have hardcoded addresses, hardcoded version assumptions, or naive retry loops.
What V2 actually tested:
- Whether your bot detects cancel-only mode cleanly (most don't)
- Whether you have a kill switch that pauses the bot on first sign of a system-wide error (most don't)
- Whether your slippage cost is being measured against on-chain reality or against your own DB optimism (most aren't)
If any of those answers were "no", today is the day to fix them. I open-sourced three things that handle exactly these failure modes:
- polymarket-v2-migration — the cookbook for this specific cutover. 12 errors with fixes, smoke test, hour-by-hour timeline.
-
pnl-truthteller — fill-level P&L audit. Find your hidden slippage. (
pip install pnl-truthteller) -
quant-rollout — staged-deployment toolkit with kill switch and veto window. Stops you from shipping bad params into production. (
pip install quant-rollout)
All MIT, all zero-dep cores, all PyPI-published. If they save you a few hours over the next week, star them.
Resources
- Bot source: github.com/LuciferForge/polymarket-crash-bot — the same bot that ran through today's migration
- V2 cookbook: github.com/LuciferForge/polymarket-v2-migration
-
Smoke test: clone the cookbook and run
python tests/test_v2_imports.pyagainst your env. Tells you whether your wallet is migrated and your SDK is V2-ready. - Free Polymarket data API: api.protodex.io — 9,500+ markets, no key required for basic access
-
Labeled crash-recovery dataset (free): github.com/LuciferForge/cross-signal-data — 308 trades, 80.2% WR, real markets.
pip install cross-signal-data.
If your bot is still 503-ing right now, the cookbook tells you whether it's the SDK or your wallet allowances. If it's silent, run pnl-truthteller. The chain is the source of truth.
LuciferForge is a solo operator running a public-audited Polymarket trading bot. Also runs protodex.io (5,800+ MCP servers indexed) and the free Polymarket data API.
Top comments (0)