A Next.js reference implementation for integrators to add Polymarket trading to their platforms. Works with any EIP-6963 compatible browser wallet.
This implementation demonstrates:
- Geoblocking - Enforce Polymarket's geographic restrictions
- Market Discovery - Surface liquid markets via Gamma API
- Wallet Infrastructure - Deploy Polymarket-compatible Safe wallets
- Builder Codes - Attribute orderflow to your integration
- Trading - FAK market orders with automatic slippage
- Fee Collection - Collect integrator fees on market orders
- Position Management - Display user positions via Data API
- Overview
- Quick Start
- Integration Guidelines
- Implementation Details
- Project Structure
- Environment Variables
- Key Dependencies
This repository serves as a technical reference for integrators who want to add Polymarket trading to their platforms. It implements the same patterns used by Polymarket.com, ensuring:
- Seamless user experience - Users can log into Polymarket.com with the same wallet and see identical positions
- Regulatory compliance - Same geoblocking rules as Polymarket.com
- Reliable trading - FAK (Fill-And-Kill) market orders with automatic slippage
- Revenue sharing - Fee collection infrastructure for integrators
-
Builder API Credentials from Polymarket
- Visit
polymarket.com/settings?tab=builderto obtain credentials - You'll need:
API_KEY,SECRET, andPASSPHRASE
- Visit
-
Polygon RPC URL - Any Polygon mainnet RPC
-
Browser Wallet - Any EIP-6963 compatible wallet (MetaMask, Rabby, Phantom, Bitget, OKX, Coinbase, etc.)
npm installCreate .env.local:
# Polygon RPC endpoint
NEXT_PUBLIC_POLYGON_RPC_URL=https://polygon-rpc.com
# Builder credentials (from polymarket.com/settings?tab=builder)
POLYMARKET_BUILDER_API_KEY=your_builder_api_key
POLYMARKET_BUILDER_SECRET=your_builder_secret
POLYMARKET_BUILDER_PASSPHRASE=your_builder_passphrase
# Optional: Integrator fee collection
NEXT_PUBLIC_INTEGRATOR_FEE_ADDRESS=0x... # Your fee receiving wallet
NEXT_PUBLIC_INTEGRATOR_FEE_BPS=50 # Fee in basis points (50 = 0.5%)npm run devIntegrators adhere to the same geoblocking rules as Polymarket.com.
Implementation:
// hooks/useGeoblock.ts
const response = await fetch("https://polymarket.com/api/geoblock");
const { blocked, country, region } = await response.json();
if (blocked) {
// Disable trading UI, show informational message
// Users can still VIEW markets, just not trade
}Key Points:
- Check geoblock status on app load
- Prevent trading session initialization if blocked
- Allow market viewing even when blocked (read-only)
- Display clear messaging about restrictions
Files: hooks/useGeoblock.ts, components/GeoBlockedBanner.tsx, providers/TradingProvider.tsx
Docs: Geographic Restrictions
Use Polymarket's Gamma API /events endpoint to surface liquid markets.
Canonical Pattern:
Polymarket uses hardcoded tag_id values for category filtering:
| Category | tag_id | Description |
|---|---|---|
| Trending | (none) | All markets sorted by volume |
| Politics | 2 | Political markets |
| Finance | 120 | Financial markets |
| Crypto | 21 | Crypto + subcategories (BTC, ETH) |
| Sports | 100639 | Sports games |
| Tech | 1401 | Technology markets |
| Culture | 596 | Entertainment, pop culture |
| Geopolitics | 100265 | Geopolitical events |
Implementation:
// constants/categories.ts - Hardcode official tag IDs
export const CATEGORIES = [
{ id: "trending", label: "Trending", tagId: null },
{ id: "politics", label: "Politics", tagId: 2 },
{ id: "finance", label: "Finance", tagId: 120 },
{ id: "crypto", label: "Crypto", tagId: 21 },
{ id: "sports", label: "Sports", tagId: 100639 },
{ id: "tech", label: "Tech", tagId: 1401 },
{ id: "culture", label: "Culture", tagId: 596 },
{ id: "geopolitics", label: "Geopolitics", tagId: 100265 },
];
// app/api/polymarket/markets/route.ts - Use tag_id for filtering
const url = `https://gamma-api.polymarket.com/events?tag_id=21&related_tags=true&closed=false&order=volume24hr&ascending=false`;API Flow:
Client request: /api/polymarket/markets?tag_id=21
|
Gamma API: /events?tag_id=21&related_tags=true&closed=false
Filtering for Quality:
const validMarkets = markets.filter((market) => {
if (market.acceptingOrders === false) return false;
const liquidity = parseFloat(market.liquidity || "0");
if (liquidity < 5000) return false;
const prices = JSON.parse(market.outcomePrices);
const hasTradeablePrice = prices.some((p) => {
const price = parseFloat(p);
return price >= 0.05 && price <= 0.95;
});
return hasTradeablePrice;
});Files: app/api/polymarket/markets/route.ts, hooks/useMarkets.ts, utils/gamma.ts, constants/categories.ts
Docs: Gamma API Reference
Deploy Polymarket proxy wallets (Safes) on behalf of users and use the relayer for gasless transactions.
Benefits:
- Seamless integration - Users see same positions on Polymarket.com
- Gasless transactions - Via builder relayer
- Efficiency - Batch transactions
The RelayClient handles Safe deployment, token approvals, and CTF operations (splitting, merging, redeeming positions). It requires your builder credentials via a BuilderConfig:
// hooks/useRelayClient.ts
import { BuilderConfig } from "@polymarket/builder-signing-sdk";
import { RelayClient } from "@polymarket/builder-relayer-client";
// Remote signing keeps builder credentials server-side
const builderConfig = new BuilderConfig({
remoteBuilderConfig: {
url: "/api/polymarket/sign", // Your signing endpoint
},
});
const relayClient = new RelayClient(
"https://relayer.polymarket.com",
137, // Polygon chain ID
ethersSigner,
builderConfig
);Safe addresses are deterministic - derived from the user's EOA address. This means you can know the Safe address before it's deployed:
// hooks/useSafeDeployment.ts
import { deriveSafe } from "@polymarket/builder-relayer-client/dist/builder/derive";
import { getContractConfig } from "@polymarket/builder-relayer-client/dist/config";
const config = getContractConfig(137); // Polygon
const safeAddress = deriveSafe(eoaAddress, config.SafeContracts.SafeFactory);For new users, deploy their Safe before they can trade:
// Check if already deployed
const deployed = await relayClient.getDeployed(safeAddress);
if (!deployed) {
// Prompts user to sign - gasless deployment via builder relayer
const response = await relayClient.deploy();
const result = await response.wait();
console.log("Safe deployed at:", result.proxyAddress);
}Files: hooks/useSafeDeployment.ts, hooks/useRelayClient.ts
Docs: Builder Relayer Client
Use builder codes for orderflow attribution and access to the relayer.
Builder credentials should stay server-side. The BuilderConfig supports remote signing, where the client requests signatures from your server:
┌─────────────────────────────────────────────────────────────┐
│ Client (Browser) │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ BuilderConfig({ remoteBuilderConfig: { url: "/sign" }}) ││
│ │ │ ││
│ │ ↓ ││
│ │ SDK needs signature → POST /api/polymarket/sign ││
│ └─────────────────────────────────────────────────────────┘│
└──────────────────────────────│──────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────┐
│ Server (Next.js API Route) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ buildHmacSignature(secret, timestamp, method, path, body)││
│ │ │ ││
│ │ ↓ ││
│ │ Returns: { POLY_BUILDER_SIGNATURE, POLY_BUILDER_TIMESTAMP,│ │
│ │ POLY_BUILDER_API_KEY, POLY_BUILDER_PASSPHRASE } │ │
│ └─────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
// app/api/polymarket/sign/route.ts
import { buildHmacSignature, BuilderApiKeyCreds } from "@polymarket/builder-signing-sdk";
const BUILDER_CREDENTIALS: BuilderApiKeyCreds = {
key: process.env.POLYMARKET_BUILDER_API_KEY!,
secret: process.env.POLYMARKET_BUILDER_SECRET!,
passphrase: process.env.POLYMARKET_BUILDER_PASSPHRASE!,
};
export async function POST(request: NextRequest) {
const { method, path, body } = await request.json();
const timestamp = Date.now().toString();
const signature = buildHmacSignature(
BUILDER_CREDENTIALS.secret,
parseInt(timestamp),
method,
path,
body
);
return NextResponse.json({
POLY_BUILDER_SIGNATURE: signature,
POLY_BUILDER_TIMESTAMP: timestamp,
POLY_BUILDER_API_KEY: BUILDER_CREDENTIALS.key,
POLY_BUILDER_PASSPHRASE: BUILDER_CREDENTIALS.passphrase,
});
}// hooks/useClobClient.ts (and useRelayClient.ts)
const builderConfig = new BuilderConfig({
remoteBuilderConfig: { url: "/api/polymarket/sign" },
});Files: app/api/polymarket/sign/route.ts, hooks/useClobClient.ts, hooks/useRelayClient.ts
Security Note: This reference implementation exposes builder credentials (API key + passphrase) to the client via the /api/polymarket/sign endpoint. For production deployments, implement one of:
- Proxy pattern - Server makes all CLOB/Relay requests, credentials never reach client
- Auth token validation - Require authenticated session before returning credentials
Docs: Builder Program | CLOB Authentication
Each user needs their own CLOB API credentials (separate from your Builder credentials).
Important Distinction:
- Builder credentials - Your integrator credentials for order attribution
- User API credentials - User-specific credentials for placing their orders
Implementation:
Users either have existing credentials (if they've traded before) or need new ones created:
// hooks/useUserApiCredentials.ts
import { ClobClient } from "@polymarket/clob-client";
// Create a temporary client (no credentials needed) to derive/create user creds
const tempClient = new ClobClient(
"https://clob.polymarket.com",
137,
ethersSigner
);
// Try to derive existing credentials first
const existingCreds = await tempClient.deriveApiKey().catch(() => null);
if (existingCreds?.key && existingCreds?.secret && existingCreds?.passphrase) {
// User has traded before - reuse their credentials
return existingCreds;
}
// New user - create fresh credentials (prompts signature)
const newCreds = await tempClient.createApiKey();
return newCreds; // { key, secret, passphrase }Flow:
User connects wallet
↓
[Has existing credentials?]
│
Yes ──┴──> deriveApiKey() - deterministic from signature
│
No
↓
createApiKey() - creates new credentials (prompts signature)
Files: hooks/useUserApiCredentials.ts
Docs: API Keys
Approve Polymarket contracts to spend USDC.e and outcome tokens (ERC-1155).
Required Approvals:
| Token | Spender Contract | Purpose |
|---|---|---|
| USDC.e | CTF Contract | Splitting collateral into outcome tokens |
| USDC.e | Neg Risk Adapter | Neg risk market collateral |
| USDC.e | CTF Exchange | Standard market orders |
| USDC.e | Neg Risk CTF Exchange | Neg risk market orders |
| CTF (ERC-1155) | CTF Exchange | Selling outcome tokens |
| CTF (ERC-1155) | Neg Risk CTF Exchange | Selling neg risk tokens |
| CTF (ERC-1155) | Neg Risk Adapter | Redeeming neg risk positions |
Batched Execution:
All approvals can be batched into a single transaction using the RelayClient:
// utils/approvals.ts
import { OperationType, SafeTransaction } from "@polymarket/builder-relayer-client";
const createAllApprovalTxs = (): SafeTransaction[] => {
const txs: SafeTransaction[] = [];
// USDC.e approvals (ERC-20)
for (const spender of USDC_SPENDERS) {
txs.push({
to: USDC_E_CONTRACT,
operation: OperationType.Call,
data: encodeFunctionData({
abi: erc20Abi,
functionName: "approve",
args: [spender, MAX_UINT256],
}),
value: "0",
});
}
// Outcome token approvals (ERC-1155)
for (const spender of OUTCOME_TOKEN_SPENDERS) {
txs.push({
to: CTF_CONTRACT,
operation: OperationType.Call,
data: encodeFunctionData({
abi: erc1155Abi,
functionName: "setApprovalForAll",
args: [spender, true],
}),
value: "0",
});
}
return txs;
};
// hooks/useTokenApprovals.ts - Execute all approvals in one tx
const approvalTxs = createAllApprovalTxs();
await relayClient.execute(approvalTxs, "Set token approvals");Files: hooks/useTokenApprovals.ts, utils/approvals.ts
Create an authenticated ClobClient for placing orders on behalf of users.
Key Parameters:
// hooks/useClobClient.ts
const clobClient = new ClobClient(
"https://clob.polymarket.com", // CLOB API URL
137, // Chain ID (Polygon)
ethersSigner, // User's ethers signer
userApiCredentials, // { key, secret, passphrase }
2, // signatureType = 2 for Safe funder
safeAddress, // User's Safe (derived from EOA)
undefined, // Placeholder (unused)
false, // Placeholder (unused)
builderConfig // For order attribution
);Parameter Details:
| Parameter | Value | Purpose |
|---|---|---|
signatureType |
2 |
Indicates the signer is a Safe funder (not direct EOA) |
safeAddress |
Derived address | The Safe that holds funds and executes trades |
builderConfig |
Your config | Attaches builder code to all orders |
Files: hooks/useClobClient.ts
Use FAK (Fill-And-Kill) market orders with automatic slippage.
Order Types:
| Type | Use Case | Description |
|---|---|---|
OrderType.FAK |
Market orders | Fill-And-Kill - fills available, cancels rest |
OrderType.GTC |
Limit orders | Good-Til-Cancelled |
Market Order Flow:
For FAK market orders, omit the price field - the SDK calculates the optimal price based on orderbook depth:
// hooks/useClobOrder.ts
const marketOrder: UserMarketOrder = {
tokenID,
amount, // $ amount for BUY, shares for SELL
side,
feeRateBps: 0,
// NOTE: Don't provide 'price', SDK calculates from orderbook
};
const response = await clobClient.createAndPostMarketOrder(
marketOrder,
{ negRisk },
OrderType.FAK
);Error Handling:
FAK orders will fail if there's no liquidity on the orderbook:
if (error.message.includes("no orders found to match")) {
// No buyers/sellers available - market is illiquid
// User should wait or use a limit order instead
}Files: hooks/useClobOrder.ts, constants/trading.ts
Docs: Order Placement | CLOB Client
Collect integrator fees separately after successful market orders.
Flow:
┌─────────────────────────────────────────────────────────┐
│ 1. User places market order │
│ └─> FAK order with automatic slippage │
├─────────────────────────────────────────────────────────┤
│ 2. Poll for order confirmation │
│ └─> Check order status every 500ms (30s timeout) │
├─────────────────────────────────────────────────────────┤
│ 3. After order is MATCHED/FILLED │
│ └─> Execute fee transfer from Safe to integrator │
└─────────────────────────────────────────────────────────┘
Implementation:
// hooks/useOrderStatus.ts - Poll for confirmation
const pollOrderStatus = async (clobClient, orderId) => {
while (Date.now() - startTime < ORDER_POLL_TIMEOUT_MS) {
const order = await clobClient.getOrder(orderId);
if (order.status === "MATCHED") {
return { isFullyFilled: true, sizeFilled: order.size_matched };
}
await sleep(ORDER_POLL_INTERVAL_MS);
}
};
// hooks/useFeeCollection.ts - Transfer fee
const collectFee = async (relayClient, orderValueUsdc) => {
const feeAmount = orderValueUsdc * (INTEGRATOR_FEE_BPS / 10000);
const feeTransferTx = createUsdcTransferTx({
recipient: INTEGRATOR_FEE_ADDRESS,
amount: parseUnits(feeAmount.toFixed(6), 6),
});
await relayClient.execute([feeTransferTx], "Collect integrator fee");
};Configuration:
# .env.local
NEXT_PUBLIC_INTEGRATOR_FEE_ADDRESS=0x... # Your wallet
NEXT_PUBLIC_INTEGRATOR_FEE_BPS=50 # 0.5% feeFiles: hooks/useOrderStatus.ts, hooks/useFeeCollection.ts, hooks/useClobOrder.ts, constants/config.ts
Docs: CLOB Client
Use Polymarket's Data API for position tracking.
// hooks/useUserPositions.ts
const DATA_API = "https://data-api.polymarket.com";
const fetchPositions = async (safeAddress: string) => {
const response = await fetch(
`${DATA_API}/positions?user=${safeAddress}`
);
return response.json();
};Benefits:
- Same API as Polymarket.com
- Highly available
Files: hooks/useUserPositions.ts
Docs: Data API Reference
Note for Production: Contact Polymarket to whitelist your order-posting servers. This ensures:
- No rate limiting from general order posting limits
- Reliable order execution during high-volume periods
┌─────────────────────────────────────────┐
│ WagmiProvider (v3) │ ← Wallet connection
│ ┌───────────────────────────────────┐ │
│ │ QueryProvider │ │ ← React Query
│ │ ┌─────────────────────────────┐ │ │
│ │ │ WalletProvider │ │ │ ← Ethers + Viem
│ │ │ ┌───────────────────────┐ │ │ │
│ │ │ │ TradingProvider │ │ │ │ ← Geoblock + Trading
│ │ │ │ App Components │ │ │ │
│ │ │ └───────────────────────┘ │ │ │
│ │ └─────────────────────────────┘ │ │
│ └───────────────────────────────────┘ │
└─────────────────────────────────────────┘
User Connects Wallet
↓
Check Geoblock
↓
[BLOCKED?] ──Yes──> Show Banner, Disable Trading
│
No
↓
┌────────────────────────────────────────────────────┐
│ Initialize Trading Session │
├────────────────────────────────────────────────────┤
│ 1. Initialize RelayClient (builder config) │
│ 2. Derive Safe address from EOA │
│ 3. Deploy Safe if needed │
│ 4. Get/Create User API Credentials │
│ 5. Set token approvals (batch) │
│ 6. Initialize authenticated ClobClient │
└────────────────────────────────────────────────────┘
↓
Ready to Trade
↓
┌────────────────────────────────────────────────────┐
│ Market Order Flow │
├────────────────────────────────────────────────────┤
│ 1. Get market price + calculate slippage │
│ 2. Place FAK order │
│ 3. Poll for order confirmation │
│ 4. If filled: collect integrator fee │
│ 5. Update UI │
└────────────────────────────────────────────────────┘
wallet-integration/
├── app/
│ ├── api/
│ │ └── polymarket/
│ │ ├── sign/route.ts # Remote signing endpoint
│ │ └── markets/route.ts # Gamma API proxy
│ ├── layout.tsx
│ └── page.tsx
│
├── providers/
│ ├── index.tsx # Provider composition
│ ├── WagmiProvider.tsx # Wagmi v3 config
│ ├── WalletProvider.tsx # Wallet abstraction
│ └── TradingProvider.tsx # Trading + geoblock state
│
├── hooks/
│ ├── useGeoblock.ts # Geoblock API check
│ ├── useMarkets.ts # Gamma API market discovery
│ ├── useTradingSession.ts # Session orchestration
│ ├── useRelayClient.ts # RelayClient init
│ ├── useSafeDeployment.ts # Safe deployment
│ ├── useTokenApprovals.ts # Token approvals
│ ├── useClobClient.ts # Authenticated CLOB client
│ ├── useClobOrder.ts # Order placement + fee collection
│ ├── useOrderStatus.ts # Order confirmation polling
│ ├── useFeeCollection.ts # Fee transfer logic
│ └── useUserPositions.ts # Position tracking
│
├── components/
│ ├── GeoBlockedBanner.tsx # Geoblock UI
│ ├── Header/ # Wallet connection
│ ├── TradingSession/ # Session UI
│ └── Trading/
│ └── Markets/
│ ├── CategoryTabs.tsx # Category navigation
│ ├── MarketCard.tsx # Market display
│ └── index.tsx # Markets list
│
├── constants/
│ ├── api.ts # API URLs
│ ├── config.ts # Chain, fees, session
│ ├── trading.ts # Slippage, polling
│ ├── categories.ts # Market categories
│ ├── tokens.ts # Contract addresses
│ └── validation.ts # Order validation
│
└── utils/
├── approvals.ts # Approval utilities
├── transfer.ts # USDC transfer utils
└── session.ts # Session persistence
# Required
NEXT_PUBLIC_POLYGON_RPC_URL=https://polygon-rpc.com
POLYMARKET_BUILDER_API_KEY=your_key
POLYMARKET_BUILDER_SECRET=your_secret
POLYMARKET_BUILDER_PASSPHRASE=your_passphrase
# Optional: Fee Collection
NEXT_PUBLIC_INTEGRATOR_FEE_ADDRESS=0x...
NEXT_PUBLIC_INTEGRATOR_FEE_BPS=50| Package | Purpose |
|---|---|
@polymarket/clob-client |
Order placement, credentials |
@polymarket/builder-relayer-client |
Safe deployment, approvals |
@polymarket/builder-signing-sdk |
Builder HMAC signatures |
wagmi |
Wallet connection |
viem |
Ethereum interactions |
ethers |
Signing (Polymarket SDK compat) |
@tanstack/react-query |
Server state |
next |
Framework |
Questions? Email builder@polymarket.com
MIT