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 /version and 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

AreaV1V2
Installpip 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 importfrom py_clob_client.client import ClobClientfrom py_clob_client_v2 import ClobClient
Order types importfrom py_clob_client.clob_types import OrderArgs, MarketOrderArgs, OrderTypefrom py_clob_client_v2 import OrderArgs, MarketOrderArgs, OrderType, PartialCreateOrderOptions
Side enum importfrom py_clob_client.order_builder.constants import BUY, SELLfrom py_clob_client_v2 import Side (use Side.BUY / Side.SELL)
Balance / allowance typesfrom py_clob_client.clob_types import BalanceAllowanceParams, AssetTypefrom 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) — not create_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:

FieldV1V2
fee_rate_bpsUser-settable; signed into the orderRemoved. Fees set by the protocol at match time.
nonceUser-settable; used for uniquenessRemoved. Replaced by timestamp (ms) for uniqueness.
takerUser-settableRemoved.
expirationUser-settableRemoved from the signed type.
timestampn/aNew — milliseconds, gives per-address order uniqueness.
metadatan/aNewbytes32.
buildern/aNewbytes32 (set via builderCode).
userUSDCBalancen/aNew 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

ValueNameNotes
0EOAMetaMask, hardware wallets; default if you omit signature_type.
1POLY_PROXYMagic Link / Google login proxy wallets.
2GNOSIS_SAFEExisting Safe-based browser wallet flow.
3POLY_1271New 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:

  1. EIP-712 Exchange domain version bumps from "1" to "2". The ClobAuthDomain (used for L1/L2 API auth) stays at "1" — your existing API key/secret/passphrase do not need to be regenerated.
  2. verifyingContract moves to the V2 Exchange addresses — 0xE111180000d2663C0091e4f400237545B87B996B for standard markets, 0xe2222d279d744050d28e00520010520000310F59 for Neg Risk. See Contracts for the canonical list.
  3. The signed Order struct drops taker, expiration, nonce, feeRateBps and adds timestamp, 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.
  • chainId was renamed to chain.
  • tickSizeTtlMs and geoBlockToken were removed.
  • createOrDeriveApiKey() is the new method name (was createOrDeriveApiCreds()).
  • The signer is a viem WalletClient from createWalletClient(...), not an ethers Signer.

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.com or data-api.polymarket.com keeps working.
  • WebSocket URLs (wss://ws-subscriptions-clob.polymarket.com/ws/market, /ws/user) — unchanged, and message payloads are mostly unchanged. The fee_rate_bps field on last_trade_price still reflects the actual fee charged.
  • API key / secret / passphrase — you do not need to regenerate them. The ClobAuthDomain EIP-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, /spread are unchanged.
  • get_balance_allowance and update_balance_allowance — same names, same BalanceAllowanceParams(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-v2 is fine, but make sure your script imports from py_clob_client_v2, not the legacy py_clob_client. The two have overlapping symbol names. A from py_clob_client import … near the top will silently undo the migration.
  • Assuming chain_id was renamed in Python. It wasn’t — only TypeScript’s chainId became chain. Python keeps chain_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.bids attribute access. Anywhere you persisted code that does book.bids or book.asks, expect AttributeError. Either switch to book["bids"] or wrap with parse_raw_orderbook_summary to get a typed view.
  • post_only on FOK/FAK. The V2 client raises ValueError("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; check client.get_tick_size(token_id).
  • signature_type confusion. If your wallet was deployed via the Polymarket UI as a Safe before V2, you’re still signature_type=2. If it’s a new deposit wallet deployed post-cutover, you’re signature_type=3 (POLY_1271). Mixing the two is the most common cause of INVALID_SIGNATURE.
  • builder_code formatting. It’s a bytes32 — 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

DateChange
May 25, 2026Initial 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

AgentBets Guides

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.