Proxy / Funder Footguns
Polymarket's signature_type matrix. When your EOA is the funder, when it is not, and how to detect the misconfiguration before it costs you. The balance check that lied because it polled the wrong address — extended to proxy wallets.
Polymarket supports three signature types. For type 0, the signer EOA is also the funder — the address that holds USDC.e is the same address that signs orders. For types 1 and 2, the signer and funder are different: the signer is an EOA controlled by the user, and the funder is a proxy smart contract that actually holds the collateral.
The proxy/funder footgun is what happens when a bot built for one signature type is configured against the assumptions of another — or when the bot queries the wrong side of the signer/funder split and ends up reporting "balance = 0" on a fully-funded account.
The Two Variants
Variant A — signature_type=0, queries expected wallet (correct):
The signer derives from the private key. The funder is the same address. usdc_e.balanceOf(derived) returns the correct balance. This is the clean case — and the case Agent Framework was originally designed for.
Variant B — signature_type=1/2 bot, queries signer EOA (wrong):
The signer derives from a private key held by the user. The funder is a proxy wallet controlled by that signer via a smart contract relationship. usdc_e.balanceOf(derived) returns zero because the signer EOA holds nothing — the proxy does. The bot reports "unfunded" while the proxy is full.
The footgun is that the second variant can look exactly like the first variant from the bot's perspective. If the bot was ported from a type-0 codebase to a type-1 codebase without updating the balance-query logic, it will silently poll the wrong address.
Inline Diagram — The Proxy / Funder Address Matrix
The Detection Pattern
At startup, any bot using signature_type 1 or 2 should log BOTH balances:
from py_clob_client.clob_types import AssetType, BalanceAllowanceParams
signer = Account.from_key(private_key).address
# The funder (proxy) address is operator-supplied configuration — there is
# no exchange-API lookup for it. It is fixed when the proxy wallet is
# created (deterministically derived from the signer by the proxy factory),
# so treat it like any other config value and pin it in the environment.
funder = os.environ["FUNDER_ADDRESS"]
# web3.py contract reads return raw 6-decimal ints (micro-USDC)
signer_balance = usdc_e.functions.balanceOf(signer).call()
funder_balance = usdc_e.functions.balanceOf(funder).call()
# The CLOB reports the collateral balance for the configured funder as a
# string in the same raw 6-decimal units — normalize to int before comparing.
clob_resp = clob.get_balance_allowance(
BalanceAllowanceParams(asset_type=AssetType.COLLATERAL)
)
clob_reported = int(clob_resp["balance"])
logger.info(f"wallets signer={signer} signer_bal={signer_balance} "
f"funder={funder} funder_bal={funder_balance} clob={clob_reported}")
# The canonical balance is the funder balance. Both sides are now raw
# micro-USDC ints, but they will not match exactly: the CLOB-reported
# balance nets out collateral locked in open orders, so on a healthy
# account with resting orders funder_balance > clob_reported by the
# locked amount. Compare with a tolerance, never with ==.
divergence = abs(funder_balance - clob_reported)
TOLERANCE = 1_000_000 # 1 USDC in micro-USDC; widen if you run many open orders
if divergence > TOLERANCE and clob_reported > funder_balance:
# clob_reported should never EXCEED on-chain funds — that is a real
# inconsistency. Halt the same way the wrong-address check does: log
# loudly, then exit so no trade runs on a broken balance view.
logger.critical(
f"balance inconsistency: funder_bal={funder_balance} < clob={clob_reported}"
)
raise SystemExit("on-chain funder balance below exchange report — refusing to trade")
This makes the gap visible in a single log line. If the signer is empty and the funder is funded, the bot's worldview is consistent and trading can proceed. If the signer is funded and the funder is empty, something is deeply wrong — probably a signature type misconfiguration.
The Rule
Types 0, 1, and 2 each have a different signer/funder relationship. The bot must know which one it uses and query the correct side for balance. Type 0 is simple. Types 1 and 2 require explicit proxy lookup and two-address logging. The footgun is skipping the proxy step — and the cost is a false "unfunded" reading that can burn hours of debugging.