Skip to main content
Account linking creates Polymarket CLOB API credentials for a user’s wallet. This is a one-time setup required before placing orders. After linking, credentials should be stored in your database for subsequent order placement.

Overview

The linking flow has two steps:
  1. Prepare - Server generates an EIP-712 payload for the user to sign
  2. Complete - User signs the payload, server derives Polymarket credentials
┌─────────────────────────────────────────────────────────────────────┐
│  Your App                                          Dome Server      │
│                                                                     │
│  POST /v1/polymarket/link-prepare ──────────────────────►           │
│  { walletAddress, walletType }                                      │
│                                                                     │
│                                     ◄─────────────────────────────  │
│       { sessionId, eip712Payload, safeInfo? }                       │
│                                                                     │
│  [User signs EIP-712 payload in wallet]                             │
│                                                                     │
│  POST /v1/polymarket/link-complete ─────────────────────►           │
│  { sessionId, signature }                                           │
│                                                                     │
│                                     ◄─────────────────────────────  │
│  { credentials, signerAddress, safeAddress? }                       │
└─────────────────────────────────────────────────────────────────────┘

When to Use Direct Endpoints vs SDK

Use CaseRecommendation
Server-side with full wallet accessSDK linkUser() method
TEE/Enclave environmentsDirect endpoints
Mobile with on-device signing (Privy)Direct endpoints
Custom signing workflowsDirect endpoints

SDK Examples

The SDK handles the two-step flow automatically with a single linkUser() call.

EOA Wallet (Privy Embedded)

import { PolymarketRouter, createPrivySignerFromEnv } from '@dome-api/sdk';

const router = new PolymarketRouter({
  chainId: 137,
  apiKey: process.env.DOME_API_KEY,
});

// Create signer for Privy embedded wallet
const signer = createPrivySignerFromEnv(walletId, walletAddress);

// Link user - handles prepare + sign + complete internally
const credentials = await router.linkUser({
  userId: 'user-123',
  signer,
  walletType: 'eoa',
});

// Store these credentials in your database
console.log('API Key:', credentials.apiKey);
console.log('API Secret:', credentials.apiSecret);
console.log('Passphrase:', credentials.apiPassphrase);

Safe Wallet (External Wallets)

import { PolymarketRouter } from '@dome-api/sdk';

const router = new PolymarketRouter({
  chainId: 137,
  apiKey: process.env.DOME_API_KEY,
});

// Create signer for external wallet (MetaMask, Rabby, etc.)
const signer = {
  getAddress: async () => walletAddress,
  signTypedData: async (payload) => {
    return await externalWallet.signTypedData(payload);
  },
};

// Link user with Safe wallet
const result = await router.linkUser({
  userId: 'user-123',
  signer,
  walletType: 'safe',
  autoDeploySafe: true,
});

// Store credentials and Safe address
console.log('API Key:', result.credentials.apiKey);
console.log('Safe Address:', result.safeAddress);

Direct Endpoint Examples

Use the direct endpoints when you need control over the signing step, such as in TEE environments or mobile apps with on-device signing.

Endpoint URLs

EnvironmentBase URL
Productionhttps://api.domeapi.io/v1/polymarket
Developmenthttp://localhost:3000

Headers

All requests require:
Content-Type: application/json

Step 1: Prepare Session

POST /v1/polymarket/link-prepare Request the EIP-712 payload that the user needs to sign.
curl -X POST https://api.domeapi.io/v1/polymarket/link-prepare \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "method": "linkPrepare",
    "id": "req-1",
    "params": {
      "walletAddress": "0x1234567890123456789012345678901234567890",
      "walletType": "eoa"
    }
  }'
Request Parameters:
ParameterTypeRequiredDefaultDescription
walletAddressstringYes-User’s EOA wallet address (0x…)
walletType"eoa" | "safe"No"eoa"Wallet type
autoDeploySafebooleanNotrueAuto-deploy Safe if not deployed (only for walletType: "safe")
chainIdnumberNo137Chain ID (137 = Polygon mainnet)
Success Response:
{
  "jsonrpc": "2.0",
  "id": "req-1",
  "result": {
    "sessionId": "550e8400-e29b-41d4-a716-446655440000",
    "expiresAt": 1704067800,
    "eip712Payload": {
      "domain": {
        "name": "ClobAuthDomain",
        "version": "1",
        "chainId": 137
      },
      "types": {
        "ClobAuth": [
          { "name": "address", "type": "address" },
          { "name": "timestamp", "type": "string" },
          { "name": "nonce", "type": "uint256" },
          { "name": "message", "type": "string" }
        ]
      },
      "primaryType": "ClobAuth",
      "message": {
        "address": "0x1234567890123456789012345678901234567890",
        "timestamp": "1704067200",
        "nonce": 0,
        "message": "This message attests that I control the given wallet"
      }
    }
  }
}
Safe Wallet Response (includes safeInfo):
{
  "jsonrpc": "2.0",
  "id": "req-1",
  "result": {
    "sessionId": "550e8400-e29b-41d4-a716-446655440000",
    "expiresAt": 1704067800,
    "eip712Payload": { ... },
    "safeInfo": {
      "safeAddress": "0xABCD...",
      "isDeployed": false,
      "wasDeployed": false,
      "hasAllowances": false,
      "allowancesMissing": ["CTF_EXCHANGE", "NEG_RISK_CTF_EXCHANGE", "NEG_RISK_ADAPTER"]
    }
  }
}

Step 2: Sign the EIP-712 Payload

Sign the eip712Payload using your wallet. The exact method depends on your environment:
// In Privy TEE environment
const signature = await privy.signTypedData({
  walletId,
  typedData: eip712Payload,
});

Step 3: Complete Session

POST /v1/polymarket/link-complete Submit the signature to derive Polymarket credentials.
curl -X POST https://api.domeapi.io/v1/polymarket/link-complete \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "method": "linkComplete",
    "id": "req-2",
    "params": {
      "sessionId": "550e8400-e29b-41d4-a716-446655440000",
      "signature": "0x1234...abcd"
    }
  }'
Request Parameters:
ParameterTypeRequiredDescription
sessionIdstringYesSession ID from /v1/polymarket/link-prepare response
signaturestringYesEIP-712 signature (0x-prefixed hex)
Success Response:
{
  "jsonrpc": "2.0",
  "id": "req-2",
  "result": {
    "success": true,
    "credentials": {
      "apiKey": "ak_...",
      "apiSecret": "as_...",
      "apiPassphrase": "ap_..."
    },
    "signerAddress": "0x1234567890123456789012345678901234567890"
  }
}
Safe Wallet Response (includes safeAddress):
{
  "jsonrpc": "2.0",
  "id": "req-2",
  "result": {
    "success": true,
    "credentials": {
      "apiKey": "ak_...",
      "apiSecret": "as_...",
      "apiPassphrase": "ap_..."
    },
    "signerAddress": "0x1234567890123456789012345678901234567890",
    "safeAddress": "0xABCD..."
  }
}

Set Allowances (Safe Wallets Only)

POST /v1/polymarket/link-set-allowances For Safe wallets, USDC allowances must be set before trading. If /v1/polymarket/link-prepare returns hasAllowances: false in the safeInfo, call this endpoint to set the required approvals.
This endpoint reuses the sessionId from a completed /v1/polymarket/link-complete call. No additional signature is required - the session already proves ownership of the EOA wallet. Call this immediately after /v1/polymarket/link-complete if allowances are missing.
The session is available for 30 minutes after /v1/polymarket/link-complete. If the session expires, you’ll need to restart the linking flow.
curl -X POST https://api.domeapi.io/v1/polymarket/link-set-allowances \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "method": "setAllowances",
    "id": "req-3",
    "params": {
      "sessionId": "550e8400-e29b-41d4-a716-446655440000"
    }
  }'
Request Parameters:
ParameterTypeRequiredDefaultDescription
sessionIdstringYes-Session ID from a completed /v1/polymarket/link-complete call
chainIdnumberNo137Chain ID (137 = Polygon mainnet)
Success Response:
{
  "jsonrpc": "2.0",
  "id": "req-3",
  "result": {
    "success": true,
    "safeAddress": "0xABCD...",
    "allowancesSet": ["CTF_EXCHANGE", "NEG_RISK_CTF_EXCHANGE", "NEG_RISK_ADAPTER"],
    "transactionId": "0x..."
  }
}
Response when allowances already set:
{
  "jsonrpc": "2.0",
  "id": "req-3",
  "result": {
    "success": true,
    "safeAddress": "0xABCD...",
    "allowancesSet": []
  }
}

Complete Direct Endpoint Example

Full example for TEE/mobile environments:
// 1. Prepare - get EIP-712 payload
const prepareResponse = await fetch('https://api.domeapi.io/v1/polymarket/link-prepare', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    jsonrpc: '2.0',
    method: 'linkPrepare',
    id: crypto.randomUUID(),
    params: {
      walletAddress: userWalletAddress,
      walletType: 'eoa',
    },
  }),
});

const prepareData = await prepareResponse.json();

if (prepareData.error) {
  throw new Error(`Prepare failed: ${prepareData.error.message}`);
}

const { sessionId, eip712Payload } = prepareData.result;

// 2. Sign - user signs in their wallet (TEE, mobile, etc.)
const signature = await signEip712InTEE(eip712Payload);

// 3. Complete - exchange signature for credentials
const completeResponse = await fetch('https://api.domeapi.io/v1/polymarket/link-complete', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    jsonrpc: '2.0',
    method: 'linkComplete',
    id: crypto.randomUUID(),
    params: {
      sessionId,
      signature,
    },
  }),
});

const completeData = await completeResponse.json();

if (completeData.error) {
  throw new Error(`Complete failed: ${completeData.error.message}`);
}

const { credentials, signerAddress } = completeData.result;

// 4. Store credentials for future order placement
await database.savePolymarketCredentials(userId, {
  apiKey: credentials.apiKey,
  apiSecret: credentials.apiSecret,
  apiPassphrase: credentials.apiPassphrase,
  signerAddress,
});

console.log('Account linked successfully!');

Health Check

GET /v1/polymarket/link-health Check the link service status.
curl https://api.domeapi.io/v1/polymarket/link-health
Response:
{
  "status": "ok",
  "service": "link",
  "region": "us-east-1",
  "activeSessions": 5,
  "timestamp": 1704067200000
}

Error Handling

Error Codes

CodeNameDescription
-32600INVALID_REQUESTMalformed request or validation error
-32602INVALID_PARAMSInvalid parameters
2001INVALID_WALLET_ADDRESSInvalid wallet address format
2002SESSION_NOT_FOUNDSession ID not found
2003SESSION_EXPIREDSession expired (10 min incomplete, 30 min after completion)
2004INVALID_SIGNATURESignature verification failed
2005CREDENTIAL_DERIVATION_FAILEDFailed to derive credentials from Polymarket
2006SAFE_DEPLOYMENT_FAILEDSafe wallet deployment failed
2007SAFE_NOT_DEPLOYEDSafe wallet not deployed and autoDeploySafe=false
2009ALLOWANCE_SET_FAILEDFailed to set allowances on Safe wallet

Error Response Example

{
  "jsonrpc": "2.0",
  "id": "req-1",
  "error": {
    "code": 2003,
    "message": "Session expired",
    "data": {
      "reason": "Session expired",
      "sessionId": "550e8400-e29b-41d4-a716-446655440000"
    }
  }
}

Handling Errors

const response = await fetch('https://api.domeapi.io/v1/polymarket/link-complete', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ ... }),
});

const data = await response.json();

if (data.error) {
  switch (data.error.code) {
    case 2002:
    case 2003:
      // Session expired or not found - restart flow
      await startLinkFlow();
      break;
    case 2004:
      // Invalid signature - user may have signed wrong data
      console.error('Signature verification failed');
      break;
    case 2005:
      // Polymarket API error - retry or contact support
      console.error('Credential derivation failed:', data.error.data?.reason);
      break;
    default:
      console.error('Link error:', data.error.message);
  }
}

Session Lifecycle

  • Incomplete sessions expire after 10 minutes
  • Completed sessions are kept for 30 minutes (for /v1/polymarket/link-set-allowances)
  • Each session can only be completed once via /v1/polymarket/link-complete
  • After successful /v1/polymarket/link-complete, the session remains available for /v1/polymarket/link-set-allowances
  • If a session expires, call /v1/polymarket/link-prepare again to start a new session

Security Considerations

  1. Store credentials securely - Encrypt apiSecret and apiPassphrase at rest
  2. Session IDs are single-use - Cannot replay /v1/polymarket/link-complete with same sessionId
  3. Signature verification - Server verifies the signature matches the wallet address
  4. No private keys transmitted - Only the signature is sent to the server

Next Steps

After linking, use the credentials to place orders:
const order = await router.placeOrder(
  {
    userId: 'user-123',
    marketId: '104173557214744537570424345347209544585775842950109756851652855913015295701992',
    side: 'buy',
    size: 100,
    price: 0.50,
    signer,
  },
  credentials
);
Or call the placeOrder endpoint directly with the credentials.