> ## Documentation Index
> Fetch the complete documentation index at: https://docs.domeapi.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Link Account

> Link user wallets to Polymarket to obtain trading credentials

<Warning>
  **Beta - Use Only If Necessary**

  These endpoints are in beta and still being tested. For most use cases, we strongly recommend using the [SDK's `linkUser()` method](/order-router/index#3-link-user-one-time-setup) 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.
</Warning>

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 }                        │
└─────────────────────────────────────────────────────────────────────┘
```

<Note>
  **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`.
</Note>

## 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)

```typescript theme={null}
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)

```typescript theme={null}
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/polymarket` |
| Development | `http://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.

<CodeGroup>
  ```bash cURL theme={null}
  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"
      }
    }'
  ```

  ```typescript TypeScript theme={null}
  const response = 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: 'req-1',
      params: {
        walletAddress: '0x1234567890123456789012345678901234567890',
        walletType: 'eoa',
      },
    }),
  });

  const data = await response.json();
  const { sessionId, eip712Payload } = data.result;
  ```

  ```python Python theme={null}
  import requests

  response = requests.post(
      'https://api.domeapi.io/v1/polymarket/link-prepare',
      json={
          'jsonrpc': '2.0',
          'method': 'linkPrepare',
          'id': 'req-1',
          'params': {
              'walletAddress': '0x1234567890123456789012345678901234567890',
              'walletType': 'eoa',
          },
      },
  )

  data = response.json()
  session_id = data['result']['sessionId']
  eip712_payload = data['result']['eip712Payload']
  ```
</CodeGroup>

**Request Parameters:**

| Parameter        | Type              | Required | Default | Description                                                      |
| ---------------- | ----------------- | -------- | ------- | ---------------------------------------------------------------- |
| `walletAddress`  | `string`          | Yes      | -       | User's EOA wallet address (0x...)                                |
| `walletType`     | `"eoa" \| "safe"` | No       | `"eoa"` | Wallet type                                                      |
| `autoDeploySafe` | `boolean`         | No       | `true`  | Auto-deploy Safe if not deployed (only for `walletType: "safe"`) |
| `chainId`        | `number`          | No       | `137`   | Chain ID (137 = Polygon mainnet)                                 |

**Success Response:**

```json theme={null}
{
  "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`):**

```json theme={null}
{
  "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:

```json theme={null}
{
  "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"
      }
    }
  }
}
```

<Note>
  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.
</Note>

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

Sign the `eip712Payload` using your wallet. The exact method depends on your environment:

<CodeGroup>
  ```typescript Privy (TEE/Mobile) theme={null}
  // In Privy TEE environment
  const signature = await privy.signTypedData({
    walletId,
    typedData: eip712Payload,
  });
  ```

  ```typescript ethers.js theme={null}
  // Using ethers.js signer
  const signature = await signer._signTypedData(
    eip712Payload.domain,
    eip712Payload.types,
    eip712Payload.message
  );
  ```

  ```typescript viem theme={null}
  // Using viem
  const signature = await walletClient.signTypedData({
    domain: eip712Payload.domain,
    types: eip712Payload.types,
    primaryType: eip712Payload.primaryType,
    message: eip712Payload.message,
  });
  ```

  ```javascript MetaMask (Browser) theme={null}
  // Using MetaMask in browser
  const signature = await ethereum.request({
    method: 'eth_signTypedData_v4',
    params: [walletAddress, JSON.stringify(eip712Payload)],
  });
  ```
</CodeGroup>

**For Safe Wallets with Auto-Deploy:**

If `safeDeployPayload` was returned in the prepare response, you must also sign it to authorize Safe deployment:

<CodeGroup>
  ```typescript Privy (TEE/Mobile) theme={null}
  // 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,
    });
  }
  ```

  ```typescript ethers.js theme={null}
  // Sign credentials payload
  const signature = await signer._signTypedData(
    eip712Payload.domain,
    eip712Payload.types,
    eip712Payload.message
  );

  // If safeDeployPayload exists, sign it too
  let deploymentSignature;
  if (safeDeployPayload) {
    deploymentSignature = await signer._signTypedData(
      safeDeployPayload.domain,
      safeDeployPayload.types,
      safeDeployPayload.message
    );
  }
  ```

  ```typescript viem theme={null}
  // Sign credentials payload
  const signature = await walletClient.signTypedData({
    domain: eip712Payload.domain,
    types: eip712Payload.types,
    primaryType: eip712Payload.primaryType,
    message: eip712Payload.message,
  });

  // If safeDeployPayload exists, sign it too
  let deploymentSignature;
  if (safeDeployPayload) {
    deploymentSignature = await walletClient.signTypedData({
      domain: safeDeployPayload.domain,
      types: safeDeployPayload.types,
      primaryType: safeDeployPayload.primaryType,
      message: safeDeployPayload.message,
    });
  }
  ```
</CodeGroup>

### Step 3: Complete Session

**`POST /v1/polymarket/link-complete`**

Submit the signature to derive Polymarket credentials.

<CodeGroup>
  ```bash cURL theme={null}
  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"
      }
    }'
  ```

  ```typescript TypeScript theme={null}
  const response = 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: 'req-2',
      params: {
        sessionId: sessionId,
        signature: signature,
      },
    }),
  });

  const data = await response.json();
  const { credentials, signerAddress, safeAddress } = data.result;

  // Store credentials in your database
  await db.storeCredentials(userId, {
    apiKey: credentials.apiKey,
    apiSecret: credentials.apiSecret,
    apiPassphrase: credentials.apiPassphrase,
    signerAddress,
    safeAddress, // Only present for Safe wallets
  });
  ```

  ```python Python theme={null}
  import requests

  response = requests.post(
      'https://api.domeapi.io/v1/polymarket/link-complete',
      json={
          'jsonrpc': '2.0',
          'method': 'linkComplete',
          'id': 'req-2',
          'params': {
              'sessionId': session_id,
              'signature': signature,
          },
      },
  )

  data = response.json()
  credentials = data['result']['credentials']
  signer_address = data['result']['signerAddress']

  # Store credentials in your database
  db.store_credentials(
      user_id,
      api_key=credentials['apiKey'],
      api_secret=credentials['apiSecret'],
      api_passphrase=credentials['apiPassphrase'],
  )
  ```
</CodeGroup>

**Request Parameters:**

| Parameter             | Type     | Required | Description                                                                                                            |
| --------------------- | -------- | -------- | ---------------------------------------------------------------------------------------------------------------------- |
| `sessionId`           | `string` | Yes      | Session ID from `/v1/polymarket/link-prepare` response                                                                 |
| `signature`           | `string` | Yes      | EIP-712 signature of `eip712Payload` (0x-prefixed hex)                                                                 |
| `deploymentSignature` | `string` | No       | EIP-712 signature of `safeDeployPayload` for Safe deployment (0x-prefixed hex). Required when deploying a Safe wallet. |

**Success Response:**

```json theme={null}
{
  "jsonrpc": "2.0",
  "id": "req-2",
  "result": {
    "success": true,
    "credentials": {
      "apiKey": "ak_...",
      "apiSecret": "as_...",
      "apiPassphrase": "ap_..."
    },
    "signerAddress": "0x1234567890123456789012345678901234567890"
  }
}
```

**Safe Wallet Response (includes `safeAddress`):**

```json theme={null}
{
  "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:

```json theme={null}
{
  "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 Field     | Type       | Description                                                                            |
| ------------------ | ---------- | -------------------------------------------------------------------------------------- |
| `safeDeployed`     | `boolean`  | `true` if the Safe was deployed during this request                                    |
| `safeDeployTxHash` | `string`   | Transaction hash of the Safe deployment (only present when `safeDeployed: true`)       |
| `safeTxPayload`    | `object`   | SafeTx EIP-712 payload for setting allowances (only for freshly deployed Safes)        |
| `allowancesToSet`  | `string[]` | List of allowances to set: `CTF_EXCHANGE`, `NEG_RISK_CTF_EXCHANGE`, `NEG_RISK_ADAPTER` |

<Note>
  **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.
</Note>

***

## 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

<Warning>
  **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**.
</Warning>

<Warning>
  The session is available for **30 minutes** after `/v1/polymarket/link-complete`. If the session expires, you'll need to restart the linking flow.
</Warning>

### Streamlined Flow (Fresh Safes)

When the Safe was just deployed (nonce=0), `/link-complete` returns `safeTxPayload` with the `messageHash` to sign:

```typescript theme={null}
// 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.

<CodeGroup>
  ```bash cURL theme={null}
  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
      }
    }'
  ```

  ```typescript TypeScript theme={null}
  // After successful /v1/polymarket/link-complete, use the same sessionId
  const prepareResponse = 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: 'req-3',
      params: {
        sessionId: sessionId,
        chainId: 137,
      },
    }),
  });

  const prepareData = await prepareResponse.json();
  const { safeTxPayload, allowancesToSet, nonce } = prepareData.result;
  ```

  ```python Python theme={null}
  import requests

  response = requests.post(
      'https://api.domeapi.io/v1/polymarket/link-set-allowances-prepare',
      json={
          'jsonrpc': '2.0',
          'method': 'setAllowancesPrepare',
          'id': 'req-3',
          'params': {
              'sessionId': session_id,
              'chainId': 137,
          },
      },
  )

  data = response.json()
  safe_tx_payload = data['result']['safeTxPayload']
  message_hash = safe_tx_payload['messageHash']
  ```
</CodeGroup>

**Request Parameters:**

| Parameter   | Type     | Required | Default | Description                                                     |
| ----------- | -------- | -------- | ------- | --------------------------------------------------------------- |
| `sessionId` | `string` | Yes      | -       | Session ID from a completed `/v1/polymarket/link-complete` call |
| `chainId`   | `number` | No       | `137`   | Chain ID (137 = Polygon mainnet)                                |

**Success Response:**

```json theme={null}
{
  "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:**

```json theme={null}
{
  "jsonrpc": "2.0",
  "id": "req-3",
  "result": {
    "sessionId": "550e8400-e29b-41d4-a716-446655440000",
    "safeAddress": "0xABCD...",
    "safeTxPayload": { ... },
    "allowancesToSet": [],
    "nonce": 0
  }
}
```

<Note>
  If `allowancesToSet` is empty, all allowances are already set. You can skip the signing and submission steps.
</Note>

### 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.

<CodeGroup>
  ```typescript ethers.js theme={null}
  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)
  );
  ```

  ```typescript viem theme={null}
  import { toBytes } from 'viem';

  // Sign the messageHash using signMessage (eth_sign)
  const allowanceSignature = await walletClient.signMessage({
    message: { raw: toBytes(safeTxPayload.messageHash) },
  });
  ```

  ```typescript Privy theme={null}
  // Sign the messageHash using Privy's signMessage
  const allowanceSignature = await privy.signMessage({
    walletId,
    message: safeTxPayload.messageHash,
  });
  ```

  ```javascript MetaMask (Browser) theme={null}
  // Sign using personal_sign
  const allowanceSignature = await ethereum.request({
    method: 'personal_sign',
    params: [safeTxPayload.messageHash, walletAddress],
  });
  ```
</CodeGroup>

<Warning>
  Use `signMessage` (eth\_sign / personal\_sign), NOT `signTypedData`. The server expects a signature created with the Ethereum message prefix.
</Warning>

### Step 3: Submit Allowances

**`POST /v1/polymarket/link-set-allowances`**

Submit the signature to execute the allowance transactions on-chain.

<CodeGroup>
  ```bash cURL theme={null}
  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"
      }
    }'
  ```

  ```typescript TypeScript theme={null}
  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: 'req-4',
      params: {
        sessionId: sessionId,
        chainId: 137,
        allowanceSignature: allowanceSignature,
      },
    }),
  });

  const data = await response.json();

  if (data.result?.success) {
    console.log('Allowances set for:', data.result.safeAddress);
    console.log('Allowances set:', data.result.allowancesSet);
    console.log('Transaction hash:', data.result.transactionHash);
  }
  ```

  ```python Python theme={null}
  import requests

  response = requests.post(
      'https://api.domeapi.io/v1/polymarket/link-set-allowances',
      json={
          'jsonrpc': '2.0',
          'method': 'setAllowances',
          'id': 'req-4',
          'params': {
              'sessionId': session_id,
              'chainId': 137,
              'allowanceSignature': allowance_signature,
          },
      },
  )

  data = response.json()
  if data.get('result', {}).get('success'):
      print(f"Allowances set for: {data['result']['safeAddress']}")
      print(f"Transaction hash: {data['result'].get('transactionHash')}")
  ```
</CodeGroup>

**Request Parameters:**

| Parameter            | Type     | Required | Default | Description                                                                          |
| -------------------- | -------- | -------- | ------- | ------------------------------------------------------------------------------------ |
| `sessionId`          | `string` | Yes      | -       | Session ID from a completed `/v1/polymarket/link-complete` call                      |
| `chainId`            | `number` | No       | `137`   | Chain ID (137 = Polygon mainnet)                                                     |
| `allowanceSignature` | `string` | Yes      | -       | Signature of the `messageHash` from `/link-set-allowances-prepare` (0x-prefixed hex) |

**Success Response:**

```json theme={null}
{
  "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:

```typescript theme={null}
// 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:

```typescript theme={null}
// 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.

```bash theme={null}
curl https://api.domeapi.io/v1/polymarket/link-health
```

**Response:**

```json theme={null}
{
  "status": "ok",
  "service": "link",
  "region": "us-east-1",
  "activeSessions": 5,
  "timestamp": 1704067200000
}
```

***

## Error Handling

### Error Codes

| Code     | Name                           | Description                                                                                           |
| -------- | ------------------------------ | ----------------------------------------------------------------------------------------------------- |
| `-32600` | `INVALID_REQUEST`              | Malformed request or validation error                                                                 |
| `-32602` | `INVALID_PARAMS`               | Invalid parameters                                                                                    |
| `2001`   | `INVALID_WALLET_ADDRESS`       | Invalid wallet address format                                                                         |
| `2002`   | `SESSION_NOT_FOUND`            | Session ID not found                                                                                  |
| `2003`   | `SESSION_EXPIRED`              | Session expired (10 min incomplete, 30 min after completion)                                          |
| `2004`   | `INVALID_SIGNATURE`            | Signature verification failed. For allowances, ensure you're using `signMessage`, not `signTypedData` |
| `2005`   | `CREDENTIAL_DERIVATION_FAILED` | Failed to derive credentials from Polymarket                                                          |
| `2006`   | `SAFE_DEPLOYMENT_FAILED`       | Safe wallet deployment failed                                                                         |
| `2007`   | `SAFE_NOT_DEPLOYED`            | Safe wallet not deployed and autoDeploySafe=false                                                     |
| `2009`   | `ALLOWANCE_SET_FAILED`         | Failed to set allowances on Safe wallet                                                               |

### Error Response Example

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

### Handling Errors

```typescript theme={null}
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](/order-router#place-orders):

```typescript theme={null}
const order = await router.placeOrder(
  {
    userId: 'user-123',
    marketId: '104173557214744537570424345347209544585775842950109756851652855913015295701992',
    side: 'buy',
    size: 100,
    price: 0.50,
    signer,
  },
  credentials
);
```

Or call the [placeOrder endpoint](/order-router#rest-endpoint) directly with the credentials.
