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
    ...
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;
  }
}

OrderSizeTooSmall

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 2buyat2 buy at 0.78/share is only 2.5 shares — rejected.
  • **1notionalminimum(marketableBUY).A5sharebuyat1 notional minimum (marketable BUY).** A 5-share buy at 0.138/share is 0.69passesthe5sharerulebutisrejectedbythevenuewiththeliteralerrorinvalidamountforamarketableBUYorder(0.69 — passes the 5-share rule but is rejected by the venue with the literal error `invalid amount for a marketable BUY order (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(1 rule, passed through from venue):** `invalid amount for a marketable BUY order (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 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, ...)
import { OrderSizeTooSmall } from "pmxtjs";

try {
  await client.createOrder({ amount: 2, ... });
} catch (e) {
  if (e instanceof OrderSizeTooSmall) {
    await client.createOrder({ amount: 5, ... });
  } else {
    throw e;
  }
}

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")
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;
}

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, **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");
}

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, 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 NoLiquidity

try:
    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;
  }
}

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.
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.

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)
import { isHostedError } from "pmxtjs";

try {
  await client.createOrder({ ... });
} catch (e) {
  if (isHostedError(e)) {
    log.error("hosted trade failed", e);
  } else {
    throw e;
  }
}
For the full error class reference, parent classes, and status codes, see API Reference / Errors.