> ## Documentation Index
> Fetch the complete documentation index at: https://docs.polynode.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Polymarket V2 Migration

> polynode is ready for the Polymarket V2 exchange upgrade. Here's what you need to know.

<Info>
  **V2 cutover: April 28, 2026 at \~11am UTC.** Polymarket announced the cutover date on April 18, 2026 (pushed from April 22 for extra builder testing time). The V2 test environment is open to all now at `clob-v2.polymarket.com`. V2 mainnet contracts have been processing real trades since April 3, 2026. polynode is V2-ready today — set `exchange_version: V2` in your SDK config to start placing V2 orders now, or keep V1 until cutover. After April 28, V1 orders will be rejected with `order_version_mismatch`.
</Info>

Polymarket is deploying a V2 exchange system on Polygon mainnet. polynode has identified, decoded, and tested against the V2 contracts deployed today. Our settlement stream and trading SDK are built to handle V2 alongside V1.

**No action is required from existing settlement stream users.** When V2 settlements begin on-chain, they will flow through the same WebSocket feed automatically.

## Before Cutover: Your Checklist

If you place orders on Polymarket, here's exactly what to do before April 28:

1. **Upgrade your SDK to the V2-ready version.** Older versions won't generate the correct V2 payload.
   * Rust: `polynode >= 0.12.0`. Install with `cargo add polynode@0.12.0`.
   * TypeScript: `polynode-sdk >= 0.9.0`. Install with `npm install polynode-sdk@^0.9.0`.
   * Python: `polynode >= 0.9.0`. Install with `pip install "polynode>=0.9.0"`.
2. **Flip the exchange version flag** in your `TraderConfig`. See [The Switch](#the-switch) below. This is the only code change required.
3. **Mint a V2 builder code** at [polymarket.com/settings?tab=builder](https://polymarket.com/settings?tab=builder) if you want attribution on your orders. Optional, orders still work without one.
4. **Cancel V1 resting orders** you don't want cleared at cutover. Open V1 orders get wiped when Polymarket flips the switch.
5. **Fund PolyUSD.** V2 orders settle in PolyUSD (pUSD), not USDC.e. If your Safe holds USDC.e, call `trader.wrap_to_polyusd(amount)` (Python) / `trader.wrapToPolyUsd(amount)` (TS) / `trader.wrap_to_polyusd(amount)` (Rust) to convert it via the Collateral Onramp. **`amount` is in raw 6-decimal units** (`1_000_000` = \$1; TS takes a `bigint` — use `1_000_000n`). Existing V1 pUSD balance carries over.
6. **Test against V2 now.** Point your SDK at V2 (step 2) and place a small order against `clob-v2.polymarket.com`. `ensure_ready` / `ensureReady` sets the V2 approvals and refreshes the CLOB's cached balance/allowance view for you.
7. **Verify V2 confirmation.** Your settlement stream tags V2 trades with `"exchange_version": "v2"` on every event.

## The Switch

V2 activation is **one line in your SDK config.** Flip it before April 28 and orders route through the V2 exchange automatically. Leave it as V1 and you keep working against V1 until cutover. After that, V1 orders will be rejected.

<CodeGroup>
  ```rust Rust theme={null}
  let config = TraderConfig {
      exchange_version: ExchangeVersion::V2,  // ← this line
      ..Default::default()
  };
  ```

  ```typescript TypeScript theme={null}
  const trader = new PolyNodeTrader({
    exchangeVersion: "v2",  // ← this line
  });
  ```

  ```python Python theme={null}
  config = TraderConfig(
      exchange_version=ExchangeVersion.V2,  # ← this line
  )
  ```
</CodeGroup>

That's it. No other code changes required. The SDK handles:

* New contract addresses (V2 CTF Exchange + NegRisk A/B)
* New EIP-712 domain (version `"2"`, new `verifyingContract`)
* New Order struct (adds `builder`, `metadata`, `timestamp`; drops `nonce`, `feeRateBps` from the signed hash)
* CLOB host swap to `clob-v2.polymarket.com`
* PolyUSD collateral wrapping via the Onramp contract

## What We've Verified

Tested against live V2 contracts on Polygon mainnet:

* V2 settlement decoding verified against real on-chain transactions
* V2 trade events (OrderFilled, OrdersMatched) decoded from block receipts
* PolyUSD wrapping and unwrapping tested and detected in real time
* V2 order placement tested live on the V2 CLOB
* Full order lifecycle verified: place, check status, cancel, re-place
* All existing market data, token IDs, and enrichment confirmed identical between V1 and V2
* **Order hash verification:** our EIP-712 order hash computation produces byte-for-byte identical results to the live V2 exchange contract's `hashOrder()` function on Polygon mainnet — cryptographic proof that our implementation is correct

## V2 Contract Addresses

| Contract                      | Address                                      |
| ----------------------------- | -------------------------------------------- |
| PolyUSD                       | `0xc011a7e12a19f7b1f670d46f03b03f3342e82dfb` |
| Collateral Onramp             | `0x93070a847efef7f70739046a929d47a521f5b8ee` |
| Collateral Offramp            | `0x2957922eb93258b93368531d39facca3b4dc5854` |
| CTFCollateralAdapter          | `0xAdA100Db00Ca00073811820692005400218FcE1f` |
| NegRiskCTFCollateralAdapter   | `0xadA2005600Dec949baf300f4C6120000bDB6eAab` |
| V2 CTF Exchange               | `0xe111180000d2663c0091e4f400237545b87b996b` |
| V2 NegRisk Exchange A         | `0xe2222d279d744050d28e00520010520000310f59` |
| V2 NegRisk Exchange B         | `0xe2222d002000ba0053cef3375333610f64600036` |
| ConditionalTokens (unchanged) | `0x4D97DCd97eC945f40cF65F87097ACe5EA0476045` |

<Warning>
  All V2 contracts listed above are deployed and live on Polygon mainnet. On April 30 2026, Polymarket updated the collateral adapter contracts. The new adapters are effective May 1 2026 at 15:00 UTC — the relayer stops accepting transactions through the old adapters after that cutoff.
</Warning>

## How V2 Fits Together

The key architectural insight: V2 only changes the **exchange layer** (how orders are matched and settled). The **token layer** (ConditionalTokens) is completely unchanged and cannot be replaced.

### Why Token IDs Don't Change

All prediction market positions live inside the [ConditionalTokens](https://polygonscan.com/address/0x4D97DCd97eC945f40cF65F87097ACe5EA0476045) contract. This is a Gnosis protocol contract that has been on Polygon since Polymarket launched. Every token ID is derived from a condition ID and outcome index inside this contract.

V2 does not replace ConditionalTokens. It can't. Doing so would invalidate every open position on the platform. Instead, V2 introduces an **adapter layer** between the new exchange contracts and the existing ConditionalTokens contract:

```
V1 (current):
  USDC.e → V1 Exchange → ConditionalTokens (mints/burns tokens directly)

V2 (new):
  PolyUSD → V2 Exchange → CollateralAdapter → ConditionalTokens
                                 |
                           Unwraps PolyUSD → USDC.e
                           Deposits USDC.e into ConditionalTokens
                           Same tokens minted as V1
```

The CollateralAdapter translates between PolyUSD (V2's collateral) and USDC.e (what ConditionalTokens expects). ConditionalTokens never sees PolyUSD. From its perspective, nothing changed.

This means:

* Same token IDs across V1 and V2
* Same condition IDs
* Same market metadata (Gamma API, enrichment)
* Positions opened on V1 are fully compatible with V2 and vice versa

### New V2 Contracts

Five new contracts support the V2 architecture. These are all live on Polygon mainnet:

| Contract                        | Role                                                          | Status   |
| ------------------------------- | ------------------------------------------------------------- | -------- |
| **PolyUSD**                     | Wrapped USDC.e (1:1, 6 decimals)                              | Deployed |
| **Collateral Onramp**           | Wraps USDC.e → PolyUSD                                        | Deployed |
| **Collateral Offramp**          | Unwraps PolyUSD → USDC.e                                      | Deployed |
| **CTFCollateralAdapter**        | Bridges PolyUSD ↔ USDC.e for standard market settlements      | Deployed |
| **NegRiskCTFCollateralAdapter** | Bridges PolyUSD ↔ USDC.e for multi-outcome market settlements | Deployed |
| **V2 CTF Exchange**             | New order matching for standard markets                       | Deployed |
| **V2 NegRisk Exchange A**       | New order matching for multi-outcome markets                  | Deployed |
| **V2 NegRisk Exchange B**       | Additional multi-outcome capacity                             | Deployed |

All V2 contracts are deployed on Polygon mainnet. V2 trading volume is currently minimal as Polymarket has not yet migrated users to V2.

### What's New in V2

**PolyUSD Collateral** — V2 uses PolyUSD instead of USDC.e as the exchange collateral. PolyUSD is a 1:1 wrapper around USDC.e with 6 decimals. See the [PolyUSD Guide](/guides/polyusd) for details on wrapping and unwrapping.

**Updated Order Struct** — The EIP-712 signed hash drops two fields (`nonce`, `feeRateBps`) and adds three (`timestamp`, `metadata`, `builder`). The HTTP POST body still carries `expiration` (defaulted to `"0"` for no expiration). The polynode SDK handles all of this automatically.

The dropped `feeRateBps` field was Polymarket's protocol fee slot inside the V1 order. V2 doesn't have a per-order protocol fee slot — Polymarket's fee model moves to a CLOB-level config plus the `builder` bytes32 rev share. **The polynode Fee Escrow is unaffected** by this change: it was never connected to the `feeRateBps` field. Our fee is pulled via a separate signed authorization (`FeeAuth` under our own EIP-712 domain) and only moves collateral. See the [Fee Escrow Guide](/guides/fee-escrow) for the V2-native FeeEscrow contract address and the three-fee breakdown.

**EIP-712 Domain** — The signing domain version changed from `"1"` to `"2"`. The SDK handles this based on your exchange version config.

**No More Settlement Routers** — V1 used FeeModule router contracts between operators and the exchange. V2 eliminates routers. Operators call the exchange contracts directly.

**exchange\_version Field** — V2 trades include `"exchange_version": "v2"` on each trade object in settlement and trade events. V1 trades omit this field. Use it to distinguish which exchange system processed a settlement.

### What Didn't Change

* **ConditionalTokens contract** — same address, same position system, same token IDs
* **Market data** — same condition IDs, same Gamma metadata, same enrichment pipeline
* **Authentication** — same API key derivation, same HMAC headers
* **WebSocket subscriptions** — same event types, same format
* **Position splits/merges/conversions** — same ConditionalTokens events
* **Oracle system** — unchanged UMA adapters and resolution flow
* **Orderbook streaming** — same WebSocket protocol and message format

## Impact on polynode Users

### Settlement Stream

No changes required. V2 settlements produce the same event types you already receive:

* `settlement` — early detection (3-5 seconds pre-confirmation)
* `status_update` — block confirmation with latency measurement
* `trade` — confirmed trade from OrderFilled event
* `deposit` — now includes PolyUSD wrapping/unwrapping alongside USDC.e

polynode detects V2 transactions automatically alongside V1. Both versions are supported simultaneously.

### Trading SDK

The polynode SDK supports V2 with a single config change:

<CodeGroup>
  ```rust Rust theme={null}
  use polynode::trading::{PolyNodeTrader, TraderConfig, OrderParams, OrderSide, ExchangeVersion};

  let mut config = TraderConfig {
      polynode_key: "pn_live_...".into(),
      exchange_version: ExchangeVersion::V2,  // ← only change needed
      ..Default::default()
  };

  let mut trader = PolyNodeTrader::new(config)?;
  let signer = PrivateKeySigner::from_hex("0x...")?;
  let status = trader.ensure_ready(Box::new(signer), None).await?;

  // Order placement is identical to V1
  let result = trader.order(OrderParams {
      token_id: "10293...".into(),
      side: OrderSide::Buy,
      price: 0.05,
      size: 10.0,
      ..Default::default()
  }).await?;
  ```

  ```typescript TypeScript (SDK) theme={null}
  // npm install polynode-sdk
  import { PolyNodeTrader } from "polynode-sdk";

  const trader = new PolyNodeTrader({
    polynodeKey: "pn_live_...",
    exchangeVersion: "v2",  // ← only change needed
  });

  await trader.ensureReady("0xprivatekey...");

  // Order placement is identical to V1
  const result = await trader.order({
    tokenId: "10293...",
    side: "BUY",
    price: 0.05,
    size: 10,
  });
  ```

  ```python Python theme={null}
  # pip install polynode[trading]
  from polynode.trading import PolyNodeTrader, TraderConfig, ExchangeVersion, OrderParams

  config = TraderConfig(
      polynode_key="pn_live_...",
      exchange_version=ExchangeVersion.V2,  # ← only change needed
  )

  trader = PolyNodeTrader(config)
  await trader.ensure_ready("0xprivatekey...")

  # Order placement is identical to V1 — pass OrderParams, add builder for V2 attribution
  result = await trader.order(OrderParams(
      token_id="10293...",
      side="BUY",
      price=0.05,
      size=10,
      builder="0x5472fdd7...",  # optional; your V2 builder code
  ))
  ```
</CodeGroup>

<Info>
  The SDK handles V2 order structs, EIP-712 signing, CLOB endpoint routing, and PolyUSD collateral automatically. You don't need to change how you construct orders or manage positions.
</Info>

## Builder Attribution in V2

V2 moves builder attribution from HTTP headers into the signed order struct itself as a `bytes32` field. To attribute trades to your own builder profile, mint a builder code in Polymarket's settings and pass it to the SDK on each order.

**Step 1: Mint your V2 builder code.** Visit `polymarket.com/settings?tab=builder` with your connected wallet, create a builder profile, and copy the generated bytes32 code.

**Step 2: Pass it on each order.** All three polynode SDKs accept a `builder` field on the order params. If you don't pass one, orders sign with `0x0000...0000` (no attribution).

<CodeGroup>
  ```rust Rust theme={null}
  let result = trader.order(OrderParams {
      token_id: "10293...".into(),
      side: OrderSide::Buy,
      price: 0.05,
      size: 10.0,
      builder: Some("0x0000000000000000000000000000000000000000000000000000000000000001".into()),
      ..Default::default()
  }).await?;
  ```

  ```typescript TypeScript theme={null}
  const result = await trader.order({
    tokenId: "10293...",
    side: "BUY",
    price: 0.05,
    size: 10,
    builder: "0x0000000000000000000000000000000000000000000000000000000000000001",
  });
  ```

  ```python Python theme={null}
  result = await trader.order(OrderParams(
      token_id="10293...",
      side="BUY",
      price=0.05,
      size=10,
      builder="0x0000000000000000000000000000000000000000000000000000000000000001",
  ))
  ```
</CodeGroup>

Your existing V1 `builder_credentials` (HMAC key/secret/passphrase) are unrelated to the V2 `builder` bytes32. The HMAC credentials are still used to query builder trade history at `/builder/trades`. The V2 bytes32 is what attributes newly-placed orders on-chain.

## Pre-Migration V1 Orders

Open V1 orders resting at cutover get cleared from V1 orderbooks. Polymarket exposes `GET /data/pre-migration-orders` on the V2 CLOB for fetching your pre-cutover V1 order history. Cancel V1 resting orders before April 28 if you need the capital free for V2 trading.

## Common V2 Errors

If something goes wrong during migration, these are the ones you'll hit.

**`order_version_mismatch`**

Your SDK sent a V1-formatted order but the V2 CLOB expected V2. Flip `exchange_version: V2` in your config.

**`not enough balance / allowance`**

Most common causes, in order:

1. **Your wallet has USDC.e but not PolyUSD.** V2 uses PolyUSD as the collateral token. Call `trader.wrap_to_polyusd(amount_raw)` (Python/Rust) or `trader.wrapToPolyUsd(amount_raw)` (TS) to convert. Amounts are raw 6-decimal integers.
2. **Approvals not yet visible to the CLOB.** The V2 CLOB caches per-API-key balance + allowance state. `ensureReady` / `ensure_ready` refreshes this cache after setting approvals; if you changed approvals outside that flow, call `trader.refreshBalanceAllowance()` / `trader.refresh_balance_allowance()` manually.
3. **Order notional + fee exceeds balance.** V2 markets charge a fee (\~2–5% of notional depending on `base_fee`). For a BUY, `balance >= makerAmount + platformFee` must hold or the CLOB rejects.

**`error parsing fee rate bps () to int64`**

Your SDK is too old and isn't sending the V2 payload correctly. Upgrade to the V2-ready version listed in [the checklist above](#before-cutover-your-checklist).

**Rust-specific: `not enough balance / allowance` even though you have PolyUSD**

If you call `link_wallet(signer, None)` with an EOA wallet, the SDK silently uses the config's default signature type (POLY\_GNOSIS\_SAFE) and derives a Safe address as the maker. That Safe has zero balance, so the CLOB rejects the order. For EOA wallets, pass explicit opts:

```rust theme={null}
use polynode::trading::{LinkOpts, SignatureType};

trader.link_wallet(Box::new(signer), Some(LinkOpts {
    signature_type: Some(SignatureType::Eoa),
})).await?;
```

## Timeline

* **Now:** V2 test environment live at `clob-v2.polymarket.com`. V2 contracts live on Polygon mainnet processing real trades. polynode settlement stream tags V2 trades with `exchange_version: "v2"`.
* **Before April 28:** Flip your SDK config to `exchange_version: V2` (or keep V1 until cutover). Mint a builder code if you want attribution. Cancel V1 resting orders you don't want cleared.
* **April 28, \~11am UTC:** Polymarket cutover. V2 becomes the primary exchange. V1 orderbooks cleared.
* **After cutover:** Any integration still on V1 will fail with `order_version_mismatch`. Flip your config.

polynode supports both V1 and V2 simultaneously throughout the transition — no downtime, no disruption.
