---
description: Use this skill for Polymarket prediction market operations -- placing buy/sell orders, checking positions and balances, funding the CLOB (USDC.e -> pUSD wrap), CLOB API auth (HMAC + EIP-712), and order signing on Polygon. Covers the 2026 CLOB V2 migration (pUSD collateral) and the common funding/geoblock/partial-fill pitfalls.
---

# Polymarket Prediction Market Trading

## Infrastructure Overview

- **Network**: Polygon (Chain ID: 137)
- **Wallet / private key**: check memory (do NOT hardcode keys in the skill)
- **Trading SDK**: `py_clob_client_v2` (the CLOB migrated to **V2** in 2026 — see below)
- **API credentials**: cached at `~/.polymarket_creds` (apiKey, secret, passphrase via L1 EIP-712 auth)
- **Scripts**: check memory for locations (typically `~/scripts/polymarket/`)

## ⚠️ READ FIRST: CLOB V2 migration (2026)

The CLOB migrated to V2. Two consequences that break old code/playbooks:

1. **Do NOT hand-build/sign orders** (the old `py_order_utils` LimitOrderBuilder + manual
   `owner = apiKey UUID` flow now fails with `order_version_mismatch`). Use the
   **`py_clob_client_v2` SDK**, which signs and posts for you:
   ```python
   from py_clob_client_v2 import ClobClient
   from py_clob_client_v2.clob_types import ApiCreds, OrderArgs, OrderType
   client = ClobClient(host="https://clob.polymarket.com", chain_id=137,
                       key=PRIVATE_KEY.removeprefix("0x"), creds=ApiCreds(...))
   client.create_and_post_order(OrderArgs(token_id, price, size, side), order_type=OrderType.GTC)
   ```
2. **Collateral is pUSD, NOT USDC.e.** Orders are funded by **pUSD**
   (`0xc011a7e12a19f7b1f670d46f03b03f3342e82dfb`, 6 decimals) held inside the exchange,
   not by the USDC.e sitting in your wallet. See "Funding" below.

## The two cash buckets (critical mental model)

| Bucket | What it is | Tradeable? | How to read |
|--------|-----------|------------|-------------|
| On-chain wallet **USDC.e** | ERC-20 in your wallet | ❌ not until wrapped | `balanceOf(wallet)` on USDC.e via Polygon RPC |
| CLOB **COLLATERAL** (pUSD) | deposited collateral inside the exchange | ✅ funds orders directly | authenticated `GET /balance-allowance?asset_type=COLLATERAL` |

A buy for $X **fails with `not enough balance / allowance: balance: <small>, order amount: <X>`**
whenever CLOB COLLATERAL < X — even if the wallet has plenty of USDC.e. Fix = wrap more USDC.e → pUSD.

## Funding: wrap USDC.e → pUSD (CollateralOnramp)

To make wallet USDC.e tradeable:
1. (one-time) approve USDC.e spending to the Onramp `0x93070a847efEf7F70739046A929D47a521F5B8ee`.
2. Call `wrap(address asset, address to, uint256 amount)` on the Onramp
   — selector `0x62355638`, amount in 6-decimal units (e.g. $1785 → `1785000000`).
3. Sync the CLOB's view:
   `client.update_balance_allowance(BalanceAllowanceParams(asset_type=AssetType.COLLATERAL))`.
4. Re-check `GET /balance-allowance?asset_type=COLLATERAL` — it should now reflect the new pUSD.

Reverse direction (pUSD → USDC.e) uses CollateralOfframp `0x2957922Eb93258b93368531d39fAcCA3B4dC5854`.

web3 note: Polygon is POA — inject the POA middleware or `get_block`/sends fail with
`ExtraDataLengthError`:
`from web3.middleware import ExtraDataToPOAMiddleware; w3.middleware_onion.inject(ExtraDataToPOAMiddleware, layer=0)`.

## API Endpoints

| Endpoint | Base URL | Notes |
|----------|----------|-------|
| CLOB (orders, book, balance) | `https://clob.polymarket.com` | **geoblocked** + needs `User-Agent` (see below) |
| Market data | `https://gamma-api.polymarket.com` | markets, events, public-search |
| Positions / value | `https://data-api.polymarket.com` | no auth |

### Geoblock + 403 pitfalls
- The CLOB API is **geoblocked** in some regions → route through a proxy
  (e.g. Clash Verge): `HTTPS_PROXY=http://127.0.0.1:7897`. Open the proxy app first.
- `GET /book?token_id=...` returns **HTTP 403 without a `User-Agent` header**. Always send
  `User-Agent: Mozilla/5.0`. A silent 403 swallowed by a try/except looks like "zero liquidity".

## Contract Addresses (Polygon)

| Contract | Address |
|----------|---------|
| pUSD (V2 collateral) | `0xc011a7e12a19f7b1f670d46f03b03f3342e82dfb` |
| CollateralOnramp (USDC.e→pUSD) | `0x93070a847efEf7F70739046A929D47a521F5B8ee` |
| CollateralOfframp (pUSD→USDC.e) | `0x2957922Eb93258b93368531d39fAcCA3B4dC5854` |
| NegRisk Adapter | `0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296` |
| USDC.e | `0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174` |

> **V2 approvals** (one-time, unlimited): approve **pUSD** to the V2 CTF Exchange, V2 NegRisk
> Exchange, and NegRisk Adapter; and approve **USDC.e** to the CollateralOnramp.
> Note the NegRisk Adapter `0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296` differs from the GitHub
> py-clob-client value — the CLOB checks this one.

## Authentication

- **L1 (EIP-712)**: one-time, signs a typed message with the wallet key → returns
  `{apiKey, secret, passphrase}` (cached at `~/.polymarket_creds`).
- **L2 (HMAC)**: every API call. **CRITICAL**: sign the **base path only**, no query params —
  sign `/balance-allowance`, NOT `/balance-allowance?asset_type=COLLATERAL`. Signing the full
  path is the classic cause of bogus "not enough balance/allowance" errors.
```python
msg = timestamp + method.upper() + path.split("?")[0] + body
sig = base64.urlsafe_b64encode(hmac.new(base64.urlsafe_b64decode(secret),
        msg.encode(), hashlib.sha256).digest()).decode()
# headers: POLY_ADDRESS, POLY_SIGNATURE, POLY_TIMESTAMP, POLY_API_KEY, POLY_PASSPHRASE,
#          Content-Type: application/json, User-Agent: Mozilla/5.0
```
(The V2 SDK handles auth/signing internally once constructed with `creds=ApiCreds(...)`; the manual
HMAC above is for direct REST calls like reading balance.)

## Placing orders (V2 SDK)

```bash
# typical wrapper (trade.py):
python3 trade.py markets [keyword]
python3 trade.py buy  <condition_id> yes|no <usdc_amount>
python3 trade.py sell <condition_id> yes|no <shares>
python3 trade.py positions
```
- Use **GTC** (not FOK) for size — FOK rejects if it can't fill instantly.
- Buy = consume the **ask** side of the book; size/slippage = walk asks cumulatively.

### ⚠️ Partial-fill caveat (verify after every order)
A "market" buy is a marketable **limit** order at the top-of-book price. If size at that price is
thin, it can **partially fill and silently leave the rest unfilled** (collateral barely moves,
position size barely changes). **Always re-read the position size after an order**; if it didn't
move as expected, re-place the remainder.

## Checking positions & balances

```python
# positions (no auth) — use the wallet from memory
GET https://data-api.polymarket.com/positions?user=<WALLET>
GET https://data-api.polymarket.com/value?user=<WALLET>     # total positions value
```
Polymarket settles API trades to the EOA; the web UI may show a proxy wallet (for these
proxy-wallet setups EOA == proxyWallet — confirm in memory). Read CLOB collateral via the
authenticated `/balance-allowance?asset_type=COLLATERAL`, and on-chain USDC.e/POL via Polygon RPC.

## Finding markets & READING THE RULES (do not skip)

```python
GET https://gamma-api.polymarket.com/markets?closed=false&active=true&order=liquidityNum&ascending=false&limit=500&offset=0
# key fields: conditionId, question, outcomePrices, clobTokenIds (->[yesToken,noToken]),
#             liquidityNum, volume24hr, endDate, negRisk, description
# (the gamma ?keyword=/?question= text filters are unreliable — fetch & filter client-side,
#  or fetch one market by condition_ids=<cid>.)
```
**Always read the full `description` (resolution rules) before trading.** Recurring traps that
flip the EV of an otherwise "cheap" bet:
- **50/50 fallback**: "if neither event occurs by <date>, resolves 50-50" → paying >$0.50 is
  negative-EV (e.g. several "X before GTA VI" meme markets).
- **Observation window ≠ deadline**: e.g. "SpaceX market cap above $X" resolves on the
  **IPO first-trading-day close**, with the end-date only the deadline for an IPO to occur — NOT
  the company's value at the end date.
- **Wording precision**: "party that **gains the most seats vs. before**" ≠ "party with the most
  seats" (an incumbent supermajority usually *gains* the fewest). Read literally.
- Resolution source / "Other" buckets / immediate-resolution clauses.

## POL gas
Keep a small POL balance (~tens of POL) for gas on wraps/approvals/redemptions.

## NegRisk redemption (winning positions)
For `negativeRisk=true` winners, do **not** use `CTF.redeemPositions` (returns 0). Use
`NegRiskAdapter.redeemPositions(conditionId, amounts)` (`0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296`);
first `CTF.setApprovalForAll(NegRiskAdapter, true)`; `amounts=[yesBalance*1e6, 0]` for YES winners.

## Common Errors & Fixes

| Error / symptom | Cause | Fix |
|-----------------|-------|-----|
| `not enough balance / allowance` (balance tiny) | CLOB collateral (pUSD) too low | wrap USDC.e→pUSD, then `update_balance_allowance()` |
| `not enough balance / allowance` (full balance) | HMAC signed full path w/ query | sign base path only `path.split("?")[0]` |
| `order_version_mismatch` | hand-built V1 order | use `py_clob_client_v2` SDK |
| HTTP 403 on `/book` (looks like 0 liquidity) | missing User-Agent | send `User-Agent: Mozilla/5.0` |
| connection refused / timeouts to CLOB | geoblock | route via proxy `HTTPS_PROXY=http://127.0.0.1:7897` |
| order "placed" but position barely changed | thin top-of-book partial fill | verify position size; re-place remainder |
| `ExtraDataLengthError` in web3 | Polygon POA | inject `ExtraDataToPOAMiddleware` (layer 0) |
| redeem returns 0 | used CTF for negRisk | use NegRiskAdapter.redeemPositions |
