Halloween Sale: 35% off all Ryzen 9950X VPS plans

Use code Ends Oct 31 See Ryzen plans

Polymarket Protocol V2: how to update your trading bot for V1 and V2 markets

Written by TradoxVPS Engineering Team
|
Polymarket Protocol V2 for trading bots: CTF markets use clobTokenIds, the CTF exchanges, domain version 2 and CONDITIONAL balances; Protocol V2 markets use positionIds, ExchangeV3, domain version 3 and CONDITIONAL-V2.

Polymarket Protocol V2 is the new set of smart contracts underneath Polymarket’s markets. It replaces the Conditional Tokens Framework (CTF), the Gnosis contracts Polymarket has used since it launched, with one positions contract, one exchange and a router. It is already running in production on a small set of canary markets. According to coverage of Polymarket’s announcement, newly created markets will tentatively start moving to it on 2 November 2026.

For a trading bot, the important part is that nothing switches over in one go. Existing markets stay on the CTF contracts, and existing holdings are not converted. From November, a bot will therefore see two kinds of market side by side: version v1 markets on the old contracts and version v2 markets on the new ones. Each kind needs its own asset IDs, exchange, signing domain, approvals and balance type. A bot that signs a V2 market’s orders the V1 way produces signatures that ExchangeV3 cannot verify.

This guide covers what changes, how to tell the two kinds of market apart and what each one needs. It also covers the Data API v1 retirement, which lands in the same window, the state of the client libraries, and tested code in Python and TypeScript.

Current status

Last verified: 19 October 2026.

  • Canary markets: Polymarket said they began trading in production on 5 October 2026 and would run until 30 October. The earliest one we found on Gamma was created on 13 October. On 19 October, Gamma’s version=v2 filter returned five-minute and 15-minute Zcash Up or Down markets (slugs starting polyv2zec-updown-) and Central Park weather markets (slugs starting polyv2-central-park-).
  • New markets on Protocol V2: tentatively from 2 November 2026, according to Polymarket’s announcement of 5 October. Polymarket’s documentation does not give this date.
  • Existing markets and holdings: stay on the CTF contracts. Holdings are not converted.
  • Data API v1: retires on 24 October 2026, except GET /v1/accounting/snapshot.
  • SDKs: Protocol V2 trading needs polymarket-client (Python) or @polymarket/client (TypeScript) at version 0.12.0 or later. The latest release is 0.13.0, from 9 October 2026.
  • Older clients: py-clob-client-v2 and @polymarket/clob-client-v2 have been marked deprecated since 15 October 2026. They sign V2 orders only when told to.
  • People using the Polymarket app: no technical migration. They only need to accept any approval prompts.

How to move a bot to Polymarket Protocol V2 in six steps

  1. Upgrade your client. Move to the unified SDK at 0.12.0 or later, or add a V2 path to your own signing code.
  2. Choose IDs by version. Read each market’s version from Gamma. Take the asset ID from positionIds for v2 and from clobTokenIds for v1, and skip any market without a version you recognise.
  3. Grant the V2 approvals. Approve ExchangeV3 to spend pUSD for buys, and approve it as an operator on PositionManager for sells.
  4. Sign for the right exchange. Sign V2 orders for ExchangeV3 with EIP-712 domain version “3”. CTF orders keep version “2” and their current exchange.
  5. Read V2 state the V2 way. Query V2 balances with asset_type=CONDITIONAL-V2, and follow V2 resolution with resolutionStatus.
  6. Clear the deadlines. Move portfolio and history reads to Data API v2 before 24 October, then test V2 orders on the canary markets, and V1 orders on a CTF market, before 2 November.

Once you pass them the right ID, the official SDKs choose the exchange and signing domain, and they can send the approvals for you. Reading V2 balances and resolution stays with your code. The sections below explain each step, with tested code.

What Polymarket Protocol V2 is

Since launch, every Polymarket position has been an ERC-1155 token in the Conditional Tokens Framework, which Gnosis wrote in 2019. Polymarket announced Protocol V2 on 5 October 2026 in an article on X by Rajath Alex, its head of protocol. The new system has six parts that matter to a bot:

  • PositionManager: one ERC-1155 contract that holds every V2 position. Each position ID encodes its market type, market and outcome, which a later section unpacks.
  • Modules: separate contracts hold the market logic. BinaryModule runs yes or no markets, NegRiskModule runs multi-outcome events, and CombinatorialModule runs combinations of outcomes.
  • ExchangeV3: a single exchange for every V2 order, neg-risk markets included.
  • Router: the entry point for splitting collateral into positions, merging them back and redeeming them.
  • OracleAggregator: resolution, with reporter modules for UMA’s Optimistic Oracle, Chainlink and a designated address. On V2 markets, Gamma’s resolvedBy field points at it.
  • pUSD: the collateral does not change. It is the same Polymarket USD token introduced in April 2026, with six decimals and backed one for one by USDC or USDC.e.

The main contracts sit behind upgradeable proxies whose owner can change the code without changing the address. Polymarket’s public V2 contracts repository includes audit reports from Cantina, Certora, Quantstamp, Pashov, Sigma Prime and Zellic, plus a Certora formal verification report.

What stays the same matters as much. Polymarket’s migration overview says that if you already use pUSD and the order format from April 2026, your collateral, wallets, CLOB credentials and endpoints stay the same. The CLOB, Gamma and the WebSockets are the same services. What changes is which IDs you send, which contract you sign for and which contracts you approve.

Protocol V2 is not the April CLOB V2 upgrade

On 28 April 2026, Polymarket moved order matching to new exchange contracts called CTF Exchange V2 and Neg Risk CTF Exchange V2. It changed the order format, moved the EIP-712 domain version from “1” to “2” and introduced pUSD. That upgrade is widely called “Polymarket V2” or “CLOB V2”, and our guide to the April CLOB V2 migration covers it. Protocol V2 is a separate, later change that replaces the positions layer underneath those exchanges.

The overlap in names causes real mistakes, because “2” and “3” now mean different things in different places:

What you seeWhat it refers toPart of Protocol V2?
The CLOB’s GET /version returns {"version":2}The order format from April 2026. Older clients read it to decide how to sign.No
CTF Exchange V2 and Neg Risk CTF Exchange V2The April exchanges. They take domain version “2” and still serve every CTF market.No
version: "v2" on a Gamma marketThe market runs on the Protocol V2 contracts.Yes
EIP-712 domain version “3”The signing domain for orders on ExchangeV3.Yes
Data API v2The portfolio and history API, live since 4 September 2026. Version 1 retires on 24 October.No, but same window
py-clob-client-v2 and @polymarket/clob-client-v2Client libraries named after the April upgrade, now deprecated.No. They sign V2 orders only when told to.

Tools get caught as well as people. NautilusTrader’s Polymarket adapter, for example, reports an order_version_mismatch error as “adapter supports V2 only”, where V2 means the April order format. We found no Protocol V2 handling in its code on 19 October.

Polymarket Protocol V2 timeline

DateWhat happened or is planned
28 April 2026CLOB V2: new CTF exchanges, domain version “2” and pUSD
4 September 2026Data API v2 goes live and version 1 is frozen
25 September 2026py-clob-client-v2 and @polymarket/clob-client-v2 1.2.0 add an opt-in V2 order path
1 October 2026Unified SDK 0.12.0, the first release that trades Protocol V2 markets
5 October 2026Protocol V2 announced. Polymarket said canary markets were trading in production; the earliest we found on Gamma was created on 13 October
15 October 2026The two older CLOB clients are marked deprecated
24 October 2026Data API v1 retires, except the accounting snapshot
30 October 2026Planned end of the canary period
2 November 2026Tentative date for new markets to start moving to Protocol V2

Of the dates still ahead, only the Data API date appears in Polymarket’s documentation. The canary and November dates come from the 5 October announcement, as reported by several outlets including Unchained and crypto.news, so treat 2 November as a plan rather than a promise.

What changes for your bot: V1 and V2 markets compared

CTF market (v1)Protocol V2 market (v2)
Asset ID field on GammaclobTokenIdspositionIds
Positions contractCTFPositionManager
Exchange to sign forCTF Exchange, or Neg Risk CTF Exchange when negRisk is trueExchangeV3 for every market
EIP-712 domain version“2”“3”
Approval to buypUSD allowance for the CTF exchange you usepUSD allowance for ExchangeV3, covering fees
Approval to sellCTF operator approval for that exchangePositionManager operator approval for ExchangeV3
Balance asset_type for outcome tokensCONDITIONALCONDITIONAL-V2
Split, merge and redeemPolymarket’s pUSD collateral adapters for CTF, with CTF index sets 1 and 2Router, with outcome index 0 and 1
Resolution field on GammaumaResolutionStatusresolutionStatus
CollateralpUSDpUSD

Polymarket Protocol V2 contract addresses

All on Polygon (chain ID 137), as listed on Polymarket’s contracts page. The official SDKs use the same trading addresses.

ContractAddressUsed for
ExchangeV30xe3333700cA9d93003F00f0F71f8515005F6c00AaV2 orders
PositionManager0x006F54F7f9A22e0000CC2AB60031000000ae9fEFV2 balances and sell approvals
Router0x12121212006e4CD160D18e3f00711DA5c3372600V2 split, merge and redeem
AutoRedeemer0xa1200000d0002264C9a1698e001292D00E1b00afOptional automatic redemption
OracleAggregator0x0A0a0A0A8B00C51b7D810501b03F230028C04a87V2 resolution
pUSD0xC011a7E12a19f7B1f670d46F03B03f3342E82DFBCollateral for both kinds of market
CTF Exchange0xE111180000d2663C0091e4f400237545B87B996BV1 orders
Neg Risk CTF Exchange0xe2222d279d744050d28e00520010520000310F59V1 neg-risk orders
CTF0x4D97DCd97eC945f40cF65F87097ACe5EA0476045V1 positions

The Protocol V2 contracts, the AutoRedeemer, the OracleAggregator and pUSD are upgradeable proxies. Take their addresses from Polymarket’s contracts page, not from the implementation field a block explorer shows, because an upgrade changes the implementation but not the proxy.

Choose the market ID by version, not by which field is present

This is the change most likely to break a bot quietly. Polymarket’s API migration guide says to read each market’s version and pick the ID field from it, “even when both fields are present”, and to reject missing or unsupported versions and non-decimal IDs. Both fields now turn up on both kinds of market:

  • V2 markets repeat their position IDs in clobTokenIds. We saw this on every canary market we read on 19 October. A bot written for CTF markets reads clobTokenIds, gets a V2 ID and signs it the old way, for the old exchange.
  • Many V1 markets carry positionIds as well. The developers of polyoxide, an open-source Rust client, counted 13,977 such markets among the first 40,000 open markets they sampled on 7 October. They also found that “the CLOB returns 404 No orderbook exists for those ids”. On the binary CTF market we decoded, these were the IDs the V2 contracts would give the market if it were migrated. Nothing trades under them today.

So version decides, every time. Use the selected ID for everything that takes an asset ID: the order book (/book?token_id=), prices, the market WebSocket’s assets_ids, orders and balance queries. The user WebSocket’s markets field and market-wide cancellation take the condition ID instead. As a cross-check, the CLOB’s order book response for a V2 asset carries "version": "v2", and a CTF book has no version key.

Polymarket Protocol V2 order routing: read the Gamma market's version. V2 markets take positionIds, sign for ExchangeV3 with domain version 3, read balances as CONDITIONAL-V2 and approve on PositionManager. V1 markets take clobTokenIds, sign for the CTF exchanges with version 2, use CONDITIONAL and approve on CTF. Markets with a missing or unknown version are skipped.
Polymarket Protocol V2 order routing: read the Gamma market’s version. V2 markets take positionIds, sign for ExchangeV3 with domain version 3, read balances as CONDITIONAL-V2 and approve on PositionManager. V1 markets take clobTokenIds, sign for the CTF exchanges with version 2, use CONDITIONAL and approve on CTF. Markets with a missing or unknown version are skipped.

Route by version in Python

This function takes one Gamma market object and returns everything an order needs. It uses only the standard library:

"""Route an order for a Polymarket CTF market ("v1") or Protocol V2 market ("v2").
Choose the ID field by the market's version, never by which fields are present.
Standard library only, Python 3.10 or later."""

import json
from dataclasses import dataclass

EXCHANGE_V3 = "0xe3333700cA9d93003F00f0F71f8515005F6c00Aa"            # every v2 order
CTF_EXCHANGE = "0xE111180000d2663C0091e4f400237545B87B996B"           # v1 orders
NEG_RISK_CTF_EXCHANGE = "0xe2222d279d744050d28e00520010520000310F59"  # v1 neg-risk orders
POSITION_MANAGER = "0x006F54F7f9A22e0000CC2AB60031000000ae9fEF"       # holds v2 positions
CTF = "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045"                    # holds v1 positions
V2_RESERVED_BITS = ((1 << 64) - 1) << 40  # bits 40 to 103: always zero in a v2 position ID
V2_MODULES = {1: "binary", 2: "neg-risk", 3: "combinatorial"}


@dataclass(frozen=True)
class Route:
    version: str             # "v1" or "v2"
    asset_id: str            # for the order book, prices, market channel, orders and balances
    exchange: str            # verifyingContract in the order's EIP-712 domain
    domain_version: str      # "2" for v1 orders, "3" for v2 orders
    sell_asset_type: str     # asset_type for GET /balance-allowance before a sell
    positions_contract: str  # call setApprovalForAll(exchange, true) here before a sell


def json_list(value):
    """Gamma sends some lists as JSON-encoded strings and others as arrays."""
    items = json.loads(value) if isinstance(value, str) else value
    if not isinstance(items, list):
        raise ValueError(f"expected a list, got {value!r}")
    return items


def is_v2_position_id(asset_id: str) -> bool:
    value = int(asset_id)
    return value < 1 << 256 and value & V2_RESERVED_BITS == 0


def decode_v2_position_id(position_id: str) -> dict:
    if not is_v2_position_id(position_id):
        raise ValueError(f"not a v2 position ID: {position_id}")
    value = int(position_id)
    module = V2_MODULES.get(value >> 248)
    if module is None:
        raise ValueError(f"unknown v2 module {value >> 248}")
    return {"module": module,
            "condition_id": f"0x{value & ~0xFF:064x}",  # the same ID with outcome index 0
            "outcome_index": value & 0xFF}               # 0 is Yes and 1 is No in a binary market


def route_for_outcome(market: dict, outcome: str) -> Route:
    """market is one Gamma market object, for example from GET /markets/slug/{slug}."""
    version, slug = market.get("version"), market.get("slug")
    if version == "v2":
        ids = json_list(market.get("positionIds"))
    elif version == "v1":
        ids = json_list(market.get("clobTokenIds"))
    else:  # missing or new: skip the market and log it rather than guess
        raise ValueError(f"{slug}: unsupported version {version!r}")
    labels = json_list(market.get("outcomes"))
    if len(ids) != len(labels) or not all(
            isinstance(i, str) and i.isascii() and i.isdigit() for i in ids):
        raise ValueError(f"{slug}: IDs do not line up with outcomes")
    asset_id = ids[labels.index(outcome)]  # raises ValueError for an unknown outcome label
    if is_v2_position_id(asset_id) != (version == "v2"):
        raise ValueError(f"{slug}: a {version} market carries an ID of the wrong kind")
    if version == "v2":  # one exchange for every v2 market, neg-risk included
        return Route(version, asset_id, EXCHANGE_V3, "3", "CONDITIONAL-V2", POSITION_MANAGER)
    exchange = NEG_RISK_CTF_EXCHANGE if market.get("negRisk") else CTF_EXCHANGE
    return Route(version, asset_id, exchange, "2", "CONDITIONAL", CTF)

How it works:

  • The version decides. A market without a version, or with one the code does not know, raises an error. Log those and skip them, so that a new market type never gets traded by accident.
  • IDs are validated. They must be plain decimal strings and line up one for one with the outcome labels.
  • The bit check catches bad data. Bits 40 to 103 of a V2 position ID are reserved and zero in every V2 ID today, which the official SDK relies on too. A CTF token ID is a hash, so the chance of those 64 bits all being zero by accident is about one in 18 quintillion. If the bit check and the version disagree, the data is wrong and the market is skipped.
  • The neg-risk flag only matters for V1. Every V2 order goes to ExchangeV3.

We tested this against hand-built markets, including a V2 market that repeats its IDs in clobTokenIds and a V1 market that carries V2-format positionIds. Every answer matched the routing functions inside Polymarket’s Python SDK, and the decoder returned the exact Gamma condition IDs of two live canary markets.

Sign V2 orders for ExchangeV3 with domain version “3”

The order itself has not changed since April. It still has eleven fields, salt, maker, signer, tokenId, makerAmount, takerAmount, side, signatureType, timestamp, metadata and builder, and tokenId now carries the V2 position ID. Only the EIP-712 domain changes:

{
  "name": "Polymarket CTF Exchange",
  "version": "3",
  "chainId": 137,
  "verifyingContract": "0xe3333700cA9d93003F00f0F71f8515005F6c00Aa"
}

CTF orders keep version “2” and the exchange they already use. A signature made under one domain does not verify under the other. When we recovered the signer from orders the official SDKs signed, V2 orders verified under version “3” at ExchangeV3 and not under version “2”, and CTF orders verified under version “2” at the CTF Exchange.

Three traps catch hand-written signers:

  • The neg-risk flag no longer picks the exchange. On a V2 neg-risk market, Gamma’s negRisk is still true and the CLOB’s /neg-risk endpoint still answers {"neg_risk":true}. Code that maps neg-risk to the Neg Risk CTF Exchange will sign for the wrong contract. MetaMask’s prediction market code, for example, points both of its exchange slots at ExchangeV3 in a V2 signing path merged on 12 October, which was not yet wired into its market routing.
  • One document in the contracts repository is out of date. The docs/exchange.md file in Polymarket’s V2 contracts repository gives the domain as name “Polymarket Exchange” and version “1”. The contract source, Exchange.sol, returns “Polymarket CTF Exchange” and “3”, which matches the developer documentation and both SDKs. Sign with those.
  • Deposit wallets change too. If you sign for a Polymarket deposit wallet (signature type 3), the outer domain of the wrapped signature also moves to version “3”. The SDKs handle this.

Order amounts and fills

Amounts are integers in six-decimal base units, as before. A buy of 10 shares at 0.50 has a makerAmount of 5,000,000 (5 pUSD) and a takerAmount of 10,000,000 (10 shares). Polymarket’s API guide adds four rules for anyone who does their own fill maths:

  • The counter-asset amount of a fill is the maker asset filled, times takerAmount, divided by makerAmount, rounded down. The maker asset is collateral for a buy and shares for a sell.
  • GTC and GTD buy orders target shares, while FOK and FAK buy orders target collateral.
  • Buy fees add to the collateral you spend, and sell fees come out of the proceeds.
  • ExchangeV3 reduces a buy order’s remaining collateral budget by the amount actually spent, which matters if you reconcile partial fills yourself.

Approvals: CTF approvals do not carry over to Protocol V2

Polymarket’s docs are blunt: existing CTF permissions do not grant the V2 approvals. A hand-written client that has traded for months can buy and sell CTF markets normally and still fail its first V2 order. These are the approvals, all granted from the wallet that trades:

To do thisCall this contractWith this call
Buy on a V2 marketpUSDapprove(ExchangeV3, amount), with the amount covering collateral and fees
Sell on a V2 marketPositionManagersetApprovalForAll(ExchangeV3, true)
Split pUSD into positionspUSDapprove(Router, amount)
Merge or redeemPositionManagersetApprovalForAll(Router, true)
Automatic redemption (optional)PositionManagersetApprovalForAll(AutoRedeemer, true)

After approving, call GET /balance-allowance/update so the CLOB refreshes its cached view, then read GET /balance-allowance and check the ExchangeV3 entry under allowances.

How the official SDKs grant approvals

The SDKs can grant these for you in three ways, and it is worth knowing all three:

  • setupTradingApprovals() and setup_trading_approvals() send whatever is missing from a standard set, and Polymarket says they cover the Protocol V2 contracts. In version 0.13.0 that set is broad. It grants unlimited pUSD allowances to seven contracts, among them ExchangeV3, the Router and the perpetuals deposit contract. It also grants ten operator approvals, among them the AutoRedeemer and the two V2 modules as operators of your CTF positions.
  • approveErc20() and approveErc1155ForAll() (approve_erc20() and approve_erc1155_for_all() in Python) send one approval at a time. Use them to grant only what your bot needs: the two ExchangeV3 approvals, plus the Router ones if you split, merge or redeem.
  • Order placement can approve on its own. In 0.13.0, if the CLOB rejects an order with “allowance is not enough” and the allowance for that order’s exchange is below the order’s maker amount, place_limit_order() and place_market_order() (placeLimitOrder() and placeMarketOrder() in TypeScript) send an unlimited pUSD approval for a buy, or an operator approval for a sell, then retry the order once. An SDK bot’s first V2 order can therefore send an approval transaction without being asked.

Both SDKs also count any pUSD allowance below the maximum as missing when they check approvals, and the setup call raises such an allowance to the maximum. If you prefer a bounded allowance, set it yourself and check it the way the samples below do: read the ExchangeV3 entry of a COLLATERAL balance query and compare it with what the order will spend. Gasless wallets need a Relayer or Builder API key in the client configuration before the SDK can submit any approval.

Balances and allowances: CONDITIONAL-V2

The CLOB’s balance endpoints take a new asset_type for V2 outcome tokens. CTF tokens keep CONDITIONAL, and pUSD stays COLLATERAL for both kinds of market. This is the request the Python SDK sent for a V2 position in our tests:

GET /balance-allowance?asset_type=CONDITIONAL-V2&signature_type=0&token_id=<V2 position ID>

Values come back in base units, and allowances is keyed by spender address. Two traps apply here:

  • The older clients’ asset type lists stop at CONDITIONAL. In py-clob-client-v2, @polymarket/clob-client-v2 and the Rust polymarket_client_sdk_v2, the asset type has only COLLATERAL and CONDITIONAL. A bot on those libraries has to pass the new value itself.
  • Check the type before you trust a balance. Before you conclude that a sell failed for lack of shares, check that the balance query used the type that matches the market’s version.

Split, merge and redeem through the Router

Bots that create or close positions outside the order book, such as market makers that split collateral or bots that redeem winnings, need the Router. Polymarket’s contract integration guide gives its interface:

function split(bytes31 conditionId, uint256 amount);
function merge(bytes31 conditionId, uint256 amount);
function redeem(bytes31 conditionId, uint256 outcomeIndex, uint256 amount);

What differs from the CTF calls:

  • Condition IDs are 31 bytes. Gamma shows a V2 condition ID as 32 bytes ending in 00. Check that the last byte is zero, then drop it.
  • Outcome indexes are 0 for Yes and 1 for No. They replace the CTF index sets 1 and 2, so CTF redemption code that passes index sets will redeem the wrong side or fail.
  • Amounts use six decimals. One share or one pUSD is 1,000,000. A merge spends that amount of each outcome, and a redemption needs an explicit amount rather than redeeming the whole balance.
  • The Router does the transfers. It pulls your assets and returns the outputs in one transaction. Do not transfer assets to a module yourself first.
  • Payouts come from getPayout. getPayout(positionId, amount) on PositionManager returns the pUSD payout in base units. It can revert while a position is unresolved, and zero is a valid losing payout.

The AutoRedeemer is optional. Approving it as an operator lets an authorised operator redeem a resolved position’s full balance back to the same holder, but, as the docs put it, “approval alone does not schedule redemption.”

Resolution: resolutionStatus replaces umaResolutionStatus

Polymarket’s API guide says V2 markets report resolution in Gamma’s resolutionStatus, with the values inactive, active and resolved, while V1 markets keep umaResolutionStatus. Do not wait for a UMA status on a V2 market. On 19 October no canary market carried one, although the unresolved V1 market we checked omitted it too, so its absence proves nothing on its own.

The SDKs’ market objects in 0.13.0 expose only the UMA field, uma_resolution_status in Python and umaResolutionStatus in TypeScript. An SDK bot should therefore follow V2 resolution through the resolutions endpoint below, or read resolutionStatus from Gamma’s raw JSON.

Polymarket’s Data API has a resolution endpoint that covers both kinds of market, GET /v2/resolutions. Its rows carry a status (from initialized through proposed, disputed and resolved), a reporter (UMA_OO, CHAINLINK or EOA) and payouts per share in millionths (the Python SDK converts them to pUSD per share). Since 28 September it can also include an estimated settlement time. The SDKs wrap it as get_resolutions() and fetchResolutions(). Use payouts only from a row whose status is resolved.

Polymarket neg-risk markets on Protocol V2

Neg-risk events, where exactly one outcome of several wins, work differently under the hood:

  • One exchange. As covered above, V2 neg-risk orders go to ExchangeV3.
  • The ID records the event size. A V2 neg-risk condition ID encodes the number of real outcomes in the event. One canary weather market we decoded carried 5.
  • A synthetic Other outcome. Every V2 neg-risk event has an extra “Other” condition at the index after the last real outcome. It resolves Yes only if every real outcome resolves No. Horizontal split, horizontal merge and convert all include it.
  • Convert works on the full set. Converting one No position gives one Yes position on every other outcome, Other included, with no collateral moving.
  • Losers can be derived. Once one outcome resolves Yes, the rest can be read as No without a resolution event of their own. Polymarket’s contract guide says to refresh on the RemainingConditionsDerivableAsNo and SyntheticConditionDerivableAsYes events, then read getResult on the module or getPayout on PositionManager.

Combinatorial positions, which combine outcomes from several markets into one position, live in module 3. They cannot be bridged to other chains, and this guide does not cover them.

Anatomy of a Polymarket Protocol V2 position ID

A V2 position ID is a 256-bit number with seven fields, documented in Polymarket’s contracts repository:

BitsFieldMeaning
248 to 255Module1 is binary, 2 is neg-risk, 3 is combinatorial
120 to 247Base hashA 128-bit hash of the event or question
104 to 119ArityThe number of real outcomes in a neg-risk event, otherwise zero
40 to 103ReservedZero today
24 to 39Resolution chain0 is Polygon
8 to 23Condition indexThe market’s position within a neg-risk event, where each real outcome is one market; 0 for binary markets
0 to 7Outcome index0 is Yes and 1 is No

A condition ID is the same number with the outcome index set to zero. Take the Central Park rain canary market, which Gamma showed on 19 October with this condition ID:

0x0118375b48443dbc85984171d6cdcf12f3000000000000000000000000000000

The first byte, 01, is the binary module. The next 16 bytes are the base hash, and everything after them is zero: no arity, no reserved bits, Polygon, condition index 0. The Yes position ID is that number read as an integer, and the No position ID is the same number plus one. In code, the module is positionId >> 248 and the outcome index is positionId & 0xff.

Anatomy of a Polymarket Protocol V2 position ID: 64 hex digits split into module, base hash, arity, reserved bits, resolution chain, condition index and outcome index, shown on the Yes position of a Central Park rain canary market. The condition ID is the same number with outcome index 0.
Anatomy of a Polymarket Protocol V2 position ID: 64 hex digits split into module, base hash, arity, reserved bits, resolution chain, condition index and outcome index, shown on the Yes position of a Central Park rain canary market. The condition ID is the same number with outcome index 0.
Anatomy of a Polymarket Protocol V2 position ID: 64 hex digits split into module, base hash, arity, reserved bits, resolution chain, condition index and outcome index, shown on the Yes position of a Central Park rain canary market. The condition ID is the same number with outcome index 0.

This structure lets the official SDK tell a V2 ID from a CTF ID without looking anything up, by testing the reserved bits. It is also why the V1 markets that carry positionIds are a trap: their pre-computed migration IDs pass the same test.

Existing CTF positions and migration

Polymarket’s documentation says existing CTF holdings are not converted, and CTF support stays in place for V1 markets. A position in a CTF market stays a CTF token, trades on the CTF exchanges and redeems through CTF unless it is migrated, which the next paragraph covers.

The V2 contracts do contain a migration path. A CTF condition must first be registered for migration, which only an authorised account can do. After that, any holder can move their own positions into PositionManager with migratePositions, or an operator can do it for a holder who has approved the module. On the binary CTF market we checked, the pre-computed IDs in Gamma were exactly the IDs that path would mint. We found no announcement of a plan to use it as of 19 October.

One related detail is worth knowing. The standard approval set in SDK 0.13.0 already makes BinaryModule and NegRiskModule operators of your CTF positions, which is the approval migratePositions needs. A wallet that has run setup_trading_approvals() has granted it.

For planning, assume your bot runs both code paths for months. Long-dated CTF markets will keep trading on the old contracts well after new markets move.

Data API v1 retires on 24 October

This is not part of Protocol V2, but it lands in the same window and breaks the same bots. Polymarket’s Data API migration guide says “Data API v1 is retired on October 24, 2026”. The changes for a bot that reads positions, trades or activity:

  • Paths move under /v2 on the same host, data-api.polymarket.com.
  • Responses are wrapped in a data field, and paginated routes add a pagination object.
  • Pagination uses a cursor. Follow pagination.next_cursor until it is null. Version 1’s offset stopped at 10,000 rows.
  • Field names are snake_case, so proxyWallet becomes proxy_wallet and conditionId becomes condition_id.
  • Positions are one endpoint. /positions, /closed-positions and /v1/market-positions fold into GET /v2/positions, filtered by a status of OPEN, REDEEMABLE, REDEEMABLE_LOST, MERGEABLE or CLOSED.
  • Some routes are renamed. /traded becomes /v2/user-stats, for example.
  • A token lookup knows about Protocol V2. GET /v2/tokens, added on 7 October, returns a module for each token (v1_ctf for CTF tokens) and a Protocol V2 structural_condition_id, which makes a useful cross-check. The SDKs did not wrap it on 19 October.

GET /v1/accounting/snapshot has no v2 counterpart and is not affected. The SDKs use Data API v2 from version 0.10.0.

Which Polymarket client libraries support Protocol V2

LibraryProtocol V2 ordersNotes
polymarket-client (Python) 0.12.0 or laterAutomaticRoutes by the asset ID. Needs Python 3.11 or later. Latest 0.13.0.
@polymarket/client (TypeScript) 0.12.0 or laterAutomaticRoutes by the asset ID. Declares Node 24 or later. Latest 0.13.0.
py-clob-client-v2 1.2.0 or laterOnly with position_id=, or version=3 in the order optionsDeprecated on 15 October 2026.
@polymarket/clob-client-v2 1.2.0Only with positionID, version: 3 or createExchangeV3OrderFromAmountsDeprecated on 15 October 2026.
polymarket_client_sdk_v2 (Rust) 0.8.0Only with the order builder’s position_idOn 19 October, Polymarket’s SDK migration page still pointed Rust users to 0.7.0, which predates it.

The trap in the older clients is that nothing tells them a market is V2. Put a V2 position ID in token_id and py-clob-client-v2 signs a version “2” order for a CTF exchange, which ExchangeV3 cannot verify. The same happens in @polymarket/clob-client-v2 with tokenID. Both libraries now carry a notice asking users to move to the unified SDK.

The unified SDK decides from the ID itself: if the reserved bits are zero, it treats the ID as V2 and picks ExchangeV3, domain version “3” and CONDITIONAL-V2. That test cannot tell a real V2 market from a V1 market’s pre-computed migration IDs, which is why the choice of ID still has to come from the market’s version. Hand the SDK a V1 market’s positionIds and it will route the order to ExchangeV3.

If your bot runs on someone else’s code

  • NautilusTrader: we found no Protocol V2 handling in its Polymarket adapter on 19 October. Its version check expects the April order format.
  • polyoxide: Protocol V2 support was tracked in an issue still open on 19 October, which also notes that its signer hard-codes domain version “2” and its balance query hard-codes CONDITIONAL.
  • Hosted bots and copy-trading services: ask the provider whether they route by market version and have granted the ExchangeV3 approvals, before V2 markets reach the markets you trade.

Trading Protocol V2 markets with the official SDKs

Polymarket’s SDK integration guide has the pattern: fetch the market, choose the ID by version and pass it as the asset ID. The SDK then handles the exchange, the domain and the signature.

Pass wallet when you create the client. Without it, the SDK assumes the signer’s default deposit wallet and tries to deploy one if none exists, which fails without a Relayer or Builder API key. Use the wallet address your account already trades from.

Python

"""Place a post-only limit order on a CTF ("v1") or Protocol V2 ("v2") market with the
unified Polymarket SDK, which picks the exchange and signing domain from the asset ID.
pip install "polymarket-client>=0.13"   (tested with 0.13.0; V2 orders need 0.12.0 or later)"""

import asyncio
import os
import sys
from decimal import Decimal

from polymarket import AsyncSecureClient

EXCHANGE_V3 = "0xe3333700cA9d93003F00f0F71f8515005F6c00Aa"
PRICE, SIZE = "0.50", "10"


def allowance_for(result, spender: str) -> int:
    return next((v for k, v in result.allowances.items() if k.lower() == spender.lower()), 0)


async def main(slug: str) -> None:
    client = await AsyncSecureClient.create(
        private_key=os.environ["POLYMARKET_PRIVATE_KEY"],
        wallet=os.environ["POLYMARKET_WALLET"],  # the wallet your account already uses
    )
    async with client:
        market = await client.get_market(slug=slug)
        yes = market.outcomes.yes
        if market.version == "v2":
            asset_id, sell_type = yes.position_id, "CONDITIONAL-V2"
        elif market.version == "v1":
            asset_id, sell_type = yes.token_id, "CONDITIONAL"
        else:  # missing or new: skip rather than guess
            sys.exit(f"{slug}: unsupported version {market.version}, skipping")
        if asset_id is None:
            sys.exit(f"{slug}: no {market.version} ID for the Yes outcome")

        held = await client.get_balance_allowance(asset_type=sell_type, asset_id=asset_id)
        print(f"{market.version} {slug}: Yes is {asset_id}, you hold {held.balance / 1e6:g}")

        if market.version == "v2":  # CTF approvals do not cover ExchangeV3
            pusd = await client.get_balance_allowance(asset_type="COLLATERAL")
            if allowance_for(pusd, EXCHANGE_V3) < Decimal(PRICE) * Decimal(SIZE) * 10**6:
                sys.exit("pUSD allowance for ExchangeV3 is too low: approve it first")

        result = await client.place_limit_order(
            asset_id=asset_id, side="BUY", price=PRICE, size=SIZE, post_only=True)
        print(result)


asyncio.run(main(sys.argv[1]))

Run it with the market’s slug as the only argument. The price and size are placeholders, and the order is post-only, so it never takes liquidity: an order that would cross the book is refused. The allowance check reads the ExchangeV3 entry of a COLLATERAL balance query, the same reading the SDK makes before its own approval step, so a bounded allowance passes as long as it covers the order. With too little allowance, the sample stops before the SDK can send an approval by itself.

TypeScript

// Place a post-only limit order on a CTF ("v1") or Protocol V2 ("v2") market with the unified
// Polymarket SDK. npm install @polymarket/client@^0.13 viem  (tested with 0.13.0; V2 needs 0.12.0+)
import { AssetType, OrderSide, ProtocolVersion, createSecureClient } from '@polymarket/client';
import { fetchBalanceAllowance } from '@polymarket/client/actions';
import { privateKey } from '@polymarket/client/viem';

const EXCHANGE_V3 = '0xe3333700cA9d93003F00f0F71f8515005F6c00Aa'.toLowerCase();
const [PRICE, SIZE] = ['0.50', '10'];
const slug = process.argv[2];

const client = await createSecureClient({
  signer: privateKey(process.env.POLYMARKET_PRIVATE_KEY),
  wallet: process.env.POLYMARKET_WALLET, // the wallet your account already uses
});

const market = await client.fetchMarket({ slug });
const yes = market.outcomes.yes;
let assetId: string | null;
let sellType: AssetType;
if (market.version === ProtocolVersion.V2) {
  [assetId, sellType] = [yes.positionId, AssetType.CONDITIONAL_V2];
} else if (market.version === ProtocolVersion.V1) {
  [assetId, sellType] = [yes.tokenId, AssetType.CONDITIONAL];
} else {
  throw new Error(`${slug}: unsupported version ${market.version}, skipping`); // never guess
}
if (!assetId) throw new Error(`${slug}: no ${market.version} ID for the Yes outcome`);

const held = await fetchBalanceAllowance(client, { assetType: sellType, assetId });
console.log(`${market.version} ${slug}: Yes is ${assetId}, balance ${held.balance}`);

if (market.version === ProtocolVersion.V2) { // CTF approvals do not cover ExchangeV3
  const pusd = await fetchBalanceAllowance(client, { assetType: AssetType.COLLATERAL });
  const allowed = Object.entries(pusd.allowances)
    .find(([spender]) => spender.toLowerCase() === EXCHANGE_V3)?.[1] ?? 0n;
  if (allowed < BigInt(Math.round(Number(PRICE) * Number(SIZE) * 1_000_000))) {
    throw new Error('pUSD allowance for ExchangeV3 is too low: approve it first');
  }
}

const result = await client.placeLimitOrder({
  assetId, side: OrderSide.BUY, price: PRICE, size: SIZE, postOnly: true,
});
console.log(result);

In 0.13.0 the balance read is a standalone function from @polymarket/client/actions. On 19 October, Polymarket’s SDK guide showed it as a client method, client.fetchBalanceAllowance(), which does not type-check against that release.

We ran both samples end to end against the real SDK 0.13.0 code, with every HTTP response served locally. On a V2 market they read CONDITIONAL-V2 and the ExchangeV3 pUSD allowance, and the SDK’s order verified under domain version “3” at ExchangeV3 but not under “2”. On a V1 market that also carried V2-format positionIds, they picked the CTF ID and the order verified under version “2” at the CTF Exchange. With a zero allowance neither posted an order or attempted an approval, and with a bounded allowance that covered the order both went ahead.

Test on the canary markets

The canary markets are the only V2 markets until new markets start moving, so they are where to test. Polymarket’s documentation does not list a version filter, but on 19 October GET https://gamma-api.polymarket.com/markets?version=v2 and the same query on /events both returned them. The slugs start with polyv2.

Test in this order:

  1. Read path. Fetch a canary market, check that version is v2, run the IDs through your router and decode them. The decoded condition ID should equal Gamma’s conditionId exactly.
  2. Market data. Read the order book for the selected ID and confirm the response carries "version": "v2". Expect thin books: the one we read on 19 October was empty.
  3. Approvals and balances. Grant the ExchangeV3 approvals, refresh the CLOB cache and read the allowance back.
  4. One small order. Place a small post-only order away from the market, confirm it rests, then cancel it. That proves the signature without taking a fill.
  5. A CTF market as well. Polymarket’s guide says to verify buy and sell fills and balances on both a V2 market and a CTF market.

Polymarket’s geographic restrictions apply to canary markets as to any other market, and every canary market we saw on 19 October carried Gamma’s restricted flag. Do not use a server in another country to get round a restriction that applies to you.

Polymarket Protocol V2 switch-over checklist

  1. Find every assumption. Search your code for clobTokenIds, positionIds, negRisk, umaResolutionStatus, CONDITIONAL, the exchange addresses and the domain version “2”.
  2. Upgrade the client to the unified SDK 0.12.0 or later, or add a V2 path to your signer.
  3. Route by version. Reject missing or unknown versions and non-decimal IDs, and log what you skip.
  4. Sign V2 orders with domain version “3” for ExchangeV3, and stop choosing the exchange from the neg-risk flag on V2 markets.
  5. Grant the V2 approvals, refresh the CLOB cache and check the ExchangeV3 allowance.
  6. Read V2 balances with CONDITIONAL-V2.
  7. Key caches by asset ID and store the condition ID separately for the user stream and market-wide cancels.
  8. Follow resolution with resolutionStatus or /v2/resolutions, and use payouts only from resolved rows.
  9. Update position operations if you split, merge or redeem: Router, 31-byte condition IDs, six decimals and outcome indexes 0 and 1.
  10. Handle neg-risk events: the synthetic Other outcome and derived results.
  11. Move Data API calls to v2 before 24 October.
  12. Test on canary markets and a CTF market, then check Gamma for V2 markets again from 2 November.
  13. Keep the CTF path. Existing markets and holdings stay on CTF.

Where to run a Polymarket Protocol V2 bot

Protocol V2 does not change Polymarket’s endpoints, so it does not change where a bot should run. A bot that holds CLOB market and user connections today keeps the same connections. The extra work is in the code and in monitoring: alert on rejected orders, on markets skipped for an unknown version and on a balance query that returns zero for a position you hold.

If you are choosing where to host a bot, our guide to the best server location for a Polymarket bot covers the trade-offs, and our guide to running a Polymarket bot 24/7 covers restarts and monitoring. For the rest of the API, see our Polymarket API guide for trading bots. If orders rest without filling once you are live, our guide to Polymarket orders not filling works through the usual causes. Our PolyBolt guide covers the separate move of price feeds off RTDS.

Frequently asked questions

What is Polymarket Protocol V2?

Polymarket Protocol V2 is Polymarket’s new set of smart contracts for positions, trading and resolution. It replaces the Gnosis Conditional Tokens Framework with a single positions contract called PositionManager, a single exchange called ExchangeV3, a Router for splitting, merging and redeeming, and an oracle aggregator for resolution. Collateral stays pUSD. Polymarket announced it on 5 October 2026 and says it has run it on a small set of canary markets since then. The earliest canary market we found on Gamma was created on 13 October.

When do Polymarket markets move to Protocol V2?

Polymarket has said that newly created markets will tentatively start moving to Protocol V2 on 2 November 2026, after a canary period planned to end on 30 October. The date comes from Polymarket’s announcement rather than its documentation, so it could move. Existing markets do not move: they keep running on the CTF contracts until they resolve.

Is Polymarket Protocol V2 the same as the CLOB V2 upgrade?

No. The CLOB V2 upgrade of 28 April 2026 moved trading to new CTF exchanges, changed the order format, set the signing domain version to “2” and introduced pUSD. Protocol V2 is a later, separate change that replaces the positions layer, adds ExchangeV3 with domain version “3” and changes how markets are identified. A bot needs both: the April changes for every market, and the Protocol V2 changes for V2 markets.

Will my existing Polymarket positions be converted to V2?

No. Polymarket’s documentation says existing CTF holdings are not converted, and CTF support stays in place for V1 markets. Your positions keep trading and redeeming on the CTF contracts. The V2 contracts include a migration function that can be enabled market by market, but we found no announcement of a plan to use it as of 19 October 2026.

Why are my orders on V2 markets rejected?

The usual causes are an order signed with domain version “2” or for a CTF exchange, an asset ID taken from the wrong Gamma field, or missing approvals. V2 orders must be signed for ExchangeV3 with domain version “3”, and the wallet needs a pUSD allowance and an operator approval for ExchangeV3, because CTF approvals do not carry over. Older client libraries sign the old way unless told otherwise, and balance checks for V2 positions need the CONDITIONAL-V2 type. With the unified SDK, a missing allowance can instead lead the SDK to send an approval transaction and retry the order.

Do I need new API keys, a new wallet or new collateral for Protocol V2?

No. Polymarket says integrations that already use pUSD and the April order format keep the same collateral, wallets, CLOB credentials and endpoints. What you need are the new approvals for ExchangeV3 and, if you split, merge or redeem, for the Router. Those are on-chain transactions from your existing wallet, not new credentials.

Which Polymarket SDK supports Protocol V2?

The unified SDKs, polymarket-client for Python and @polymarket/client for TypeScript, from version 0.12.0. They detect a V2 position ID and pick the exchange and signing domain themselves, as long as you pass them the ID chosen by the market’s version. You still pass the CONDITIONAL-V2 type when you read a V2 balance. The older py-clob-client-v2 and @polymarket/clob-client-v2 were deprecated on 15 October 2026 and sign V2 orders only when explicitly told to.

How can I find Polymarket V2 canary markets?

Add a version filter to Gamma’s market or event listing, for example the markets endpoint with version set to v2. On 19 October 2026 that returned five-minute and 15-minute Zcash Up or Down markets and Central Park weather markets, all with slugs starting polyv2. The filter is not in Polymarket’s documentation, so check that it still behaves the same way, and confirm the version field on each market rather than relying on the slug.

How we checked this

On 19 October 2026 we read Polymarket’s Protocol V2 migration overview, API guide, contract guide and SDK guide, the Data API migration guide, the contracts page and the changelog. We read the documentation and contract source in Polymarket’s V2 contracts repository. On the client side, that meant the source of polymarket-client 0.13.0, the type definitions of @polymarket/client 0.13.0, and the code of py-clob-client-v2 1.2.1, @polymarket/clob-client-v2 1.2.0 and the Rust polymarket_client_sdk_v2 0.8.0. Release dates and deprecation notices came from PyPI, npm and crates.io.

Live data was checked on 19 October: Gamma’s canary markets and events, a long-dated CTF market, the CLOB’s order book and neg-risk responses for V2 and CTF assets, and the CLOB’s version endpoint. For developer reports we read polyoxide’s Protocol V2 issue, MetaMask’s V2 order signing pull request and NautilusTrader’s adapter code. The dates of the canary period and the November move come from coverage of Polymarket’s announcement, because the announcement itself is an article on X that we could not load.

Our routing function passed 143 checks, most of them cross-checked against the routing functions in Polymarket’s Python SDK, and decoded two live canary markets correctly. We also confirmed the SDK’s approval step after an allowance rejection with a simulated rejection. Both SDK samples ran against the real SDK 0.13.0 code with local HTTP responses, and we verified each signed order by recovering its signer. We did not place orders on Polymarket’s production servers for this article.

Share this article:
Facebook
X
LinkedIn

TradoxVPS Engineering Team

Infrastructure specialists focused on low-latency trading VPS and CME-proximal hosting.
Published:
Discover how TradoxVPS can power your trading with speed, stability, and 24/7 uptime to stay ahead in the markets.