# V2 Order Flow — Reference
End-to-end recipe for placing a Polymarket V2 CLOB order with builder attribution.
Verified 2026-04-21 with matched order `0xaecd1060f7c978d0ab947eacc12a17106b7a5d087aefe04c275f3d83d2aa6281` (tx `0xb86168562057efd161a1f9ac77ecf3728653cf9668a21567c4923a298cb24d77`).
## TL;DR
```
1. Approve pUSD to: CTF_EXCHANGE_V2, NEG_RISK_EXCHANGE_V2_A, NEG_RISK_ADAPTER (V1!)
2. setApprovalForAll CTF → same three addresses
3. GET /balance-allowance/update — force CLOB cache refresh after approvals
4. Sign EIP-712 Order (domain name "Polymarket CTF Exchange", version "2")
5. POST /order with L2 HMAC headers
```
Rust SDK defaults as of `0.13.11`:
```rust
let mut trader = PolyNodeTrader::new(TraderConfig {
polynode_key: "pn_live_...".into(),
exchange_version: ExchangeVersion::V2,
..Default::default()
})?;
```
- `ensure_ready()` creates or loads the user's CLOB API credentials. These credentials remain the order owner for `/order`, cancel, open-order, and balance-allowance calls.
- `TraderConfig.builder_code` defaults to PolyNode's public V2 builder code. Use `TraderConfig.builder_code` or `OrderParams.builder` to pass your own bytes32 builder attribution.
- `RelayerMode::Auto` defaults to PolyNode managed relay when `polynode_key` and `cosigner_url` are set. The SDK provisions a per-user relayer key through the cosigner before smart-wallet deploy/approve/wrap calls and falls back to builder relayer auth when configured.
- `RelayerMode::BuilderCredentials` sends smart-wallet relayer calls directly to Polymarket with `TraderConfig.builder_credentials`.
- `RelayerMode::DirectRpc` signs Safe `execTransaction` calls and broadcasts them through `TraderConfig.rpc_url`. It requires `TradingSigner::sign_hash` support, such as `PrivateKeySigner`. It applies to Safe/proxy calls only; deposit-wallet factory envelopes still require managed or builder relayer auth.
## Wallet Identity Modes
Polymarket V2 supports both existing Safe users and new deposit-wallet users. The maker/funder identity must match the account type Polymarket has for that user.
| Existing Safe user | `2` / `POLY_GNOSIS_SAFE` | Safe address | EOA address |
| New deposit-wallet user | `3` / `POLY_1271` | Deposit wallet address | Deposit wallet address |
Do not switch a user between these modes just because both deterministic contracts exist. If a user is tagged by the CLOB as a deposit-wallet user, Safe-maker orders can fail with:
```
maker address not allowed, please use the deposit wallet flow
```
If a user is an existing Safe user, continue routing them through `POLY_GNOSIS_SAFE`. The Rust SDK accepts an explicit `signature_type` and `funder_address` for this reason.
Safe user:
```rust
trader.ensure_ready(
Box::new(signer),
Some(EnsureReadyOpts {
signature_type: Some(SignatureType::PolyGnosisSafe),
funder_address: Some(safe_address),
}),
).await?;
```
Deposit-wallet user:
```rust
trader.ensure_ready(
Box::new(signer),
Some(EnsureReadyOpts {
signature_type: Some(SignatureType::Poly1271),
funder_address: Some(deposit_wallet_address),
}),
).await?;
```
Both modes use pUSD collateral on V2. Existing USDC.e must be wrapped into pUSD before BUY orders can spend it.
## Required Approvals (V2 CLOB balance-allowance check)
The V2 CLOB checks **three** pUSD allowances for orders:
| `0xE111180000d2663C0091e4f400237545B87B996B` | `CTF_EXCHANGE_V2` | Standard market orders |
| `0xe2222d279d744050d28e00520010520000310F59` | `NEG_RISK_EXCHANGE_V2_A` | Neg-risk market orders |
| `0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296` | `NegRiskAdapter` (V1) | Settlement delegation |
**The V1 `NegRiskAdapter` MUST be approved for pUSD even though it's a V1 contract.** The V2 neg-risk exchange delegates settlement to it. Without this allowance set, the CLOB rejects every order with:
```
{"error":"not enough balance / allowance"}
```
## Order Flow Step-by-Step
### 1. On-chain approvals (gasless via relayer for Safe and deposit wallets)
For **every** spender above:
- `pUSD.approve(spender, MAX_UINT256)`
- `CTF.setApprovalForAll(spender, true)` — only needed for SELL orders, but safe to set
Batch these into one Safe multisend for Safe users, or one deposit-wallet `WALLET` batch for deposit-wallet users.
### 2. Refresh CLOB balance/allowance cache
```
GET https://clob.polymarket.com/balance-allowance/update?asset_type=COLLATERAL&signature_type=2
Headers: POLY_ADDRESS, POLY_SIGNATURE, POLY_TIMESTAMP, POLY_API_KEY, POLY_PASSPHRASE
HMAC path: /balance-allowance/update (NO query string in HMAC)
```
The CLOB caches balance/allowance per-API-key. Approvals on-chain are invisible until this endpoint is pinged. **Call this after every approval change.** Wait ~1s before placing orders.
Verify:
```
GET /balance-allowance?asset_type=COLLATERAL&signature_type=2
```
Response:
```json
{
"balance": "2179236",
"allowances": {
"0xE111180000d2663C0091e4f400237545B87B996B": "115792089...",
"0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296": "115792089...",
"0xe2222d279d744050d28e00520010520000310F59": "115792089..."
}
}
```
All three allowances must be non-zero.
### 3. Determine `neg_risk` and pick exchange address
```
GET /neg-risk?token_id={tokenId}
```
Response is `{"neg_risk": true|false}`. Do NOT use `!!response` — the **object itself** is always truthy, you need to read the `.neg_risk` field.
- `neg_risk: false` → sign against `CTF_EXCHANGE_V2` (`0xe111180000d2663c0091e4f400237545b87b996b`)
- `neg_risk: true` → sign against `NEG_RISK_EXCHANGE_V2_A` (`0xe2222d279d744050d28e00520010520000310f59`)
### 4. Compute raw amounts (string-based, no floats)
V2 CLOB requires clean decimal places:
| LIMIT BUY (tick 0.01) | ≤ 4 decimals | ≤ 2 decimals |
| LIMIT SELL (tick 0.01) | ≤ 2 decimals | ≤ 4 decimals |
| MARKET BUY (FAK/FOK) | ≤ 2 decimals | ≤ 4 decimals |
Use string-based conversion to avoid float precision bugs:
```ts
// BUY 4 tokens at $0.43 (cost $1.72)
const usdcAmount = (0.43 * 4).toFixed(4); // "1.7200"
const tokenAmount = (4).toFixed(2); // "4.00"
// → parseUnits(usdcAmount, 6) = 1720000n (raw makerAmount)
// → parseUnits(tokenAmount, 6) = 4000000n (raw takerAmount)
```
`Math.trunc(0.18 * 10 * 1e6)` returns `1799999` (not `1800000`) due to IEEE 754 — DO NOT use float math.
### 5. EIP-712 sign
Domain:
```json
{
"name": "Polymarket CTF Exchange",
"version": "2",
"chainId": 137,
"verifyingContract": "<exchange address from step 3>"
}
```
Order struct (uint256 for amounts, uint8 for side/signatureType, bytes32 for metadata/builder):
```ts
Order: [
{ name: 'salt', type: 'uint256' },
{ name: 'maker', type: 'address' },
{ name: 'signer', type: 'address' },
{ name: 'tokenId', type: 'uint256' },
{ name: 'makerAmount', type: 'uint256' },
{ name: 'takerAmount', type: 'uint256' },
{ name: 'side', type: 'uint8' }, // 0 = BUY, 1 = SELL
{ name: 'signatureType', type: 'uint8' }, // 0 = EOA, 1 = PROXY, 2 = SAFE, 3 = DEPOSIT
{ name: 'timestamp', type: 'uint256' },
{ name: 'metadata', type: 'bytes32' },
{ name: 'builder', type: 'bytes32' },
]
```
`taker` is **NOT** in the signed struct or current V2 order body. `expiration` is **NOT** in the signed struct, but remains in the POST body for GTD expiry handling.
### 6. POST /order
Endpoint: `POST https://clob.polymarket.com/order`
Exact payload shape (matches `@polymarket/clob-client-v2`):
```json
{
"order": {
"salt": 1776743397975,
"maker": "0xbdc1453718AEc6836F03964c254A5F08bFB3c038",
"signer": "0x3d5BB69B72E731Ac7510b6ae451D523dAd55555e",
"tokenId": "102941331082550455210851718765952983636922827683407411724225360467552735563141",
"makerAmount": "1720000",
"takerAmount": "4000000",
"side": "BUY",
"signatureType": 2,
"timestamp": "1776743397975",
"metadata": "0x0000000000000000000000000000000000000000000000000000000000000000",
"builder": "0x5472fdd700a9b2b6613d103095048c92304e97215a2607f73a9d5aa3701f3f09",
"signature": "0x9f52a79de5cf438330f8c894a8a111e70de944d165b5e774f91a14906302c71a2b8cb5a1194aef4291827985885d54e201a7574b6c0aa1f002436ffda185344b1b",
"expiration": "0"
},
"owner": "<YOUR_CLOB_API_KEY_UUID>",
"orderType": "FAK",
"postOnly": false,
"deferExec": false
}
```
Field notes:
- `owner` = the API key UUID (not the wallet address, not the apiKey UUID of the BUILDER)
- `orderType` = `GTC` | `GTD` | `FAK` | `FOK`
- `expiration` = unix seconds, `"0"` = no expiration
- `postOnly: true` is incompatible with `FOK`/`FAK`
- `deferExec: false` is standard — true defers on-chain execution to a later bucket
### 7. L2 HMAC headers
```
POLY_ADDRESS = EOA that signed the API key (NOT the Safe/funder)
POLY_API_KEY = UUID from creds.apiKey
POLY_PASSPHRASE= creds.apiPassphrase
POLY_TIMESTAMP = unix seconds (string)
POLY_SIGNATURE = base64url(HMAC-SHA256(base64_decode(secret), timestamp + method + path + body))
where base64url = base64 with "+" → "-", "/" → "_"
```
Path in HMAC is `/order` only (no query string).
## Fees and Budget Math
Every market has a `base_fee` (in basis points of a 1.0 coefficient):
```
GET /fee-rate?token_id={tokenId} → {"base_fee": 1000} // 10%
```
Effective fee per trade (from `@polymarket/clob-client-v2`):
```
platformFeeRate = feeRate * (price * (1 - price)) ** feeExponent
platformFee = (amount / price) * platformFeeRate
totalCost = amount + platformFee + amount * builderTakerFeeRate
```
For a BUY of `size` tokens at `price`:
- `amount = price * size` (raw USDC in 6 decimals)
- `tokens_out = size`
- Fee on trade ≈ `(price * size) * feeRate * (price * (1-price))^exponent`
**You need `balance >= amount + platformFee`.** The CLOB rejects orders with "not enough balance / allowance" if balance can't cover cost + fee.
Example: our 5-tokens-at-$0.42 attempts failed because `5 * 0.42 + fee = $2.15-2.20` and balance was `$2.179`. Dropping to 4 tokens put notional at `$1.72` with $0.07 fee — room to spare.
Minimum notional for a MARKETABLE BUY is `$1` (not checked for non-crossing limits). Minimum size `min_order_size` in the book response is a per-market floor that the matching engine enforces at fill time, but the CLOB accepts smaller notional orders (we tested size 4 against a market with `min_order_size: 5`).
## Verifying Builder Attribution
After a match, the builder code flows to `/builder/trades`:
```
GET /builder/trades?builder_code=0x5472fdd700a9b2b6613d103095048c92304e97215a2607f73a9d5aa3701f3f09
```
Response entry confirms the on-chain settlement:
```json
{
"id": "019dae25-6845-7df6-8811-8e1bae76cf6a",
"tradeType": "TAKER",
"takerOrderHash": "0xaecd1060f7c978d0ab947eacc12a17106b7a5d087aefe04c275f3d83d2aa6281",
"market": "0xd76e7cbb6d6442801ca88e8433d1721c33a34a2f6d85d334e1d8331c76bf6c55",
"side": "BUY",
"size": "4.095237",
"sizeUsdc": "1.719999",
"price": "0.42",
"status": "TRADE_STATUS_MATCHED",
"transactionHash": "0xb86168562057efd161a1f9ac77ecf3728653cf9668a21567c4923a298cb24d77",
"feeUsdc": "0.07182",
"builderFee": "0",
"builderCode": "0x5472fdd700a9b2b6613d103095048c92304e97215a2607f73a9d5aa3701f3f09"
}
```
The `builderCode` field ties the on-chain trade to the attribution record — the exchange contract reads it from the signed order struct, so it's cryptographically bound and can't be stripped by any intermediate.
## Common Failure Modes
| `"not enough balance / allowance"` | (a) V1 NegRiskAdapter not approved for pUSD; (b) balance-allowance cache stale — run `/balance-allowance/update`; (c) balance < notional + fee |
| `"invalid signature"` | Wrong exchange address in EIP-712 domain. Almost always caused by `neg_risk` misdetection — check the `.neg_risk` field, don't `!!` the response object |
| `"invalid amounts, ... max accuracy of N decimals"` | Float precision in amount calc. Use `parseUnits(decimalString, 6)`, not `Math.trunc(x * 1e6)` |
| `"no orders found to match with FAK order"` | The matching engine ran and found no crossing asks. Either (a) book API showing stale asks, or (b) price didn't actually cross — check you're using the correct tick size |
| `"invalid amount for a marketable BUY order ($0.5), min size: $1"` | Marketable BUY notional is below $1 |
| `"Unauthorized/Invalid api key"` on GET | HMAC computed over the **base path only** — query string must NOT be in the signing message |
## Working Example
See `/home/polygon/trading-lab/test-v2-real-match.mjs` for a full end-to-end example that placed and matched the order above.
## Exchange Constants (Polygon mainnet, chain 137)
```
pUSD : 0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB
CTF (ERC-1155) : 0x4D97DCd97eC945f40cF65F87097ACe5EA0476045
CTF_EXCHANGE_V2 : 0xe111180000d2663c0091e4f400237545b87b996b
NEG_RISK_EXCHANGE_V2_A : 0xe2222d279d744050d28e00520010520000310f59
NegRiskAdapter (V1) : 0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296
CtfCollateralAdapter : 0xAdA100Db00Ca00073811820692005400218FcE1f
NegRiskCtfCollateralAdapter : 0xadA2005600Dec949baf300f4C6120000bDB6eAab
CLOB_V2_HOST : https://clob.polymarket.com
RELAYER : https://relayer-v2.polymarket.com
```