polynode 0.12.2

Rust SDK for the PolyNode API — real-time Polymarket data
Documentation
# 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
```

## Required Approvals (V2 CLOB balance-allowance check)

The V2 CLOB checks **three** pUSD allowances for orders:

| Address | Name | Why |
|---|---|---|
| `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 wallets)

For **every** spender above:
- `pUSD.approve(spender, MAX_UINT256)`
- `CTF.setApprovalForAll(spender, true)` — only needed for SELL orders, but safe to set

Batch all 6 TXs into one Safe multisend via `RelayClient.execute(...)`.

### 2. Refresh CLOB balance/allowance cache

```
GET https://clob-v2.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:

| Order class | maker amount | taker amount |
|---|---|---|
| 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
  { name: 'timestamp',     type: 'uint256' },
  { name: 'metadata',      type: 'bytes32' },
  { name: 'builder',       type: 'bytes32' },
]
```

`taker` and `expiration` are **NOT** in the signed struct — they are post-signing payload fields.

### 6. POST /order

Endpoint: `POST https://clob-v2.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",
    "taker": "0x0000000000000000000000000000000000000000",
    "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`
- `taker` = zero address for public orders (private orders set this to the intended counterparty)
- `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

| Error | Cause |
|---|---|
| `"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         : 0xADa100874d00e3331D00F2007a9c336a65009718
NegRiskCtfCollateralAdapter  : 0xAdA200001000ef00D07553cEE7006808F895c6F1

CLOB_V2_HOST                 : https://clob-v2.polymarket.com
RELAYER                      : https://relayer-v2.polymarket.com
```