Skip to main content
Hosted trading errors all descend from HostedTradingError. Each subclass also inherits from a semantic parentInsufficientEscrowBalance 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.

InsufficientEscrowBalance

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 InsufficientFunds
from pmxt._hosted_errors import InsufficientEscrowBalance

try:
    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 order
except InsufficientFunds:
    # Self-hosted path also lands here
    ...

OrderSizeTooSmall

When it fires: the resolved share count is below the venue’s minimum. Polymarket’s minimum is 5 shares per order. A 2buyat2 buy at 0.78/share is only 2.5 shares — rejected. Detail string: Order size 2.564 below the minimum 5 shares for venue polymarket. 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 OrderSizeTooSmall

try:
    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, ...)

InvalidApiKey

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 AuthenticationError
from pmxt._hosted_errors import InvalidApiKey

try:
    client.fetch_balance()
except InvalidApiKey:
    # Rotate the key; don't retry with the same one
    raise SystemExit("PMXT_API_KEY invalid — rotate from dashboard")

BuiltOrderExpired

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 BuiltOrderExpired

def submit_with_retry(client, *, market_id, outcome_id, **kwargs):
    for attempt in range(2):
        built = client.build_order(market_id=market_id, outcome_id=outcome_id, **kwargs)
        sig = signer.sign_typed_data(built.typed_data)
        try:
            return client.submit_order(
                built_order_id=built.built_order_id,
                signature=sig,
            )
        except BuiltOrderExpired:
            if attempt == 1:
                raise
            continue

NoLiquidity

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, post a limit order instead of a market order, or pick a different outcome.
from pmxt._hosted_errors import NoLiquidity

try:
    client.create_order(order_type="market", ...)
except NoLiquidity:
    # Fall back to a limit order at a price you'd accept
    client.create_order(order_type="limit", price=0.50, ...)

Workaround warnings

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.
fetch_markets returns venue-native IDs, but create_order needs catalog UUIDs. If you see OutcomeNotFound despite having a valid-looking ID, you are almost certainly passing a venue-native ID to a hosted endpoint. See Catalog UUID vs venue ID.

Catching everything hosted

If you only want to know “did the hosted layer reject this?”, catch HostedTradingError (Python) or use isHostedError(e) (TS):
from pmxt._hosted_errors import HostedTradingError

try:
    client.create_order(...)
except HostedTradingError as e:
    log.error("hosted trade failed", status=e.status, detail=e.detail)
For the full error class reference, parent classes, and status codes, see API Reference / Errors.