Skip to main content
Beta - Use Only If NecessaryThese endpoints are in beta and still being tested. For most use cases, we strongly recommend using the SDK’s linkUser() method instead, which handles the complex signing logic automatically.Only use these direct endpoints if you have specific requirements like TEE/enclave environments, mobile on-device signing, or custom signing workflows where the SDK cannot be used.
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 for EOA wallets, and three steps for Safe wallets that need allowances:
  1. Prepare - Server generates an EIP-712 payload for the user to sign
  2. Complete - User signs the payload, server derives Polymarket credentials
  3. Set Allowances (Safe wallets only) - Sign and submit allowance transaction
┌─────────────────────────────────────────────────────────────────────┐
│  Your App                                          Dome Server      │
│                                                                     │
│  POST /v1/polymarket/link-prepare ──────────────────────►           │
│  { walletAddress, walletType, autoDeploySafe? }                     │
│                                                                     │
│                                     ◄─────────────────────────────  │
│  { sessionId, eip712Payload, safeInfo?, safeDeployPayload? }        │
│                                                                     │
│  [User signs eip712Payload in wallet]                               │
│  [If safeDeployPayload returned: User also signs safeDeployPayload] │
│                                                                     │
│  POST /v1/polymarket/link-complete ─────────────────────►           │
│  { sessionId, signature, deploymentSignature? }                     │
│                                                                     │
│                                     ◄─────────────────────────────  │
│  { credentials, signerAddress, safeAddress?,                        │
│    safeDeployed?, safeDeployTxHash?,                                │
│    safeTxPayload?, allowancesToSet? }      ◄── For fresh Safes      │
│                                                                     │
│  ═══════════════════════════════════════════════════════════════    │
│  [For Safe wallets only, if allowancesToSet present]                │
│  ═══════════════════════════════════════════════════════════════    │
│                                                                     │
│  [User signs safeTxPayload.messageHash with signMessage (eth_sign)] │
│                                                                     │
│  POST /v1/polymarket/link-set-allowances ────────────────►          │
│  { sessionId, chainId, allowanceSignature }                         │
│                                                                     │
│                                     ◄─────────────────────────────  │
│  { success, allowancesSet, transactionHash }                        │
└─────────────────────────────────────────────────────────────────────┘
Streamlined Flow for Fresh Safes: When a Safe is deployed during /link-complete, the response includes safeTxPayload and allowancesToSet. You can sign the messageHash directly and call /link-set-allowances - no need to call /link-set-allowances-prepare.

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"]
    }
  }
}
Safe Wallet Response with Auto-Deploy (includes safeDeployPayload): When walletType: "safe", autoDeploySafe: true, and the Safe is not yet deployed, the response includes an additional EIP-712 payload for deploying the Safe wallet:
{
  "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"]
    },
    "safeDeployPayload": {
      "domain": {
        "name": "Polymarket Contract Proxy Factory",
        "chainId": 137,
        "verifyingContract": "0xaacFeEa03eb1561C4e67d661e40682Bd20E3541b"
      },
      "types": {
        "CreateProxy": [
          { "name": "paymentToken", "type": "address" },
          { "name": "payment", "type": "uint256" },
          { "name": "paymentReceiver", "type": "address" }
        ]
      },
      "primaryType": "CreateProxy",
      "message": {
        "paymentToken": "0x0000000000000000000000000000000000000000",
        "payment": "0",
        "paymentReceiver": "0x0000000000000000000000000000000000000000"
      }
    }
  }
}
When safeDeployPayload is present, you must sign both eip712Payload (for credentials) and safeDeployPayload (for Safe deployment), then include both signatures in the /link-complete request.

Step 2: Sign the EIP-712 Payload(s)

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,
});
For Safe Wallets with Auto-Deploy: If safeDeployPayload was returned in the prepare response, you must also sign it to authorize Safe deployment:
// Sign credentials payload
const signature = await privy.signTypedData({
  walletId,
  typedData: eip712Payload,
});

// If safeDeployPayload exists, sign it too
let deploymentSignature;
if (safeDeployPayload) {
  deploymentSignature = await privy.signTypedData({
    walletId,
    typedData: safeDeployPayload,
  });
}

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 of eip712Payload (0x-prefixed hex)
deploymentSignaturestringNoEIP-712 signature of safeDeployPayload for Safe deployment (0x-prefixed hex). Required when deploying a Safe wallet.
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..."
  }
}
Safe Wallet Response with Deployment (when Safe was deployed): When deploymentSignature was provided and the Safe was deployed during this request:
{
  "jsonrpc": "2.0",
  "id": "req-2",
  "result": {
    "success": true,
    "credentials": {
      "apiKey": "ak_...",
      "apiSecret": "as_...",
      "apiPassphrase": "ap_..."
    },
    "signerAddress": "0x1234567890123456789012345678901234567890",
    "safeAddress": "0xABCD...",
    "safeDeployed": true,
    "safeDeployTxHash": "0x1234567890abcdef...",
    "safeTxPayload": {
      "domain": {
        "chainId": 137,
        "verifyingContract": "0xABCD..."
      },
      "types": {
        "SafeTx": [
          { "name": "to", "type": "address" },
          { "name": "value", "type": "uint256" },
          { "name": "data", "type": "bytes" },
          { "name": "operation", "type": "uint8" },
          { "name": "safeTxGas", "type": "uint256" },
          { "name": "baseGas", "type": "uint256" },
          { "name": "gasPrice", "type": "uint256" },
          { "name": "gasToken", "type": "address" },
          { "name": "refundReceiver", "type": "address" },
          { "name": "nonce", "type": "uint256" }
        ]
      },
      "primaryType": "SafeTx",
      "message": { ... },
      "messageHash": "0x1234567890abcdef..."
    },
    "allowancesToSet": ["CTF_EXCHANGE", "NEG_RISK_CTF_EXCHANGE", "NEG_RISK_ADAPTER"]
  }
}
Response FieldTypeDescription
safeDeployedbooleantrue if the Safe was deployed during this request
safeDeployTxHashstringTransaction hash of the Safe deployment (only present when safeDeployed: true)
safeTxPayloadobjectSafeTx EIP-712 payload for setting allowances (only for freshly deployed Safes)
allowancesToSetstring[]List of allowances to set: CTF_EXCHANGE, NEG_RISK_CTF_EXCHANGE, NEG_RISK_ADAPTER
Streamlined Flow: When safeTxPayload is present, you can sign safeTxPayload.messageHash directly using signMessage (eth_sign) and call /link-set-allowances without calling /link-set-allowances-prepare. This is possible because freshly deployed Safes have nonce=0.

Set Allowances (Safe Wallets Only)

For Safe wallets, USDC allowances must be set before trading. There are two ways to set allowances:
  1. Streamlined Flow (recommended for fresh Safes): When /link-complete returns safeTxPayload, sign the messageHash directly and call /link-set-allowances
  2. Standard Flow: Call /link-set-allowances-prepare to get the SafeTx payload, then sign and submit
Critical: Use signMessage, NOT signTypedData!The allowance signature uses a different signing method than the credential signature:
  • Credentials (link-complete): Use signTypedData / EIP-712
  • Allowances (link-set-allowances): Use signMessage / personal_sign / eth_sign
Using the wrong signing method will result in error code 2004: Invalid allowance signature.
The session is available for 30 minutes after /v1/polymarket/link-complete. If the session expires, you’ll need to restart the linking flow.

Streamlined Flow (Fresh Safes)

When the Safe was just deployed (nonce=0), /link-complete returns safeTxPayload with the messageHash to sign:
// After /link-complete, check for safeTxPayload
const { credentials, safeTxPayload, allowancesToSet } = completeData.result;

if (safeTxPayload && allowancesToSet?.length > 0) {
  // Sign the messageHash directly using signMessage (eth_sign)
  const allowanceSignature = await wallet.signMessage(
    ethers.utils.arrayify(safeTxPayload.messageHash)
  );

  // Submit to /link-set-allowances (skip /link-set-allowances-prepare)
  const response = await fetch('https://api.domeapi.io/v1/polymarket/link-set-allowances', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      jsonrpc: '2.0',
      method: 'setAllowances',
      id: crypto.randomUUID(),
      params: { sessionId, chainId: 137, allowanceSignature },
    }),
  });
}

Standard Flow (Existing Safes)

For Safes that already exist (not freshly deployed), use /link-set-allowances-prepare to get the SafeTx payload.

Step 1: Prepare Allowances (Standard Flow)

POST /v1/polymarket/link-set-allowances-prepare Get the SafeTx payload that the user needs to sign.
curl -X POST https://api.domeapi.io/v1/polymarket/link-set-allowances-prepare \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "method": "setAllowancesPrepare",
    "id": "req-3",
    "params": {
      "sessionId": "550e8400-e29b-41d4-a716-446655440000",
      "chainId": 137
    }
  }'
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": {
    "sessionId": "550e8400-e29b-41d4-a716-446655440000",
    "safeAddress": "0xABCD...",
    "safeTxPayload": {
      "domain": {
        "chainId": 137,
        "verifyingContract": "0xABCD..."
      },
      "types": {
        "SafeTx": [
          { "name": "to", "type": "address" },
          { "name": "value", "type": "uint256" },
          { "name": "data", "type": "bytes" },
          { "name": "operation", "type": "uint8" },
          { "name": "safeTxGas", "type": "uint256" },
          { "name": "baseGas", "type": "uint256" },
          { "name": "gasPrice", "type": "uint256" },
          { "name": "gasToken", "type": "address" },
          { "name": "refundReceiver", "type": "address" },
          { "name": "nonce", "type": "uint256" }
        ]
      },
      "primaryType": "SafeTx",
      "message": {
        "to": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761",
        "value": "0",
        "data": "0x8d80ff0a...",
        "operation": 1,
        "safeTxGas": "0",
        "baseGas": "0",
        "gasPrice": "0",
        "gasToken": "0x0000000000000000000000000000000000000000",
        "refundReceiver": "0x0000000000000000000000000000000000000000",
        "nonce": 0
      },
      "messageHash": "0x1234567890abcdef..."
    },
    "allowancesToSet": ["CTF_EXCHANGE", "NEG_RISK_CTF_EXCHANGE", "NEG_RISK_ADAPTER"],
    "nonce": 0
  }
}
Response when allowances already set:
{
  "jsonrpc": "2.0",
  "id": "req-3",
  "result": {
    "sessionId": "550e8400-e29b-41d4-a716-446655440000",
    "safeAddress": "0xABCD...",
    "safeTxPayload": { ... },
    "allowancesToSet": [],
    "nonce": 0
  }
}
If allowancesToSet is empty, all allowances are already set. You can skip the signing and submission steps.

Step 2: Sign the SafeTx Hash

Sign the messageHash from the prepare response using eth_sign (personal sign). This is different from EIP-712 signing - you sign the hash directly as a message.
import { ethers } from 'ethers';

// Sign the messageHash using signMessage (eth_sign)
// This applies the Ethereum message prefix automatically
const allowanceSignature = await wallet.signMessage(
  ethers.utils.arrayify(safeTxPayload.messageHash)
);
Use signMessage (eth_sign / personal_sign), NOT signTypedData. The server expects a signature created with the Ethereum message prefix.

Step 3: Submit Allowances

POST /v1/polymarket/link-set-allowances Submit the signature to execute the allowance transactions on-chain.
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-4",
    "params": {
      "sessionId": "550e8400-e29b-41d4-a716-446655440000",
      "chainId": 137,
      "allowanceSignature": "0x1234...abcd"
    }
  }'
Request Parameters:
ParameterTypeRequiredDefaultDescription
sessionIdstringYes-Session ID from a completed /v1/polymarket/link-complete call
chainIdnumberNo137Chain ID (137 = Polygon mainnet)
allowanceSignaturestringYes-Signature of the messageHash from /link-set-allowances-prepare (0x-prefixed hex)
Success Response:
{
  "jsonrpc": "2.0",
  "id": "req-4",
  "result": {
    "success": true,
    "safeAddress": "0xABCD...",
    "allowancesSet": ["CTF_EXCHANGE", "NEG_RISK_CTF_EXCHANGE", "NEG_RISK_ADAPTER"],
    "transactionId": "019c022e-a853-78de-aa52-f59fd469d3d7",
    "transactionHash": "0x423b0c5c691aa34c37f793ed874b719ceb03a74cd3a98e3b550ecb5d095d86c8"
  }
}

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

Safe Wallet with Auto-Deploy Example (Privy)

Full example for Safe wallets that need to be deployed using Privy managed wallets:
// 1. Prepare - get EIP-712 payloads
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: 'safe',
      autoDeploySafe: true,
    },
  }),
});

const prepareData = await prepareResponse.json();

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

const { sessionId, eip712Payload, safeDeployPayload, safeInfo } = prepareData.result;

// 2. Sign credentials payload using Privy
const signature = await privy.signTypedData({
  walletId,
  typedData: eip712Payload,
});

// 3. If Safe needs deployment, sign the deployment payload
let deploymentSignature;
if (safeDeployPayload) {
  deploymentSignature = await privy.signTypedData({
    walletId,
    typedData: safeDeployPayload,
  });
}

// 4. Complete - exchange signatures for credentials (and deploy Safe if needed)
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,
      ...(deploymentSignature && { deploymentSignature }),
    },
  }),
});

const completeData = await completeResponse.json();

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

const { credentials, signerAddress, safeAddress, safeDeployed, safeDeployTxHash } = completeData.result;

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

if (safeDeployed) {
  console.log(`Safe deployed! Transaction: ${safeDeployTxHash}`);
}

// 6. Set allowances if needed
// For freshly deployed Safes, safeTxPayload is included in link-complete response (streamlined flow)
// For existing Safes, call /link-set-allowances-prepare first (standard flow)
const { safeTxPayload: allowancesPayload, allowancesToSet } = completeData.result;

if (allowancesPayload && allowancesToSet?.length > 0) {
  // Streamlined flow: safeTxPayload already included in link-complete response
  // Sign the messageHash using signMessage (eth_sign)
  const allowanceSignature = await privy.signMessage({
    walletId,
    message: allowancesPayload.messageHash,
  });

  // Submit - execute the allowance transactions
  const allowanceResponse = await fetch('https://api.domeapi.io/v1/polymarket/link-set-allowances', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      jsonrpc: '2.0',
      method: 'setAllowances',
      id: crypto.randomUUID(),
      params: { sessionId, chainId: 137, allowanceSignature },
    }),
  });

  const allowanceData = await allowanceResponse.json();
  console.log('Allowances set:', allowanceData.result.allowancesSet);
  console.log('Transaction hash:', allowanceData.result.transactionHash);
} else if (safeInfo && !safeInfo.hasAllowances) {
  // Standard flow for existing Safes: call /link-set-allowances-prepare first
  const prepareAllowanceResponse = await fetch('https://api.domeapi.io/v1/polymarket/link-set-allowances-prepare', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      jsonrpc: '2.0',
      method: 'setAllowancesPrepare',
      id: crypto.randomUUID(),
      params: { sessionId, chainId: 137 },
    }),
  });

  const prepareAllowanceData = await prepareAllowanceResponse.json();
  const { safeTxPayload, allowancesToSet: toSet } = prepareAllowanceData.result;

  if (toSet.length > 0) {
    const allowanceSignature = await privy.signMessage({
      walletId,
      message: safeTxPayload.messageHash,
    });

    const allowanceResponse = await fetch('https://api.domeapi.io/v1/polymarket/link-set-allowances', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        jsonrpc: '2.0',
        method: 'setAllowances',
        id: crypto.randomUUID(),
        params: { sessionId, chainId: 137, allowanceSignature },
      }),
    });

    const allowanceData = await allowanceResponse.json();
    console.log('Allowances set:', allowanceData.result.allowancesSet);
    console.log('Transaction hash:', allowanceData.result.transactionHash);
  }
}

console.log('Safe wallet 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. For allowances, ensure you’re using signMessage, not signTypedData
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.