Skip to content

ayv8er/integration-guides

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 

Repository files navigation

Polymarket Integration Guide

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.

Table of Contents


Architecture Overview

┌──────────────────────────────────────────────────────────────────────┐
│                     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.

Authentication Model

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.

Integration Architectures

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)

Architecture A: Non-Custodial Dapp

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.

Architecture B: CEX / Self-Custodial Pattern

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.

CEX / Self-Custodial Architecture

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.

Architecture C: Direct CEX Integration

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.

Direct CEX Architecture

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.

Architecture D: Omnibus Account Model

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.

Omnibus Account Model

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.


Integration Checklist

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

Prerequisites

Install Client Libraries

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 construction

Obtain Builder Credentials (Platform-Level Auth)

Contact 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.

Environment Variables

# 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_PHRASE

Phase 1: Wallet Infrastructure

The 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).

Why a Safe?

  • 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.)

Backend Key Management (CEX / Exchange Pattern)

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.

Lifecycle Sequence

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

1.1 Initialize the Relay Client

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
);

1.2 Derive & Deploy the Safe

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);
}

1.3 Token Approvals

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,
);

Checking Existing Approvals

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;

1.4 Derive CLOB API Credentials (Per-User Auth)

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:

  1. deriveApiKey() sends a GET request to /auth/derive-api-key on the CLOB server
  2. 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
  3. The CLOB server verifies the signature and returns HMAC credentials (key, secret, passphrase) tied to that wallet
  4. 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_PASSPHRASE

Key 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.

1.5 Session Persistence

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;
}

Phase 2: Market Discovery (Gamma API)

The Gamma API is the public API for discovering markets. No authentication required.

Base URL: https://gamma-api.polymarket.com

2.1 Fetch Active Events

# 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();
}

2.2 Filter by Category

# 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.

2.3 Parse Market Data

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],
    },
  };
}

2.4 Liquidity & Volume Filtering

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 volume24hr descending 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=false to only show tradeable markets

Phase 3: Trading via CLOB

3.1 Initialize the CLOB Client

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.

3.2 Get Prices & Order Book

// 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"

3.3 Place Market Orders (Recommended)

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

3.4 Place 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.

3.5 Cancel Orders

// 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();

3.6 Batch Order Placement

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

3.7 Poll Order Status & Settlement

// 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 settlement

Order 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.

3.8 Slippage Guidance

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);
  }
}

Phase 4: Position Management (Data API)

The Data API provides portfolio and trading analytics. This is the same API used by polymarket.com.

Base URL: https://data-api.polymarket.com

4.1 Get User Positions

// 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 price
curl 'https://data-api.polymarket.com/v1/data/user/0xSAFE_ADDRESS/positions'

4.2 Activity & Trade History

# 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'

4.3 Portfolio Value & P&L

# 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.


Phase 5: Market Resolution

Markets resolve when the real-world outcome is determined. Understanding the resolution process is important for knowing when positions can be redeemed.

How Resolution Works

Polymarket uses the UMA Optimistic Oracle — a decentralized system where anyone can propose an outcome and anyone can dispute it:

  1. An outcome is proposed to the UMA oracle on Polygon
  2. A challenge period begins (~2 hours if undisputed)
  3. If disputed, UMA token holders vote on the correct outcome (4-6 days total)
  4. Once finalized, the market is marked closed=true in the Gamma API
  5. 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)

Checking Resolution Status

// 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)
}

Phase 6: Redemption, Split/Merge & Transfers

After a market resolves, winning outcome tokens can be redeemed for USDC. USDC can then be transferred out of the Safe.

6.1 Redeem Winning Positions

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",
);

6.2 Split & Merge Positions

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.

6.3 Transfer USDC from Safe

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",
);

Phase 7: Geoblocking

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.


Phase 8: Real-Time Data (WebSockets)

8.1 CLOB WebSocket — Order Book & Trades

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);

8.2 Sports WebSocket — Live Scores

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();

Phase 9: Deposits & Withdrawals (Bridge API)

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)

Create Deposit Addresses

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.

Withdraw to Another Chain

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"
  }'

Get a Quote

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"
  }'

Track Status

curl 'https://gamma-api.polymarket.com/status/DEPOSIT_ADDRESS'

Supported Chains

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.

CEX Funding Flows

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 Addresses

Polygon Mainnet (Chain ID: 137)

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

Client Libraries

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

Builder Codes

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>

Builder Volume Tracking

# 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'

Data Model Reference

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 to outcomePrices[i] maps to clobTokenIds[i] — always index by position
  • Prices are strings representing implied probabilities (sum to ~1.0 within a market)
  • conditionId identifies the market on-chain (CTF contract)
  • tokenId / clobTokenIds identifies 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.

Authentication Reference

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

HMAC Headers (CLOB)

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>

Builder HMAC Headers (Relay + Order Attribution)

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>

Appendix: Tag & Series IDs

Category Tags

Use with: GET /events?tag={id}

Sports

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

Non-Sports

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

League Series IDs

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

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors