For the complete documentation index, see llms.txt. This page is also available as Markdown.

Smart Wallet Integration

How to open and close leveraged positions from Polymarket smart-contract wallets — Safe, Proxy, and the push-funded deposit wallet flow used by new API users.

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.


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. You can also 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.

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();

USDC 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. USDC approval can be batched in the same call.

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

Deposit wallets are 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 USDC 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 USDC to the vault inside an atomic 3-call batch, and the vault verifies the exact balance delta. No approval is ever needed.

reserveDeposit records a transient snapshot of the vault's USDC 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.

Note: 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:

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

Closing a position

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

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.

Relayer submission

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

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 the 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 for wallet deployment and builder API key setup.

Last updated