This guide is for developers who built against V1 and need to port their bot before it stops trading (or, if you missed the cutover, get it trading again). It’s a single playbook covering every break we hit migrating our own integrations — verified against the V2 SDK source. For a method-by-method V2 SDK reference see the py_clob_client Reference. For auth-specific debugging see Polymarket Auth Troubleshooting. For real reported errors after the cutover see py-clob-client-v2 Errors.
Last verified against Polymarket’s V2 SDKs and docs: May 2026.
What actually changed (and what didn’t)
The cutover is bigger than a SemVer bump. Polymarket shipped, in one coordinated release: new on-chain Exchange contracts (CTF Exchange V2 + Neg Risk CTF Exchange V2), a rewritten CLOB matching backend, a new EIP-712 Exchange domain version ("1" → "2"), a new collateral token (pUSD, replacing USDC.e), a redesigned order struct, a native builder-attribution model, and new SDK packages for all three languages. Old packages can’t sign valid V2 orders, V1-signed orders are rejected by the new matcher, and resting orders were wiped at cutover.
What is not changing: the host URL (clob.polymarket.com), the Gamma API (gamma-api.polymarket.com), the Data API (data-api.polymarket.com), all WebSocket URLs, and your existing API key/secret/passphrase — the L1/L2 auth model is unchanged (the ClobAuthDomain EIP-712 domain stays at version "1"). Read-only code against Gamma, the Data API, or public CLOB endpoints (/book, /price, /midpoint, /spread) continues to work.
So the work is mechanical but pervasive: anywhere you sign or place an order, anywhere you import from py_clob_client or @polymarket/clob-client, anywhere your code touches USDC.e or fee_rate_bps or the V1 builder HMAC headers — that’s where you’ll be editing.
A useful safety net: the V2 SDKs call
GET /versionand silently auto-detect whether the backend is on V1 or V2, then sign accordingly. That means a fresh V2 client can talk to either backend during partial environments. But there is no V1 client that can talk to V2 — that’s the asymmetry you need to plan around.
The complete V1 → V2 diff (Python)
This is the table to keep open while you migrate. Every row was verified against the live py_clob_client_v2 source on the main branch.
Install & imports
| Area | V1 | V2 |
|---|---|---|
| Install | pip install py-clob-client (last release 0.34.6, Feb 19, 2026) | pip install py-clob-client-v2 (1.0.0, released Apr 17, 2026) |
| Top-level import | from py_clob_client.client import ClobClient | from py_clob_client_v2 import ClobClient |
| Order types import | from py_clob_client.clob_types import OrderArgs, MarketOrderArgs, OrderType | from py_clob_client_v2 import OrderArgs, MarketOrderArgs, OrderType, PartialCreateOrderOptions |
Side enum import | from py_clob_client.order_builder.constants import BUY, SELL | from py_clob_client_v2 import Side (use Side.BUY / Side.SELL) |
| Balance / allowance types | from py_clob_client.clob_types import BalanceAllowanceParams, AssetType | from py_clob_client_v2 import BalanceAllowanceParams, AssetType |
Removing the old BUY/SELL constants in favor of a real enum is one of the small but pervasive changes — it’s responsible for a lot of the “ImportError” issues people are hitting.
Constructor
In V1 (Python) you instantiated positionally, then attached creds with set_api_creds():
# V1
from py_clob_client.client import ClobClient
client = ClobClient(
"https://clob.polymarket.com",
key="<pk>",
chain_id=137,
signature_type=2,
funder="<proxy-address>",
)
client.set_api_creds(client.create_or_derive_api_creds())
In V2 (Python) the recommended pattern is two-step: build a level-1 client to derive creds, then build the trading client with creds= in the constructor. The chain_id= keyword is unchanged (this is the TypeScript-only rename). The set_api_creds() method still exists if you prefer that flow.
# V2
from py_clob_client_v2 import ClobClient
# Step 1 — L1 client to derive credentials
l1 = ClobClient(
host="https://clob.polymarket.com",
chain_id=137,
key="<pk>",
)
creds = l1.create_or_derive_api_key()
# Step 2 — L2 trading client
client = ClobClient(
host="https://clob.polymarket.com",
chain_id=137,
key="<pk>",
creds=creds,
signature_type=3, # POLY_1271 for deposit wallets (new)
funder="<deposit-wallet>", # the funded wallet
)
Note the method name:
create_or_derive_api_key()(V2) — notcreate_or_derive_api_creds()(V1). This rename alone breaks every script that does the V1 idiom in one line.
Order placement (limit + market)
V1 placed an order in two steps with the constants-based BUY:
# V1
from py_clob_client.clob_types import OrderArgs, OrderType
from py_clob_client.order_builder.constants import BUY
order = OrderArgs(token_id=tid, price=0.50, size=10.0, side=BUY)
signed = client.create_order(order)
resp = client.post_order(signed, OrderType.GTC)
V2 has a single fused call (create_and_post_order), takes a PartialCreateOrderOptions for tick size / neg-risk, and uses Side.BUY:
# V2
from py_clob_client_v2 import OrderArgs, OrderType, PartialCreateOrderOptions, Side
resp = client.create_and_post_order(
order_args=OrderArgs(token_id=tid, price=0.50, size=10.0, side=Side.BUY),
options=PartialCreateOrderOptions(tick_size="0.01"), # neg_risk optional; auto-detected
order_type=OrderType.GTC,
)
Market orders mirror the same pattern:
# V2
from py_clob_client_v2 import MarketOrderArgs
resp = client.create_and_post_market_order(
order_args=MarketOrderArgs(
token_id=tid,
amount=25.0, # USDC notional
side=Side.BUY,
order_type=OrderType.FOK,
),
options=PartialCreateOrderOptions(tick_size="0.01"),
order_type=OrderType.FOK,
)
Both V1 step-by-step calls (create_order + post_order, create_market_order + post_order) are still available in the V2 client, but the fused versions are now idiomatic.
Order-args field changes
Three V1 user-settable fields are gone — feeRateBps/fee_rate_bps, nonce, and taker. They are not just deprecated; they are removed from the signed EIP-712 Order struct entirely. Setting them in your code does nothing in V2. The on-the-wire order body adds three new fields:
| Field | V1 | V2 |
|---|---|---|
fee_rate_bps | User-settable; signed into the order | Removed. Fees set by the protocol at match time. |
nonce | User-settable; used for uniqueness | Removed. Replaced by timestamp (ms) for uniqueness. |
taker | User-settable | Removed. |
expiration | User-settable | Removed from the signed type. |
timestamp | n/a | New — milliseconds, gives per-address order uniqueness. |
metadata | n/a | New — bytes32. |
builder | n/a | New — bytes32 (set via builderCode). |
userUSDCBalance | n/a | New optional on market buys — lets the SDK compute fee-aware fill amounts. |
If you were reading or persisting raw orders, your schema needs to change. If your bot was relying on the matcher to honor a custom fee_rate_bps to “lock in” a fee — that lever no longer exists. Fees are now read live with getClobMarketInfo(conditionID).
Order book — now a plain dict
This one bites everyone. In V1, get_order_book(token_id) returned a typed OrderBookSummary object — you could do book.bids[0].price. In V2, get_order_book returns the raw JSON response — a plain Python dict.
# V1
book = client.get_order_book(token_id)
best_bid = book.bids[0].price # ← attribute access
# V2
book = client.get_order_book(token_id)
best_bid = book["bids"][0]["price"] # ← dict access
# or, if you want the typed object back:
from py_clob_client_v2 import OrderBookSummary
from py_clob_client_v2.utilities import parse_raw_orderbook_summary
typed = parse_raw_orderbook_summary(book)
typed.bids[0].price # works
The V2 client itself accepts both shapes downstream (e.g. calculate_market_price does book.get("asks") if isinstance(book, dict) else book.asks). If you had call sites that did book.bids on the raw return, change them to book["bids"] or wrap with parse_raw_orderbook_summary.
Removed client methods
There are no more get_balance or get_positions methods on the V2 CLOB client. The V1 get_positions was always a Data-API call dressed up as a client method; V2 makes the architecture honest. Use:
| V1 (gone) | V2 replacement |
|---|---|
client.get_positions() | GET https://data-api.polymarket.com/positions?user=<address> (Data API) |
client.get_balance() | client.get_balance_allowance(BalanceAllowanceParams(asset_type=AssetType.COLLATERAL)) — returns pUSD balance + allowance |
client.get_balance(token_id=…) | client.get_balance_allowance(BalanceAllowanceParams(asset_type=AssetType.CONDITIONAL, token_id=tid)) |
# V2 balance check
from py_clob_client_v2 import BalanceAllowanceParams, AssetType
bal = client.get_balance_allowance(
BalanceAllowanceParams(asset_type=AssetType.COLLATERAL)
)
print(bal) # {'balance': '12345600', 'allowance': '999...', ...} (pUSD, 6 decimals)
The Data API is fully public and works without authentication — you can query any wallet’s positions.
Signature types: POLY_1271 (type 3) is new
| Value | Name | Notes |
|---|---|---|
0 | EOA | MetaMask, hardware wallets; default if you omit signature_type. |
1 | POLY_PROXY | Magic Link / Google login proxy wallets. |
2 | GNOSIS_SAFE | Existing Safe-based browser wallet flow. |
3 | POLY_1271 | New in V2. EIP-1271 smart-contract signature — the recommended path for new API users via Polymarket deposit wallets. |
If you’re starting fresh, sign up via the Polymarket UI to deploy a deposit wallet, then pass that wallet’s address as funder and set signature_type=3. The signature is verified on-chain via ERC-1271 (isValidSignature). Existing Safe and proxy users keep signature_type=2 or 1 — V2 didn’t migrate you off your wallet type.
Collateral: pUSD, not USDC.e
The collateral token changed from USDC.e (the bridged USDC on Polygon) to pUSD — Polymarket USD, a standard ERC-20 on Polygon backed 1:1 by USDC, with backing enforced on-chain by the Collateral Onramp contract.
- Trading via the polymarket.com UI: the front end wraps USDC.e → pUSD for you (one-time approval).
- API-only: you have to wrap manually. Call
wrap()on the Collateral Onramp contract, depositing USDC.e (or USDC) and receiving pUSD 1:1. All positions are denominated in pUSD post-cutover.
Your get_balance_allowance(AssetType.COLLATERAL) query now returns the pUSD balance; the response shape is unchanged. Any code that hard-coded the USDC.e contract address (0x2791Bca1...) for allowance approvals needs to be repointed to the pUSD address — see Polymarket’s Contracts page for the canonical V2 addresses.
Builder attribution: HMAC headers → builderCode
The V1 builder model required a separate @polymarket/builder-signing-sdk and four extra HMAC headers (POLY_BUILDER_API_KEY, POLY_BUILDER_SECRET, POLY_BUILDER_PASSPHRASE, POLY_BUILDER_SIGNATURE). All of that is gone. V2 replaces it with one public field on each order:
# V2 — builder attribution per-order
from py_clob_client_v2 import OrderArgs, Side
resp = client.create_and_post_order(
order_args=OrderArgs(
token_id=tid, price=0.5, size=10, side=Side.BUY,
builder_code="0x...your-builder-code", # bytes32
),
options=PartialCreateOrderOptions(tick_size="0.01"),
order_type=OrderType.GTC,
)
Or set it once at construction time via BuilderConfig and every order inherits it:
from py_clob_client_v2 import BuilderConfig
client = ClobClient(
host="https://clob.polymarket.com", chain_id=137,
key=os.environ["PK"], creds=creds,
builder_config=BuilderConfig(builder_code="0x..."),
)
Your builderCode is a public identifier (it appears on-chain in the builder field of every attributed order) — copy it from Settings → Builder. Note: the HMAC builder API key is still used to authenticate with the Relayer for gasless transactions; only order attribution moved to builderCode.
Raw-order signing (EIP-712)
If you sign orders yourself (no SDK), three things change in the wire protocol:
- EIP-712 Exchange domain version bumps from
"1"to"2". TheClobAuthDomain(used for L1/L2 API auth) stays at"1"— your existing API key/secret/passphrase do not need to be regenerated. verifyingContractmoves to the V2 Exchange addresses —0xE111180000d2663C0091e4f400237545B87B996Bfor standard markets,0xe2222d279d744050d28e00520010520000310F59for Neg Risk. See Contracts for the canonical list.- The signed Order struct drops
taker,expiration,nonce,feeRateBpsand addstimestamp,metadata,builder(all the same changes the SDK abstracts).
TypeScript: the ethers → viem rewrite
The V2 TypeScript SDK drops ethers entirely in favor of viem. If your bot used ethers.Wallet to build a Signer, that wiring has to change.
// V1
import { ClobClient } from "@polymarket/clob-client";
import { ethers } from "ethers";
const provider = new ethers.providers.JsonRpcProvider(RPC);
const signer = new ethers.Wallet(process.env.PK!, provider);
const client = new ClobClient(
"https://clob.polymarket.com",
137, // chainId — positional
signer,
creds,
);
// V2
import { ClobClient, Side, OrderType } from "@polymarket/clob-client-v2";
import { createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { polygon } from "viem/chains";
const account = privateKeyToAccount(process.env.PK as `0x${string}`);
const walletClient = createWalletClient({
account,
chain: polygon,
transport: http(),
});
const client = new ClobClient({
host: "https://clob.polymarket.com",
chain: 137, // ← renamed from chainId
signer: walletClient, // ← viem WalletClient, not ethers Signer
creds,
signatureType: 3, // POLY_1271 for deposit wallets (optional)
funderAddress: process.env.FUNDER,
});
// Place a GTC limit order
const resp = await client.createAndPostOrder(
{ tokenID, price: 0.50, size: 10, side: Side.BUY },
{ tickSize: "0.01" }, // neg-risk auto-detected if omitted
OrderType.GTC,
);
The biggest API-shape changes in TS:
- The constructor is now an options object (
{ host, chain, signer, creds, signatureType, funderAddress }) — not positional args. chainIdwas renamed tochain.tickSizeTtlMsandgeoBlockTokenwere removed.createOrDeriveApiKey()is the new method name (wascreateOrDeriveApiCreds()).- The
signeris a viemWalletClientfromcreateWalletClient(...), not an ethersSigner.
For a code-heavy walkthrough including a full ethers→viem agent port see the Polymarket TypeScript SDK Reference and the dedicated ethers → viem migration guide.
Rust: polymarket_client_sdk_v2
The Rust SDK was renamed polymarket_client_sdk_v2 and rewrote its construction model around a builder:
# Cargo.toml
[dependencies]
polymarket_client_sdk_v2 = "=0.6.0-canary.1"
use polymarket_client_sdk_v2::clob::{Client, Config};
use polymarket_client_sdk_v2::clob::types::SignatureType;
use polymarket_client_sdk_v2::auth::{LocalSigner, Signer};
let signer = LocalSigner::from_str(&private_key)?.with_chain_id(Some(137));
let client = Client::new("https://clob.polymarket.com", Config::default())?
.authentication_builder(&signer)
.signature_type(SignatureType::Poly1271) // optional — for deposit wallets
.funder(deposit_wallet) // optional — required when sig type 1/2/3
.authenticate()
.await?;
Orders use a builder pattern, then sign and post:
use polymarket_client_sdk_v2::types::dec;
use polymarket_client_sdk_v2::clob::types::Side;
let order = client
.limit_order()
.token_id(token_id.parse()?)
.price(dec!(0.50))
.size(dec!(10))
.side(Side::Buy)
.build()
.await?;
let signed = client.sign(&signer, order).await?;
let resp = client.post_order(signed).await?;
Like the Python and TS clients, the Rust client auto-detects the backend version via GET /version and silently signs accordingly — V2 fields (metadata, builder_code, defer_exec) are ignored on V1, and vice versa.
Before / after: a complete minimal Python bot
A trivial “find a market → check the book → place a $5 limit a couple cents under the ask” bot, written first in V1, then ported to V2.
V1 (no longer works in production)
import os, requests
from py_clob_client.client import ClobClient
from py_clob_client.clob_types import OrderArgs, OrderType
from py_clob_client.order_builder.constants import BUY
HOST = "https://clob.polymarket.com"
client = ClobClient(
HOST, key=os.environ["PK"], chain_id=137,
signature_type=2, funder=os.environ["FUNDER"],
)
client.set_api_creds(client.create_or_derive_api_creds())
markets = requests.get(
"https://gamma-api.polymarket.com/markets",
params={"closed": False, "limit": 5, "order": "volume", "ascending": False},
).json()
target = markets[0]
token_id = target["clobTokenIds"][0]
book = client.get_order_book(token_id)
best_ask = float(book.asks[0].price) if book.asks else None # typed attr access
if best_ask and best_ask > 0.05:
my_price = round(best_ask - 0.02, 2)
order = OrderArgs(token_id=token_id, price=my_price, size=10.0, side=BUY)
signed = client.create_order(order)
print(client.post_order(signed, OrderType.GTC))
V2 (working)
import os, requests
from py_clob_client_v2 import (
ClobClient, OrderArgs, OrderType, PartialCreateOrderOptions, Side,
BalanceAllowanceParams, AssetType,
)
HOST = "https://clob.polymarket.com"
# Two-step client construction
l1 = ClobClient(host=HOST, chain_id=137, key=os.environ["PK"])
creds = l1.create_or_derive_api_key()
client = ClobClient(
host=HOST, chain_id=137, key=os.environ["PK"], creds=creds,
signature_type=3, # POLY_1271 for deposit wallets
funder=os.environ["FUNDER"], # your deposit-wallet address
)
# (Optional) sanity-check pUSD balance & allowance
bal = client.get_balance_allowance(
BalanceAllowanceParams(asset_type=AssetType.COLLATERAL)
)
print("pUSD balance/allowance:", bal)
# Find a market
markets = requests.get(
"https://gamma-api.polymarket.com/markets",
params={"closed": False, "limit": 5, "order": "volume", "ascending": False},
).json()
target = markets[0]
token_id = target["clobTokenIds"][0]
# Order book is now a dict
book = client.get_order_book(token_id)
best_ask = float(book["asks"][0]["price"]) if book.get("asks") else None
if best_ask and best_ask > 0.05:
my_price = round(best_ask - 0.02, 2)
resp = client.create_and_post_order(
order_args=OrderArgs(
token_id=token_id, price=my_price, size=10.0, side=Side.BUY,
),
options=PartialCreateOrderOptions(tick_size="0.01"),
order_type=OrderType.GTC,
)
print(resp)
That’s the whole migration in microcosm: package rename, Side.BUY instead of BUY, two-step client construction with creds= in the kwarg, signature_type=3 if you use a deposit wallet, dict-style book access, fused create_and_post_order with PartialCreateOrderOptions. Everything else (Gamma discovery, the host URL, the API key flow) is unchanged.
Things you can keep as-is
Not everything moved. To avoid over-correcting:
- Host URL
https://clob.polymarket.com— unchanged. - Gamma API and Data API — unchanged. Any code using
gamma-api.polymarket.comordata-api.polymarket.comkeeps working. - WebSocket URLs (
wss://ws-subscriptions-clob.polymarket.com/ws/market,/ws/user) — unchanged, and message payloads are mostly unchanged. Thefee_rate_bpsfield onlast_trade_pricestill reflects the actual fee charged. - API key / secret / passphrase — you do not need to regenerate them. The
ClobAuthDomainEIP-712 used for L1/L2 API auth stays at version"1". - Order types — GTC, FOK, FAK still exist; post-only orders still exist (with the same rule: not combinable with FOK/FAK).
- Read-only public CLOB endpoints —
/book,/price,/midpoint,/spreadare unchanged. get_balance_allowanceandupdate_balance_allowance— same names, sameBalanceAllowanceParams(asset_type=…, token_id=…)shape. Only the underlying collateral asset (pUSD instead of USDC.e) changed.
Pre-migration orders
The cutover wiped all open orders. If you had a bot tracking orders by orderID and want a historical record of what existed pre-cutover, the V2 client exposes a dedicated endpoint:
historical = client.get_pre_migration_orders()
for o in historical:
print(o["id"], o["status"])
This is read-only — you can’t cancel or interact with V1 orders; they’re gone. The endpoint exists strictly so accounting/reconciliation jobs can audit pre-cutover state.
Common gotchas while migrating
Field reports from porting a half-dozen of our own bots and from public discussions in the Polymarket Discord (see py-clob-client-v2 Errors for the full troubleshooting table):
- Double-installing both packages.
pip install py-clob-client py-clob-client-v2is fine, but make sure your script imports frompy_clob_client_v2, not the legacypy_clob_client. The two have overlapping symbol names. Afrom py_clob_client import …near the top will silently undo the migration. - Assuming
chain_idwas renamed in Python. It wasn’t — only TypeScript’schainIdbecamechain. Python keepschain_id=. - Forgetting to wrap USDC.e → pUSD. If you trade via the polymarket.com UI, the front end did this for you. If you’re API-only and never opened the site, your balance is still in USDC.e and your bot will fail allowance checks until you call
wrap(). book.bidsattribute access. Anywhere you persisted code that doesbook.bidsorbook.asks, expectAttributeError. Either switch tobook["bids"]or wrap withparse_raw_orderbook_summaryto get a typed view.post_onlyon FOK/FAK. The V2 client raisesValueError("post_only is not supported for FOK/FAK orders"). In V1 this was sometimes silently ignored; in V2 it’s an explicit failure. Use post-only only with GTC.- Tick-size violations.
PolyException("invalid price (X), min: 0.01 - max: 0.99")— your price has to be a multiple of the market’s minimum tick. Some sports markets are 0.001; checkclient.get_tick_size(token_id). signature_typeconfusion. If your wallet was deployed via the Polymarket UI as a Safe before V2, you’re stillsignature_type=2. If it’s a new deposit wallet deployed post-cutover, you’resignature_type=3(POLY_1271). Mixing the two is the most common cause ofINVALID_SIGNATURE.builder_codeformatting. It’s abytes32— 32 bytes (64 hex chars +0x). Copy it verbatim from Settings → Builder; don’t try to derive it.
Cutover day (historical)
The pre-cutover test environment lived at https://clob-v2.polymarket.com and ran in parallel with V1 production at clob.polymarket.com from early April. At ~11:00 UTC on April 28, V2 took over the production host, V1 stopped accepting orders, and all resting orders were wiped. The total downtime was about an hour. If you’re reading this after the fact, you don’t need to point your client anywhere different — V2 is on the standard CLOB host. The clob-v2.polymarket.com URL still resolves but should not be used for new integrations.
Changelog
| Date | Change |
|---|---|
| May 25, 2026 | Initial publication. Verified against py-clob-client-v2==1.0.0, @polymarket/[email protected], and polymarket_client_sdk_v2==0.6.0-canary.1. Cross-referenced live source for get_order_book dict shape, get_balance/get_positions removal, and the constructor signature. |
Official Resources
- Polymarket — Migrating to CLOB V2
- Polymarket Changelog
- py-clob-client-v2 (PyPI) · GitHub
- @polymarket/clob-client-v2 (npm) · GitHub
- polymarket_client_sdk_v2 (GitHub)
- Polymarket Contracts
- Polymarket pUSD
AgentBets Guides
- py_clob_client Reference — method-by-method V2 SDK reference
- py-clob-client-v2 Errors — symptom → cause → fix for post-cutover errors
- Polymarket Auth Troubleshooting —
INVALID_SIGNATURE, L1/L2 confusion,signature_typemismatches - Polymarket Trading Bot Quickstart — full V2 bot in Python
- Polymarket TypeScript SDK Reference
- Polymarket Rust SDK Reference
- Polymarket Rate Limits Guide
- What Is pUSD? — the collateral switch explained
- POLY_1271 & Smart-Contract Wallets — signature type 3, deposit wallets, Safe, agentic wallets
Where This Fits in the Agent Betting Stack
This guide is a maintenance task at Layer 3 (Trading) of the Agent Betting Stack — the execution layer. Migrating to V2 is the prerequisite to everything above it on the stack: any Layer 4 strategy you write only matters if your execution adapter can actually place orders, and after April 28 a V1 adapter cannot.
If you’re rebuilding a bot from scratch rather than porting one, start at the Polymarket Trading Bot Quickstart — it’s V2-native end to end.
This guide is maintained by AgentBets.ai. Found an error or an SDK change we missed? Let us know on Twitter.
Not financial advice. Built for builders.
