A guide for integrating Polymarket's prediction market infrastructure — whether you're a wallet provider (MetaMask, Phantom, Coinbase Wallet, etc.) or an exchange / platform building a self-custodial trading environment. Covers the full lifecycle: Safe deployment, token approvals, market discovery, trading, position management, token redemption, and withdrawals.
- Architecture Overview
- Integration Checklist
- Prerequisites
- Phase 1: Wallet Infrastructure
- Phase 2: Market Discovery (Gamma API)
- Phase 3: Trading via CLOB
- Phase 4: Position Management (Data API)
- Phase 5: Market Resolution
- Phase 6: Redemption, Split/Merge & Transfers
- Phase 7: Geoblocking
- Phase 8: Real-Time Data (WebSockets)
- Phase 9: Deposits & Withdrawals (Bridge API)
- Contract Addresses
- Client Libraries
- Builder Codes
- Data Model Reference
- Authentication Reference
- Appendix: Tag & Series IDs
┌──────────────────────────────────────────────────────────────────────┐
│ Wallet Application │
├──────────┬──────────┬──────────┬──────────┬──────────┬──────────────┤
│ Relay │ Gamma │ CLOB │ Data │ Bridge │ WebSockets │
│ Client │ API │ API │ API │ API │ (CLOB/Sports)│
├──────────┴──────────┴──────────┴──────────┴──────────┴──────────────┤
│ Polygon Network (Chain ID: 137) │
│ ┌──────────┐ ┌──────────────┐ ┌────────────────────────────────┐ │
│ │ Gnosis │ │ CTF │ │ CTF Exchange / │ │
│ │ Safe │ │ (ERC-1155) │ │ Neg Risk CTF Exchange │ │
│ └──────────┘ └──────────────┘ └────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────┘
| API | Base URL | Auth | Purpose |
|---|---|---|---|
| Relay | Via RelayClient SDK |
Builder HMAC | Safe deployment, gasless transactions, redemptions, transfers |
| Gamma | https://gamma-api.polymarket.com |
None | Market discovery, event metadata, categories |
| CLOB | https://clob.polymarket.com |
HMAC + EIP-712 | Place/cancel orders, order books, prices |
| Data | https://data-api.polymarket.com |
None | Positions, trades, P&L, portfolio value |
| Bridge | https://gamma-api.polymarket.com |
None | Multi-chain deposits/withdrawals |
| Sports WS | wss://sports-api.polymarket.com/ws |
None | Live sports scores |
| CLOB WS | wss://ws-subscriptions-clob.polymarket.com/ws |
API key (user channel) | Live order book, trades |
Data model: Event → Market → Outcome. Each Market has parallel outcomes[], outcomePrices[], and clobTokenIds[] arrays. Prices are implied probabilities (0.0–1.0). Collateral is USDC.e (6 decimals) on Polygon.
Polymarket uses two independent sets of HMAC credentials that serve different purposes:
| Builder Credentials | User Credentials | |
|---|---|---|
| Identifies | Your platform / integration | Each individual user |
| Source | Issued by Polymarket when you join the Builder Program | Derived programmatically from the user's wallet via EIP-712 signature (Section 1.4) |
| Scope | One set for your entire integration — shared across all users | One set per user (per EOA wallet), deterministic |
| Used for | Relay operations (Safe deploy, approvals, transfers) + CLOB order attribution | All authenticated CLOB trading operations (place/cancel orders, balances) |
| HTTP headers | POLY_BUILDER_API_KEY, POLY_BUILDER_SIGNATURE, POLY_BUILDER_TIMESTAMP, POLY_BUILDER_PASSPHRASE |
POLY_ADDRESS, POLY_API_KEY, POLY_SIGNATURE, POLY_TIMESTAMP, POLY_PASSPHRASE |
| When obtained | Once, before any integration code runs | At runtime, once per user during their first session |
Both are required for a working integration. Builder credentials identify who built the app; user credentials identify who is trading. When placing orders, both credential sets are sent — user creds authenticate the trade, builder creds attribute it to your platform.
Polymarket supports multiple integration architectures. From Polymarket's perspective, all that matters is that a valid EOA signer exists and the standard APIs are called correctly. The internal architecture of your application — client-side, server-side, hybrid — is entirely your domain.
Regardless of architecture, every integration needs:
- A valid EOA signer per user (any source — browser wallet, server-side key, managed signer)
- A Gnosis Safe per user (deployed via Relay)
- Token approvals on the Safe (one-time setup)
- User CLOB API credentials (derived per user via EIP-712 signature)
- Builder credentials (per platform, from the Builder Program)
The user controls their own EOA wallet (e.g., MetaMask, Privy, Turnkey). Your application connects to the user's wallet and calls Polymarket APIs directly. This is the default pattern documented throughout this guide.
User Wallet (EOA) ─── Your Application ─── Polymarket APIs ─── Polygon (Safe)
For architectural decisions specific to this pattern (wallet provider choice, builder credential signing, session persistence), see
guides/non-custodial-dapp.md.
Many integrators are exchanges, banks, or vaults that custody user funds but want to offer prediction market trading through a segregated self-custodial environment — similar to the Coinbase / Base Wallet model. The user withdraws funds from the centralized platform into a separate self-custodial wallet, and all Polymarket integration happens from there.
Three layers:
| Layer | Description |
|---|---|
| Centralized Environment | CEX, bank, vault, or any custodial key manager. Users hold funds here and withdraw to trade. |
| Self-Custodial Environment | A segregated wallet where the EOA private key lives and all Polymarket API calls originate. Architecture is the integrator's choice (client, server, hybrid, HSM, KMS — all work). |
| Polygon | Where the proxy wallet (Safe) is deployed via CREATE2, exchange contracts settle matched trades, and CTF tokens are held. |
Key principles:
- Polymarket does not prescribe how the self-custodial environment is built. Whether signing is client-side, server-side, or via a managed signer (Turnkey, AWS KMS, Fireblocks) — the integration pattern is the same.
- The self-custodial environment calls the same Polymarket APIs (Gamma, CLOB, Data, Relay, Bridge, WebSocket) documented in this guide.
- Funds can flow from the custodial wallet directly to the proxy wallet (Safe) on Polygon, or route through the self-custodial wallet first. Both are valid.
- Once an EOA signer exists, the entire 9-phase lifecycle below applies identically.
Phase differences for CEX integrators:
| Phase | What Changes |
|---|---|
| 1. Wallet Infrastructure | EOA is generated/managed server-side instead of connected via browser wallet. See Backend Key Management. |
| 2–8 | Identical — same APIs, same flows. |
| 9. Deposits & Withdrawals | Funds originate from CEX withdrawal instead of user's external wallet. See CEX Funding Flows. |
For a comprehensive walkthrough, see
guides/cex-dex-integration.md.
Some exchanges integrate Polymarket directly into their platform — the CEX manages user private keys, makes all API calls from its own infrastructure, and offers prediction markets as a native product. There is no separate self-custodial environment. Users never interact with wallets or blockchain.
Two layers:
| Layer | Description |
|---|---|
| CEX Environment | The exchange manages EOA keys, produces EIP-712 signatures, and calls all Polymarket APIs from its own servers. |
| Polygon | Where the proxy wallet (Safe) is deployed, exchange contracts settle trades, and CTF tokens are held. |
Key principles:
- The CEX funds user Safes directly from its own wallets — no user withdrawal step.
- How the CEX manages keys, structures its services, or handles signing internally is entirely its domain.
- The same Polymarket APIs and lifecycle apply.
Phase differences:
| Phase | What Changes |
|---|---|
| 1. Wallet Infrastructure | EOA managed by the CEX. See Backend Key Management. |
| 2–8 | Identical — same APIs, same flows. |
| 9. Deposits & Withdrawals | CEX funds Safes from its own wallets. See CEX Funding Flows. |
For a comprehensive walkthrough, see
guides/direct-cex-integration.md.
The project operates one or a few proxy wallets on behalf of all users — rather than deploying a separate Safe per user. Users deposit fiat or crypto into the project; the project manages all Polymarket interactions through an omnibus wallet and tracks per-user positions in an internal DB/Ledger.
Four layers:
| Layer | Description |
|---|---|
| Project Client | User-facing interface. Users browse markets, submit order intents, view positions/P&L. No wallet connection. |
| Project Server | DB/Ledger for internal per-user accounting + Omnibus Wallet (EOA + Safe) for all Polymarket API calls. |
| Polymarket Infra | Same APIs — Gamma, CLOB, Data, Relay, Bridge, WebSocket. |
| Polygon | The omnibus proxy wallet (Safe), exchange contracts, CTF tokens. All positions held here. |
Key principles:
- Users may not have a wallet at all — they deposit fiat or crypto into the project.
- The project signs and submits all CLOB orders from the omnibus wallet. Per-user positions exist only in the project's internal ledger.
- The Data API is used for reconciliation against on-chain state, not direct per-user queries.
- Resolution and redemption happen in bulk from the omnibus wallet; the project credits users internally.
Phase differences:
| Phase | What Changes |
|---|---|
| 1. Wallet Infrastructure | One (or few) EOA + Safe to set up, not one per user. Single set of CLOB API credentials per omnibus wallet. |
| 2. Market Discovery | Same — Gamma API feeds into the project's DB/Ledger. |
| 3. Trading | Project translates user order intents into CLOB orders signed by the omnibus wallet. |
| 4. Position Management | Internal ledger is the source of truth. Data API used for reconciliation. |
| 5–6. Resolution & Redemption | Omnibus wallet redeems in bulk; project credits users via ledger. |
| 7–8 | Same — geoblocking, WebSocket for real-time data. |
| 9. Deposits & Withdrawals | Project funds omnibus Safe from its own wallets. Users deposit into the project, not into a Safe. |
For a comprehensive walkthrough, see
guides/omnibus-account-model.md.
A wallet integration consists of these sequential phases:
| # | Phase | What Happens | Required APIs/SDKs |
|---|---|---|---|
| 1 | Wallet Infrastructure | Deploy Polymarket Safe, set approvals, get API creds | RelayClient, ClobClient |
| 2 | Market Discovery | Show markets, prices, categories to users | Gamma API |
| 3 | Trading | Place/cancel orders with builder attribution | ClobClient + Builder keys |
| 4 | Position Management | Show user positions, P&L, activity | Data API |
| 5 | Market Resolution | Understand resolution process, check resolved markets | Gamma API |
| 6 | Redemption & Transfers | Redeem winnings, split/merge tokens, withdraw USDC | RelayClient |
| 7 | Geoblocking | Enforce Polymarket's geoblock rules | Geoblock API |
| 8 | Real-Time Data | Live order book, sports scores | WebSockets |
| 9 | Deposits & Withdrawals | Multi-chain funding | Bridge API |
npm install @polymarket/clob-client # CLOB REST + WS + order building
npm install @polymarket/builder-relayer-client # Safe deploy + gasless relay
npm install @polymarket/builder-signing-sdk # Builder HMAC auth
npm install @polymarket/order-utils # Low-level order constructionContact Polymarket to join the Builder Program. You will receive one set of credentials for your entire integration (not per-user):
POLYMARKET_BUILDER_API_KEY= # Your builder API key
POLYMARKET_BUILDER_SECRET= # Your builder secret (base64-encoded)
POLYMARKET_BUILDER_PASSPHRASE= # Your builder passphrase (hex-encoded)
These are your platform credentials. They:
- Identify your integration to Polymarket and attribute orderflow/volume to your platform
- Authenticate all Relay operations (Safe deployment, token approvals, transfers, redemptions)
- Are auto-injected on CLOB order submissions for builder attribution
- Are the same credentials used for every user of your application
Store these server-side. For browser-based integrations, use a remote signing server via BuilderConfig({ remoteBuilderConfig: { url } }) to avoid exposing secrets to the client.
Not to be confused with user credentials (Section 1.4), which are derived per-user from their wallet and authenticate trading operations on the CLOB.
# Network
CHAIN_ID=137 # Polygon mainnet
# CLOB API
CLOB_API_URL=https://clob.polymarket.com
WS_URL=wss://ws-subscriptions-clob.polymarket.com/ws
# Builder credentials (from Polymarket Builder Program)
POLYMARKET_BUILDER_API_KEY=
POLYMARKET_BUILDER_SECRET=
POLYMARKET_BUILDER_PASSPHRASE=
# Per-user credentials (derived at runtime per user — not stored in env)
# CLOB_API_KEY, CLOB_SECRET, CLOB_PASS_PHRASEThe canonical integration path deploys a Gnosis Safe for each user. This is required for relay-based gasless transactions and ensures cross-compatibility with polymarket.com (users see the same positions everywhere).
- Cross-compatibility: Users can log into polymarket.com with the same wallet and see positions from your integration
- Gasless transactions: Relay pays gas on behalf of users
- Batched operations: 7 approval transactions execute in one batched Safe tx
- Deterministic: Same EOA always derives the same Safe address (known before deployment)
- Signer-agnostic: Works with any wallet provider (MetaMask, Phantom, Privy, Turnkey, etc.)
If your platform manages signing keys on behalf of users (e.g., a centralized exchange with a segregated self-custodial environment), the EOA signer is created in your self-custodial environment rather than connected via the user's browser wallet. The rest of the flow is identical: derive Safe, deploy via Relay, set approvals, derive API creds, trade.
All that Polymarket requires is a valid EOA signer — any object that can produce EIP-712 signatures. How you generate, store, or manage the underlying key (raw private key, KMS, HSM, embedded wallet, managed signer) is entirely your choice. The RelayClient and ClobClient constructors accept the same signer interface regardless of its source.
Each user still gets their own deterministic Safe. The signatureType remains POLY_GNOSIS_SAFE (value 2), and funderAddress is still the Safe address — same as any Safe-based integration.
For the full CEX integration architecture, see Integration Architectures and
guides/cex-dex-integration.md.
User connects wallet (EOA)
↓
Initialize RelayClient with builder credentials
↓
Derive Safe address (deterministic, no tx needed)
↓
Deploy Safe (if not already deployed — one-time)
↓
Set token approvals (one-time, batched)
↓
Derive CLOB API credentials (one-time)
↓
Ready to trade
import { RelayClient } from "@polymarket/builder-relayer-client";
import { BuilderConfig } from "@polymarket/builder-signing-sdk";
// Platform-level builder credentials (same for all users of your app)
const builderConfig = new BuilderConfig({
localBuilderCreds: {
key: process.env.POLYMARKET_BUILDER_API_KEY,
secret: process.env.POLYMARKET_BUILDER_SECRET,
passphrase: process.env.POLYMARKET_BUILDER_PASSPHRASE,
},
});
const relayClient = new RelayClient(
"https://relay.polymarket.com", // Relay server URL
137, // Polygon mainnet
signer, // User's wallet signer (ethers Wallet / JsonRpcSigner / viem WalletClient)
builderConfig, // Builder HMAC credentials — authenticates relay requests
);import { deriveSafe } from "@polymarket/builder-relayer-client/dist/builder/derive";
import { getContractConfig } from "@polymarket/builder-relayer-client/dist/config";
// Derive deterministic Safe address (no transaction needed)
const config = getContractConfig(137);
const eoaAddress = await signer.getAddress();
const safeAddress = deriveSafe(eoaAddress, config.SafeContracts.SafeFactory);
// Check if already deployed
const isDeployed = await relayClient.getDeployed(safeAddress);
if (!isDeployed) {
// Deploy the Safe — prompts wallet for a signature (not a transaction)
const response = await relayClient.deploy();
// Poll until confirmed (relay pays gas)
const result = await relayClient.pollUntilState(
response.transactionID,
["STATE_MINED", "STATE_CONFIRMED"], // Target states
"STATE_FAILED", // Fail state
30, // Max polls
2000, // Poll interval (ms)
);
if (!result) throw new Error("Safe deployment failed");
console.log("Safe deployed at:", safeAddress);
}Before trading, the Safe must approve exchange contracts to move tokens. This is a one-time setup per Safe, batched into a single gasless transaction.
What gets approved:
| Token | Contract Method | Spenders |
|---|---|---|
| USDC.e (ERC-20) | approve(spender, MAX_UINT256) |
CTF, Neg Risk Adapter, CTF Exchange, Neg Risk CTF Exchange |
| CTF (ERC-1155) | setApprovalForAll(spender, true) |
CTF Exchange, Neg Risk CTF Exchange, Neg Risk Adapter |
import { encodeFunctionData, maxUint256 } from "viem";
const USDC_E = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174";
const CTF = "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045";
// Contracts that need USDC.e approval
const usdcSpenders = [
"0x4D97DCd97eC945f40cF65F87097ACe5EA0476045", // CTF
"0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296", // Neg Risk Adapter
"0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E", // CTF Exchange
"0xC5d563A36AE78145C45a50134d48A1215220f80a", // Neg Risk CTF Exchange
];
// Contracts that need CTF (outcome token) approval
const ctfSpenders = [
"0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E", // CTF Exchange
"0xC5d563A36AE78145C45a50134d48A1215220f80a", // Neg Risk CTF Exchange
"0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296", // Neg Risk Adapter
];
const erc20Abi = [{ name: "approve", type: "function", inputs: [
{ name: "spender", type: "address" }, { name: "amount", type: "uint256" }
], outputs: [{ type: "bool" }], stateMutability: "nonpayable" }] as const;
const erc1155Abi = [{ name: "setApprovalForAll", type: "function", inputs: [
{ name: "operator", type: "address" }, { name: "approved", type: "bool" }
], outputs: [], stateMutability: "nonpayable" }] as const;
const approvalTxs = [
// 4 USDC.e approvals
...usdcSpenders.map(spender => ({
to: USDC_E,
data: encodeFunctionData({ abi: erc20Abi, functionName: "approve", args: [spender, maxUint256] }),
value: "0",
})),
// 3 CTF approvals
...ctfSpenders.map(spender => ({
to: CTF,
data: encodeFunctionData({ abi: erc1155Abi, functionName: "setApprovalForAll", args: [spender, true] }),
value: "0",
})),
];
// Execute all 7 in a single batched gasless Safe transaction
const response = await relayClient.execute(approvalTxs, "Set all token approvals");
const result = await relayClient.pollUntilState(
response.transactionID,
["STATE_MINED", "STATE_CONFIRMED"],
"STATE_FAILED",
30,
2000,
);To skip approval setup for returning users:
import { createPublicClient, http } from "viem";
import { polygon } from "viem/chains";
const publicClient = createPublicClient({ chain: polygon, transport: http() });
// Check USDC.e allowance for a specific spender
const allowance = await publicClient.readContract({
address: USDC_E,
abi: [{ name: "allowance", type: "function", inputs: [
{ name: "owner", type: "address" }, { name: "spender", type: "address" }
], outputs: [{ type: "uint256" }], stateMutability: "view" }],
functionName: "allowance",
args: [safeAddress, "0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E"],
});
// Check CTF approval
const isApproved = await publicClient.readContract({
address: CTF,
abi: [{ name: "isApprovedForAll", type: "function", inputs: [
{ name: "account", type: "address" }, { name: "operator", type: "address" }
], outputs: [{ type: "bool" }], stateMutability: "view" }],
functionName: "isApprovedForAll",
args: [safeAddress, "0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E"],
});
// Threshold: 1,000,000 USDC (1e12 base units) as "enough" remaining allowance
const APPROVAL_THRESHOLD = 1_000_000_000_000n;
const needsApproval = allowance < APPROVAL_THRESHOLD || !isApproved;Each user needs their own CLOB API credentials to authenticate trading operations. These are separate from builder credentials — builder credentials identify your platform, while these credentials identify the individual user.
How it works:
deriveApiKey()sends aGETrequest to/auth/derive-api-keyon the CLOB server- The request includes headers signed by the user's EOA wallet via EIP-712:
- Domain:
{ name: "ClobAuthDomain", version: "1", chainId: 137 } - Signed message:
"This message attests that I control the given wallet" - Headers sent:
POLY_ADDRESS,POLY_SIGNATURE,POLY_TIMESTAMP,POLY_NONCE
- Domain:
- The CLOB server verifies the signature and returns HMAC credentials (key, secret, passphrase) tied to that wallet
- Credentials are deterministic — the same EOA always derives the same credentials
import { ClobClient } from "@polymarket/clob-client";
// Create a temporary client with just the signer (no API creds yet, no builder config needed)
const tempClient = new ClobClient(
process.env.CLOB_API_URL,
137,
signer, // User's EOA wallet signer
);
// Derive deterministic credentials — prompts the user's wallet for an EIP-712 signature
let credentials;
try {
credentials = await tempClient.deriveApiKey();
// Returns: { key: string, secret: string, passphrase: string }
} catch {
// First-time user: create new credentials if none exist yet
credentials = await tempClient.createApiKey();
}
// These user credentials are then used for all authenticated CLOB requests
// (placing/cancelling orders, querying open orders, balances, etc.)
// They produce HMAC headers: POLY_ADDRESS, POLY_API_KEY, POLY_SIGNATURE, POLY_TIMESTAMP, POLY_PASSPHRASEKey distinction: Builder credentials (Prerequisites) are issued once to your platform by Polymarket. User credentials are derived here per-user from their wallet signature. Both are needed when placing orders — user creds authenticate the trade, builder creds attribute it to your platform.
Store the session state so returning users skip deployment and approval steps:
interface TradingSession {
eoaAddress: string;
safeAddress: string;
isSafeDeployed: boolean;
hasApprovals: boolean;
hasApiCredentials: boolean;
apiCredentials?: {
key: string;
secret: string;
passphrase: string;
};
lastChecked: number;
}
// Save after each setup step completes
function saveSession(session: TradingSession) {
const key = `polymarket_trading_session_${session.eoaAddress.toLowerCase()}`;
localStorage.setItem(key, JSON.stringify(session));
}
// Restore on wallet reconnect
function loadSession(eoaAddress: string): TradingSession | null {
const key = `polymarket_trading_session_${eoaAddress.toLowerCase()}`;
const raw = localStorage.getItem(key);
if (!raw) return null;
const session = JSON.parse(raw);
// Validate the session matches the connected wallet
if (session.eoaAddress.toLowerCase() !== eoaAddress.toLowerCase()) return null;
return session;
}The Gamma API is the public API for discovering markets. No authentication required.
Base URL: https://gamma-api.polymarket.com
# All active events, sorted by 24h volume (most liquid first)
curl 'https://gamma-api.polymarket.com/events?active=true&closed=false&order=volume24hr&ascending=false&limit=20'
# Search by title
curl 'https://gamma-api.polymarket.com/events?title=super+bowl&active=true'interface GammaEvent {
id: string;
title: string;
slug: string;
description: string;
markets: GammaMarket[];
active: boolean;
closed: boolean;
volume: string;
volume24hr: string;
liquidity: string;
negRisk: boolean;
tags: { id: string; label: string }[];
}
async function fetchActiveEvents(limit = 20): Promise<GammaEvent[]> {
const url = new URL("https://gamma-api.polymarket.com/events");
url.searchParams.set("active", "true");
url.searchParams.set("closed", "false");
url.searchParams.set("order", "volume24hr");
url.searchParams.set("ascending", "false");
url.searchParams.set("limit", String(limit));
const res = await fetch(url);
return res.json();
}# Sports
curl 'https://gamma-api.polymarket.com/events?tag=52&active=true&limit=50'
# Politics
curl 'https://gamma-api.polymarket.com/events?tag=7&active=true&limit=50'
# Crypto
curl 'https://gamma-api.polymarket.com/events?tag=13&active=true&limit=50'
# NFL specifically
curl 'https://gamma-api.polymarket.com/events?tag=95&active=true'
# NBA specifically
curl 'https://gamma-api.polymarket.com/events?tag=82&active=true'
# By league series (e.g., NFL Regular Season)
curl 'https://gamma-api.polymarket.com/events?seriesId=0x2864e60d15b58c4e72c6b97decfdb36ca8eed28d&active=true'See Appendix: Tag & Series IDs for the full list.
interface GammaMarket {
id: string;
question: string;
conditionId: string;
outcomes: string[]; // ["Yes", "No"]
outcomePrices: string[]; // ["0.65", "0.35"] — parallel to outcomes
clobTokenIds: string[]; // parallel to outcomes — used for CLOB orders
volume: string;
liquidity: string;
active: boolean;
closed: boolean;
negRisk: boolean;
}
// Parse a market from Gamma
function parseMarket(market: GammaMarket) {
return {
question: market.question,
conditionId: market.conditionId,
negRisk: market.negRisk,
// outcomes[i] maps to outcomePrices[i] maps to clobTokenIds[i]
yes: {
label: market.outcomes[0],
price: parseFloat(market.outcomePrices[0]),
tokenId: market.clobTokenIds[0],
},
no: {
label: market.outcomes[1],
price: parseFloat(market.outcomePrices[1]),
tokenId: market.clobTokenIds[1],
},
};
}Surface the most liquid markets to users. New markets may have thin order books and are riskier for market orders.
// Filter for markets with meaningful liquidity
function filterByLiquidity(events: GammaEvent[], minLiquidity = 10000): GammaEvent[] {
return events.filter(event => {
const liquidity = parseFloat(event.liquidity || "0");
return liquidity >= minLiquidity;
});
}Recommendations:
- Sort by
volume24hrdescending for a "trending" feed - Filter out events with
liquidity< $10,000 for best execution - Be cautious surfacing brand-new markets — they may be illiquid
- Use
active=true&closed=falseto only show tradeable markets
After Phase 1 setup is complete, create the full CLOB client for trading:
import { ClobClient } from "@polymarket/clob-client";
const clobClient = new ClobClient(
process.env.CLOB_API_URL, // "https://clob.polymarket.com"
137, // Polygon mainnet
signer, // User's EOA wallet signer
// --- USER AUTH (per-user, from deriveApiKey() in Section 1.4) ---
// These HMAC credentials authenticate this specific user for all CLOB operations.
// Derived from the user's wallet via EIP-712 signature. Unique per EOA.
{
key: credentials.key, // User's CLOB API key
secret: credentials.secret, // User's CLOB HMAC secret
passphrase: credentials.passphrase,
},
2, // signatureType=2: EOA signs, Safe executes (proxy signature)
safeAddress, // The user's Safe address (funder)
undefined, // geoBlockToken placeholder
false, // useServerTime
// --- BUILDER AUTH (per-platform, from Polymarket Builder Program) ---
// These credentials identify your integration. Auto-injected as POLY_BUILDER_* headers
// on order posting to attribute volume. Same config used across all users.
builderConfig,
);signatureType: 2 is critical — it tells the CLOB that the EOA signs orders on behalf of the Safe (proxy wallet pattern). Without this, orders will be rejected.
// Quick price check
const midpoint = await clobClient.getMidpoint(tokenId); // e.g., "0.52"
const bestBuy = await clobClient.getPrice(tokenId, "BUY"); // Best ask price
const bestSell = await clobClient.getPrice(tokenId, "SELL"); // Best bid price
// Full order book
const book = await clobClient.getOrderBook(tokenId);
// book.bids = [{ price: "0.50", size: "500" }, ...]
// book.asks = [{ price: "0.55", size: "300" }, ...]
// Check tick size (minimum price increment)
const tickSize = await clobClient.getTickSize(tokenId); // e.g., "0.01"Integrators should use market/taker orders. Specifically, FAK (Fill and Kill) order types are recommended — they fill as much as possible immediately and cancel the remainder.
// Buy YES tokens — market order
const buyOrder = await clobClient.createMarketOrder({
tokenID: yesTokenId,
amount: 50, // Amount in USDC to spend
side: "BUY",
worstPrice: 0.70, // Max price willing to pay (slippage protection)
});
const buyResult = await clobClient.postOrder(buyOrder, "FAK");
console.log("Order ID:", buyResult.orderID);
// FAK = Fill and Kill — fills immediately, cancels unfilled remainder
// Sell YES tokens — market order
const sellOrder = await clobClient.createMarketOrder({
tokenID: yesTokenId,
amount: 100, // Number of shares to sell
side: "SELL",
worstPrice: 0.45, // Min price willing to accept
});
const sellResult = await clobClient.postOrder(sellOrder, "FAK");For BUY orders: amount is in USDC (how much to spend). Price is calculated from the order book.
For SELL orders: amount is in shares (how many outcome tokens to sell).
FOK (Fill or Kill) — if you need guaranteed full fills, use FOK instead. The entire order must fill or it is cancelled:
const fokResult = await clobClient.postOrder(buyOrder, "FOK");
// If the full amount cannot be filled immediately, the entire order is cancelled
// Convenience method (defaults to FOK):
const result = await clobClient.createAndPostMarketOrder({
tokenID: yesTokenId,
amount: 50,
side: "BUY",
});Order Type Reference:
| Type | Behavior | Use Case |
|---|---|---|
| FAK | Fill what's available, cancel remainder | Recommended for most integrators — tolerates partial fills |
| FOK | Fill entirely or cancel everything | When you need guaranteed full fills |
| GTC | Rest on book until filled or cancelled | Limit orders, market making |
| GTD | Rest on book until expiration time | Time-limited limit orders |
// GTC (Good Till Cancelled) limit order
const limitOrder = await clobClient.createOrder({
tokenID: yesTokenId,
price: 0.50, // Limit price
size: 100, // Number of shares
side: "BUY",
});
const result = await clobClient.postOrder(limitOrder, "GTC");
// GTD (Good Till Date) — expires at a specific time
const gtdOrder = await clobClient.createOrder({
tokenID: yesTokenId,
price: 0.50,
size: 100,
side: "BUY",
expiration: Math.floor(Date.now() / 1000) + 3600, // 1 hour from now
});
const gtdResult = await clobClient.postOrder(gtdOrder, "GTD");
// Post-only — guaranteed maker status, rejected if it would immediately match
const postOnlyOrder = await clobClient.createOrder({
tokenID: yesTokenId,
price: 0.50,
size: 100,
side: "BUY",
});
const postOnlyResult = await clobClient.postOrder(postOnlyOrder, "GTC", false, true);
// 4th param = postOnly. Only valid for GTC and GTD orders.
// If the order would cross the book (immediately match), it is rejected.// Cancel a specific order
await clobClient.cancelOrder({ orderID: "order-id-here" });
// Cancel all orders for a specific market
await clobClient.cancelMarketOrders({ market: conditionId });
// Cancel all open orders across all markets
await clobClient.cancelAll();Place up to 15 orders in a single request using postOrders():
import { OrderType } from "@polymarket/clob-client";
const orders = [
{
order: await clobClient.createOrder({
tokenID: yesTokenId,
price: 0.40,
size: 100,
side: "BUY",
}),
orderType: OrderType.GTC,
},
{
order: await clobClient.createOrder({
tokenID: yesTokenId,
price: 0.60,
size: 100,
side: "SELL",
}),
orderType: OrderType.GTC,
},
];
const result = await clobClient.postOrders(orders);
// Server-enforced limit: max 15 orders per request// Get a specific order
const order = await clobClient.getOrder("order-id-here");
// order.status: "live" | "matched" | "delayed" | "unmatched" | "canceled"
// Get all open orders
const openOrders = await clobClient.getOpenOrders();
// Get trade history
const trades = await clobClient.getTrades();
// Each trade has a status field tracking on-chain settlementOrder Statuses:
| Status | Meaning |
|---|---|
live |
On the book, waiting to be matched |
matched |
Matched with counterparty, pending on-chain settlement |
delayed |
Match delayed due to market conditions |
unmatched |
Not matched (e.g., FAK with no available liquidity) |
canceled |
Cancelled by user or expired (GTD) |
Trade Settlement: After matching, trades settle on-chain on Polygon. Settlement is atomic — the full trade succeeds or nothing happens.
MATCHED → MINED → CONFIRMED
↘ FAILED / RETRYING
| Status | Meaning |
|---|---|
MATCHED |
Orders matched off-chain, trade submitted to Exchange contract |
MINED |
Transaction included in a Polygon block |
CONFIRMED |
Transaction confirmed, tokens swapped between proxy wallets |
FAILED |
On-chain settlement failed (e.g., insufficient balance) |
RETRYING |
Settlement being retried after a transient failure |
Confirmation typically takes a few seconds. Poll getTrades() or use the CLOB WebSocket user channel for real-time fill notifications.
When placing market orders, include a generous worstPrice to reduce the likelihood of failed orders:
| Market Type | Recommended Slippage |
|---|---|
| High liquidity (volume24hr > $100k) | 1.5–2% |
| Medium liquidity | 2–3% |
| Low liquidity / crypto markets | 3–5% |
// Calculate worstPrice with slippage
function addSlippage(currentPrice: number, side: "BUY" | "SELL", slippagePct: number): number {
if (side === "BUY") {
return Math.min(currentPrice * (1 + slippagePct / 100), 0.99);
} else {
return Math.max(currentPrice * (1 - slippagePct / 100), 0.01);
}
}The Data API provides portfolio and trading analytics. This is the same API used by polymarket.com.
Base URL: https://data-api.polymarket.com
// Fetch all active positions for a user
const positions = await fetch(
`https://data-api.polymarket.com/v1/data/user/${safeAddress}/positions`
).then(r => r.json());
// Each position includes:
// - market info (conditionId, question, outcomes)
// - token balances (how many YES/NO shares held)
// - current value based on live prices
// - average entry pricecurl 'https://data-api.polymarket.com/v1/data/user/0xSAFE_ADDRESS/positions'# Activity feed (orders, fills, deposits, withdrawals)
curl 'https://data-api.polymarket.com/v1/data/user/0xSAFE_ADDRESS/activity'
# Closed/settled positions
curl 'https://data-api.polymarket.com/v1/data/user/0xSAFE_ADDRESS/closed-positions'# Total portfolio value
curl 'https://data-api.polymarket.com/v1/data/user/0xSAFE_ADDRESS/value'
# Unrealized P&L
curl 'https://data-api.polymarket.com/v1/data/user/0xSAFE_ADDRESS/upnl'
# User statistics
curl 'https://data-api.polymarket.com/v1/data/user/0xSAFE_ADDRESS/stats'Important: The {address} parameter is always the Safe address, not the EOA.
Markets resolve when the real-world outcome is determined. Understanding the resolution process is important for knowing when positions can be redeemed.
Polymarket uses the UMA Optimistic Oracle — a decentralized system where anyone can propose an outcome and anyone can dispute it:
- An outcome is proposed to the UMA oracle on Polygon
- A challenge period begins (~2 hours if undisputed)
- If disputed, UMA token holders vote on the correct outcome (4-6 days total)
- Once finalized, the market is marked
closed=truein the Gamma API - Winning outcome tokens become redeemable for $1.00 USDC.e each; losing tokens become worthless
| Scenario | Duration |
|---|---|
| Undisputed resolution | ~2 hours |
| Disputed resolution | 4-6 days (includes challenge period + UMA vote) |
// Poll Gamma API for market resolution
const market = await fetch(
`https://gamma-api.polymarket.com/markets/${conditionId}`
).then(r => r.json());
if (market.closed) {
// Market is resolved — check which outcome won
// outcomePrices will show "1" for winner, "0" for loser
const winningIndex = market.outcomePrices.indexOf("1");
const winningTokenId = market.clobTokenIds[winningIndex];
// Proceed to redemption (Phase 6)
}After a market resolves, winning outcome tokens can be redeemed for USDC. USDC can then be transferred out of the Safe.
Use the relay client to execute redemption transactions from the Safe:
import { encodeFunctionData } from "viem";
// ABI for CTF redeemPositions
const ctfRedeemAbi = [{
name: "redeemPositions",
type: "function",
inputs: [
{ name: "collateralToken", type: "address" },
{ name: "parentCollectionId", type: "bytes32" },
{ name: "conditionId", type: "bytes32" },
{ name: "indexSets", type: "uint256[]" },
],
outputs: [],
stateMutability: "nonpayable",
}] as const;
// ABI for Neg Risk Adapter redeemPositions
const negRiskRedeemAbi = [{
name: "redeemPositions",
type: "function",
inputs: [
{ name: "conditionId", type: "bytes32" },
{ name: "amounts", type: "uint256[]" },
],
outputs: [],
stateMutability: "nonpayable",
}] as const;
// Binary market redemption (negRisk = false)
function createBinaryRedeemTx(conditionId: string, outcomeIndex: number) {
const USDC_E = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174";
const CTF = "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045";
const ZERO_BYTES32 = "0x0000000000000000000000000000000000000000000000000000000000000000";
return {
to: CTF,
data: encodeFunctionData({
abi: ctfRedeemAbi,
functionName: "redeemPositions",
args: [USDC_E, ZERO_BYTES32, conditionId, [BigInt(1 << outcomeIndex)]],
}),
value: "0",
};
}
// Neg risk market redemption (negRisk = true)
function createNegRiskRedeemTx(conditionId: string, outcomeIndex: number, shares: number) {
const NEG_RISK_ADAPTER = "0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296";
const amounts = [0n, 0n]; // Two outcomes
amounts[outcomeIndex] = BigInt(Math.floor(shares * 1e6)); // USDC.e has 6 decimals
return {
to: NEG_RISK_ADAPTER,
data: encodeFunctionData({
abi: negRiskRedeemAbi,
functionName: "redeemPositions",
args: [conditionId, amounts],
}),
value: "0",
};
}
// Execute redemption via relay (gasless)
const redeemTx = market.negRisk
? createNegRiskRedeemTx(conditionId, outcomeIndex, shares)
: createBinaryRedeemTx(conditionId, outcomeIndex);
const response = await relayClient.execute([redeemTx], `Redeem position`);
await relayClient.pollUntilState(
response.transactionID,
["STATE_MINED", "STATE_CONFIRMED"],
"STATE_FAILED",
);The CTF contract supports converting between USDC.e and outcome tokens directly, without going through the order book:
- Split: $1 USDC.e → 1 Yes token + 1 No token
- Merge: 1 Yes token + 1 No token → $1 USDC.e
This is useful for acquiring both sides of a position, exiting by merging complementary tokens back to USDC.e, or market making (acquire full sets, sell sides individually).
import { encodeFunctionData } from "viem";
const CTF = "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045";
const USDC_E = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174";
const ZERO_BYTES32 = "0x0000000000000000000000000000000000000000000000000000000000000000";
const splitMergeAbi = [
{
name: "splitPosition",
type: "function",
inputs: [
{ name: "collateralToken", type: "address" },
{ name: "parentCollectionId", type: "bytes32" },
{ name: "conditionId", type: "bytes32" },
{ name: "partition", type: "uint256[]" },
{ name: "amount", type: "uint256" },
],
outputs: [],
stateMutability: "nonpayable",
},
{
name: "mergePositions",
type: "function",
inputs: [
{ name: "collateralToken", type: "address" },
{ name: "parentCollectionId", type: "bytes32" },
{ name: "conditionId", type: "bytes32" },
{ name: "partition", type: "uint256[]" },
{ name: "amount", type: "uint256" },
],
outputs: [],
stateMutability: "nonpayable",
},
] as const;
// Split: Convert 100 USDC.e into 100 Yes + 100 No tokens
const splitTx = {
to: CTF,
data: encodeFunctionData({
abi: splitMergeAbi,
functionName: "splitPosition",
args: [USDC_E, ZERO_BYTES32, conditionId, [1n, 2n], BigInt(100 * 1e6)],
}),
value: "0",
};
// Merge: Convert 100 Yes + 100 No tokens back into 100 USDC.e
const mergeTx = {
to: CTF,
data: encodeFunctionData({
abi: splitMergeAbi,
functionName: "mergePositions",
args: [USDC_E, ZERO_BYTES32, conditionId, [1n, 2n], BigInt(100 * 1e6)],
}),
value: "0",
};
// Execute via relay (gasless)
await relayClient.execute([splitTx], "Split position");Note: For neg risk markets (multi-outcome events), use the Neg Risk Adapter (
0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296) instead of the CTF directly. The adapter handles the shared collateral pool.
To withdraw USDC from the Safe to any address:
const erc20TransferAbi = [{
name: "transfer",
type: "function",
inputs: [
{ name: "to", type: "address" },
{ name: "amount", type: "uint256" },
],
outputs: [{ type: "bool" }],
stateMutability: "nonpayable",
}] as const;
const USDC_E = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174";
// Transfer 100 USDC from Safe to recipient
const transferTx = {
to: USDC_E,
data: encodeFunctionData({
abi: erc20TransferAbi,
functionName: "transfer",
args: [recipientAddress, BigInt(100 * 1e6)], // 100 USDC (6 decimals)
}),
value: "0",
};
const response = await relayClient.execute([transferTx], "Transfer USDC");
await relayClient.pollUntilState(
response.transactionID,
["STATE_MINED", "STATE_CONFIRMED"],
"STATE_FAILED",
);Integrators must use Polymarket's geoblock API to ensure compliance with geographic restrictions. This ensures alternative frontends enforce the same geoblocking as polymarket.com.
// Check if a user should be blocked based on their IP/location
async function checkGeoblock(): Promise<boolean> {
const res = await fetch("https://gamma-api.polymarket.com/geoblock");
const data = await res.json();
return data.blocked; // true if user should be blocked
}Block users from trading if the geoblock API returns blocked: true. This check should be performed before displaying any trading UI.
The CLOB WebSocket provides live order book updates, trade notifications, and user-specific events (fills, cancellations).
import { ClobClient } from "@polymarket/clob-client";
// The ClobClient includes WebSocket support
// Subscribe to market updates and user events via the client's WS methods
const client = new ClobClient(host, 137, signer, creds);For sports markets, live game scores are broadcast via a public WebSocket:
const ws = new WebSocket("wss://sports-api.polymarket.com/ws");
ws.onopen = () => {
// No subscription message needed — all data broadcasts automatically
// Keep-alive: send ping every 5 seconds
setInterval(() => ws.send("ping"), 5000);
};
ws.onmessage = (event) => {
if (event.data === "pong") return;
const data = JSON.parse(event.data);
// {
// slug: "nfl-chiefs-vs-ravens", ← matches Gamma event slug
// live: true,
// ended: false,
// score: { home: 17, away: 10 },
// period: "Q3",
// elapsed: "8:42"
// }
};
ws.onclose = () => {
// Reconnect with exponential backoff
setTimeout(connect, Math.min(1000 * Math.pow(2, attempt), 30000));
};Reconnection pattern (use for all WebSocket connections):
let attempt = 0;
function connect() {
const ws = new WebSocket(url);
ws.onopen = () => { attempt = 0; };
ws.onclose = () => {
const delay = Math.min(1000 * Math.pow(2, attempt), 30000);
attempt++;
setTimeout(connect, delay);
};
ws.onerror = () => ws.close();
}
connect();The Bridge API (Fun.xyz proxy) enables multi-chain deposits into the user's Safe and withdrawals to any supported chain.
Base URL: https://gamma-api.polymarket.com (same host as Gamma, different endpoints)
curl -X POST 'https://gamma-api.polymarket.com/deposit' \
-H 'Content-Type: application/json' \
-d '{"address": "0xUSER_SAFE_ADDRESS"}'Returns deposit addresses for EVM chains (Ethereum, Base, Arbitrum, Optimism), Solana, and Bitcoin. User sends funds to the appropriate address; they are automatically bridged and converted to USDC.e on Polygon.
curl -X POST 'https://gamma-api.polymarket.com/withdraw' \
-H 'Content-Type: application/json' \
-d '{
"address": "0xUSER_SAFE_ADDRESS",
"toChainId": "8453",
"toTokenAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"recipientAddr": "0xDESTINATION_ADDRESS"
}'curl -X POST 'https://gamma-api.polymarket.com/quote' \
-H 'Content-Type: application/json' \
-d '{
"fromAmountBaseUnit": "10000000",
"fromChainId": "137",
"fromTokenAddress": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174",
"recipientAddress": "0x...",
"toChainId": "8453",
"toTokenAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
}'curl 'https://gamma-api.polymarket.com/status/DEPOSIT_ADDRESS'| Chain | Chain ID |
|---|---|
| Ethereum | 1 |
| Optimism | 10 |
| Polygon | 137 |
| Base | 8453 |
| Arbitrum | 42161 |
| Solana | 1151111081099710 |
| Bitcoin | 8253038 |
Use GET /supported-assets for the complete and current list.
When a user's funds originate from a centralized exchange or custodial platform, there are several paths to fund their Polymarket Safe:
| Path | How It Works | When to Use |
|---|---|---|
| CEX → Safe (direct) | CEX withdraws USDC.e on Polygon directly to the user's Safe address | Simplest. Use when CEX supports Polygon USDC.e withdrawals. |
| CEX → Bridge API → Safe | CEX withdraws to any supported chain, then call POST /deposit with the Safe address to get a deposit address on that chain |
Use when CEX doesn't support Polygon or when depositing from non-EVM chains (Solana, Bitcoin). |
| CEX → Self-Custodial Wallet → Safe | CEX withdraws to the self-custodial wallet, which then transfers USDC.e to the Safe on Polygon (direct ERC-20 transfer or via Bridge) | Use when the self-custodial environment manages an intermediate wallet. |
For withdrawals from Polymarket back to the CEX, reverse the flow: transfer USDC from Safe to a destination address (Section 6.3), then deposit into the CEX. Alternatively, use POST /withdraw to bridge directly to the CEX's deposit address on another chain.
| Contract | Address | Purpose |
|---|---|---|
| USDC.e | 0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174 |
Collateral token (6 decimals) |
| CTF | 0x4D97DCd97eC945f40cF65F87097ACe5EA0476045 |
Conditional Token Framework (ERC-1155 outcome tokens) |
| CTF Exchange | 0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E |
Binary market order settlement |
| Neg Risk CTF Exchange | 0xC5d563A36AE78145C45a50134d48A1215220f80a |
Multi-outcome market order settlement |
| Neg Risk Adapter | 0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296 |
Multi-outcome market collateral management |
| Language | Package | Covers |
|---|---|---|
| TypeScript | @polymarket/clob-client |
CLOB REST, WS, order building, HMAC signing |
| TypeScript | @polymarket/builder-relayer-client |
Safe deployment, gasless relay, batched txs |
| TypeScript | @polymarket/builder-signing-sdk |
Builder HMAC authentication |
| TypeScript | @polymarket/order-utils |
Low-level EIP-712 order construction |
| Python | py-clob-client |
CLOB REST, WS, order building |
| Python | py-order-utils |
Order construction |
| Rust | rs-clob-client |
CLOB, WS, Data, Gamma, Bridge, RTDS |
Integrators must use builder codes to attribute orderflow and volume. Builder credentials are passed to both the RelayClient (for Safe operations) and the ClobClient (for order posting).
import { BuilderConfig } from "@polymarket/builder-signing-sdk";
// Same config used for both clients — one set per platform, shared across all users
const builderConfig = new BuilderConfig({
localBuilderCreds: {
key: process.env.POLYMARKET_BUILDER_API_KEY,
secret: process.env.POLYMARKET_BUILDER_SECRET,
passphrase: process.env.POLYMARKET_BUILDER_PASSPHRASE,
},
});
// For relay operations (Safe deploy, approvals, transfers)
const relayClient = new RelayClient(relayUrl, 137, signer, builderConfig);
// For order posting (auto-injects builder headers)
const clobClient = new ClobClient(
clobUrl, 137, signer, apiCreds, 2, safeAddress,
undefined, false, builderConfig
);Builder headers are automatically injected on order submission:
POLY_BUILDER_API_KEY: <your_key>
POLY_BUILDER_PASSPHRASE: <your_passphrase>
POLY_BUILDER_SIGNATURE: HMAC-SHA256(secret, timestamp + method + path + body)
POLY_BUILDER_TIMESTAMP: <unix_timestamp>
# View builder-attributed trades
curl 'https://clob.polymarket.com/builder/trades' \
-H 'POLY-BUILDER-API-KEY: <key>' \
-H 'POLY-BUILDER-SIGNATURE: <sig>' \
-H 'POLY-BUILDER-TIMESTAMP: <ts>' \
-H 'POLY-BUILDER-PASSPHRASE: <pass>'
# Builder leaderboard
curl 'https://data-api.polymarket.com/v1/data/builders/leaderboard'
# Builder volume
curl 'https://data-api.polymarket.com/v1/data/builders/volume'Event (question or topic)
├── Market (tradeable contract)
│ ├── outcome: "Yes" → tokenId → price (implied probability 0.0–1.0)
│ └── outcome: "No" → tokenId → price
└── Market (another outcome, for multi-outcome events)
├── outcome: "Yes" → tokenId
└── outcome: "No" → tokenId
Key rules:
outcomes[i]maps tooutcomePrices[i]maps toclobTokenIds[i]— always index by position- Prices are strings representing implied probabilities (sum to ~1.0 within a market)
conditionIdidentifies the market on-chain (CTF contract)tokenId/clobTokenIdsidentifies outcome positions for CLOB orders
Binary vs Neg Risk:
- Binary (
negRisk: false): Single market, two outcomes (Yes/No), standard CTF Exchange - Neg Risk (
negRisk: true): Multiple markets under one event, shared collateral pool, uses Neg Risk CTF Exchange. The CLOB client handles the routing automatically based on the tokenID.
For a high-level overview of the two credential types (builder vs user), see Authentication Model near the top of this guide.
Three layers of authentication serve different purposes. EIP-712 and HMAC are per-user credentials. Builder HMAC is your per-platform credential.
| Layer | Mechanism | When Used | Who Signs |
|---|---|---|---|
| EIP-712 | Wallet typed data signature | Safe deployment, order signing, API key creation | User's EOA wallet |
| HMAC | HMAC-SHA256(secret, timestamp+method+path+body) |
All authenticated CLOB requests | CLOB API credentials |
| Builder HMAC | Same HMAC scheme, separate credentials | Relay operations, order attribution | Builder credentials |
POLY-ADDRESS: <safe_address>
POLY-API-KEY: <api_key>
POLY-SIGNATURE: base64(HMAC-SHA256(base64_decode(secret), timestamp + method + path + body))
POLY-TIMESTAMP: <unix_timestamp>
POLY-NONCE: <nonce>
POLY-PASSPHRASE: <passphrase>
POLY_BUILDER_API_KEY: <builder_key>
POLY_BUILDER_SIGNATURE: url_safe_base64(HMAC-SHA256(base64_decode(secret), timestamp + method + path + body))
POLY_BUILDER_TIMESTAMP: <unix_timestamp>
POLY_BUILDER_PASSPHRASE: <builder_passphrase>
Use with: GET /events?tag={id}
| Tag | ID |
|---|---|
| Sports (all) | 52 |
| NFL | 95 |
| NBA | 82 |
| MLB | 96 |
| NHL | 97 |
| Soccer | 90 |
| MMA | 91 |
| Tennis | 101 |
| Golf | 103 |
| Boxing | 117 |
| F1 | 98 |
| Cricket | 108 |
| March Madness | 127 |
| Tag | ID |
|---|---|
| Politics | 7 |
| Finance | 71 |
| Crypto | 13 |
| Geopolitics | 137 |
| AI & Tech | 72 |
| Culture | 74 |
| Pop Culture | 43 |
| Climate & Weather | 75 |
| Health | 76 |
| Science | 78 |
| Global Elections | 120 |
Use with: GET /events?seriesId={id}
| League | Series ID |
|---|---|
| NFL Regular Season | 0x2864e60d15b58c4e72c6b97decfdb36ca8eed28d |
| NFL Playoffs | 0xb393f66e2c3d2f1fc3d1e3e3c49c6b3cf02e7aa6 |
| Super Bowl | 0x8f7ebef7b1c4e3b4a1c42fe77d43f5e901c6da97 |
| NBA Regular Season | 0x3d7e5c8f1a6b94e2d0f1c8a5b3e9d7f2a4c6b8e0 |
| NBA Playoffs | 0xa1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0 |
| NBA Finals | 0xf1e2d3c4b5a6f7e8d9c0b1a2f3e4d5c6b7a8f9e0 |
| March Madness | 0x7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b |
| MLB Regular Season | 0x4f5e6d7c8b9a0f1e2d3c4b5a6f7e8d9c0b1a2f3e |
| World Series | 0x2c3b4a5f6e7d8c9b0a1f2e3d4c5b6a7f8e9d0c1b |
| NHL Regular Season | 0x9d8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e |
| Stanley Cup | 0x5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b |
| Premier League | 0x1f2e3d4c5b6a7f8e9d0c1b2a3f4e5d6c7b8a9f0e |
| Champions League | 0x8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f |
| La Liga | 0x6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d |
| UFC | 0x3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c |
| PGA Tour | 0x0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b |


