How to make Polymarket CLOB live orders actually execute from position_manager. After a multi-hour debugging marathon, the answer is the deposit-wallet flow with signature type Poly1271 (ERC-1271): authenticate with the EOA that the deposit-wallet contract’s owner() returns, let the SDK derive the L2 API key (do NOT supply captured creds), and set order.maker = order.signer = funder = deposit wallet. The CLOB then validates that the api-key EOA owns order.signer and accepts the order.

For Agents

Full repo writeup: docs/EOA_TRADING_SOLUTION.md in the position_manager repo. Reference impl: SDK example examples/clob/orders/gtc_limit_buy_deposit_wallet.rs (rs-clob-client-v2). Working image: acef9d1 (has configurable signature_type). Funded deposit wallet 0xAdAc17C0A3725D2CA2Df66E6DcBBAC82ebA0aEb2, owner EOA 0x932E3f… (MetaMask, key held). This note is the solution; pmv2-signer-prearm-proxy-owner-gotcha-2026-06-25 is the pre-arm owner() gate that made it pass.

The Problem — a chain of failures, each revealing the next

Live orders kept getting rejected by the CLOB. Each fix exposed the next error:

  1. 403 geoblockTrading restricted in your region. Fixed by region migration.
  2. 400 maker address not allowed, please use the deposit wallet flow — hit for both the old Magic proxy and for a raw EOA maker.
  3. 400 order signer address has to be the address of the API KEY — signer/api-key mismatch.
  4. 401 Unauthorized / Invalid api key — when supplying captured L2 creds for the wrong/stale account.

Root Cause — Polymarket migrated to deposit wallets (Poly1271 / ERC-1271)

The Rust SDK / official CLI (rs-clob-client-v2) still derive the old CREATE2 addresses:

  • derive_proxy_wallet(eoa, 137) — the legacy “proxy” wallet.
  • derive_safe_wallet(eoa, 137) — the Gnosis “safe” wallet.

Both of those are EMPTY. The user’s actual funds live in a deposit-wallet contract that the Polymarket web UI creates — which is NOT the SDK-derived proxy/safe. And a raw EOA maker is rejected because an EOA is not an onboarded Polymarket account. So every classic maker the SDK can produce is wrong, which is exactly what error #2 means.

The three rejections are all the same root cause

“maker not allowed”, “signer must be the api key”, and “invalid api key” are all symptoms of trying to trade a wallet the CLOB doesn’t recognize as yours, or authenticating as the wrong account. The fix is to point at the real funded deposit wallet and prove ownership of it via its owner() EOA.

Key Insight — how to identify the right wallet AND the right key

A Polymarket deposit wallet is a CONTRACT (eth_getCode != 0x) that exposes an owner() method (selector 0x8da5cb5b). To trade it you must sign with the EOA that owner() returns.

Deposit walletowner() returnsKey held?Tradeable?
0xAdAc17C0A3725D2CA2Df66E6DcBBAC82ebA0aEb2 (funded)0x932E3f… (user MetaMask EOA)yesYES
0xdb0d23c2…d9efe (old Magic, $491.85)0xfa77… (Magic custody)noNO — withdraw via web UI

So the discriminator is: does owner(deposit_wallet) equal an EOA whose private key we hold? If yes → tradeable via the flow below. If no (Magic custody) → the funds can only be moved by withdrawing through the Polymarket web UI.

The Solution — the official deposit-wallet flow

From SDK example examples/clob/orders/gtc_limit_buy_deposit_wallet.rs:

let client = ClobClient::authentication_builder(&signer)   // signer = OWNER EOA key
    .funder(deposit_wallet)                                  // the funded deposit wallet
    .signature_type(SignatureType::Poly1271)                // ERC-1271
    .authenticate()                                          // DERIVES the L2 api key from the owner EOA
    .await?;

Then on the order:

  • order.maker = deposit_wallet (the funder)
  • order.signer = funder (same deposit wallet)
  • The CLOB validates that the api-key EOA is the owner() of order.signer. Since owner(deposit_wallet) == authenticated EOA, owner == signer → accepted.

DO NOT supply captured L2 creds

The earlier 401 was caused by providing POLYMARKET_L2_API_KEY/SECRET/PASSPHRASE. The deposit-wallet flow derives the L2 key from the owner EOA during .authenticate(). Supplying stale/wrong creds overrides the derivation and breaks auth. Derive, don’t supply.

PM Config That Works

On image acef9d1 (which exposes a configurable signature_type):

SettingValue
DB pmv2_autotrade_config.maker_proxythe deposit wallet 0xAdAc17C0A3725D2CA2Df66E6DcBBAC82ebA0aEb2
env PM_SIGNATURE_TYPEpoly1271
signer key POLYGON_V2_PRIVATE_KEYthe owner EOA (0x932E3f)
POLYMARKET_L2_API_KEY / _SECRET / _PASSPHRASEREMOVE (derive, don’t supply)

The PM signer_arm gate (see pmv2-signer-prearm-proxy-owner-gotcha-2026-06-25) checks proxy_matches: owner(maker_proxy) == signer_eoa. Boot log on success:

signer armed eoa=0x932e3f proxy=0xadac... onchain_owner=Some(0x932e3f)

That line means the gate passed and live execution is armed.

Outcome

PM armed clean in dry, then on live the order was ACCEPTED by the CLOB (the maker wall cleared). End-to-end live execution from PM now works through the deposit-wallet / Poly1271 path.

Reference Constants (for future work)

SDK constants / methods used

  • derive_proxy_wallet(eoa, 137) — legacy proxy (EMPTY for this account)
  • derive_safe_wallet(eoa, 137) — Gnosis safe (EMPTY)
  • owner() selector — 0x8da5cb5b
  • pUSD collateral — 0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB
  • Working public Polygon RPC for balanceOf / getCode / owner()https://polygon-bor-rpc.publicnode.com (note: polygon-rpc.com was rate-limited)