API · Troubleshooting
This page takes the exact error strings the Polymarket CLOB returns and explains each one: what it
means, why it usually happens, and the fix, with code for the official clients. Polymarkets.co.il is an
independent guide, not Polymarket. Every string, address and method name below was checked on
25 October 2026 against the official documentation (docs.polymarket.com), the official
repositories under github.com/Polymarket and, for the Deposit Wallet contracts, the verified
source and live reads on Polygon. Where a cause is only reported by developers on GitHub, we say so.
not enough balance / allowancewhen selling: the CLOB cannot see your outcome tokens or their approval. Refresh the cache withupdate_balance_allowanceusingasset_type=CONDITIONALand thetoken_idyou are selling, and make sure the client has the rightsignature_typeandfunder. Details.maker address not allowed, please use the deposit wallet flow: the account trades through a Deposit Wallet (every wallet created since 4 May 2026), but your order uses an EOA, proxy or Safe as maker. Sign as the Deposit Wallet (signature type 3), ideally with the unified SDK. Details.the order signer address has to be the address of the API KEY: the order was signed for a different address than the one your API key belongs to. Create the API key and sign the order with the same client setup. Details.order_version_mismatch: you are still on the V1 client (py-clob-client0.34.x or@polymarket/clob-client), which stopped working on 28 April 2026. Move to the unified SDK. Details.
- Current: the unified SDKs,
polymarket-client0.14.0 on PyPI (importpolymarket) and@polymarket/client0.14.0 on npm. Rust:polymarket_client_sdk_v20.7.0 (repors-clob-client-v2). - Deprecated but still working:
py-clob-client-v21.2.1 and@polymarket/clob-client-v21.2.0. Both now point to the unified SDK. - Dead since the 28 April 2026 exchange upgrade:
py-clob-client0.34.6 and@polymarket/clob-client5.8.1 (repositories archived). Orders they sign are rejected.
Every CLOB error comes back as JSON with an error field, for example
{"error": "not enough balance / allowance"}. Order responses can instead carry
"success": false and the reason in errorMsg. Read that field first: the message
is usually more specific than the HTTP status.
not enough balance / allowance
What it means. The CLOB checked your account before accepting the order and found
too little to cover it, or no approval for the exchange to move it. For a BUY it checks pUSD; for a SELL
it checks the outcome tokens (the conditional tokens) of that exact token_id. Polymarket's
docs say the CLOB "rejects buy orders without the pUSD approval and sell orders without the outcome token
approval". An allowance error names one missing spender at a time.
Why it happens most often when you SELL
- You checked the wrong asset. A Polymarket team member explained on GitHub
(
py-clob-clientissue #265) that sell orders check your conditional token balance, not collateral. Query and refreshAssetType.CONDITIONALwith thetoken_id, notCOLLATERAL. - The CLOB's cache is stale. The CLOB keeps its own view of your balances and approvals.
Polymarket's docs say to refresh the conditional allowance for each outcome before its first sell order
(
GET /balance-allowance/update). - The client points at the wrong wallet. If your shares sit in a proxy, Safe or Deposit
Wallet, the client needs that wallet as
funderand the matchingsignature_type. Inpy-clob-client-v2the balance calls send the client's signature type and ignore thesignature_typefield insideBalanceAllowanceParams, so build the client right. - Open orders reserve the shares. The docs define the largest order you can place as your balance minus what your open orders still reserve. A resting sell for the same shares blocks a second sell.
- The buy has not settled yet. A matched trade moves through
MATCHED,MINEDandCONFIRMED. Developers report (issue #287 onpy-clob-client) that selling the full size right after an instantly matched buy can fail while a slightly smaller sell, or the same sell a little later, goes through. Treat this as reported behaviour, not documented. - Negative-risk market. On CTF negative-risk markets the CLOB also checks approvals for
the Neg Risk Adapter
0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296. If the error names that address, the wallet is missing those approvals. - Buys only: collateral must be pUSD (since 28 April 2026), with enough left after open buy orders.
Fix: refresh the conditional allowance (py-clob-client-v2 1.2.1)
# pip install py-clob-client-v2 (deprecated, still works; unified SDK below)
import os
from py_clob_client_v2 import (
ApiCreds, AssetType, BalanceAllowanceParams, ClobClient, SignatureTypeV2,
)
client = ClobClient(
host="https://clob.polymarket.com",
chain_id=137,
key=os.environ["PK"], # the signing key
creds=ApiCreds(
api_key=os.environ["CLOB_API_KEY"],
api_secret=os.environ["CLOB_SECRET"],
api_passphrase=os.environ["CLOB_PASS_PHRASE"],
),
signature_type=SignatureTypeV2.POLY_PROXY, # 1 proxy, 2 Safe, 3 Deposit Wallet
funder=os.environ["FUNDER"], # the wallet that holds the shares
)
token_id = "YOUR_OUTCOME_TOKEN_ID"
params = BalanceAllowanceParams(asset_type=AssetType.CONDITIONAL, token_id=token_id)
client.update_balance_allowance(params) # refresh the CLOB's cache
print(client.get_balance_allowance(params)) # check balance and allowance
Same fix in TypeScript (@polymarket/clob-client-v2 1.2.0)
import { AssetType, ClobClient, SignatureTypeV2 } from "@polymarket/clob-client-v2";
const client = new ClobClient({
host: "https://clob.polymarket.com",
chain: 137,
signer: walletClient, // viem wallet client for your signing key
creds, // { key, secret, passphrase }
signatureType: SignatureTypeV2.POLY_PROXY,
funderAddress: process.env.FUNDER,
});
await client.updateBalanceAllowance({ asset_type: AssetType.CONDITIONAL, token_id: tokenId });
console.log(await client.getBalanceAllowance({ asset_type: AssetType.CONDITIONAL, token_id: tokenId }));
Fix with the unified SDK (polymarket-client 0.14.0)
The unified clients set up approvals for you. Per the docs, setup_trading_approvals() checks
what exists and submits only what is missing, and order placement can recover a missing exchange allowance
on its own and retry. Neither covers the Neg Risk Adapter, so add those two approvals once.
# pip install polymarket-client
import os
from polymarket import AsyncSecureClient, RelayerApiKey
client = await AsyncSecureClient.create(
private_key=os.environ["SIGNER_PRIVATE_KEY"],
wallet=os.environ["POLYMARKET_WALLET_ADDRESS"],
api_key=RelayerApiKey( # needed for gasless approvals
key=os.environ["POLYMARKET_RELAYER_API_KEY"],
address=os.environ["POLYMARKET_RELAYER_API_KEY_ADDRESS"],
),
)
await client.setup_trading_approvals()
# CTF negative-risk markets: approvals for the Neg Risk Adapter
adapter = "0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296"
await (await client.approve_erc20(
token_address="0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB", # pUSD, for buys
spender_address=adapter, amount="max")).wait()
await (await client.approve_erc1155_for_all(
token_address="0x4D97DCd97eC945f40cF65F87097ACe5EA0476045", # Conditional Tokens, for sells
operator_address=adapter, approved=True)).wait()
print(await client.get_balance_allowance(asset_type="CONDITIONAL", asset_id=token_id))
Positions in Polymarket Protocol V2 markets live in the PositionManager contract, not in Conditional Tokens.
For those, the asset type is CONDITIONAL-V2 with the position ID. py-clob-client-v2
only knows COLLATERAL and CONDITIONAL, so use the unified SDK or the raw endpoint there.
| Token | Approved to | Call |
|---|---|---|
pUSD 0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB | Standard Exchange 0xE111180000d2663C0091e4f400237545B87B996B, Neg Risk Exchange 0xe2222d279d744050d28e00520010520000310F59, Neg Risk Adapter 0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296, Polymarket V2 Exchange 0xe3333700cA9d93003F00f0F71f8515005F6c00Aa | approve(spender, max) |
Conditional Tokens 0x4D97DCd97eC945f40cF65F87097ACe5EA0476045 | Standard Exchange, Neg Risk Exchange, Neg Risk Adapter | setApprovalForAll(operator, true) |
PositionManager 0x006F54F7f9A22e0000CC2AB60031000000ae9fEF | Polymarket V2 Exchange | setApprovalForAll(operator, true) |
0xadA2005600Dec949baf300f4C6120000bDB6eAab) does not
count as the Neg Risk Adapter approval: they are different contracts.signature_type and funder: which values for a proxy, Safe or Deposit Wallet
Many balance and signer errors come down to these two settings. The signing key (your EOA, or a session
key) signs; the funder is the wallet that actually holds pUSD and shares.
| Account type | signature_type | funder |
|---|---|---|
| Deposit Wallet (default for wallets deployed since 4 May 2026) | 3, POLY_1271 | the Deposit Wallet address |
| Proxy Wallet (older email, Magic or Google accounts) | 1, POLY_PROXY | the proxy wallet address |
| Safe Wallet (older MetaMask or Rabby accounts) | 2, POLY_GNOSIS_SAFE | the Safe address |
| Plain EOA (only if allowlisted for trading) | 0, EOA | none; the EOA itself |
Your account wallet address is in the profile menu on polymarket.com. With the unified SDK you do not set
these numbers: the client resolves the wallet type (client.account.walletType in TypeScript,
client.wallet_type in Python). If you call GET /balance-allowance yourself and get
Invalid signature_type, the value is not one the endpoint accepts; the docs' own Deposit Wallet
example sends signature_type=3.
maker address not allowed, please use the deposit wallet flow
What it means. The exchange refused the address in the order's maker field
and asks you to trade through a Deposit Wallet instead. This string is not in Polymarket's error reference
yet, but the docs explain the background: every account wallet deployed on or after 4 May 2026 is a Deposit
Wallet, and a plain EOA can only be the trading wallet if it is allowlisted.
Usual causes.
- You sign with
signature_type=0(EOA) on a key that is not allowlisted. - A newer account set up with type 1 or 2 and a proxy or Safe as funder. Developers have reported this
error for both setups since May 2026 (
py-clob-client-v2issues #48, #51 to #53, #61, #116). - The account has no Deposit Wallet deployed yet.
Fix. Use the Deposit Wallet as maker and funder with signature type 3. The unified SDK
does this for you; omit wallet and it uses the default Deposit Wallet flow, or pass the wallet
address from your polymarket.com profile.
// npm install @polymarket/client viem (@polymarket/client 0.14.0)
import { createSecureClient, OrderSide } from "@polymarket/client";
import { privateKey } from "@polymarket/client/viem";
const client = await createSecureClient({
wallet: process.env.POLYMARKET_WALLET_ADDRESS, // your Deposit Wallet
signer: privateKey(process.env.POLYMARKET_PRIVATE_KEY),
});
console.log(client.account); // walletType 3 = DEPOSIT_WALLET
await client.placeLimitOrder({ assetId, side: OrderSide.BUY, price: "0.52", size: "10" });
In Rust, the docs show the same with polymarket_client_sdk_v2 0.7.0:
.funder(deposit_wallet).signature_type(SignatureType::Poly1271) on the authentication builder.
The Rust SDK places orders for an existing Deposit Wallet but does not deploy one. A developer report
(py-clob-client-v2 issue #111, August 2026) found the legacy Python client could not produce
the nested signature a Deposit Wallet needs, which is another reason to move to the unified SDK.
the order signer address has to be the address of the API KEY
What it means. Your L2 API key belongs to one address and the order was signed for
another. Its sibling, the order owner has to be the owner of the API KEY, is documented as:
the maker in the order does not match the address associated with your API key.
Usual causes.
- API credentials created with one key, orders signed with another (for example after rotating keys or
copying
.envfiles between bots). - For proxy and Safe accounts: the signing key is the EOA and the funder is the proxy or Safe. Swapping
them, or leaving out
funderandsignature_type, breaks the match. - Deposit Wallet orders (type 3) through the legacy v2 clients. Many reports since May 2026
(
py-clob-client-v2#64, #70, #71, #75, #77, #90;clob-client-v2#73, #103). Developers report that switching to the unified SDK resolved it.
Fix. Create or derive the API key and place orders from one consistently configured
client. In the unified SDK, createSecureClient / AsyncSecureClient.create derives or
creates the CLOB credentials, resolves the account wallet and picks the signing flow, so they cannot drift
apart. With a session key, the session key creates its own credentials (see below).
# pip install polymarket-client (polymarket-client 0.14.0)
import os
from polymarket import AsyncSecureClient
client = await AsyncSecureClient.create(
private_key=os.environ["POLYMARKET_PRIVATE_KEY"],
wallet=os.environ["POLYMARKET_WALLET_ADDRESS"],
)
credentials = client.credentials # store these; pass them back to resume the same key
order_version_mismatch
What it means. The order was built for a different exchange version than the one the
CLOB expects. Since the 28 April 2026 upgrade, Polymarket says V1 SDKs and V1-signed orders are no longer
supported: the order struct dropped nonce, feeRateBps and taker,
added timestamp (milliseconds), metadata and builder, and the Exchange
EIP-712 domain version moved from "1" to "2" (the ClobAuth domain stays "1").
Fix. Remove py-clob-client or @polymarket/clob-client and install
the unified SDK (pip install polymarket-client or npm install @polymarket/client viem).
If you sign orders by hand, follow the current order typed data in the docs' Place Orders page. Orders
resting before the upgrade were wiped and must be placed again.
Unauthorized/Invalid api key, Invalid L1 Request headers, Could not create api key
Unauthorized/Invalid api key(401): the L2 key is missing, expired or wrong on an authenticated endpoint. Send all ofPOLY_ADDRESS,POLY_API_KEY,POLY_PASSPHRASE,POLY_SIGNATUREandPOLY_TIMESTAMP.Invalid L1 Request headers(401): the EIP-712 ClobAuth signature used to create or derive a key is malformed or does not matchPOLY_ADDRESS. The L1 message is signed by the signing key (domainClobAuthDomain, version "1", chain 137) with a fresh Unix timestamp in seconds.Could not create api key/Could not derive api key!(400): creation or derivation failed. The docs say to derive the credentials when they already exist for that signer and nonce (nonce 0 unless you manage several credential sets), so switch between create and derive. Developers have also reported Cloudflare 403 blocks on key creation (py-clob-client-v2#41).
Reading market data needs no key at all. Credentials are only for orders, cancels and account data.
Price breaks minimum tick size rule
Full string: order {id} is invalid. Price ({price}) breaks minimum tick size rule: {tick}.
Your price is not a multiple of the market's tick size. Read tick_size from
GET /book or GET /tick-size and round to it. Tick sizes can change (World Cup
markets moved to 0.0025 in July 2026), so listen for the tick_size_change event if you cache it.
Size lower than the minimum
Full string: order {id} is invalid. Size ({size}) lower than the minimum: {min}. The book
returns min_order_size (5 shares in the docs' example). Raise the size above it.
FOK and FAK messages
order couldn't be fully filled. FOK orders are fully filled or killed.: a Fill-or-Kill order could not fill completely against resting orders, so nothing filled. Size to the visible depth, adjust the price, or use FAK.no orders found to match with FAK order. FAK orders are partially filled or killed if no match is found.: a Fill-and-Kill order found nothing at your price.invalid post-only order: order crosses book: a post-only order would have matched at once. Move the price so it rests.
Background: FOK, FAK and GTC orders.
Trading is currently cancel-only, post-only mode, Trading is currently disabled, 425
Trading is currently disabled. Check polymarket.com for updates(503): the exchange is paused; no orders or cancels are accepted.Trading is currently cancel-only...(503): cancels work, new orders do not.post-only mode: only post-only orders and cancels are allowed(503): the response carriesretry_after_secondsand aRetry-Afterheader. Send post-only orders or wait.- HTTP 425 Too Early: the matching engine is restarting. Retry with exponential backoff; the docs' Rust example rebuilds and re-signs the order before each retry.
address in closed only mode, address banned
'{address}' address in closed only mode and '{address}' address banned are
listed by Polymarket without further detail. Polymarket also restricts order placement by location, with
some regions limited to closing positions. Check the requesting IP with
GET https://polymarket.com/api/geoblock (fields blocked, country,
region) and contact Polymarket support about the address itself.
Too Many Requests (429)
You passed a rate limit. Most limits are per IP and Polymarket throttles (delays or queues) requests over the limit; order and cancel requests also have per-signer limits. Back off exponentially and keep a client-side limiter. Current numbers are in the docs' Rate Limits page.
"Order failed" with no clear message
When an order "just fails", check these in order:
- The
errorMsgin the response (orerrorfor non-200 replies). Batch posts return per-order errors inside a 200 response. order timed out(500): the order never reached the book, usually during bursts from one account. It can be resubmitted safely.the market is not yet ready to process new orders: the market is not open for orders yet.invalid expiration: GTD expiration must be at least 3 minutes ahead, and orders expire one minute before the time you set.- The client, wallet type and approvals described above.
Certificate error (SSL: CERTIFICATE_VERIFY_FAILED)
Polymarket's docs do not cover this one, so these are general TLS checks, not Polymarket guidance. A
certificate error when calling clob.polymarket.com usually comes from your side: a wrong system
clock, an outdated CA bundle in your Python or Node environment (update certifi or the OS
certificates), or a corporate proxy or antivirus that intercepts TLS. Fix the trust store rather than turning
off verification: a bot that skips certificate checks sends its signed requests to whoever answers.
The Deposit Wallet contracts: factory, execute(Batch), authorizeSessionSigner
Developers searching for function authorizeSessionSigner or function execute(Batch
are usually reading the Deposit Wallet source. Here is what the verified code does.
- Deposit Wallet Factory:
0x00000000000Fb5C9ADea0298D729A0CB3823Cc07. Listed on Polymarket's Contracts page (docs.polymarket.com/resources/contracts) and used as the Relayertoaddress in the docs. On chain it is an ERC-1967 proxy whose implementation is0x528cc05efac2b0d255e423272187efd41248abd7. - Deposit Wallet Beacon:
0x7A18EDfe055488A3128f01F563e5B479D92ffc3a(Contracts page). Itsimplementation()returned0xf7f27c29e60fe6325bef8da7f93250353d2e3294, the current DepositWallet code for beacon wallets (deployed after 29 June 2026). - Legacy implementation: the factory's
LEGACY_IMPL()returned0x58ca52ebe0dadfdf531cde7062e76746de4db1eb, the DepositWallet code behind the earlier UUPS wallets.
execute(Batch) and onlyFactory
function execute(Batch calldata _batch, bytes calldata _signature) external onlyFactory
modifier onlyFactory() {
require(msg.sender == factory(), OnlyFactory());
_;
}
execute runs a batch of calls (target, value, data)
from the wallet. onlyFactory means only the factory contract may call it; anyone else gets the
OnlyFactory() revert. The factory in turn exposes proxy(Batch[], bytes[]), which
only addresses with the factory's operator role may call. In the docs, signed batches are sent to Polymarket's
Relayer with the factory as the to address, so wallet actions are gasless and you never call
execute yourself.
Inside execute the wallet checks, in order: the batch is not empty, wallet is
this wallet, nonce equals its next nonce (InvalidNonce), the
deadline has not passed (Expired), and the EIP-712 signature (domain
DepositWallet, version "1") comes from the owner or a still-valid session signer
(InvalidSignature). It then runs the calls in order; if any call fails, the whole batch
reverts. A batch signed by a session signer may not call the wallet itself or the beacon.
authorizeSessionSigner
function authorizeSessionSigner(address _sessionSigner, uint256 _validUntil) external onlySelf
This stores an expiry time for a session signer. onlySelf means the wallet must call it on
itself, so the only way in is an owner-signed batch whose call targets the wallet's own address, run through
the factory. It rejects the zero address and a validUntil that is not in the future.
revokeSessionSigner(address) sets the expiry back to zero. Because session-signed batches cannot
call the wallet, a session signer cannot add other signers or change the owner.
Session keys and API keys are different things
- A session key is an ordinary key pair that the Deposit Wallet owner authorizes on chain to
trade for the wallet. Per the docs it lasts 180 days (the Relayer rejects other values; revoke to end it
early), can be scoped to
CLOB,COMBOSRFQorALL, and cannot withdraw funds. The scopes are not in the wallet contract we read; they are recorded when you authorize through the Relayer. Authorizing one needs a Builder API key, which during the beta Polymarket's builder team has to enable. - An API key (L2 credentials: key, secret, passphrase) authenticates HTTP requests to the
CLOB. The session key creates its own API key by signing the ClobAuth message, and its orders still name
the Deposit Wallet as
makerandsignerwith signature type 3. - A session key only sees its own orders, trades and notifications. Revoking it cancels its open orders.
# polymarket-client 0.14.0: owner authorizes, session key trades
from polymarket import AsyncSecureClient, BuilderApiKey
owner = await AsyncSecureClient.create(
private_key=os.environ["POLYMARKET_PRIVATE_KEY"],
wallet=os.environ["POLYMARKET_DEPOSIT_WALLET"],
api_key=BuilderApiKey(
key=os.environ["POLYMARKET_BUILDER_API_KEY"],
secret=os.environ["POLYMARKET_BUILDER_SECRET"],
passphrase=os.environ["POLYMARKET_BUILDER_PASSPHRASE"],
),
)
await owner.authorize_session_key(address=session_key_address) # 180 days
session = await AsyncSecureClient.create(
private_key=os.environ["POLYMARKET_SESSION_PRIVATE_KEY"],
wallet=os.environ["POLYMARKET_DEPOSIT_WALLET"],
)
await session.place_limit_order(asset_id=asset_id, side="BUY", price="0.52", size="10")
The owner can also pause the wallet and, after the factory's timelock (it read 3,600 seconds, one hour, on 25 October 2026), recover assets or opt out of beacon upgrades. The docs warn that an opted-out wallet gets no future fixes.
All errors at a glance
| Error string | Usual cause | Fix |
|---|---|---|
not enough balance / allowance | Missing pUSD or shares, missing approval, stale CLOB cache, wrong funder, shares reserved by open orders | Refresh with CONDITIONAL + token_id (sells) or COLLATERAL (buys); set approvals; correct signature_type and funder |
maker address not allowed, please use the deposit wallet flow | Maker is an EOA, proxy or Safe on an account that must trade through a Deposit Wallet | Signature type 3 with the Deposit Wallet as funder; unified SDK |
the order signer address has to be the address of the API KEY | API key and order signed for different addresses | Create the key and sign orders from one client setup |
the order owner has to be the owner of the API KEY | Order maker differs from the API key's address | Same as above; check funder |
order_version_mismatch | V1 client or V1 order struct after 28 April 2026 | Unified SDK; current order typed data |
Unauthorized/Invalid api key | L2 credentials missing or wrong | Create or derive credentials; send all five headers |
Invalid L1 Request headers | Bad ClobAuth signature or headers | Sign ClobAuth with the signing key, fresh timestamp |
Could not create api key / Could not derive api key! | Key exists or cannot be derived | Derive instead of create (or the reverse), same nonce |
Invalid signature_type | Unaccepted value on balance-allowance | Use the wallet's type (3 for Deposit Wallets) |
Price ({price}) breaks minimum tick size rule | Price not on the tick grid | Round to tick_size; watch tick_size_change |
Size ({size}) lower than the minimum | Below min_order_size | Increase size |
FOK orders are fully filled or killed | Not enough depth for a full fill | Smaller size, other price, or FAK |
invalid post-only order: order crosses book | Post-only order would match | Price it to rest |
post-only mode / cancel-only / Trading is currently disabled | Exchange mode or pause | Wait for Retry-After; cancels still work in the first two |
| 425 Too Early | Matching engine restart | Backoff, re-sign, retry |
order timed out | Burst of orders from one account | Resubmit; spread out submissions |
address in closed only mode | Address limited to closing positions | Check geoblock; contact support |
| 429 Too Many Requests | Rate limit | Exponential backoff, client-side limiter |
OnlyFactory() revert | Called DepositWallet.execute directly | Submit the signed batch through the Relayer |
InvalidNonce() / Expired() revert | Stale wallet nonce or past deadline | Fetch a fresh WALLET nonce, set a future deadline |
docs.polymarket.com); the official repositories
github.com/Polymarket/py-clob-client-v2, github.com/Polymarket/clob-client-v2,
github.com/Polymarket/py-sdk and github.com/Polymarket/ts-sdk, and the issue threads
cited above; package versions on PyPI and npm; the Deposit Wallet source verified on Sourcify; and direct reads
of the factory and beacon on Polygon.Next: Authentication and wallets · Which SDK to use · CLOB API reference · The pUSD migration · API quickstart.



