commit 91ee44ae113e958affd20cd505c6e9d9d6100e0b Author: Suhail Kakar Date: Thu Feb 19 21:29:35 2026 +0530 add skills diff --git a/README.md b/README.md new file mode 100644 index 0000000..bd2f5a8 --- /dev/null +++ b/README.md @@ -0,0 +1,145 @@ +# Polymarket Integration Skill + +Agent skill for building on Polymarket — the world's largest prediction market. Gives agents the knowledge to authenticate, place orders, read markets, stream real-time data, manage positions, bridge assets across chains, and execute gasless transactions. + +## What's Included + +``` +web3-polymarket/ +├── SKILL.md # Entry point — quick reference, client setup, core patterns +├── README.md # This file +├── authentication.md # L1/L2 auth, builder headers, credential lifecycle +├── order-patterns.md # Order types, tick sizes, cancel, heartbeat, errors +├── market-data.md # Gamma API, Data API, CLOB orderbook, subgraph +├── websocket.md # Market/user/sports channels, subscribe, heartbeat +├── ctf-operations.md # Split, merge, redeem, negative risk, token IDs +├── bridge.md # Deposits, withdrawals, supported chains/tokens +└── gasless.md # Relayer client, wallet deployment, builder setup +``` + +## How It Works + +The skill uses **progressive disclosure** to stay efficient with context: + +1. **SKILL.md loads first** — contains API endpoints, contract addresses, client setup, and core code patterns. Enough for most tasks. +2. **Reference files load on demand** — when a task needs deeper detail (e.g., full error code list, bridge chain support, WebSocket event schemas), the agent reads the relevant file. + +This keeps the initial context small (~200 lines) while giving access to ~1,700 lines of detailed reference material when needed. + +## When Agents Use This Skill + +An agent activates this skill when a user asks about: + +- **Authentication** — API keys, EIP-712 signing, HMAC-SHA256, builder credentials +- **Trading** — placing limit/market orders (GTC, GTD, FOK, FAK), batch orders, cancellation, heartbeat keepalive +- **Market data** — fetching events/markets from Gamma API, reading orderbook prices/spreads/midpoints, price history +- **Real-time data** — WebSocket subscriptions for orderbook updates, trade notifications, sports scores +- **Token operations** — splitting USDC.e into Yes/No tokens, merging, redeeming after resolution +- **Bridging** — depositing from 15+ chains, withdrawing, checking status +- **Gasless transactions** — relayer client for gas-free onchain operations +- **Negative risk** — multi-outcome markets, token conversion, augmented neg risk + +## Quick Start for Humans + +If you're a developer reading this directly (not an agent), here's the fastest path: + +### 1. Install the SDK + +```bash +# TypeScript +npm install @polymarket/clob-client ethers@5.8.0 + +# Python +pip install py-clob-client +``` + +### 2. Get API Credentials + +```typescript +import { ClobClient } from "@polymarket/clob-client"; +import { Wallet } from "ethers"; + +const client = new ClobClient( + "https://clob.polymarket.com", + 137, + new Wallet(process.env.PRIVATE_KEY) +); +const creds = await client.createOrDeriveApiKey(); +``` + +### 3. Place an Order + +```typescript +const tradingClient = new ClobClient( + "https://clob.polymarket.com", + 137, + signer, + creds, + 2, // GNOSIS_SAFE (most common) + "FUNDER_ADDR" // from polymarket.com/settings +); + +const response = await tradingClient.createAndPostOrder( + { tokenID: "TOKEN_ID", price: 0.50, size: 10, side: "BUY" }, + { tickSize: "0.01", negRisk: false }, + "GTC" +); +``` + +## Key Concepts + +| Concept | Description | +|---------|-------------| +| **USDC.e** | Bridged USDC on Polygon — the collateral token for all markets | +| **Condition ID** | Identifies a market (used in API as `market` or `conditionID`) | +| **Token ID** | Identifies a specific outcome token (Yes or No) within a market | +| **Funder** | The proxy wallet address that holds funds — find at polymarket.com/settings | +| **Signature Type** | `0` = EOA, `1` = POLY_PROXY (Magic Link), `2` = GNOSIS_SAFE (most common) | +| **Neg Risk** | Multi-outcome markets where outcomes are linked — set `negRisk: true` in order options | +| **Tick Size** | Minimum price increment for a market — must match or orders are rejected | + +## API Endpoints + +| API | Base URL | Auth Required | +|-----|----------|---------------| +| CLOB | `https://clob.polymarket.com` | L2 headers for trades, none for reads | +| Gamma | `https://gamma-api.polymarket.com` | None | +| Data | `https://data-api.polymarket.com` | None | +| Bridge | `https://bridge.polymarket.com` | None | +| Relayer | `https://relayer-v2.polymarket.com/` | Builder headers | +| WS Market | `wss://ws-subscriptions-clob.polymarket.com/ws/market` | None | +| WS User | `wss://ws-subscriptions-clob.polymarket.com/ws/user` | API creds in message | +| WS Sports | `wss://sports-api.polymarket.com/ws` | None | + +## Contract Addresses (Polygon) + +| Contract | Address | +|----------|---------| +| USDC.e (Bridged USDC) | `0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174` | +| CTF | `0x4D97DCd97eC945f40cF65F87097ACe5EA0476045` | +| CTF Exchange | `0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E` | +| Neg Risk CTF Exchange | `0xC5d563A36AE78145C45a50134d48A1215220f80a` | +| Neg Risk Adapter | `0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296` | + +## File Guide + +| File | Read when you need to... | +|------|--------------------------| +| [SKILL.md](SKILL.md) | Get started — has everything for basic integration | +| [authentication.md](authentication.md) | Understand L1/L2 auth flow, builder headers, or troubleshoot credential issues | +| [order-patterns.md](order-patterns.md) | Use advanced order types (GTD, post-only, batch), handle errors, or implement heartbeat | +| [market-data.md](market-data.md) | Query markets by slug/tag, paginate results, use subgraph, or estimate fill prices | +| [websocket.md](websocket.md) | Stream real-time orderbook updates, trade notifications, or sports scores | +| [ctf-operations.md](ctf-operations.md) | Split/merge/redeem tokens, work with neg risk markets, or compute token IDs | +| [bridge.md](bridge.md) | Deposit from other chains, withdraw, check supported assets, or track transaction status | +| [gasless.md](gasless.md) | Set up gas-free transactions via the relayer, deploy wallets, or configure builder credentials | + +## SDKs + +- **TypeScript**: [@polymarket/clob-client](https://github.com/Polymarket/clob-client) +- **Python**: [py-clob-client](https://github.com/Polymarket/py-clob-client) +- **Rust**: [rs-clob-client](https://github.com/Polymarket/rs-clob-client) +- **Builder Relayer (TS)**: [@polymarket/builder-relayer-client](https://github.com/Polymarket/builder-relayer-client) +- **Builder Relayer (Python)**: [py-builder-relayer-client](https://github.com/Polymarket/py-builder-relayer-client) +- **Builder Signing (TS)**: [@polymarket/builder-signing-sdk](https://github.com/Polymarket/builder-signing-sdk) +- **Builder Signing (Python)**: [py-builder-signing-sdk](https://github.com/Polymarket/py-builder-signing-sdk) diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..171cbfb --- /dev/null +++ b/SKILL.md @@ -0,0 +1,204 @@ +--- +name: web3-polymarket +description: Polymarket integration for prediction market trading on Polygon. Covers authentication (L1 EIP-712, L2 HMAC-SHA256, builder headers), order placement (GTC/GTD/FOK/FAK, batch, post-only, heartbeat), market data (Gamma API, Data API, orderbook, subgraph), WebSocket streaming (market/user/sports channels), CTF operations (split, merge, redeem, negative risk), bridge (deposits, withdrawals, multi-chain), and gasless relayer transactions. Use when building AI agents, autonomous market makers, prediction market UIs, or any application integrating with Polymarket on Polygon. +compatibility: Requires network access to Polymarket APIs (clob.polymarket.com, gamma-api.polymarket.com) and Polygon RPC +--- + +# Polymarket Skill + +## When to use this skill + +Use this skill when the user asks about or needs to build: +- Polymarket API authentication (L1/L2, API keys, HMAC signing) +- Placing or managing orders (limit, market, GTC, GTD, FOK, FAK, batch, cancel) +- Reading orderbook data (prices, spreads, midpoints, depth) +- Market data fetching (events, markets, by slug, by tag, pagination) +- WebSocket subscriptions (market channel, user channel, sports) +- CTF operations (split, merge, redeem positions) +- Negative risk markets (multi-outcome, conversion, augmented neg risk) +- Bridge operations (deposits, withdrawals, multi-chain) +- Gasless transactions (relayer client, order attribution) +- Builder program integration (order attribution, API keys, tiers) +- Polymarket SDK usage (TypeScript @polymarket/clob-client, Python py-clob-client) + +## API Configuration + +| API | Base URL | Auth | Purpose | +|-----|----------|------|---------| +| CLOB | `https://clob.polymarket.com` | L2 for trade endpoints | Orderbook, prices, order submission | +| Gamma / Data | `https://gamma-api.polymarket.com` | None | Events, markets, search | +| Data API | `https://data-api.polymarket.com` | None | Trades, positions, user data | +| WebSocket (Market) | `wss://ws-subscriptions-clob.polymarket.com/ws/market` | None | Real-time orderbook | +| WebSocket (User) | `wss://ws-subscriptions-clob.polymarket.com/ws/user` | API creds in message | Trade/order updates | +| WebSocket (Sports) | `wss://sports-api.polymarket.com/ws` | None | Live scores | +| Relayer | `https://relayer-v2.polymarket.com/` | Builder headers | Gasless transactions | +| Bridge | `https://bridge.polymarket.com` | None | Deposits/withdrawals | + +## Contract Addresses (Polygon) + +| Contract | Address | +|----------|---------| +| USDC (USDC.e) | `0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174` | +| CTF (Conditional Tokens) | `0x4D97DCd97eC945f40cF65F87097ACe5EA0476045` | +| CTF Exchange | `0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E` | +| Neg Risk CTF Exchange | `0xC5d563A36AE78145C45a50134d48A1215220f80a` | +| Neg Risk Adapter | `0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296` | + +## Client Setup + +### TypeScript +```typescript +import { ClobClient, Side, OrderType } from "@polymarket/clob-client"; +import { Wallet } from "ethers"; // v5.8.0 + +const HOST = "https://clob.polymarket.com"; +const CHAIN_ID = 137; +const signer = new Wallet(process.env.PRIVATE_KEY); + +// Step 1: L1 — derive API credentials +const tempClient = new ClobClient(HOST, CHAIN_ID, signer); +const apiCreds = await tempClient.createOrDeriveApiKey(); + +// Step 2: L2 — init trading client +const client = new ClobClient( + HOST, + CHAIN_ID, + signer, + apiCreds, + 2, // signatureType: 0=EOA, 1=POLY_PROXY, 2=GNOSIS_SAFE + "FUNDER_ADDRESS" // proxy wallet address from polymarket.com/settings +); +``` + +### Python +```python +from py_clob_client.client import ClobClient +import os + +host = "https://clob.polymarket.com" +chain_id = 137 +pk = os.getenv("PRIVATE_KEY") + +# Step 1: L1 — derive API credentials +temp_client = ClobClient(host, key=pk, chain_id=chain_id) +api_creds = temp_client.create_or_derive_api_creds() + +# Step 2: L2 — init trading client +client = ClobClient( + host, + key=pk, + chain_id=chain_id, + creds=api_creds, + signature_type=2, # 0=EOA, 1=POLY_PROXY, 2=GNOSIS_SAFE + funder="FUNDER_ADDRESS", +) +``` + +## Quick Reference: Order Types + +| Type | Behavior | Use Case | +|------|----------|----------| +| **GTC** | Rests on book until filled or cancelled | Default limit orders | +| **GTD** | Active until expiration (UTC seconds). Min = `now + 60 + N` | Auto-expire before events | +| **FOK** | Fill entirely immediately or cancel | All-or-nothing market orders | +| **FAK** | Fill what's available, cancel rest | Partial-fill market orders | + +- FOK/FAK BUY: `amount` = dollar amount to spend +- FOK/FAK SELL: `amount` = number of shares to sell +- Post-only: GTC/GTD only — rejected if would cross spread + +## Quick Reference: Signature Types + +| Type | Value | Description | +|------|-------|-------------| +| EOA | `0` | Standard Ethereum wallet (MetaMask). Funder is the EOA address and will need POL for gas. | +| POLY_PROXY | `1` | Custom proxy wallet for Magic Link email/Google users who exported PK from Polymarket.com. | +| GNOSIS_SAFE | `2` | Gnosis Safe multisig proxy wallet (most common). Use for any new or returning user. | + +## Core Pattern: Place an Order + +### TypeScript +```typescript +const response = await client.createAndPostOrder( + { + tokenID: "TOKEN_ID", + price: 0.50, + size: 10, + side: Side.BUY, + }, + { + tickSize: "0.01", // from client.getTickSize(tokenID) or market object + negRisk: false, // from client.getNegRisk(tokenID) or market object + }, + OrderType.GTC +); +console.log(response.orderID, response.status); +``` + +### Python +```python +from py_clob_client.clob_types import OrderArgs, OrderType +from py_clob_client.order_builder.constants import BUY + +response = client.create_and_post_order( + OrderArgs(token_id="TOKEN_ID", price=0.50, size=10, side=BUY), + options={"tick_size": "0.01", "neg_risk": False}, + order_type=OrderType.GTC, +) +print(response["orderID"], response["status"]) +``` + +## Core Pattern: Read Orderbook + +### TypeScript +```typescript +// No auth needed +const readClient = new ClobClient("https://clob.polymarket.com", 137); +const book = await readClient.getOrderBook("TOKEN_ID"); +console.log("Best bid:", book.bids[0], "Best ask:", book.asks[0]); + +const mid = await readClient.getMidpoint("TOKEN_ID"); +const spread = await readClient.getSpread("TOKEN_ID"); +``` + +### Python +```python +read_client = ClobClient("https://clob.polymarket.com", chain_id=137) +book = read_client.get_order_book("TOKEN_ID") +mid = read_client.get_midpoint("TOKEN_ID") +spread = read_client.get_spread("TOKEN_ID") +``` + +## Core Pattern: WebSocket Subscribe + +```typescript +const ws = new WebSocket("wss://ws-subscriptions-clob.polymarket.com/ws/market"); + +ws.onopen = () => { + ws.send(JSON.stringify({ + type: "market", + assets_ids: ["TOKEN_ID"], + custom_feature_enabled: true, + })); + // Send PING every 10s to keep alive + setInterval(() => ws.send("PING"), 10_000); +}; + +ws.onmessage = (event) => { + if (event.data === "PONG") return; + const msg = JSON.parse(event.data); + // msg.event_type: "book" | "price_change" | "last_trade_price" | "tick_size_change" | "best_bid_ask" | "new_market" | "market_resolved" +}; +``` + +## Reference files (load on demand) + +Only read these when the task requires deeper detail on a specific topic: + +- **Authentication** (L1/L2, builder headers, credential lifecycle): [authentication.md](authentication.md) +- **Order patterns** (GTC/GTD/FOK/FAK, tick sizes, cancel, heartbeat, errors): [order-patterns.md](order-patterns.md) +- **Market data** (Gamma API, Data API, CLOB orderbook, subgraph): [market-data.md](market-data.md) +- **WebSocket** (market/user/sports channels, subscribe, heartbeat): [websocket.md](websocket.md) +- **CTF operations** (split, merge, redeem, neg risk, token IDs): [ctf-operations.md](ctf-operations.md) +- **Bridge** (deposits, withdrawals, supported chains/tokens, status): [bridge.md](bridge.md) +- **Gasless transactions** (relayer client, wallet deployment, builder setup): [gasless.md](gasless.md) diff --git a/authentication.md b/authentication.md new file mode 100644 index 0000000..e0a3a9f --- /dev/null +++ b/authentication.md @@ -0,0 +1,206 @@ +# Authentication + +Polymarket uses two-level auth: **L1** (EIP-712 private key signing) to create credentials, **L2** (HMAC-SHA256 API key signing) to authenticate requests. Builder program adds a separate set of **builder headers** for order attribution and relayer access. + +## L1 Authentication (Private Key) + +L1 proves wallet ownership via EIP-712 signature. Used to create or derive API credentials. + +### EIP-712 Domain + +```typescript +const domain = { + name: "ClobAuthDomain", + version: "1", + chainId: 137, +}; + +const types = { + ClobAuth: [ + { name: "address", type: "address" }, + { name: "timestamp", type: "string" }, + { name: "nonce", type: "uint256" }, + { name: "message", type: "string" }, + ], +}; + +const value = { + address: signingAddress, // The signing address + timestamp: ts, // The CLOB API server timestamp + nonce: nonce, // The nonce used + message: "This message attests that I control the given wallet", +}; +``` + +### L1 Headers + +| Header | Description | +|--------|-------------| +| `POLY_ADDRESS` | Polygon signer address | +| `POLY_SIGNATURE` | CLOB EIP-712 signature | +| `POLY_TIMESTAMP` | Current UNIX timestamp | +| `POLY_NONCE` | Nonce (default: 0) | + +### Create / Derive Credentials + +```typescript +// TypeScript +const client = new ClobClient("https://clob.polymarket.com", 137, signer); +const creds = await client.createOrDeriveApiKey(); +// { apiKey: "uuid", secret: "base64...", passphrase: "string" } +``` + +```python +# Python +client = ClobClient("https://clob.polymarket.com", key=pk, chain_id=137) +creds = client.create_or_derive_api_creds() +``` + +**REST endpoints:** +- `POST {host}/auth/api-key` — create new credentials (requires L1 headers) +- `GET {host}/auth/derive-api-key` — derive existing credentials (requires L1 headers) + +## L2 Authentication (API Key) + +L2 uses HMAC-SHA256 signatures from the API credentials. Required for all `/v1/trade/*` endpoints. + +### L2 Headers (all 5 required) + +| Header | Description | +|--------|-------------| +| `POLY_ADDRESS` | Polygon signer address | +| `POLY_SIGNATURE` | HMAC signature for request | +| `POLY_TIMESTAMP` | Current UNIX timestamp | +| `POLY_API_KEY` | User's API `apiKey` value | +| `POLY_PASSPHRASE` | User's API `passphrase` value | + +### Initialize Trading Client + +```typescript +// TypeScript +const client = new ClobClient( + "https://clob.polymarket.com", + 137, + signer, + apiCreds, // { apiKey, secret, passphrase } + 2, // signatureType + funderAddress // proxy wallet address +); +``` + +```python +# Python +client = ClobClient( + host="https://clob.polymarket.com", + chain_id=137, + key=pk, + creds=api_creds, + signature_type=2, + funder=funder_address, +) +``` + +## Signature Types + +| Type | Value | When to Use | +|------|-------|-------------| +| EOA | `0` | Standard Ethereum wallet (MetaMask). Funder is the EOA address and will need POL to pay gas on transactions. | +| POLY_PROXY | `1` | A custom proxy wallet only used with users who logged in via Magic Link email/Google. Using this requires the user to have exported their PK from Polymarket.com and imported into your app. | +| GNOSIS_SAFE | `2` | Gnosis Safe multisig proxy wallet (most common). Use this for any new or returning user who does not fit the other 2 types. | + +The **funder** is the address holding funds. For proxy wallets, find it at polymarket.com/settings. Proxy wallets are auto-deployed on first Polymarket.com login. + +## Builder Headers + +Builder authentication is separate from L1/L2. Used for order attribution and relayer access. + +### Builder Headers (4 required) + +| Header | Description | +|--------|-------------| +| `POLY_BUILDER_API_KEY` | Builder API key | +| `POLY_BUILDER_TIMESTAMP` | Unix timestamp | +| `POLY_BUILDER_PASSPHRASE` | Builder passphrase | +| `POLY_BUILDER_SIGNATURE` | HMAC-SHA256 of request | + +### Initialize Client with Builder Config + +```typescript +// TypeScript — local signing +import { BuilderConfig, BuilderApiKeyCreds } from "@polymarket/builder-signing-sdk"; + +const builderCreds: BuilderApiKeyCreds = { + key: process.env.POLY_BUILDER_API_KEY!, + secret: process.env.POLY_BUILDER_SECRET!, + passphrase: process.env.POLY_BUILDER_PASSPHRASE!, +}; + +const builderConfig = new BuilderConfig({ localBuilderCreds: builderCreds }); + +const client = new ClobClient( + "https://clob.polymarket.com", + 137, + signer, + apiCreds, + 2, + funderAddress, + undefined, + false, + builderConfig +); +// Orders automatically include builder headers +``` + +```python +# Python — local signing +from py_builder_signing_sdk import BuilderConfig, BuilderApiKeyCreds + +builder_config = BuilderConfig( + local_builder_creds=BuilderApiKeyCreds( + key=os.environ["POLY_BUILDER_API_KEY"], + secret=os.environ["POLY_BUILDER_SECRET"], + passphrase=os.environ["POLY_BUILDER_PASSPHRASE"], + ) +) + +client = ClobClient( + host="https://clob.polymarket.com", + chain_id=137, + key=pk, + creds=api_creds, + signature_type=2, + funder=funder_address, + builder_config=builder_config, +) +``` + +### Remote Signing + +Keep builder credentials on a separate server. Client points to your signing endpoint: + +```typescript +// TypeScript client +const builderConfig = new BuilderConfig({ + remoteBuilderConfig: { url: "https://your-server.com/sign" }, +}); +``` + +```python +# Python client +from py_builder_signing_sdk import BuilderConfig, RemoteBuilderConfig + +builder_config = BuilderConfig( + remote_builder_config=RemoteBuilderConfig(url="https://your-server.com/sign") +) +``` + +Your server receives `{ method, path, body }` and returns the 4 `POLY_BUILDER_*` headers. + +## Credential Lifecycle + +- **Create**: `client.createApiKey()` — generates new credentials with a nonce +- **Derive**: `client.deriveApiKey(nonce)` — recovers existing credentials if you know the nonce +- **Create or Derive**: `client.createOrDeriveApiKey()` — creates if first time, derives if existing +- **Revoke builder key**: `client.revokeBuilderApiKey()` — invalidate compromised builder credentials + +Lost credentials + lost nonce = create fresh credentials. Save your nonce. diff --git a/bridge.md b/bridge.md new file mode 100644 index 0000000..db9c14d --- /dev/null +++ b/bridge.md @@ -0,0 +1,137 @@ +# Bridge + +Polymarket uses **USDC.e** (Bridged USDC) on Polygon as collateral. The Bridge API handles deposits from and withdrawals to multiple chains. + +Base URL: `https://bridge.polymarket.com` + +## Deposit Flow + +1. `POST /deposit` with your Polymarket wallet address → get deposit addresses +2. Verify token is supported via `/supported-assets` +3. Send assets to the appropriate address for your source chain +4. Assets are bridged and auto-swapped to USDC.e on Polygon +5. Track status via `/status/{deposit_address}` + +```bash +# Create deposit addresses +curl -X POST https://bridge.polymarket.com/deposit \ + -H "Content-Type: application/json" \ + -d '{"address": "0xYOUR_POLYMARKET_WALLET"}' +``` + +Response includes three address types: + +| Address | Use For | +|---------|---------| +| `evm` | Ethereum, Arbitrum, Base, Optimism, and other EVM chains | +| `svm` | Solana | +| `btc` | Bitcoin | +| `tvm` | Tron | + +Each address is unique to your wallet. + +## Supported Chains + +| Chain | Address Type | Min Deposit | Example Tokens | +|-------|--------------|-------------|----------------| +| Ethereum | EVM | $7 | ETH, USDC, USDT, WBTC, DAI, LINK, UNI, AAVE | +| Polygon | EVM | $2 | POL, USDC, USDT, DAI, WETH, SAND | +| Arbitrum | EVM | $2 | ETH, ARB, USDC, USDT, DAI, WBTC, USDe | +| Base | EVM | $2 | ETH, USDC, USDT, DAI, cbBTC, AERO, USDS | +| Optimism | EVM | $2 | ETH, OP, USDC, USDT, DAI, USDe | +| BNB Smart Chain | EVM | $2 | BNB, USDC, USDT, DAI, ETH, BTCB, BUSD | +| Solana | SVM | $2 | SOL, USDC, USDT, USDe, TRUMP | +| Bitcoin | BTC | $9 | BTC | +| Tron | TVM | $9 | USDT | +| HyperEVM | EVM | $2 | HYPE, USDC, USDe, stHYPE, UBTC, UETH | +| Abstract | EVM | $2 | ETH, USDC, USDT | +| Monad | EVM | $2 | MON, USDC, USDT | +| Ethereal | EVM | $2 | USDe, WUSDe | +| Katana | EVM | $2 | AUSD | +| Lighter | EVM | $2 | USDC | + +Always call `/supported-assets` for the current list — assets change over time. + +## Withdrawal Flow + +1. Check destination chain/token via `/supported-assets` +2. Preview fees via `POST /quote` +3. `POST /withdraw` with wallet address, destination chain, token, and recipient → get deposit addresses +4. Send USDC.e from Polymarket wallet to the appropriate address +5. Track status via `/status/{address}` + +```bash +# Create withdrawal addresses +curl -X POST https://bridge.polymarket.com/withdraw \ + -H "Content-Type: application/json" \ + -d '{ + "address": "0xYOUR_POLYMARKET_WALLET", + "toChainId": "1", + "toTokenAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "recipientAddr": "0xDESTINATION_ADDRESS" + }' +``` + +**Do not pre-generate withdrawal addresses.** Generate them only when ready to execute. + +## Quote + +Preview fees and estimated output for deposits and withdrawals. Withdrawals are **instant** and **free** — Polymarket does not charge withdrawal fees. + +```bash +POST https://bridge.polymarket.com/quote +``` + +## Status Tracking + +```bash +# Use the deposit address (not your wallet address) +curl https://bridge.polymarket.com/status/0xDEPOSIT_ADDRESS +``` + +### Transaction Statuses + +| Status | Terminal | Description | +|--------|----------|-------------| +| `DEPOSIT_DETECTED` | No | Funds detected on source chain, not yet processing | +| `PROCESSING` | No | Being routed and swapped | +| `ORIGIN_TX_CONFIRMED` | No | Source chain transaction confirmed | +| `SUBMITTED` | No | Submitted to Polygon | +| `COMPLETED` | Yes | Funds arrived — success | +| `FAILED` | Yes | Error occurred | + +Poll every 10–30 seconds until `COMPLETED` or `FAILED`. + +### Response + +```json +{ + "transactions": [{ + "fromChainId": "1", + "fromTokenAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "fromAmountBaseUnit": "1000000000", + "toChainId": "137", + "toTokenAddress": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", + "status": "COMPLETED", + "txHash": "0x...", + "createdTimeMs": 1697875200000 + }] +} +``` + +Empty `transactions` array = no deposits detected yet. + +## Recovery + +If you deposited the wrong token: +- **Ethereum deposits**: https://recovery.polymarket.com/ +- **Polygon deposits**: https://matic-recovery.polymarket.com/ + +Sending unsupported tokens may cause **irrecoverable loss**. + +## Caveats + +- **Withdrawals >$50,000**: break into smaller amounts to minimize slippage +- **Uniswap pool exhaustion**: USDC.e → USDC swap goes through Uniswap v3 pool. If pool is exhausted, use smaller amounts or wait for rebalance +- **Deposits below minimum**: will not be processed +- **Supported assets change**: always check `/supported-assets` before depositing diff --git a/ctf-operations.md b/ctf-operations.md new file mode 100644 index 0000000..154cce3 --- /dev/null +++ b/ctf-operations.md @@ -0,0 +1,180 @@ +# CTF Operations + +The **Conditional Token Framework (CTF)** creates ERC1155 tokens for market outcomes. Three core operations: split, merge, redeem. + +## Token Model + +Every binary market has two tokens: + +| Token | Redeems for | Condition | +|-------|-------------|-----------| +| **Yes** | $1.00 USDC.e | Event occurs | +| **No** | $1.00 USDC.e | Event does not occur | + +Every Yes/No pair is backed by exactly $1.00 USDC.e locked in the CTF contract. + +## Split + +Convert USDC.e into a full set of outcome tokens. + +``` +$100 USDC.e → 100 Yes tokens + 100 No tokens +``` + +### Prerequisites +1. USDC.e balance on Polygon +2. USDC.e approval for CTF contract +3. Condition ID of the market (the condition must already be prepared on the CTF contract via `prepareCondition`) + +### Function: `splitPosition` + +| Parameter | Type | Value | +|-----------|------|-------| +| `collateralToken` | IERC20 | `0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174` (USDC.e) | +| `parentCollectionId` | bytes32 | `0x0000...0000` (32 zero bytes) | +| `conditionId` | bytes32 | Market's condition ID | +| `partition` | uint[] | `[1, 2]` for binary (Yes=1, No=2) | +| `amount` | uint256 | Amount of USDC.e to split | + +## Merge + +Convert a full set of outcome tokens back to USDC.e. Inverse of split. + +``` +100 Yes tokens + 100 No tokens → $100 USDC.e +``` + +### Prerequisites +1. Equal amounts of both Yes and No tokens +2. Condition ID (the condition must already be prepared on the CTF contract via `prepareCondition`) +3. Sufficient gas for the transaction + +### Function: `mergePositions` + +Same parameters as split. Burns one unit of each position per unit of collateral returned. + +## Redeem + +Exchange winning tokens for USDC.e after market resolution. + +``` +Market resolves YES: + 100 Yes tokens → $100 USDC.e + 100 No tokens → $0 +``` + +### Prerequisites +1. Market must be resolved +2. Hold winning tokens +3. Know the condition ID + +### Function: `redeemPositions` + +| Parameter | Type | Value | +|-----------|------|-------| +| `collateralToken` | IERC20 | USDC.e address | +| `parentCollectionId` | bytes32 | `0x0000...0000` | +| `conditionId` | bytes32 | Market's condition ID | +| `indexSets` | uint[] | `[1, 2]` — redeems both (only winner pays) | + +Redemption burns your **entire** token balance for the condition — no amount parameter. No deadline — winning tokens are always redeemable. + +### Payout Vectors + +| Outcome | Payout Vector | Redemption | +|---------|---------------|------------| +| Yes wins | `[1, 0]` | Yes = $1, No = $0 | +| No wins | `[0, 1]` | Yes = $0, No = $1 | + +## Contract Addresses + +| Contract | Address | Purpose | +|----------|---------|---------| +| CTF | `0x4D97DCd97eC945f40cF65F87097ACe5EA0476045` | Token storage and operations | +| USDC.e (Bridged USDC) | `0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174` | Collateral token | +| CTF Exchange | `0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E` | Standard market trading | +| Neg Risk CTF Exchange | `0xC5d563A36AE78145C45a50134d48A1215220f80a` | Neg risk market trading | +| Neg Risk Adapter | `0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296` | Neg risk conversions | + +## Approval Matrix + +Before trading or CTF operations, the funder must approve the relevant contracts: + +| Operation | Contract to Approve | Token | +|-----------|-------------------|-------| +| Buy order (standard) | CTF Exchange | USDC.e | +| Sell order (standard) | CTF Exchange | Conditional tokens | +| Buy order (neg risk) | Neg Risk CTF Exchange | USDC.e | +| Sell order (neg risk) | Neg Risk CTF Exchange | Conditional tokens | +| Split | CTF | USDC.e | +| Neg risk conversion | Neg Risk Adapter | Conditional tokens | + +## Standard vs Neg Risk Markets + +| Feature | Standard Markets | Neg Risk Markets | +|---------|-----------------|------------------| +| CTF Contract | ConditionalTokens | ConditionalTokens | +| Exchange Contract | CTF Exchange | Neg Risk CTF Exchange | +| Multi-outcome | Independent markets | Linked via conversion | +| `negRisk` flag | `false` | `true` | +| Order option | `negRisk: false` | `negRisk: true` | + +## Negative Risk + +Multi-outcome events where only one outcome can win. A No token in any market can be **converted** into 1 Yes token in every other market. + +### Conversion Example + +Event: "Who wins?" with outcomes Trump, Harris, Other. + +| Outcome | Before | After Conversion | +|---------|--------|------------------| +| Trump | — | 1 Yes | +| Harris | — | 1 Yes | +| Other | 1 No | — | + +Conversion is atomic through the Neg Risk Adapter contract. + +### Identifying Neg Risk Markets + +```json +{ + "negRisk": true // on event or market object from API +} +``` + +When placing orders: pass `negRisk: true` in options. + +## Augmented Negative Risk + +For events where new outcomes emerge after trading begins (e.g., new candidate enters race). + +| Outcome Type | Description | +|--------------|-------------| +| Named outcomes | Known outcomes (e.g., "Trump", "Harris") | +| Placeholder outcomes | Reserved slots clarified later (e.g., "Person A") | +| Explicit Other | Catches any unnamed outcome | + +### Identifying + +```json +{ + "enableNegRisk": true, + "negRiskAugmented": true +} +``` + +### Rules +- Only trade on **named outcomes** — ignore placeholders +- If correct outcome is not named at resolution, market resolves to "Other" +- "Other" definition changes as placeholders are clarified — avoid trading it directly + +## Token ID Computation + +Token IDs are computed onchain in three steps: + +1. `getConditionId(oracle, questionId, outcomeSlotCount)` — oracle = UMA CTF Adapter, outcomeSlotCount = 2 for binary +2. `getCollectionId(parentCollectionId, conditionId, indexSet)` — parentCollectionId = bytes32(0), indexSet = 1 (Yes) or 2 (No) +3. `getPositionId(collateralToken, collectionId)` — combines USDC.e contract address on Polygon with collection + +In practice, get token IDs from the Markets API `tokens` array. Manual computation only needed for direct contract interaction. diff --git a/gasless.md b/gasless.md new file mode 100644 index 0000000..14b52f1 --- /dev/null +++ b/gasless.md @@ -0,0 +1,236 @@ +# Gasless Transactions + +Polymarket's **Relayer Client** enables gasless transactions. Instead of requiring users to hold POL, Polymarket's infrastructure pays gas fees. Users only need USDC.e to trade. + +Requires **Builder Program** membership. You need Builder API credentials. + +## How It Works + +1. Your app creates a transaction +2. User signs it with their private key +3. App sends to Polymarket's relayer +4. Relayer submits onchain and pays gas +5. Transaction executes from the user's wallet + +## What's Covered + +| Operation | Description | +|-----------|-------------| +| Wallet deployment | Deploy Safe or Proxy wallets for new users | +| Token approvals | Approve contracts to spend USDC.e or outcome tokens | +| CTF operations | Split, merge, redeem positions | +| Transfers | Move tokens between addresses | + +## Installation + +```bash +# TypeScript +npm install @polymarket/builder-relayer-client @polymarket/builder-signing-sdk + +# Python +pip install py-builder-relayer-client py-builder-signing-sdk +``` + +## Client Setup + +### TypeScript (Local Signing) + +```typescript +import { createWalletClient, http, Hex } from "viem"; +import { privateKeyToAccount } from "viem/accounts"; +import { polygon } from "viem/chains"; +import { RelayClient } from "@polymarket/builder-relayer-client"; +import { BuilderConfig } from "@polymarket/builder-signing-sdk"; + +const account = privateKeyToAccount(process.env.PRIVATE_KEY as Hex); +const wallet = createWalletClient({ + account, + chain: polygon, + transport: http(process.env.RPC_URL), +}); + +const builderConfig = new BuilderConfig({ + localBuilderCreds: { + key: process.env.POLY_BUILDER_API_KEY!, + secret: process.env.POLY_BUILDER_SECRET!, + passphrase: process.env.POLY_BUILDER_PASSPHRASE!, + }, +}); + +const client = new RelayClient( + "https://relayer-v2.polymarket.com/", + 137, + wallet, + builderConfig +); +``` + +### Python (Local Signing) + +```python +import os +from py_builder_relayer_client.client import RelayClient +from py_builder_signing_sdk import BuilderConfig, BuilderApiKeyCreds + +builder_config = BuilderConfig( + local_builder_creds=BuilderApiKeyCreds( + key=os.getenv("POLY_BUILDER_API_KEY"), + secret=os.getenv("POLY_BUILDER_SECRET"), + passphrase=os.getenv("POLY_BUILDER_PASSPHRASE"), + ) +) + +client = RelayClient( + "https://relayer-v2.polymarket.com", + 137, + os.getenv("PRIVATE_KEY"), + builder_config, +) +``` + +### Remote Signing + +Keep credentials on your server. Client points to signing endpoint: + +```typescript +// TypeScript +const builderConfig = new BuilderConfig({ + remoteBuilderConfig: { url: "https://your-server.com/sign" }, +}); + +const client = new RelayClient( + "https://relayer-v2.polymarket.com/", + 137, + wallet, + builderConfig +); +``` + +```python +# Python +from py_builder_signing_sdk import BuilderConfig, RemoteBuilderConfig + +builder_config = BuilderConfig( + remote_builder_config=RemoteBuilderConfig(url="https://your-server.com/sign") +) + +client = RelayClient("https://relayer-v2.polymarket.com", 137, pk, builder_config) +``` + +## Wallet Types + +| Type | Deployment | Best For | +|------|------------|----------| +| **Safe** | Call `deploy()` before first transaction | Most builder integrations | +| **Proxy** | Auto-deploys on first transaction | Magic Link users | + +```typescript +// TypeScript — Safe wallet +import { RelayClient, RelayerTxType } from "@polymarket/builder-relayer-client"; + +const client = new RelayClient( + "https://relayer-v2.polymarket.com/", + 137, + wallet, + builderConfig, + RelayerTxType.SAFE +); + +// Deploy before first transaction +const response = await client.deploy(); +const result = await response.wait(); +console.log("Safe Address:", result?.proxyAddress); +``` + +```python +# Python — Safe wallet +response = client.deploy() +result = response.wait() +print("Safe Address:", result.get("proxyAddress")) +``` + +## Executing Transactions + +```typescript +interface Transaction { + to: string; // Target contract address + data: string; // Encoded function call + value: string; // POL to send (usually "0") +} + +const response = await client.execute(transactions, "Description"); +const result = await response.wait(); +``` + +### Token Approval Example + +```typescript +import { encodeFunctionData, maxUint256 } from "viem"; + +const USDC = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174"; +const CTF = "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045"; + +const approveTx = { + to: USDC, + data: encodeFunctionData({ + abi: [{ + name: "approve", type: "function", + inputs: [{ name: "spender", type: "address" }, { name: "amount", type: "uint256" }], + outputs: [{ type: "bool" }], + }], + functionName: "approve", + args: [CTF, maxUint256], + }), + value: "0", +}; + +const response = await client.execute([approveTx], "Approve USDC.e for CTF"); +await response.wait(); +``` + +### Batch Transactions + +Execute multiple operations atomically in a single call: + +```typescript +const response = await client.execute( + [approveTx, transferTx], + "Approve and transfer" +); +await response.wait(); +``` + +## Transaction States + +| State | Terminal | Description | +|-------|----------|-------------| +| `STATE_NEW` | No | Received by relayer | +| `STATE_EXECUTED` | No | Submitted onchain | +| `STATE_MINED` | No | Included in a block | +| `STATE_CONFIRMED` | Yes | Finalized successfully | +| `STATE_FAILED` | Yes | Failed permanently | +| `STATE_INVALID` | Yes | Rejected as invalid | + +## Builder Setup + +1. Go to polymarket.com/settings?tab=builder +2. Create builder profile + generate API keys +3. Implement builder signing in your CLOB client +4. All orders automatically attributed to your builder account + +## Contract Addresses + +| Contract | Address | Approval Needed | +|----------|---------|-----------------| +| USDC.e (Bridged USDC) | `0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174` | — | +| CTF | `0x4D97DCd97eC945f40cF65F87097ACe5EA0476045` | USDC.e | +| CTF Exchange | `0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E` | USDC.e, Tokens | +| Neg Risk CTF Exchange | `0xC5d563A36AE78145C45a50134d48A1215220f80a` | USDC.e, Tokens | +| Neg Risk Adapter | `0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296` | Tokens | + +## SDKs + +- [Builder Relayer Client (TypeScript)](https://github.com/Polymarket/builder-relayer-client) +- [Builder Relayer Client (Python)](https://github.com/Polymarket/py-builder-relayer-client) +- [Builder Signing SDK (TypeScript)](https://github.com/Polymarket/builder-signing-sdk) +- [Builder Signing SDK (Python)](https://github.com/Polymarket/py-builder-signing-sdk) diff --git a/market-data.md b/market-data.md new file mode 100644 index 0000000..d8ed471 --- /dev/null +++ b/market-data.md @@ -0,0 +1,216 @@ +# Market Data + +Four sources for market data: **Gamma API** (events, markets, search), **Data API** (trades, positions, user data), **CLOB** (orderbook, prices), and **Subgraph** (onchain queries). + +## Gamma API + +Base URL: `https://gamma-api.polymarket.com` — no auth required. + +### Events Endpoint + +```bash +# All active events +GET https://gamma-api.polymarket.com/events?active=true&closed=false&limit=100 + +# By slug (from polymarket.com/event/{slug}) +GET https://gamma-api.polymarket.com/events?slug=fed-decision-in-october + +# By tag +GET https://gamma-api.polymarket.com/events?tag_id=100381&limit=10&active=true&closed=false + +# By series (sports) +GET https://gamma-api.polymarket.com/events?series_id=10345&active=true&closed=false + +# Sorted by volume +GET https://gamma-api.polymarket.com/events?active=true&closed=false&order=volume_24hr&ascending=false&limit=100 +``` + +### Markets Endpoint + +```bash +# By slug +GET https://gamma-api.polymarket.com/markets?slug=fed-decision-in-october +``` + +### Sort Parameters + +| Parameter | Values | +|-----------|--------| +| `order` | `volume_24hr`, `volume`, `liquidity`, `start_date`, `end_date`, `competitive`, `closed_time` | +| `ascending` | `true` / `false` (default: `false`) | +| `active` | `true` / `false` | +| `closed` | `true` / `false` | +| `limit` | 1–500 (default: 20) | +| `offset` | Pagination offset | + +### Pagination + +```bash +# Page 1 +GET https://gamma-api.polymarket.com/events?active=true&closed=false&limit=50&offset=0 + +# Page 2 +GET https://gamma-api.polymarket.com/events?active=true&closed=false&limit=50&offset=50 +``` + +Response includes `has_more: true/false`. Increment offset by limit until `has_more` is `false`. + +### Tags & Sports + +```bash +# Discover tags +GET https://gamma-api.polymarket.com/tags + +# Sports metadata +GET https://gamma-api.polymarket.com/sports +``` + +## Data API + +Base URL: `https://data-api.polymarket.com` — no auth required. Used for trades, positions, and user-specific data. + +## CLOB Orderbook + +Base URL: `https://clob.polymarket.com` — no auth for read endpoints. + +### Get Orderbook + +```typescript +// TypeScript +const client = new ClobClient("https://clob.polymarket.com", 137); +const book = await client.getOrderBook("TOKEN_ID"); +// { bids: [{price, size}...], asks: [{price, size}...], tick_size, min_order_size, neg_risk } +``` + +```python +# Python +client = ClobClient("https://clob.polymarket.com", chain_id=137) +book = client.get_order_book("TOKEN_ID") +``` + +```bash +# REST +curl "https://clob.polymarket.com/book?token_id=TOKEN_ID" +``` + +### Prices + +```typescript +const buyPrice = await client.getPrice("TOKEN_ID", "BUY"); // best ask +const sellPrice = await client.getPrice("TOKEN_ID", "SELL"); // best bid +``` + +```bash +curl "https://clob.polymarket.com/price?token_id=TOKEN_ID&side=BUY" +``` + +### Midpoint + +```typescript +const mid = await client.getMidpoint("TOKEN_ID"); // { mid: "0.50" } +``` + +If bid-ask spread > $0.10, Polymarket UI shows last traded price instead of midpoint. + +### Spread + +```typescript +const spread = await client.getSpread("TOKEN_ID"); // { spread: "0.04" } +``` + +### Last Trade Price + +```typescript +const last = await client.getLastTradePrice("TOKEN_ID"); // { price, side } +``` + +### Price History + +```typescript +const history = await client.getPricesHistory({ + market: "TOKEN_ID", + interval: PriceHistoryInterval.ONE_DAY, + fidelity: 60, // data points every 60 minutes +}); +// Each entry: { t: timestamp, p: price } +``` + +| Interval | Description | +|----------|-------------| +| `1h` | Last hour | +| `6h` | Last 6 hours | +| `1d` | Last day | +| `1w` | Last week | +| `1m` | Last month | +| `max` | All available | + +Use `startTs`/`endTs` for absolute ranges (mutually exclusive with `interval`). + +### Estimate Fill Price + +Walk the orderbook to estimate slippage for a given order size: + +```typescript +const price = await client.calculateMarketPrice( + "TOKEN_ID", Side.BUY, 500, OrderType.FOK +); +``` + +### Batch Requests + +All orderbook queries have batch variants (up to 500 tokens): + +| Single | Batch | REST | +|--------|-------|------| +| `getOrderBook()` | `getOrderBooks()` | `POST /books` | +| `getPrice()` | `getPrices()` | `POST /prices` | +| `getMidpoint()` | `getMidpoints()` | `POST /midpoints` | +| `getSpread()` | `getSpreads()` | `POST /spreads` | +| `getLastTradePrice()` | `getLastTradesPrices()` | — | + +```typescript +const prices = await client.getPrices([ + { token_id: "TOKEN_A", side: Side.BUY }, + { token_id: "TOKEN_B", side: Side.BUY }, +]); +``` + +## Key Market Fields + +| Field | Description | +|-------|-------------| +| `tokenID` / `asset_id` | ERC1155 token ID for an outcome | +| `conditionID` / `market` | Condition ID — identifies the market | +| `questionID` | Hash of UMA ancillary data | +| `neg_risk` | `true` for multi-outcome events | +| `minimum_tick_size` | Minimum price increment | +| `enableOrderBook` | Whether orderbook is active | +| `slug` | URL-friendly identifier | +| `tokens` | Array of `{ token_id, outcome }` for both outcomes | + +## Subgraph (Onchain Data) + +GraphQL queries via Goldsky-hosted subgraphs: + +| Subgraph | Description | +|----------|-------------| +| Positions | User token balances | +| Orders | Order book and trade events | +| Activity | Splits, merges, redemptions | +| Open Interest | Market and global OI | +| PNL | User position P&L | + +```bash +curl -X POST \ + https://api.goldsky.com/api/public/project_cl6mb8i9h0003e201j6li0diw/subgraphs/orderbook-subgraph/0.0.1/gn \ + -H "Content-Type: application/json" \ + -d '{"query": "query { orderbooks { id tradesQuantity } }"}' +``` + +## Fetching Strategy + +1. **Specific market**: fetch by slug — `GET https://gamma-api.polymarket.com/events?slug=...` +2. **Category browsing**: filter by tag — `GET https://gamma-api.polymarket.com/events?tag_id=...` +3. **All active markets**: paginate events — `GET https://gamma-api.polymarket.com/events?active=true&closed=false` +4. **Always include** `active=true&closed=false` unless you need historical data +5. **Events > Markets**: events contain their markets, reducing API calls diff --git a/order-patterns.md b/order-patterns.md new file mode 100644 index 0000000..1fcf048 --- /dev/null +++ b/order-patterns.md @@ -0,0 +1,303 @@ +# Order Patterns + +All orders on Polymarket are expressed as limit orders. Market orders are limit orders with a marketable price that execute immediately. + +## Order Types + +| Type | Behavior | Use Case | +|------|----------|----------| +| **GTC** | Good-Til-Cancelled — rests on book until filled or cancelled | Default for limit orders | +| **GTD** | Good-Til-Date — active until expiration timestamp (UTC seconds), unless filled or cancelled first | Auto-expire before known events | +| **FOK** | Fill-Or-Kill — fill entirely immediately or cancel | All-or-nothing market orders | +| **FAK** | Fill-And-Kill — fill what's available, cancel rest | Partial-fill market orders | + +## Tick Sizes + +Price must conform to the market's tick size or the order is rejected. + +| Tick Size | Precision | Example Prices | +|-----------|-----------|----------------| +| `0.1` | 1 decimal | 0.1, 0.2, 0.5 | +| `0.01` | 2 decimals | 0.01, 0.50, 0.99 | +| `0.001` | 3 decimals | 0.001, 0.500, 0.999 | +| `0.0001` | 4 decimals | 0.0001, 0.5000, 0.9999 | + +Get tick size: `client.getTickSize(tokenID)` (TS) / `client.get_tick_size(token_id)` (Python). Also available as `minimum_tick_size` on market objects. + +## Limit Order (GTC) + +```typescript +// TypeScript — one-step +const response = await client.createAndPostOrder( + { tokenID: "TOKEN_ID", price: 0.50, size: 10, side: Side.BUY }, + { tickSize: "0.01", negRisk: false }, + OrderType.GTC +); +``` + +```python +# Python — one-step +response = client.create_and_post_order( + OrderArgs(token_id="TOKEN_ID", price=0.50, size=10, side=BUY), + options={"tick_size": "0.01", "neg_risk": False}, + order_type=OrderType.GTC, +) +``` + +### Two-step (sign then submit) + +```typescript +// TypeScript +const signedOrder = await client.createOrder( + { tokenID: "TOKEN_ID", price: 0.50, size: 10, side: Side.BUY }, + { tickSize: "0.01", negRisk: false } +); +const response = await client.postOrder(signedOrder, OrderType.GTC); +``` + +```python +# Python +signed_order = client.create_order( + OrderArgs(token_id="TOKEN_ID", price=0.50, size=10, side=BUY), + options={"tick_size": "0.01", "neg_risk": False}, +) +response = client.post_order(signed_order, OrderType.GTC) +``` + +## Market Order (FOK / FAK) + +- **BUY**: `amount` = dollar amount to spend +- **SELL**: `amount` = number of shares to sell +- `price` = worst-price limit (slippage protection), not target execution price + +```typescript +// TypeScript — FOK BUY: spend exactly $100 or cancel +const buyOrder = await client.createMarketOrder( + { tokenID: "TOKEN_ID", side: Side.BUY, amount: 100, price: 0.50 }, + { tickSize: "0.01", negRisk: false } +); +await client.postOrder(buyOrder, OrderType.FOK); + +// One-step convenience +const response = await client.createAndPostMarketOrder( + { tokenID: "TOKEN_ID", side: Side.BUY, amount: 100, price: 0.50 }, + { tickSize: "0.01", negRisk: false }, + OrderType.FOK +); +``` + +```python +# Python — FOK BUY +buy_order = client.create_market_order( + token_id="TOKEN_ID", side=BUY, amount=100, price=0.50, + options={"tick_size": "0.01", "neg_risk": False}, +) +client.post_order(buy_order, OrderType.FOK) +``` + +## GTD Order (Expiring) + +Expiration = UTC seconds timestamp. Security threshold: add 60 seconds minimum. + +**Effective lifetime of N seconds: `now + 60 + N`** + +```typescript +// TypeScript — expire in 1 hour +const expiration = Math.floor(Date.now() / 1000) + 60 + 3600; + +const response = await client.createAndPostOrder( + { tokenID: "TOKEN_ID", price: 0.50, size: 10, side: Side.BUY, expiration }, + { tickSize: "0.01", negRisk: false }, + OrderType.GTD +); +``` + +```python +# Python — expire in 1 hour +import time +expiration = int(time.time()) + 60 + 3600 + +response = client.create_and_post_order( + OrderArgs(token_id="TOKEN_ID", price=0.50, size=10, side=BUY, expiration=expiration), + options={"tick_size": "0.01", "neg_risk": False}, + order_type=OrderType.GTD, +) +``` + +## Post-Only Orders + +Guarantee maker status. If order would cross spread, it's rejected (not executed). + +```typescript +// TypeScript +const response = await client.postOrder(signedOrder, OrderType.GTC, true); +``` + +```python +# Python +response = client.post_order(signed_order, OrderType.GTC, post_only=True) +``` + +- Only works with GTC and GTD +- Rejected if combined with FOK or FAK + +## Batch Orders + +Up to **15 orders** in a single request. + +```typescript +// TypeScript +const orders: PostOrdersArgs[] = [ + { + order: await client.createOrder( + { tokenID: "TOKEN_ID", price: 0.48, side: Side.BUY, size: 500 }, + { tickSize: "0.01", negRisk: false } + ), + orderType: OrderType.GTC, + }, + { + order: await client.createOrder( + { tokenID: "TOKEN_ID", price: 0.52, side: Side.SELL, size: 500 }, + { tickSize: "0.01", negRisk: false } + ), + orderType: OrderType.GTC, + }, +]; +const response = await client.postOrders(orders); +``` + +```python +# Python +response = client.post_orders([ + PostOrdersArgs( + order=client.create_order( + OrderArgs(price=0.48, size=500, side=BUY, token_id="TOKEN_ID"), + options={"tick_size": "0.01", "neg_risk": False}, + ), + orderType=OrderType.GTC, + ), + PostOrdersArgs( + order=client.create_order( + OrderArgs(price=0.52, size=500, side=SELL, token_id="TOKEN_ID"), + options={"tick_size": "0.01", "neg_risk": False}, + ), + orderType=OrderType.GTC, + ), +]) +``` + +## Cancel Orders + +All cancel endpoints require L2 authentication. + +```typescript +// TypeScript +await client.cancelOrder("0xORDER_ID"); // single +await client.cancelOrders(["0xID_1", "0xID_2"]); // multiple +await client.cancelAll(); // all orders +await client.cancelMarketOrders({ market: "0xCONDITION_ID" }); // by market +await client.cancelMarketOrders({ // by token + market: "0xCONDITION_ID", + asset_id: "TOKEN_ID", +}); +``` + +```python +# Python +client.cancel(order_id="0xORDER_ID") +client.cancel_orders(["0xID_1", "0xID_2"]) +client.cancel_all() +client.cancel_market_orders( + market="0xCONDITION_ID", + asset_id="TOKEN_ID", # optional +) +``` + +### Onchain Cancellation (fallback) + +If the API is unavailable, cancel directly on the Exchange contract by calling `cancelOrder(Order order)` onchain with the full signed order struct. Use the `CTFExchange` or `NegRiskCTFExchange` contract depending on the market type. See [Contract Addresses](/resources/contract-addresses) for addresses. + +## Heartbeat + +If a valid heartbeat is not received within **10 seconds** (with up to a 5-second buffer), **all of your open orders will be cancelled**. + +```typescript +// TypeScript +let heartbeatId = ""; +setInterval(async () => { + const resp = await client.postHeartbeat(heartbeatId); + heartbeatId = resp.heartbeat_id; +}, 5000); +``` + +```python +# Python +import time +heartbeat_id = "" +while True: + resp = client.post_heartbeat(heartbeat_id) + heartbeat_id = resp["heartbeat_id"] + time.sleep(5) +``` + +- First request: use empty string for `heartbeat_id` +- If you send an invalid or expired `heartbeat_id`, the server responds with a `400 Bad Request` and provides the correct `heartbeat_id` in the response + +## Error Codes + +| Error | Description | +|-------|-------------| +| `INVALID_ORDER_MIN_TICK_SIZE` | Price doesn't conform to the market's tick size | +| `INVALID_ORDER_MIN_SIZE` | Order size is below the minimum threshold | +| `INVALID_ORDER_DUPLICATED` | Identical order has already been placed | +| `INVALID_ORDER_NOT_ENOUGH_BALANCE` | Funder doesn't have sufficient balance or allowance | +| `INVALID_ORDER_EXPIRATION` | Expiration timestamp is in the past | +| `INVALID_ORDER_ERROR` | System error while inserting order | +| `INVALID_POST_ONLY_ORDER_TYPE` | Post-only flag used with a market order type (FOK/FAK) | +| `INVALID_POST_ONLY_ORDER` | Post-only order would cross the book | +| `EXECUTION_ERROR` | System error while executing trade | +| `ORDER_DELAYED` | Order placement delayed due to market conditions | +| `DELAYING_ORDER_ERROR` | System error while delaying order | +| `FOK_ORDER_NOT_FILLED_ERROR` | FOK order couldn't be fully filled | +| `MARKET_NOT_READY` | Market is not yet accepting orders | + +## Insert Statuses + +| Status | Description | +|--------|-------------| +| `matched` | Order placed and matched with a resting order | +| `live` | Order placed and resting on the book | +| `delayed` | Order is marketable but subject to a matching delay | +| `unmatched` | Order is marketable but failed to delay — placement still successful | + +## Trade Statuses + +``` +MATCHED → MINED → CONFIRMED + ↓ ↑ +RETRYING ───┘ + ↓ + FAILED +``` + +| Status | Terminal | Description | +|--------|----------|-------------| +| `MATCHED` | No | Matched and sent to the executor service for onchain submission | +| `MINED` | No | Observed as mined on the chain, no finality threshold yet | +| `CONFIRMED` | Yes | Achieved strong probabilistic finality — trade successful | +| `RETRYING` | No | Transaction failed (revert or reorg) — being retried by the operator | +| `FAILED` | Yes | Trade failed permanently and is not being retried | + +## Prerequisites + +Before placing orders, the funder address must approve the Exchange contract: +- **Buying**: the funder must have set a **USDC.e** allowance greater than or equal to the spending amount. +- **Selling**: the funder must have set a **conditional token** allowance greater than or equal to the selling amount. + +Max order size = `balance - sum(openOrderSize - filledAmount)` + +## Sports Markets + +- Outstanding limit orders auto-cancelled when game begins +- Marketable orders have 3-second placement delay +- Game start times can shift — monitor accordingly diff --git a/websocket.md b/websocket.md new file mode 100644 index 0000000..44ea5da --- /dev/null +++ b/websocket.md @@ -0,0 +1,228 @@ +# WebSocket + +Three channels for real-time data. Market and sports channels are public; user channel requires API credentials. + +## Channels + +| Channel | Endpoint | Auth | +|---------|----------|------| +| Market | `wss://ws-subscriptions-clob.polymarket.com/ws/market` | No | +| User | `wss://ws-subscriptions-clob.polymarket.com/ws/user` | Yes | +| Sports | `wss://sports-api.polymarket.com/ws` | No | + +## Market Channel + +Public. Subscribes by **asset IDs** (token IDs). + +### Subscribe + +```json +{ + "assets_ids": ["TOKEN_ID_1", "TOKEN_ID_2"], + "type": "market", + "custom_feature_enabled": true +} +``` + +Set `custom_feature_enabled: true` to enable `best_bid_ask`, `new_market`, and `market_resolved` events. + +### Event Types + +| Event | Trigger | Key Fields | +|-------|---------|------------| +| `book` | On subscribe + when trade affects book | `bids[]`, `asks[]`, `hash`, `timestamp` | +| `price_change` | Order placed or cancelled | `price_changes[]` with `price`, `size`, `side`, `best_bid`, `best_ask` | +| `last_trade_price` | Trade executed | `price`, `side`, `size`, `fee_rate_bps` | +| `tick_size_change` | Price hits >0.96 or <0.04 | `old_tick_size`, `new_tick_size` | +| `best_bid_ask` | Top-of-book changes | `best_bid`, `best_ask`, `spread` | +| `new_market` | Market created | `question`, `assets_ids`, `outcomes` | +| `market_resolved` | Market resolved | `winning_asset_id`, `winning_outcome` | + +Events requiring `custom_feature_enabled: true`: `best_bid_ask`, `new_market`, `market_resolved`. + +**`tick_size_change` is critical for bots** — if tick size changes and you use the old one, orders are rejected. + +A `price_change` with `size: "0"` means the price level was removed from the book. + +### Example Messages + +```json +// book +{ + "event_type": "book", + "asset_id": "TOKEN_ID", + "market": "0xCONDITION_ID", + "bids": [{"price": ".48", "size": "30"}], + "asks": [{"price": ".52", "size": "25"}], + "timestamp": "123456789000", + "hash": "0x..." +} +``` + +```json +// price_change +{ + "event_type": "price_change", + "market": "0xCONDITION_ID", + "price_changes": [{ + "asset_id": "TOKEN_ID", + "price": "0.5", + "size": "200", + "side": "BUY", + "hash": "...", + "best_bid": "0.5", + "best_ask": "1" + }], + "timestamp": "..." +} +``` + +## User Channel + +Authenticated. Subscribes by **condition IDs** (market IDs), not asset IDs. The `markets` field is optional — omit it to receive events for all markets. + +### Subscribe + +```json +{ + "auth": { + "apiKey": "your-api-key", + "secret": "your-api-secret", + "passphrase": "your-passphrase" + }, + "markets": ["0xCONDITION_ID"], + "type": "user" +} +``` + +### Event Types + +| Event | Trigger | +|-------|---------| +| `trade` | Trade lifecycle: MATCHED, MINED, CONFIRMED, RETRYING, FAILED | +| `order` | Order lifecycle: PLACEMENT, UPDATE, CANCELLATION | + +### Trade Message + +```json +{ + "event_type": "trade", + "id": "trade-uuid", + "market": "0xCONDITION_ID", + "asset_id": "TOKEN_ID", + "side": "BUY", + "size": "10", + "price": "0.57", + "status": "MATCHED", + "maker_orders": [{ "order_id": "0x...", "matched_amount": "10", "price": "0.57" }], + "type": "TRADE" +} +``` + +### Order Message + +```json +{ + "event_type": "order", + "id": "0xORDER_ID", + "market": "0xCONDITION_ID", + "asset_id": "TOKEN_ID", + "side": "SELL", + "price": "0.57", + "original_size": "10", + "size_matched": "0", + "type": "PLACEMENT" +} +``` + +Order types: `PLACEMENT`, `UPDATE` (partial fill), `CANCELLATION`. + +## Sports Channel + +No subscription message needed. Connect and receive all active sports data. + +```json +// sport_result +{ "type": "sport_result", ... } // Live scores, periods, status +``` + +## Dynamic Subscribe / Unsubscribe + +Modify subscriptions without reconnecting: + +```json +// Market channel — subscribe to more +{ "assets_ids": ["NEW_TOKEN_ID"], "operation": "subscribe", "custom_feature_enabled": true } + +// Market channel — unsubscribe +{ "assets_ids": ["OLD_TOKEN_ID"], "operation": "unsubscribe" } + +// User channel — subscribe to more markets +{ "markets": ["0xNEW_CONDITION_ID"], "operation": "subscribe" } +``` + +## Heartbeat + +### Market & User Channels +Send `PING` every **10 seconds**. Server responds with `PONG`. + +```typescript +const ws = new WebSocket("wss://ws-subscriptions-clob.polymarket.com/ws/market"); + +ws.onopen = () => { + // Subscribe... + setInterval(() => ws.send("PING"), 10_000); +}; + +ws.onmessage = (event) => { + if (event.data === "PONG") return; + const msg = JSON.parse(event.data); + // handle msg.event_type +}; +``` + +### Sports Channel +Server sends `ping` every 5 seconds. Respond with `pong` within 10 seconds or connection closes. + +## Full TypeScript Example + +```typescript +const ws = new WebSocket("wss://ws-subscriptions-clob.polymarket.com/ws/market"); + +ws.onopen = () => { + ws.send(JSON.stringify({ + type: "market", + assets_ids: ["TOKEN_ID"], + custom_feature_enabled: true, + })); + setInterval(() => ws.send("PING"), 10_000); +}; + +ws.onmessage = (event) => { + if (event.data === "PONG") return; + const msg = JSON.parse(event.data); + switch (msg.event_type) { + case "book": + console.log("Book snapshot:", msg.bids.length, "bids", msg.asks.length, "asks"); + break; + case "price_change": + for (const pc of msg.price_changes) { + console.log(`${pc.side} ${pc.size}@${pc.price} (best: ${pc.best_bid}/${pc.best_ask})`); + } + break; + case "last_trade_price": + console.log(`Trade: ${msg.side} ${msg.size}@${msg.price}`); + break; + case "tick_size_change": + console.log(`Tick: ${msg.old_tick_size} → ${msg.new_tick_size}`); + break; + } +}; +``` + +## Troubleshooting + +- **Connection closes immediately**: send subscription message right after open +- **Drops after ~10s**: you're not sending PING heartbeats +- **No messages**: verify asset IDs are correct and markets are active +- **Auth failed (user channel)**: check API credentials haven't expired