Build Polymarket Trading Apps with Privy + Safe

Learn how to build Polymarket trading apps using Privy embedded wallets and Safe (Gnosis) smart accounts. Gasless trading, email login, builder attribution explained.

What Is This?

Building a prediction market trading app used to mean asking users to install MetaMask, write down seed phrases, and fund wallets with ETH for gas. Terrible UX. Users bounce.

Polymarket's Privy + Safe integration changes the game. Users sign up with their email, get an invisible embedded wallet, and trade gaslessly. No extensions. No seed phrases. Just prediction markets.

This tutorial walks through Polymarket's official example repo so you can build the same experience into your own apps.

Why Privy + Safe?

Three pieces working together:

Privy → Web2-Style Auth

Users log in with email. Behind the scenes, Privy creates an EOA (externally owned account) that the user controls through their login session. No browser extension needed.

Safe → Smart Account

A Gnosis Safe proxy wallet is deployed for each user, controlled by their Privy EOA. This is where USDC.e and outcome tokens live. The Safe address is deterministic—same EOA always gets the same Safe.

Builder Relayer → Gasless

Polymarket's builder relayer sponsors gas for Safe deployments, token approvals, and order execution. Users never need ETH or MATIC. Your builder credentials get attribution for orders routed through your app.

How It Fits Together

User signs in with email
         ↓
    [Privy Auth]
         ↓
   EOA embedded wallet (controlled via login session)
         ↓
┌────────────────────────────────────────────────┐
│  First-Time Setup (one-time per user)          │
├────────────────────────────────────────────────┤
│  1. Derive Safe address from EOA               │
│  2. Deploy Safe via RelayClient (gasless)      │
│  3. Get User API Credentials from CLOB         │
│  4. Batch approve tokens (7 approvals)         │
└────────────────────────────────────────────────┘
         ↓
   Authenticated ClobClient
         ↓
   Place orders (gasless, builder attribution)

What You Need

  • Builder API Credentials — Get these from polymarket.com/settings?tab=builder. You'll get an API key, secret, and passphrase.
  • Privy App ID — Create an app at privy.io. Free tier works fine for testing.
  • Polygon RPC URL — Alchemy, Infura, or any public Polygon RPC.
  • Node.js 18+ and familiarity with Next.js/React.

Quick Start

# Clone the example
git clone https://github.com/Polymarket/privy-safe-builder-example
cd privy-safe-builder-example

# Install dependencies
npm install

# Create .env.local
cat > .env.local << 'EOF'
NEXT_PUBLIC_POLYGON_RPC_URL=https://polygon-rpc.com
NEXT_PUBLIC_PRIVY_APP_ID=your_privy_app_id

POLYMARKET_BUILDER_API_KEY=your_builder_api_key
POLYMARKET_BUILDER_SECRET=your_builder_secret
POLYMARKET_BUILDER_PASSPHRASE=your_builder_passphrase
EOF

# Run it
npm run dev

Open localhost:3000 and you should see the demo app. Log in with email, and you're trading on Polymarket.

The Key Piece: Remote Signing

Your builder secret never touches the client. Instead, there's an API route that generates HMAC signatures server-side:

// app/api/polymarket/sign/route.ts
import { buildHmacSignature } from "@polymarket/builder-signing-sdk";

const BUILDER_CREDENTIALS = {
  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 sigTimestamp = Date.now().toString();

  const signature = buildHmacSignature(
    BUILDER_CREDENTIALS.secret,
    parseInt(sigTimestamp),
    method,
    path,
    body
  );

  return NextResponse.json({
    POLY_BUILDER_SIGNATURE: signature,
    POLY_BUILDER_TIMESTAMP: sigTimestamp,
    POLY_BUILDER_API_KEY: BUILDER_CREDENTIALS.key,
    POLY_BUILDER_PASSPHRASE: BUILDER_CREDENTIALS.passphrase,
  });
}

The CLOB client and RelayClient both use this endpoint to sign requests. Your secret stays server-side where it belongs.

Deploying the Safe

Each user's Safe address is deterministic—derived from their Privy EOA:

import { deriveSafe } from "@polymarket/builder-relayer-client/dist/builder/derive";
import { getContractConfig } from "@polymarket/builder-relayer-client/dist/config";

// Same EOA always gets same Safe address
const config = getContractConfig(137); // Polygon
const safeAddress = deriveSafe(eoaAddress, config.SafeContracts.SafeFactory);

// Check if already deployed
const deployed = await relayClient.getDeployed(safeAddress);

// Deploy if needed (gasless)
if (!deployed) {
  const response = await relayClient.deploy();
  const result = await response.wait();
  console.log("Safe deployed at:", result.proxyAddress);
}

The Safe is where all the action happens. USDC.e goes in, outcome tokens come out. The Privy EOA is just the controller.

Token Approvals (One-Time)

Before trading, the Safe needs to approve multiple contracts. Polymarket has both regular markets and "negative risk" markets, each with their own contracts:

USDC.e (ERC-20) approvals:

  • CTF Contract — manages outcome tokens
  • CTF Exchange — regular binary markets
  • Neg Risk CTF Exchange — negative risk markets
  • Neg Risk Adapter — converts between market types

Outcome tokens (ERC-1155) approvals:

  • CTF Exchange
  • Neg Risk CTF Exchange
  • Neg Risk Adapter
// Batch all 7 approvals in one transaction
const approvalTxs = createAllApprovalTxs();

const response = await relayClient.execute(
  approvalTxs,
  "Set all token approvals for trading"
);

await response.wait();
// Done. User signed once, all approvals set.

Getting User API Credentials

Users need their own API credentials to place orders. Create a temporary CLOB client to derive or create them:

import { ClobClient } from "@polymarket/clob-client";

// Temporary client (no creds yet)
const tempClient = new ClobClient(
  "https://clob.polymarket.com",
  137,
  ethersSigner // From Privy
);

// Try to derive existing creds (returning user)
let creds = await tempClient.deriveApiKey().catch(() => null);

// If that fails, create new ones (new user)
if (!creds?.key) {
  creds = await tempClient.createApiKey();
}

// Store for session (NOT localStorage in production!)
// creds = { key, secret, passphrase }

Heads up: The demo stores creds in localStorage for simplicity. In production, use httpOnly cookies or server-side sessions. XSS vulnerabilities could expose these credentials.

Placing Orders

With everything set up, create the authenticated CLOB client and start trading:

import { ClobClient } from "@polymarket/clob-client";
import { BuilderConfig } from "@polymarket/builder-signing-sdk";

const builderConfig = new BuilderConfig({
  remoteBuilderConfig: { url: "/api/polymarket/sign" },
});

const clobClient = new ClobClient(
  "https://clob.polymarket.com",
  137,
  signer,
  userApiCredentials,    // { key, secret, passphrase }
  2,                     // signatureType = EOA + Safe proxy
  safeAddress,           // The Safe that holds funds
  undefined,
  false,
  builderConfig          // For builder attribution
);

// Create and submit an order
const order = {
  tokenID: "0x...",      // Outcome token
  price: 0.65,           // 65 cents
  size: 10,              // 10 shares
  side: "BUY",
  feeRateBps: 0,
  expiration: 0,         // Good-til-cancel
  taker: "0x0000000000000000000000000000000000000000",
};

const response = await clobClient.createAndPostOrder(
  order,
  { negRisk: false },
  OrderType.GTC
);

console.log("Order placed:", response.orderID);

The Orchestration: useTradingSession

All of this is coordinated by the useTradingSession hook. It handles the entire lifecycle:

  1. Initialize RelayClient with builder config
  2. Derive Safe address from Privy EOA
  3. Check if Safe is deployed → deploy if needed
  4. Get/derive User API Credentials
  5. Check token approvals → batch approve if needed
  6. Save session state
  7. Initialize authenticated ClobClient
const {
  tradingSession,
  currentStep,           // "deploying_safe" | "setting_approvals" | etc.
  initializeTradingSession,
  endTradingSession,
  relayClient,
  isTradingSessionComplete,
} = useTradingSession();

// When user logs in
useEffect(() => {
  if (isLoggedIn && !tradingSession) {
    initializeTradingSession();
  }
}, [isLoggedIn]);

Project Structure

polymarket-privy-safe/
├── app/
│   ├── api/polymarket/sign/route.ts  # Remote signing endpoint
│   └── page.tsx                       # Main UI
├── hooks/
│   ├── useTradingSession.ts           # Orchestrates everything
│   ├── useRelayClient.ts              # Safe deployment & approvals
│   ├── useSafeDeployment.ts           # Safe derivation & deploy
│   ├── useUserApiCredentials.ts       # CLOB credential management
│   ├── useTokenApprovals.ts           # Token approval logic
│   ├── useClobClient.ts               # Authenticated CLOB client
│   └── useClobOrder.ts                # Order placement
├── providers/
│   ├── WalletProvider.tsx             # Privy setup
│   └── TradingProvider.tsx            # Trading context
└── utils/
    ├── session.ts                     # Session persistence
    └── approvals.ts                   # Approval transaction builders

Start by reading useTradingSession.ts—it shows how everything connects.

Gotchas & Tips

Safe Deployment Can Take 30+ Seconds

Show a loading state. Users are patient if you tell them what's happening.

USDC.e Balance Check

Users need USDC.e in their Safe (not their EOA) to trade. Build a clear funding flow.

Signature Type = 2

When initializing ClobClient, signature type 2 means "EOA associated with a Safe proxy." This is critical for order signing.

Session Persistence

The demo uses localStorage. For production, implement proper session management—returning users shouldn't have to wait for Safe deployment again.

Builder Credentials Exposure

The demo's /api/polymarket/sign returns API key and passphrase to the client. For production, implement a proxy pattern where all CLOB requests go through your server.

Common Issues

"Privy authentication failed"

Check your NEXT_PUBLIC_PRIVY_APP_ID. Make sure your domain is allowlisted in the Privy dashboard.

"Safe deployment failed"

Verify builder credentials. Check that the remote signing endpoint is working (curl -X POST localhost:3000/api/polymarket/sign).

Orders not appearing

Wait a few seconds for CLOB sync. Make sure the Safe has USDC.e. Check that approvals were set correctly.

Ship It

The Privy + Safe stack removes all the crypto friction. Users don't need to know what a Safe is or that they have an EOA. They sign in with email and trade.

Clone the repo, get your builder credentials, and start building. The example code covers the hard parts—wallet provisioning, Safe deployment, batch approvals, gasless execution. You just need to add your product logic.

Explore More