> For the complete documentation index, see [llms.txt](https://docs.dimes.fi/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.dimes.fi/for-developers/wallet-integration/smart-wallet-integration.md).

# Smart Wallet Integration

Most users hold a Polymarket smart-contract wallet rather than a plain EOA. The vault does not care whether `msg.sender` is an EOA or a contract, but each wallet type submits transactions differently. This guide covers all three Polymarket wallet patterns.

For the core position lifecycle, the Direct EOA flow, and contract ABIs, see [On-Chain Integration](/for-developers/wallet-integration/on-chain-integration.md).

***

## Which wallet type?

| Pattern                       | Description                                                                 | `msg.sender` to vault       |
| ----------------------------- | --------------------------------------------------------------------------- | --------------------------- |
| **Polymarket Safe**           | Standard 1-of-1 Gnosis Safe provisioned by Polymarket for some users        | The Safe contract           |
| **Polymarket Proxy**          | EIP-1167 minimal proxy provisioned by Polymarket for some users             | The Proxy contract          |
| **Polymarket Deposit Wallet** | ERC-1967 proxy deployed via Polymarket's deposit wallet factory (new users) | The deposit wallet contract |

Polymarket provisions one of these depending on how the user registered. **Deposit wallets are the default for new API users.**

To detect a type, call `eth_getCode`: a Safe has \~250 bytes of bytecode, a Proxy \~92 bytes, a deposit wallet \~252 bytes, an EOA none. Or call `getOwners()` — Safes respond, others revert.

**Critical:** The `wallet_address` you pass to `POST /prediction-markets/quotes` must be the address that will be `msg.sender` on-chain — for all three patterns that is the **contract address**, not the owner EOA. The wrong address fails the on-chain signature check.

***

## Polymarket Safe

Encode the vault calldata normally, wrap it in a SafeTx, have the owner sign, submit `execTransaction`.

```javascript
import {ethers} from "ethers";

const SAFE_ABI = [
  "function nonce() view returns (uint256)",
  "function getTransactionHash(address,uint256,bytes,uint8,uint256,uint256,uint256,address,address,uint256) view returns (bytes32)",
  "function execTransaction(address,uint256,bytes,uint8,uint256,uint256,uint256,address,address,bytes) returns (bool)",
];

const safe = new ethers.Contract(safeAddress, SAFE_ABI, provider);
const vault = new ethers.Contract(vaultAddress, VAULT_ABI, provider);

// Encode the vault call
const data = vault.interface.encodeFunctionData("createPosition", [
  quote.position_seed_hex,
  quote.polymarket_market_id,
  BigInt(quote.polymarket_token_id),
  BigInt(quote.collateral_usdc_units),
  quote.leverage_bps,
  BigInt(quote.notional_usdc_units),
  quote.origination_fee_bps,
  quote.lifetime_fee_apr_bps,
  quote.liquidation_fee_bps,
  BigInt(quote.expected_open_trading_fee_usdc_units),
  quote.contract_signature,
  BigInt(quote.signature_expiry),
]);

// Build SafeTx and get the hash
const nonce = await safe.nonce();
const safeTxHash = await safe.getTransactionHash(
  vaultAddress, 0, data, 0, 0, 0, 0, ethers.ZeroAddress, ethers.ZeroAddress, nonce,
);

// Owner signs the hash
const signature = await ownerSigner.signMessage(ethers.getBytes(safeTxHash));

// Anyone can submit (owner, partner backend, or relayer)
const tx = await safe.connect(submitterSigner).execTransaction(
  vaultAddress, 0, data, 0, 0, 0, 0, ethers.ZeroAddress, ethers.ZeroAddress, signature,
);
await tx.wait();
```

pUSD approval must be a separate SafeTx before `createPosition`, or batched via Safe's MultiSend. `requestClose` follows the same pattern — encode `requestClose(positionKey)` as the inner call.

***

## Polymarket Proxy

The owner EOA calls `factory.proxy()` directly. pUSD approval can be batched in the same call.

```javascript
import {ethers} from "ethers";

const PROXY_FACTORY = "0xaB45c5A4B0c941a2F231C04C3f49182e1A254052";
const PROXY_FACTORY_ABI = [
  "function proxy(tuple(address to, uint256 typeCode, bytes data, uint256 value)[]) payable returns (bytes[])",
];

const factory = new ethers.Contract(PROXY_FACTORY, PROXY_FACTORY_ABI, ownerSigner);
const vault = new ethers.Contract(vaultAddress, VAULT_ABI, provider);
const usdc = new ethers.Contract(usdcAddress, ["function approve(address,uint256)"], provider);

const approveData = usdc.interface.encodeFunctionData("approve", [vaultAddress, approvalAmount]);
const createData = vault.interface.encodeFunctionData("createPosition", [
  quote.position_seed_hex,
  quote.polymarket_market_id,
  BigInt(quote.polymarket_token_id),
  BigInt(quote.collateral_usdc_units),
  quote.leverage_bps,
  BigInt(quote.notional_usdc_units),
  quote.origination_fee_bps,
  quote.lifetime_fee_apr_bps,
  quote.liquidation_fee_bps,
  BigInt(quote.expected_open_trading_fee_usdc_units),
  quote.contract_signature,
  BigInt(quote.signature_expiry),
]);

// Batch approve + createPosition in one transaction
const tx = await factory.proxy([
  {to: usdcAddress, typeCode: 1, data: approveData, value: 0},
  {to: vaultAddress, typeCode: 1, data: createData, value: 0},
]);
await tx.wait();
```

The owner EOA must call `factory.proxy()` directly — the proxy does not support off-chain signatures, and Polymarket's hosted relayer will not relay vault calls for it.

***

## Polymarket Deposit Wallet

Polymarket's newest wallet type and the default for new API users. They require a different opening flow than Safe and Proxy.

### Why deposit wallets need the push-funded flow

The legacy `createPosition` path pulls pUSD with `transferFrom`, which requires the caller to first `approve` the vault. **The Polymarket relayer blocks `approve` to non-whitelisted spenders**, so a deposit wallet can never grant that allowance.

Instead, the vault exposes a **push-funded** path: the deposit wallet *pushes* pUSD to the vault inside an atomic 3-call batch, and the vault verifies the exact balance delta. No approval needed.

```
Deposit wallet batch (atomic — all-or-nothing):

  1. vault.reserveDeposit(totalUserAmount)    snapshot the vault pUSD balance
  2. pUSD.transfer(vault, totalUserAmount)    push the funds directly
  3. vault.createPositionPushFunded(...)      verify the delta, create the position
```

> **⚠️ Only pUSD is accepted.** Step 2 must transfer **pUSD** — the exact token returned as `polygon_usdc_token_address` from `GET /prediction-markets/contract-info`. Pushing any other token (e.g. USDC.e, native USDC, or any bridged variant) will not register as a balance delta, and `createPositionPushFunded` will revert the whole batch. Always source the transfer target from `contract-info`; never hardcode a stablecoin address.

`reserveDeposit` records a transient snapshot of the vault's pUSD balance. `createPositionPushFunded` re-reads the balance and requires the delta to **exactly** equal the position's total amount — any over- or under-funding reverts. The three calls are signed and submitted together, so they cannot be split or reordered.

`createPositionPushFunded` takes the **same 12 arguments** as `createPosition` and accepts the **same `contract_signature`** from the quote. Only the funding mechanism differs.

### The deposit wallet batch

A deposit wallet executes calls through Polymarket's factory:

```solidity
factory.proxy(Batch[] batches, bytes[] signatures)

struct Batch {address wallet; uint256 nonce; uint256 deadline; Call[] calls;}
struct Call  {address target; uint256 value; bytes data;}
```

The wallet owner signs each `Batch` with EIP-712. The relayer submits `factory.proxy(...)` on-chain.

| Constant         | Value                                                                                              |
| ---------------- | -------------------------------------------------------------------------------------------------- |
| Factory address  | `0x00000000000Fb5C9ADea0298D729A0CB3823Cc07`                                                       |
| EIP-712 domain   | `{ name: "DepositWallet", version: "1", chainId: 137, verifyingContract: <depositWalletAddress> }` |
| Relayer endpoint | `POST https://relayer-v2.polymarket.com/submit`                                                    |

### Opening a position

{% tabs %}
{% tab title="SDK (viem)" %}

```typescript
import {
  buildPushFundedCreateCalls,
  buildDepositWalletBatch,
  getDepositWalletBatchTypedData,
  buildRelayerSubmitPayload,
  buildRelayerAuthHeaders,
  polymarketRelayerBaseUrl,
} from "@dimes-dot-fi/sdk/contract";

// 1. Build the 3-call push-funded batch (reserveDeposit + transfer + createPositionPushFunded).
//    `pUsdAddress` is the pUSD token address from GET /prediction-markets/contract-info.
const calls = buildPushFundedCreateCalls(offer, pUsdAddress);

// 2. Read the deposit wallet nonce, assemble and sign the batch.
const nonce = await publicClient.readContract({
  address: depositWalletAddress,
  abi: [{ type: "function", name: "nonce", stateMutability: "view", inputs: [], outputs: [{ type: "uint256" }] }],
  functionName: "nonce",
});
const deadline = BigInt(Math.floor(Date.now() / 1000) + 240);
const batch = buildDepositWalletBatch({ depositWalletAddress, nonce, deadline, calls });
const signature = await walletClient.signTypedData(getDepositWalletBatchTypedData(batch));

// 3. Submit to the Polymarket relayer (builder API credentials required).
const body = JSON.stringify(buildRelayerSubmitPayload({ ownerAddress, batch, signature }));
const headers = await buildRelayerAuthHeaders({ apiKey, apiSecret, apiPassphrase, body });
const res = await fetch(`${polymarketRelayerBaseUrl}/submit`, { method: "POST", headers, body });
const { transactionID } = await res.json();
```

{% endtab %}

{% tab title="ethers.js" %}

```javascript
import {ethers} from "ethers";

const FACTORY = "0x00000000000Fb5C9ADea0298D729A0CB3823Cc07";
const vault = new ethers.Contract(vaultAddress, VAULT_ABI, provider);
const usdc = new ethers.Contract(usdcAddress, ["function transfer(address,uint256)"], provider);
const dw = new ethers.Contract(depositWalletAddress, ["function nonce() view returns (uint256)"], provider);

// reserveDeposit / createPositionPushFunded are not in the legacy VAULT_ABI — add them:
const PUSH_FUNDED_ABI = new ethers.Interface([
  "function reserveDeposit(uint256 expectedAmountUsdcUnits)",
  "function createPositionPushFunded(bytes16,bytes32,uint256,uint256,uint32,uint256,uint16,uint16,uint16,uint256,bytes,uint256)",
]);

const totalUserAmount = BigInt(quote.total_user_amount_usdc_units);

// 1. Encode the three inner calls
const reserveData = PUSH_FUNDED_ABI.encodeFunctionData("reserveDeposit", [totalUserAmount]);
const transferData = usdc.interface.encodeFunctionData("transfer", [vaultAddress, totalUserAmount]);
const createData = PUSH_FUNDED_ABI.encodeFunctionData("createPositionPushFunded", [
  quote.position_seed_hex,
  quote.polymarket_market_id,
  BigInt(quote.polymarket_token_id),
  BigInt(quote.collateral_usdc_units),
  quote.leverage_bps,
  BigInt(quote.notional_usdc_units),
  quote.origination_fee_bps,
  quote.lifetime_fee_apr_bps,
  quote.liquidation_fee_bps,
  BigInt(quote.expected_open_trading_fee_usdc_units),
  quote.contract_signature,
  BigInt(quote.signature_expiry),
]);

const calls = [
  {target: vaultAddress, value: 0, data: reserveData},
  {target: usdcAddress,  value: 0, data: transferData},
  {target: vaultAddress, value: 0, data: createData},
];

// 2. Build and sign the Batch (EIP-712)
const nonce = await dw.nonce();
const deadline = BigInt(Math.floor(Date.now() / 1000) + 240);
const batch = {wallet: depositWalletAddress, nonce, deadline, calls};

const signature = await ownerSigner.signTypedData(
  {name: "DepositWallet", version: "1", chainId: 137, verifyingContract: depositWalletAddress},
  {
    Batch: [
      {name: "wallet", type: "address"}, {name: "nonce", type: "uint256"},
      {name: "deadline", type: "uint256"}, {name: "calls", type: "Call[]"},
    ],
    Call: [
      {name: "target", type: "address"}, {name: "value", type: "uint256"},
      {name: "data", type: "bytes"},
    ],
  },
  batch,
);

// 3. Submit via the Polymarket relayer (see "Relayer submission" below)
await submitToRelayer({ownerAddress, depositWalletAddress, batch, signature});
```

{% endtab %}
{% endtabs %}

### Closing a position

Same batch mechanics with a single inner call: `requestClose(positionKey)`. The `positionKey` is the `on_chain_position_key` field from `GET /positions`.

{% tabs %}
{% tab title="SDK (viem)" %}

```typescript
import {
  buildRequestCloseCalls,
  buildDepositWalletBatch,
  getDepositWalletBatchTypedData,
  buildRelayerSubmitPayload,
  buildRelayerAuthHeaders,
  polymarketRelayerBaseUrl,
} from "@dimes-dot-fi/sdk/contract";

const calls = buildRequestCloseCalls(vaultAddress, position.on_chain_position_key);

const deadline = BigInt(Math.floor(Date.now() / 1000) + 240);
const batch = buildDepositWalletBatch({ depositWalletAddress, nonce, deadline, calls });
const signature = await walletClient.signTypedData(getDepositWalletBatchTypedData(batch));

const body = JSON.stringify(buildRelayerSubmitPayload({ ownerAddress, batch, signature }));
const headers = await buildRelayerAuthHeaders({ apiKey, apiSecret, apiPassphrase, body });
await fetch(`${polymarketRelayerBaseUrl}/submit`, { method: "POST", headers, body });
```

{% endtab %}

{% tab title="ethers.js" %}

```javascript
const closeData = vault.interface.encodeFunctionData("requestClose", [position.on_chain_position_key]);
const calls = [{target: vaultAddress, value: 0, data: closeData}];

const nonce = await dw.nonce();
const deadline = BigInt(Math.floor(Date.now() / 1000) + 240);
const batch = {wallet: depositWalletAddress, nonce, deadline, calls};

// Sign the Batch with the same EIP-712 types as the opening flow, then submit to the relayer.
const signature = await ownerSigner.signTypedData(domain, types, batch);
await submitToRelayer({ownerAddress, depositWalletAddress, batch, signature});
```

{% endtab %}
{% endtabs %}

Only the position owner — the deposit wallet that called `createPositionPushFunded` — can close the position. After confirmation the backend sells the tokens, distributes proceeds, and emits `position.closed` over [WebSocket](/for-developers/api-and-events/websocket.md).

### Relayer submission

The relayer authenticates every request with an HMAC over the request body. The SDK's `buildRelayerSubmitPayload` and `buildRelayerAuthHeaders` produce both; the manual shape is:

```javascript
import {createHmac} from "node:crypto";

async function submitToRelayer({ownerAddress, depositWalletAddress, batch, signature}) {
  const payload = {
    type: "WALLET",
    from: ownerAddress,
    to: "0x00000000000Fb5C9ADea0298D729A0CB3823Cc07", // factory
    nonce: batch.nonce.toString(),
    signature,
    depositWalletParams: {
      depositWallet: depositWalletAddress,
      deadline: batch.deadline.toString(),
      calls: batch.calls.map((c) => ({target: c.target, value: c.value.toString(), data: c.data})),
    },
  };
  const body = JSON.stringify(payload);

  // HMAC-SHA256 over `timestamp + "POST" + "/submit" + body`, base64url-encoded.
  const timestamp = Math.floor(Date.now() / 1000).toString();
  const sig = createHmac("sha256", Buffer.from(builderApiSecret, "base64"))
    .update(timestamp + "POST" + "/submit" + body)
    .digest("base64")
    .replaceAll("+", "-").replaceAll("/", "_");

  const res = await fetch("https://relayer-v2.polymarket.com/submit", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      POLY_BUILDER_API_KEY: builderApiKey,
      POLY_BUILDER_TIMESTAMP: timestamp,
      POLY_BUILDER_PASSPHRASE: builderApiPassphrase,
      POLY_BUILDER_SIGNATURE: sig,
    },
    body,
  });
  const {transactionID} = await res.json();

  // Poll GET /transaction?id=<transactionID> until state is STATE_MINED / STATE_CONFIRMED.
  return transactionID;
}
```

Submission returns a `transactionID`. Poll `GET https://relayer-v2.polymarket.com/transaction?id=<transactionID>` until `state` is `STATE_MINED` or `STATE_CONFIRMED` (terminal failures: `STATE_FAILED`, `STATE_CANCELLED`, `STATE_INVALID`).

### Constraints

* **One push-funded create per transaction.** The `reserveDeposit` lock allows a single push-funded create per batch; do not bundle two.
* **Exact funding.** The pushed amount must equal `total_user_amount_usdc_units` exactly. The SDK helper sources this from the offer, so reuse it for both `reserveDeposit` and `transfer`.
* **Signature expiry.** `signature_expiry` from the quote is short-lived — sign and submit the batch promptly, and keep the batch `deadline` tight (e.g. now + 240s).
* The relayer can refuse to submit a batch but cannot split, reorder, or alter it — the EIP-712 batch signature commits to the whole `Call[]` array.

See [Polymarket's deposit wallet docs](https://docs.polymarket.com/trading/deposit-wallet-migration) for wallet deployment and builder API key setup.
