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:
Prepare - Server generates an EIP-712 payload for the user to sign
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 Case Recommendation Server-side with full wallet access SDK linkUser() method TEE/Enclave environments Direct endpoints Mobile with on-device signing (Privy) Direct endpoints Custom signing workflows Direct 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
Environment Base URL Production https://api.domeapi.io/v1/polymarketDevelopment http://localhost:3000
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:
Parameter Type Required Default Description walletAddressstringYes - User’s EOA wallet address (0x…) walletType"eoa" | "safe"No "eoa"Wallet type autoDeploySafebooleanNo trueAuto-deploy Safe if not deployed (only for walletType: "safe") chainIdnumberNo 137Chain 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:
Privy (TEE/Mobile)
ethers.js
viem
MetaMask (Browser)
// 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:
Parameter Type Required Description sessionIdstringYes Session ID from /v1/polymarket/link-prepare response signaturestringYes EIP-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:
Parameter Type Required Default Description sessionIdstringYes - Session ID from a completed /v1/polymarket/link-complete call chainIdnumberNo 137Chain 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
Code Name Description -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
Store credentials securely - Encrypt apiSecret and apiPassphrase at rest
Session IDs are single-use - Cannot replay /v1/polymarket/link-complete with same sessionId
Signature verification - Server verifies the signature matches the wallet address
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.