Every common hosted-trading error, when it fires, and how to recover.
Hosted trading errors all descend from HostedTradingError. Each subclass also inherits from a semantic parent — InsufficientEscrowBalance is also an InsufficientFunds, OrderSizeTooSmall is also an InvalidOrder. Catch the parent and you handle both hosted and self-hosted paths with the same code. Catch the leaf and you can branch on the specific recovery action.In Python, this is true multi-inheritance — isinstance(e, InsufficientFunds) and isinstance(e, HostedTradingError) both work. In TypeScript, the same effect is achieved with a static isHostedError = true flag and the isHostedError() helper, since JS only allows single-extends.For the full class reference, see API Reference / Errors.This page covers the five errors you’ll hit most often.
When it fires: the order would draw more USDC than your escrow free balance. PMXT debits escrow when an order is submitted; if the requested amount exceeds balance.free, the build phase rejects the order.Detail string:Insufficient escrow balance: requested 50.0 USDC, available 12.34 USDC.Parent classes:InsufficientFunds, HostedTradingError.Recovery: deposit more, or shrink the order. See Escrow lifecycle.
from pmxt.errors import InsufficientFundsfrom pmxt._hosted_errors import InsufficientEscrowBalancetry: client.create_order(...)except InsufficientEscrowBalance as e: print(f"Need to deposit more. {e.detail}") # Build a deposit tx for the shortfall tx = client.escrow.deposit_tx(amount=20.0) # ... sign and broadcast, then retry the orderexcept InsufficientFunds: # Self-hosted path also lands here ...
import { InsufficientEscrowBalance, isHostedError } from "pmxtjs";try { await client.createOrder({ ... });} catch (e) { if (e instanceof InsufficientEscrowBalance) { console.log(`Need to deposit. ${e.detail}`); const tx = await client.escrow.depositTx(20); // sign and broadcast, retry } else if (isHostedError(e)) { // any other hosted-error parent } else { throw e; }}
When it fires: the resolved order is below one of Polymarket’s two independent venue-side minimums on marketable BUY orders. Both rules are enforced by the venue itself (not PMXT), and the higher of the two binds:
5-share minimum. A 2buyat0.78/share is only 2.5 shares — rejected.
**1notionalminimum(marketableBUY).∗∗A5−sharebuyat0.138/share is 0.69—passesthe5−sharerulebutisrejectedbythevenuewiththeliteralerror‘invalidamountforamarketableBUYorder(0.69), min size: 1`. The effective minimum at that price is 8 shares ($1.10).
Detail string (5-share rule):Order size 2.564 below the minimum 5 shares for venue polymarket.**Detail string (1rule,passedthroughfromvenue):∗∗‘invalidamountforamarketableBUYorder(X), min size: 1`.Parent classes:InvalidOrder, HostedTradingError.Recovery: size up the order or pick a cheaper outcome.
The 5-share minimum is enforced after PMXT resolves your USDC amount into shares using the current price. If the price moves between price-check and submit, a borderline-sized order may flip from accepted to rejected. Add a buffer for marginal sizes.
from pmxt._hosted_errors import OrderSizeTooSmalltry: client.create_order(amount=2.0, ...)except OrderSizeTooSmall as e: # Resize: at $0.78/share, 5 shares ≈ $3.90. Round up with buffer. client.create_order(amount=5.0, ...)
When it fires: the pmxt_api_key is missing, malformed, revoked, or expired. Surface is HTTP 401 from trade.pmxt.dev.Detail string:invalid api key or missing api key.Parent classes:AuthenticationError, HostedTradingError.Recovery: rotate the key from pmxt.dev/dashboard. Update your deployed config. Do not retry with the same key.
from pmxt.errors import AuthenticationErrorfrom pmxt._hosted_errors import InvalidApiKeytry: client.fetch_balance()except InvalidApiKey: # Rotate the key; don't retry with the same one raise SystemExit("PMXT_API_KEY invalid — rotate from dashboard")
import { InvalidApiKey } from "pmxtjs";try { await client.fetchBalance();} catch (e) { if (e instanceof InvalidApiKey) { throw new Error("PMXT_API_KEY invalid — rotate from dashboard"); } throw e;}
When it fires: between build_order and submit_order, the built-order TTL elapsed (typically 30 seconds). Also fires for cancel_id expired in the cancel flow.Detail string:built_order_id expired or cancel_id expired.Parent classes:InvalidOrder, HostedTradingError.Recovery: re-build, then re-sign, then submit. Don’t reuse the old built_order_id.
Hardware-wallet signing is the most common cause — Ledger confirmations can take 10–60 seconds, blowing past the TTL. If you sign with a hardware wallet, expect to retry once on BuiltOrderExpired.
from pmxt._hosted_errors import BuiltOrderExpireddef submit_with_retry(client, **kwargs): # In hosted mode, create_order handles build -> sign -> submit atomically. # On BuiltOrderExpired the internal submit raced the 30s TTL; just call again. for attempt in range(2): try: return client.create_order(**kwargs) except BuiltOrderExpired: if attempt == 1: raise continue
import { BuiltOrderExpired } from "pmxtjs";import type { CreateOrderInput, Order } from "pmxtjs";async function submitWithRetry(client, params: CreateOrderInput): Promise<Order> { // In hosted mode, createOrder handles build -> sign -> submit atomically. // On BuiltOrderExpired the internal submit raced the 30s TTL; just call again. for (let attempt = 0; attempt < 2; attempt++) { try { return await client.createOrder(params); } catch (e) { if (e instanceof BuiltOrderExpired && attempt === 0) continue; throw e; } } throw new Error("unreachable");}
When it fires: the side of the book you’re crossing is empty — there are no resting asks for a market buy, or no resting bids for a market sell.Detail string:book has no resting asks or book has no resting bids.Parent classes:InvalidOrder, HostedTradingError.Recovery: wait for liquidity, pick a different outcome, or post a resting limit order via client.create_order(order_type="limit", price=..., amount=...). Limit BUY and SELL are supported, and both use denom="shares" in hosted SDK requests.
from pmxt._hosted_errors import NoLiquiditytry: client.create_order(order_type="market", ...)except NoLiquidity: # Wait, retry against a different outcome, or post a hosted limit order. pass
import { NoLiquidity } from "pmxtjs";try { await client.createOrder({ type: "market", ... });} catch (e) { if (e instanceof NoLiquidity) { // Wait, retry against a different outcome, or post a hosted limit order. } else { throw e; }}
Use aggressive slippage_pct until the upstream economic validator tightens its worst_price checks. Pragmatic defaults: slippage_pct=30 for buys, slippage_pct=99.9 for sells. Lower values frequently trip a precision check that has nothing to do with actual slippage. This will tighten once the validator ships its fix.
Marketable limit price selection. The SDK’s _validate_worst_price gate applies a slippage buffer on top of the book — it is NOT simply “must cross top-of-book”. For an immediately-executable price:
Marketable limit BUY: use price = best_ask (a small +1-tick buffer works too).
Marketable limit SELL: use price = best_ask (i.e. at or above the ask), NOT best_bid or best_bid - 0.01. Posting at best_bid looks marketable in book terms, but the validator’s slippage floor (currently worst_price ≥ best_bid × 0.8 + 0.029, see _hosted_typeddata.py:519) will reject it for thin books or low-price outcomes. Pricing at or above best_ask is the reliable rule.
If you see OutcomeNotFound despite having a valid-looking ID, the SDK could not resolve it on either the catalog UUID or (venue, venue_outcome_id) path. Most commonly: the outcome was resolved against a different venue than the client’s exchange_name, or the outcome has since been removed from the catalog. See Catalog UUID vs venue ID.