Changelog

Every update to pmxt, from the first commit to the latest release.

2.51.4

Patch

Fixed

  • Hyperliquid normalizer now extracts resolutionDate from question prose when no expiry: tag is present. Hyperliquid's outcomeMeta payload exposes no structured end-time on either the Outcome or Question objects. Price-bracket markets get a clean expiry:YYYYMMDD-HHmm in the outcome description (parsed); everything else (World Cup teams/matches, Fed funds decisions, CPI prints) puts the deadline only in the question description's legalese, e.g. "by October 14, 2026 at 23:59 UTC". The normalizer now scans the question description for <Month> <DD>[,] <YYYY> [at HH:MM UTC] matches and takes the latest — which is always the resolution deadline (questions often mention an event date and a later cutoff). Defaults to 23:59 UTC when no time is given. Verified against all 28 live HL questions: 27 resolve to the correct deadline (World Cup matches → 2026-07-19, World Cup champion → 2026-10-14, Fed funds → 2026-09-16, CPI → 2026-08-12); the one miss is the Recurring price-bracket parent, which still falls through to the existing outcome-level expiry: tag.

2.51.3

Patch

Fixed

  • Hyperliquid normalizer no longer stamps resolutionDate with the Unix epoch when expiry is unknown. core/src/exchanges/hyperliquid/normalizer.ts used expiryDate ?? new Date(0) for the Outcome Markets path, which caused HL markets without a parseable expiry:YYYYMMDD-HHmm metadata tag (e.g., World Cup team markets) to be serialized with resolutionDate = 1970-01-01T00:00:00Z. Downstream consumers that gate on "is this market still tradable?" via resolutionDate <= now() (hosted-pmxt's ingest sweep is one) would then mass-close otherwise-live markets. The field is already optional in UnifiedMarket, so the fallback is now plain undefined — consumers decide policy.

2.51.2

Patch

Fixed

  • Hunch list ingestion now surfaces live binary prices instead of zero-priced markets. The Hunch list/catalog item now carries raw.odds; the normalizer uses those YES/NO cents on the bare list path while still letting explicit detail/quote odds win. This unblocks Hunch markets from price-gated cross-venue features such as compare, arbitrage, hedge, and matched-prices.
  • Hunch volume24h now passes through raw.volume24hUsd. The previous hard-zero made the recency/ranking signal unusable for Hunch even when the venue provided a trailing-24h figure.
  • Hunch category and tags now map into PMXT taxonomy. Native Hunch categories roll up to top-level PMXT categories (Crypto by default, eventCulture) and add granular tags such as Market Cap, Price, and the token symbol so ?category= filters and market matching work with higher confidence.
  • Hunch catalog crawls now follow nextCursor. fetchRawMarkets() drains the full paginated catalogue when no explicit limit is supplied, with a 50-page safety backstop; explicit limit calls still fetch a single page.
  • Consumer SDK/generated surfaces were synced for Hunch. Added Hunch to the Python and TypeScript SDK exports and made the client-method generators preserve existing hosted-mode overrides while codegen catches up, so the generated-sync checks stay green without regressing hosted routing.

2.51.1

Patch

Followup to the Hunch venue ship in 2.51.0: the adapter was wired into the runtime (factory, registry, exports) but hunch was missing from the ExchangeParam enum in core/scripts/generate-openapi.js. That enum is the source of truth for docs/concepts/venues.mdx (auto-generated), the OpenAPI path enum, and COMPLIANCE.md, so Hunch was invisible on pmxt.dev/docs/concepts/venues and absent from the compliance matrix.

Fixed

  • core/scripts/generate-openapi.js — added hunch to the ExchangeParam enum. Regenerated core/src/server/openapi.yaml, docs/api-reference/openapi.json, and docs/concepts/venues.mdx (now 18 venues).
  • core/scripts/generate-compliance.js — added hunch to EXCHANGE_ORDER. Regenerated core/COMPLIANCE.md (Hunch: 15/20 methods supported).
  • README.md — added Hunch to the supported-exchanges row.

2.51.0

Minor

New venue: Hunch (parimutuel prediction market on Base, x402 settlement). And a full assertion-based audit of the Hyperliquid SDK surface (both Python + TS) caught ten silent bugs that prior "did the call throw?" smoke tests had hidden — several since the venue was added. Read paths now have behavioral assertions on shape AND values; trading paths are verified end-to-end against a live mainnet wallet. Final HL state: every no-creds method behaviorally green in both SDKs (43/43 assertions), credentialed reads cross-checked vs HL raw API (open orders: 7/7 exact match; user trades: 2000/2000 match; synthesized closed orders: 104/104 match), and the full createOrder → openOrders → cancelOrder → openOrders-empty lifecycle proven live.

Added — Hunch venue (#1150)

  • core/src/exchanges/hunch/ — new adapter for playhunch.xyz, a parimutuel (pool-based, not CLOB/AMM) prediction market on Base settling in USDC via x402 / EIP-3009. Inventory is crypto-native: token market-cap ladders, up/down, launchpad, date-window markets. Modeled on the Myriad adapter — market-orders only, fetchOrderBook: 'emulated' (single level synthesized from implied price + pool), cancelOrder / limit / sell unsupported.
  • Surface: fetchMarkets / fetchEvents (single-market wrap — Hunch has no event tier) / fetchOHLCV (flat candles from the odds tape) / fetchOrderBook (emulated) / fetchTrades / fetchPositions / fetchBalance. Binary markets → YES/NO outcomes; N-way (mcap ladders, date windows) → one outcome per bucket key. outcomeId encodes ${marketId}:${side} and round-trips back to a Hunch trade side.
  • createOrder runs Hunch's x402 loop: POST /trade → 402 → sign EIP-3009 TransferWithAuthorization (viem) → re-POST with X-PAYMENT → 200 receipt. Orders > $10 fetch a price-locked quote first. Verified end-to-end against the live 402 challenge (no funds moved).
  • Registration: core/src/index.ts (export + registry + Hunch), core/src/server/exchange-factory.ts (case "hunch"), core/src/server/openapi.yaml (ExchangeParam enum), COMPLIANCE.md.
  • Self-hosted creds: HUNCH_PRIVATE_KEY / HUNCH_WALLET_ADDRESS (Base). Hosted-mode trading (Polygon escrow) is out of scope — Hunch settles on Base via x402 — so it lists as read-only hosted / tradeable self-hosted, like Kalshi.
  • Tests: core/test/normalizers/hunch-normalizer.test.ts — 47 tests covering binary + N-way mapping, the money-critical outcomeIdside round-trip, position/balance/OHLCV/emulated-orderbook. tsc --noEmit clean.
  • Known gap: volume24h is 0 (Hunch reports no 24h split; total volume is the pool size). Fast-follow on our side can add it.

Fixed — Hyperliquid

  • core/src/exchanges/hyperliquid/index.tsfetchOHLCV params was typed as required but server dispatch passes a single merged object as args[0], leaving args[1] undefined. Result: any MCP/HTTP call to HL fetchOHLCV crashed with Cannot read properties of undefined (reading 'start'). Default to { resolution: '1h' } so the type-check passes (resolution is required on OHLCVParams) and the call survives even without explicit params. (#1161, #1162)
  • sdks/python/pmxt/client.py + sdks/typescript/pmxt/client.ts — SDKs sent ?id=... for GET reads, but server method-verbs.json spec for fetchOHLCV / fetchTrades declares the first arg as outcomeId. GET dispatcher (queryToArgs in core/src/server/app.ts) peels primitives by spec name, then bundles remaining query keys into the object arg — so outcomeId arrived undefined and the raw id ended up inside params. Downstream fromMarketId(undefined) crashed with Cannot read properties of undefined (reading 'match'). POST was unaffected (positional args). Fixed both SDKs. (#1162)
  • core/src/exchanges/hyperliquid/normalizer.tsvolume24h was hardcoded to 0 for every HL market and event. Fixed by pulling dayNtlVlm from spotMetaAndAssetCtxs (outcome legs appear as coin: '#NNNN'), summing Yes + No notional per outcome, and threading the map through fetchRawMarkets / fetchEventsImpl. One extra batched call, no N+1; fallback to 0 if spotMetaAndAssetCtxs is unreachable. (#1219)
  • core/src/exchanges/hyperliquid/utils.tsfromMarketId only matched hl-outcome-{N}. Passing an actual outcome token (e.g. 100002000, as returned by UnifiedMarket.outcomes[].outcomeId) threw Invalid Hyperliquid market ID. Now accepts either form via decodeAssetId. Affects fetchOrderBook / fetchOHLCV / fetchTrades. (#1252)
  • core/src/exchanges/hyperliquid/fetcher.tsfetchRawMarkets ignored params.marketId, and fetchRawEvents ignored params.eventId. fetchMarket(marketId=X) returned the first market in the venue list instead of X; fetchEvent(eventId=X) always returned the first question. Both filters now applied via fromMarketId and direct id match. (#1254)
  • core/src/exchanges/hyperliquid/fetcher.tsfetchRawOHLCV and fetchRawTrades ignored params.limit. OHLCV returned the entire window regardless; trades returned a fixed page. Now slices client-side after the venue call returns (HL's candleSnapshot / recentTrades have no native limit param). (#1254)
  • core/src/exchanges/hyperliquid/normalizer.tsnormalizeUserTrade left marketId / outcomeId / fee undefined on every fill. Consumers couldn't tell which market a fill was on. Now populated via the existing coinToMarketId / coinToOutcomeId helpers and the raw fee field. Widened UnifiedUserTrade with optional marketId, fee. Verified 2000/2000 trades fully populated on an active wallet. (#1255)
  • core/src/exchanges/hyperliquid/normalizer.tsnormalizeBalance read crossMarginSummary (perp margin) and hardcoded the currency label as 'USDH'. Outcome markets quote against USDC on the spot account, so users with deposited funds saw USDH: 0 and assumed empty. Now reads spotClearinghouseState alongside the perp account; spot balances surface with their real coin label (USDC, USDH, etc.); funded perp surfaces as USDC_PERP so callers can tell it apart. Verified $14.90 USDC deposit now correctly returns USDC: 14.9. (#1280)
  • core/src/exchanges/hyperliquid/index.tssubmitOrder silently swallowed HL rejections. When HL returned statuses[0].error (e.g. "Order must have minimum value of 10 USDC"), the SDK reported status='filled', id='unknown' — fake success with no way for the caller to know the order wasn't placed. Now raises a typed PmxtError carrying HL's message; also correctly populates price / oid on the filled branch (was checked but never read). (#1281)
  • core/src/exchanges/hyperliquid/index.tscancelOrder hardcoded a: 0 (asset id). HL rejected every cancel with User or API Wallet 0xfaf6... does not exist because the action hash with the wrong asset recovers a different signer address. Now looks up the open order to derive the real asset id via encodeAssetId. Throws a typed "Order not found" when the supplied oid isn't open. (#1281)
  • core/src/exchanges/hyperliquid/auth.tsmsgpackr encoded positive BigInts as int64 (0xd3), HL's server (Python msgpack) encodes the same values as uint64 (0xcf). Identical bit pattern, different type byte → action hash mismatch → signature recovers to wrong address. Only manifested on cancel because order actions don't carry BigInt fields; cancel does (oid). Added fixInt64ToUint64 post-processor: HL actions never carry negative ints (oids, nonces, asset ids are all ≥ 0), so flipping d3 → cf when the following byte is < 0x80 is safe and produces byte-identical encoding to Python msgpack. (#1281)

Fixed — Python SDK (cross-venue)

  • sdks/python/pmxt/errors.pyNetworkError / ExchangeNotAvailable rejected the code / retryable kwargs that from_server_error always sets. Any 503 from Kalshi or another venue raised TypeError: ExchangeNotAvailable.__init__() got an unexpected keyword argument 'code' instead of a typed PmxtError. Both classes now accept **kwargs and default-fill their hardcoded values. Caught by the cross-venue Trump search test where Kalshi briefly errored. (#1219)

Added — Hyperliquid

  • fetchEventsPaginated wrappers in both SDKs. BaseExchange.fetchEventsPaginated existed but neither client.py nor client.ts exposed it. Added fetch_events_paginated / fetchEventsPaginated mirroring the existing fetchMarketsPaginated pattern. (#1252)
  • fetchOrderBooks on Hyperliquid. HL has no native batch order-book endpoint. Implemented as Promise.all over fetchOrderBook; inherits the outcome-token-or-marketId resolution from the single-fetch path. has.fetchOrderBooks auto-flips because BaseExchange._deriveCapabilities introspects overrides. ponytail: comment marks the unbounded concurrency for future capping. (#1253)
  • fetchClosedOrders + fetchAllOrders on Hyperliquid. HL exposes no closed-orders endpoint. Synthesize by grouping userFills by oid and excluding currently-open oids — VWAP price, summed size, summed fee, earliest fill time. fetchAllOrders = openOrders ∪ closedOrders. ponytail: comment marks the limitation: cancelled-with-no-fills orders aren't reconstructable from the public info API. Verified against an active wallet: 7 open + 104 derived closed = 111 all, exact-count match. (#1255)

Changed — types

  • core/src/types.tsUnifiedUserTrade widened with optional marketId and fee fields. Existing consumers continue to work unchanged. (#1255)

Fixed — Python SDK (test-suite rot caught at version cut)

Running the full npm test before tagging surfaced six pre-existing failures untouched by the HL audit. Two were the SDK silently broken since the 2.50.11–14 hosted-routing series; the rest were stale test fixtures from earlier intentional behavior changes.

  • sdks/python/pmxt/client.pyfetch_closed_orders missing hosted-mode guard. The 2.50.11–14 hosted-mode routing series added hosted branches to fetch_balance / fetch_positions / fetch_order / fetch_my_trades / fetch_open_orders / cancel_order but skipped fetch_closed_orders. Hosted-mode callers fell through to the sidecar /api/{exchange}/fetchClosedOrders path and received invalid api key (or whatever the sidecar returned) instead of the typed NotSupported the test asserts. Added an if self.is_hosted branch that raises NotSupported with a pointer to fetch_my_trades (which works in hosted mode and surfaces executed fills).
  • sdks/python/pmxt/client.pyfetch_all_orders missing hosted-mode guard. Identical class of bug to the above. Added the same is_hosted branch raising NotSupported with a pointer to fetch_open_orders + fetch_my_trades.
  • sdks/python/pmxt/client.pyunwatch_order_book bypassed the WebSocket transport. Every other (un)watch* method routes through _watch_required_via_ws / _unwatch_required_via_ws; unwatch_order_book alone hit POST /api/{exchange}/unwatchOrderBook over HTTP. The _unwatch_required_via_ws helper already existed and handles all the bookkeeping (find the active sub by watchOrderBook:{outcomeId} key, send unsubscribe over the WS, clean up _active_subs / _subscriptions / _data_queues / _data_store). The method now calls that helper, matching the other unwatch paths.
  • sdks/python/tests/test_feed_client.py:36 — assertion expected timeout 15, implementation has been 30 since the FeedClient default was bumped. Updated the assertion to 30.
  • **sdks/python/tests/test_hosted_dispatch.py:285 — fixture sent amount: 2.0 and expected 2.0 back, but per 2.50.13's user_trade_from_v0 the v0 wire sends amounts in 6-dec micro-shares (the SDK divides by 1e6 to produce decimal shares). Updated the fixture to 2_000_000 micro-shares so the test asserts the real wire-to-model mapping.

After these fixes the full pipeline (core unit + Python SDK + verification) is zero-failing — 28/28 Jest suites, 246/246 Python tests, "All SDK Integration Tests Passed".

2.50.16

Patch

Fixed

  • sdks/python/pmxt/client.py + sdks/typescript/pmxt/client.tsfetch_open_orders / fetchOpenOrders missing hosted-mode routing branch. Same class of bug as the 2.50.11-14 fixes for fetch_balance / fetch_positions / fetch_order / fetch_my_trades / cancel_order: hosted callers received Trading operations require authentication. Initialize LimitlessExchange with credentials: ... because the SDK fell through to the sidecar /api/{exchange}/fetchOpenOrders path instead of routing to the hosted GET /v0/orders/open?address=... endpoint. Added an is_hosted branch in both SDKs that resolves the wallet address, calls _hosted_request("fetch_open_orders", params={"address": ...}), and maps the response through order_from_v0 / orderFromV0. The self-hosted/sidecar branch is preserved unchanged. Verified live against hosted Limitless: fetch_open_orders() returns [] cleanly and no longer raises.

2.50.15

Patch

Fixed

  • sdks/python/pmxt/constants.py + sdks/typescript/pmxt/constants.ts — Opinion SELL was fully blocked on hosted clients with InvalidSignature: typed_data schema mismatch: no allowlisted verifyingContract configured for chain 56. Per the brain (docs/engineering/architecture/Cross-Chain Settlement (Buy + Sell).md, docs/knowledge/pitfalls/cross-chain-eip712-domain.md), Opinion SELL is a dual-signed parallel flow: the Polygon "pay" leg (CrossChainSellPayParams) signs against the Polygon PreFundedEscrow domain, and the BSC "pull" leg (CrossChainSellPullParams) signs against the BSC VenueEscrow domain — ecrecover runs on BSC for that leg, so the typed-data domain MUST use chainId 56 with the BSC contract's address. The SDK's _VENUE_DOMAIN schema for opinion_sell_bsc_pull correctly references chain 56 with verifyingContract allowlist VENUE_ESCROW_ADDRESSES, but both Python and TS constants files declared VENUE_ESCROW_ADDRESSES as an empty set with a "TODO: add the BSC VenueEscrow address" comment. Any hosted Opinion SELL hit the empty allowlist and was rejected client-side before submit. Added the BSC VenueEscrow address 0x6a273643d84edbb603b808d8a724fb963c7a298a to both constants files. Verified live: order id 375, position 0bf83067-… on Spain market closed from 15.171726 → 0, USDC balance went 48.056457 → 49.735623 (Δ +1.679166 USDC; ~15.17 shares × ~$0.111 effective fill price).

2.50.14

Patch

Fixed

  • core/src/BaseExchange.ts + core/src/router/Router.ts — Router silently dropped the venue filter. Discovered while verifying the Bug #6 fix on hosted-pmxt: router.fetch_markets(query="...", exchange="polymarket") was returning Myriad rows (or anything else, depending on what the catalog ranked first by volume). Root cause: MarketFilterParams (and EventFetchParams) did not declare sourceExchange / exchange fields at all. So when a caller passed exchange="polymarket" as a kwarg, the value reached Router.fetchMarketsImpl but was dropped — searchMarkets / searchEvents accept sourceExchange as a query param, but fetchMarketsImpl only forwarded query, category, limit, offset, closed. The filter was effectively a no-op.
    • Added sourceExchange?: string and exchange?: string (alias) to both MarketFilterParams (which MarketFetchParams extends) and EventFetchParams.
    • Updated Router.fetchMarketsImpl and Router.fetchEventsImpl to forward params?.sourceExchange ?? params?.exchange to the underlying searchMarkets / searchEvents calls.
    • hosted-pmxt's /v0/markets and /v0/events raw REST routes only accepted ?sourceExchange=; updated on branch fix/v0-emit-catalog-uuid-as-outcomeid to also accept ?exchange= as an alias. Same Cloud Build trigger that deployed the Bug #6 fix will redeploy with this change.
    • Per RouterMarketSearchParams / RouterEventSearchParams (types.ts:152-168), the search client interface already declared sourceExchange — the wiring was just missing in the Router layer.
    • The bug was masked until 2.50.13 + hosted-pmxt Bug #6 fix: before the catalog-UUID emission fix, every Polymarket-shaped wire response looked Polymarket-shaped, so a missing filter just returned irrelevant Polymarket-ish rows. After the fix, the wire surfaces the true sourceExchange, and the filter being broken became visible (Polymarket-filter query returned Myriad rows tagged as such).

2.50.13

Patch

A second live-verification round + brain-reading session caught 11 more issues across SDK and docs. The brain-reading reframed Bug #6 (catalog UUID emission) and corrected the docs' settlement story — Opinion is NOT uniformly dual-signature; BUY is single sig + oracle DvP, SELL is dual-signed parallel.

Fixed (Python SDK)

  • sdks/python/pmxt/client.py:1492cancel_order missing hosted-mode routing branch. Same class of bug as 2.50.11/12 fixed for fetch_balance, fetch_positions, fetch_order, fetch_my_trades. Added the hosted branch; it dispatches via the existing _hosted_cancel_order helper (which has been there at client.py:998 all along). Cancel now goes through the hosted route in hosted mode instead of accidentally working via the legacy sidecar.
  • sdks/python/pmxt/_exchanges.py:78-110Limitless constructor rejected wallet_address= kwarg. Polymarket accepts it; Limitless threw TypeError: unexpected keyword argument 'wallet_address'. Added wallet_address and signer params to Limitless.__init__ and forwarded to super().__init__(), matching the Polymarket shape. The Python exchange-class generator template (core/scripts/generate-python-exchanges.js) needs the same fix or this regression returns on the next regen.
  • sdks/python/pmxt/errors.py:66-73MarketNotFound.__init__() raised TypeError: unexpected keyword argument 'code'. Every Limitless fetch_order_book(outcome_id=...) call hit this and crashed. from_server_error at line 165 passes code= and retryable= to whatever class it instantiates, but MarketNotFound / OrderNotFound / EventNotFound all had strict 2-arg __init__. Added **_ignored to absorb the extras (the classes hardcode their own code internally). All three NotFound classes patched preemptively.
  • sdks/python/pmxt/client.py:1395-1401fetch_market("market_id_string") raised AttributeError: 'str' object has no attribute 'items'. Signature now accepts a string positional arg and coerces to {"market_id": params} before camelCase conversion. Doc examples that pass a string id no longer crash. TS strict typing already prevents this.
  • sdks/python/pmxt/_hosted_mappers.py:80-103 — Limitless trade fee field returned as raw micro-USDC. Polymarket trades report fee=0.0012 (USDC); Limitless reported fee=6136 (raw 6-decimal). Normalized in user_trade_from_v0 when venue == "limitless": divide by 1e6. Inverse multiply added to user_trade_to_v0 at line 106-128. Limitless core normalizer doesn't set fee at all on UserTrade — the raw 6136 was coming from the hosted v0 wire (trade.pmxt.dev), so the fix lives in the hosted mapper, not the sidecar normalizer.

Fixed (TypeScript SDK)

  • sdks/typescript/pmxt/client.ts:1163-1170,2344cancelOrder missing hosted-mode routing branch. Same fix as Python; added if (this.isHosted) return this._hostedCancelOrder(orderId); and a new _hostedCancelOrder method mirroring the Python helper (build → sign → cancel using cancelOrderBuild / cancelOrder routes from hosted-routing.ts). TS Limitless already accepts walletAddress via ExchangeOptions — no change needed there.

Docs — settlement architecture rewrite (Opinion + Limitless)

The brain (docs/engineering/architecture/Cross-Chain Settlement (Buy + Sell).md, docs/engineering/decisions/oracle-attested-dvp-settlement.md) is the source of truth. The docs were uniformly wrong about Opinion ("dual-signature cross-chain" applied to both directions) and oversimplified for Limitless.

  • docs/concepts/hosted-trading.mdx — Settle step (line 51). Rewrote to per-venue model with correct trust gates:
    • Polymarket — same-chain Polygon, single EIP-712 OrderParams on PreFundedEscrow, CTF exchange.
    • Opinion BUY — Polygon→BSC, single CrossChainOrderParams + oracle-attested DvP via settleCrossChainBuy. Operator fronts BSC, settlement oracle signs DeliveryAttestation, contract checks bind/oracle sig/tokensDelivered != 0/worst-price before releasing the user's USDC. Trust assumption: honest oracle.
    • Opinion SELL — BSC→Polygon, dual-signed parallel (CrossChainSellPayParams on Polygon + CrossChainSellPullParams on BSC VenueEscrow). settleCrossChainSellUSDCsettleCrossChainSellTokens. 2×2 outcome matrix: row 3 (pay✗, pull✓) is the operator-trusted gap today, bond-backstopped later.
    • Limitless — Polygon→Base, ERC-7683 single sig, v1 signature-gated front-and-reimburse with explicit trust asterisk; v2 will use delivery proof.
  • docs/concepts/hosted-trading.mdx — Custody section (lines 65-72). Rewrote from a Polygon-only frame to a three-chain custody story: Polygon PreFundedEscrow (USDC + CTF), BSC VenueEscrow (Opinion tokens), Base escrow (Limitless tokens). Notes that Opinion BUYs need no BSC user signature (user is the recipient on the depositForUser leg) while Opinion SELLs require a BSC-domain pull signature.
  • docs/guides/signing.mdx — added a "Single-signature vs dual-signature flows" subsection. Single-sig: Polymarket buy+sell, Opinion BUY, Limitless buy+sell — one payload at built.raw["typed_data"]. Dual-sig: Opinion SELL ONLY — built.raw["typed_data"] (Polygon CrossChainSellPayParams on PreFundedEscrow) plus built.raw["pull_typed_data"] (BSC CrossChainSellPullParams on VenueEscrow). Same EVM key signs both; domains distinct (chainId 137 vs 56). Custom-signer note at line 117 scoped to Opinion SELL with the two legs named explicitly.

Docs — operational fixes from the verification round

  • docs/rate-limits.mdx — replaced the single-row 60/min table with the actual three-tier table from docs/engineering/repos/hosted-pmxt.md (verified 2026-05-29 against pmxt.dev/pricing): Free (60/min, 25K credits), Starter (300/min, 250K credits, $29.99/mo), Pro (1000/min, 1M credits, $99.99/mo), Enterprise (custom). Added the credit accounting rule (1 REST = 1 credit; 1 WS message = 0.1 credits). This explains why our test key didn't trigger 429 at 120 req/42.7s — it's on a higher tier than Free.
  • docs/authentication.mdx:141-146 — the documented InvalidApiKey SDK exception class doesn't actually exist; live test confirmed both 401 cases raise the base pmxt.errors.PmxtError with message "missing api key" / "invalid api key". Updated the error table to reflect reality + added a <Note> flagging this as a temporary doc accommodation pending a future SDK release that may add the subclass.
  • docs/guides/escrow-lifecycle.mdx + docs/trading-quickstart.mdx — escrow tx builders (approve_tx, deposit_tx, withdraw_tx) return {"tx": {...}}, not a flat tx dict. Every code block that accessed tx.to / tx.value directly was rewritten to unwrap via result["tx"] (Python) or const { tx } = await client.escrow.X() (TS). Comments now show the envelope shape including chainId, gas, maxFeePerGas, maxPriorityFeePerGas, nonce. ~10 examples fixed across the two files.
  • docs/trading-quickstart.mdx — added a Note that outcome= requires a MarketOutcome instance (the object you get from client.fetch_markets()[0].outcomes[i]). Passing a bare dict raises AttributeError. Pointed at the string-id alternative: client.create_order(market_id="...", outcome_id="...", side="buy", ...).
  • docs/guides/hosted-errors.mdx — added a Warning clarifying the marketable-limit price gates. Marketable BUY: price = best_ask (small +1-tick buffer works). Marketable SELL: price = best_ask (at or above ask), NOT best_bid or best_bid - 0.01. The SDK's _validate_worst_price enforces worst_price ≥ best_bid × 0.8 + 0.029 at _hosted_typeddata.py:519; the practical floor for a marketable SELL on Spain @ $0.138 is at the ask, not below the bid.

Skipped / in flight

  • Bug #6 (catalog UUID emission for Myriad rows in /v0/markets) — background agent stopped because the brain doc's /opt/data/repos/hosted-pmxt path doesn't exist on the actual server (65.109.107.152). hosted-pmxt is deployed to GCP Cloud Run and there's no Hetzner checkout. A local clone at /Users/samueltinnerholm/Documents/GitHub/hosted-pmxt exists but the agent's sandbox can't operate there. Needs a different working location to proceed — tracked for a follow-up.

2.50.12

Patch

A live-verification sweep across Router methods, the catalog-UUID path, fetch_my_trades, curl examples, the self-hosted path, hosted SELL, hosted limit orders, and Limitless hosted writes turned up 7 HIGH-severity bugs. Six are fixed and verified live in this patch; the seventh needs a design call before any change. Plus one security note: see the bottom.

Fixed

  • sdks/python/pmxt/client.py + sdks/typescript/pmxt/client.tsfetch_my_trades was the fourth method missing the hosted-mode routing branch. The 2.50.11 fix covered fetch_balance, fetch_positions, fetch_order but I missed fetch_my_trades. Same class of bug, same symptom: hosted users saw [] even when they had real trades on the wire. Added the hosted branch in both SDKs using the existing fetch_my_trades route key (GET /v0/user/{address}/trades) and the existing user_trade_from_v0 / userTradeFromV0 mappers. Live verification with the test wallet 0xcb856…0cD1: now returns 82 trades including the recent orders 365 (Spain BUY), 366 (Spain SELL), and 367 (Limitless DOGE) with correct venue mapping. Sorry for missing this in 2.50.11.

  • sdks/python/pmxt/_hosted_typeddata.py — hosted limit BUY was un-submittable due to a validator/helper denom contradiction. _hosted_denom() in client.py correctly returned denom="shares" for limit BUY (the user passes a share quantity at an explicit price; the server computes max_cost_usdc = shares × price + slippage buffer). But _validate_polymarket_buy_economics hardcoded denom="usdc" for all BUYs regardless of order type. The validator raised InvalidSignature("economic mismatch: denom expected 'usdc' got 'shares'") locally before any HTTP call, so client.create_order(side="buy", order_type="limit", ...) could never reach the server. The validator was wrong, not the helper. Patched the validator to branch on order_type: market BUY keeps the exact-equality max_cost_usdc == amount check; limit BUY accepts denom="shares" and floor-checks max_cost_usdc >= shares × price (server-side slippage buffer means strict equality isn't possible). Verified live: hosted limit BUY for 10 Spain YES shares at $0.05 returned Order(id="368", status="queued") — accepted by the venue, no InvalidSignature, no 501.

  • docs/trading-quickstart.mdx:165 and docs/guides/hosted-errors.mdx:199 — the "hosted limit orders return 501" claim was fictional. Live test: hosted limit SELL reaches the venue and gets normal business errors (e.g. 400 Insufficient escrowed tokens); no 501 anywhere in the path. Dropped the 501 sentence from both pages. Trading-quickstart now states hosted limit orders are supported via client.create_order(order_type="limit", price=..., amount=...) with the same 5-share / $1 marketable-BUY minimums; flagged that limit BUY had an SDK denom mismatch (fixed in the same release above, so by the time anyone reads this changelog the flag is historical). Hosted-errors NoLiquidity recovery snippet had the same 501 claim — rewrote it to point at the working limit-order path.

  • docs/router/prices.mdxrouter.fetch_related_markets doc example couldn't run. Doc showed router.fetch_related_markets(market_id="...") returning typed dataclasses with r.relation, r.venue, r.best_bid attribute access. Reality: the SDK signature is fetch_related_markets(self, params: dict, **kwargs) (inherited from Exchange with no Router override at sdks/python/pmxt/router.py), so the kwarg form raises TypeError. Return is a list of plain dicts with camelCase keys (market, relation, confidence, reasoning, bestBid, bestAsk, venue), not typed objects. Rewrote the example to match the real SDK: positional dict argument ({"marketId": "..."}) and dict-key access on the results. Follow-up worth doing: add a Router-level wrapper that takes kwargs and returns a typed RelatedMarket dataclass, mirroring how compare_market_prices returns typed PriceComparison rows. Tracked, not in this patch.

  • sdks/python/pmxt/router.py + sdks/typescript/pmxt/router.tscompare_market_prices was returning empty venue, best_bid, best_ask. The wire payload from /api/router/compareMarketPrices carries the data, but under different keys than the SDK was reading: bid/ask are nested under market.bestBid/market.bestAsk (where _parse_market already maps them onto UnifiedMarket.best_bid/best_ask), and venue is market.sourceExchange — there is no top-level venue / bestBid / bestAsk field. The SDK mapper read only the top-level fields and got nulls every time. Doc's printf example f"{p.venue:12s} bid {p.best_bid:.2f} ask {p.best_ask:.2f}" crashed on TypeError. Added a fallback chain in both Python and TS mappers — top-level → market.best_bid / market.source_exchangemarket_payload["bestBid"] / market_payload["sourceExchange"]. Live verification on the 2026 World Cup Winner - Norway market returned three populated rows across polymarket / kalshi / limitless. Same fix applied to fetch_hedges which used the parallel mapper.

  • docs/api-reference/fetch-events.mdx:125 + docs/api-reference/fetch-markets.mdx:123 — two curl examples returned HTTP 400. Both passed sort=volume which the catalog now rejects: {"error":"unsupported_params","message":"Parameters not supported by the catalog: sort"}. Dropped sort=volume from both curls and the surrounding prose/Python/JS snippets, renamed those sections to "Filter active by category" / "Filter by status". Live verification: both corrected curls return HTTP 200. NOTE: sort is still declared as a real param in core/src/BaseExchange.ts:81,111 and the generated core/src/server/openapi.yaml still publishes it. The catalog dropped support without updating the SDK or OpenAPI. Either re-implement sort on the catalog or remove it from the SDK type signature — tracked separately, not in this patch.

Investigated, no fix shipped

  • "Router leaks Kalshi-shape outcome IDs into Polymarket query results" turned out to be a mis-framing. The IDs in question (42220:605:N) are not Kalshi-shape — they're Myriad-native: {networkId=42220 (Celo)}:{marketId=605}:{outcomeId=N}. Constructed by design in core/src/exchanges/myriad/normalizer.ts:52 and utils.ts:74; negative values like -8 are the "Not" leg of a binary outcome. The Router query that surfaced these returned sourceExchange=myriad rows, not polymarket rows; my initial diagnosis was wrong. The REAL issue: the SDK's _looks_like_catalog_uuid check at sdks/python/pmxt/client.py:764 doesn't recognize venue-native composite IDs like Myriad's, so they get forwarded to the hosted backend as (venue=myriad, venue_outcome_id="42220:605:N") — which is technically correct but the backend resolver may or may not accept that shape. The fix needs a design call between three options: (a) widen _looks_like_catalog_uuid to recognize Myriad composites, (b) make the catalog assign UUIDs to Myriad outcomes (currently Myriad's normalizer uses the venue-native composite as the outcome_id), or (c) ensure the backend resolver accepts the composite venue-native shape. Tracked for separate work.

Security note

During the compare_market_prices live-verification curl, the API key was emitted to stdout via curl -sv (the verbose flag echoes the Authorization header). The key was already in scope (loaded from .env), but rotating PMXT_API_KEY at pmxt.dev/dashboard would be prudent.

2.50.11

Patch

End-to-end live verification of the 2.50.10 doc claims surfaced two real SDK routing bugs and one hosted-API response-shape bug. All three fixed in parallel and verified live against trade.pmxt.dev with the test wallet 0xcb856a79c3E6490e0cFD7934eB59326E593C0cD1.

Fixed (Python SDK — sdks/python/pmxt/client.py)

  • fetch_balance (line 1643), fetch_positions (line 1622), fetch_order (line 1512) now route through the hosted v0 endpoints in hosted mode. Previously all three punted to the legacy sidecar (POST /api/polymarket/fetchBalance and equivalents), which calls Polymarket CLOB's getBalanceAllowance and an on-chain balanceOf on pUSD at 0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB — the WRONG token (pUSD instead of USDC.e) at the WRONG contract (CLOB collateral, not PreFundedEscrow). Any wallet that funded a hosted account via the dashboard's Deposit flow saw available=0, total=0, chain=None, venue=None from fetch_balance while their USDC.e was sitting safely in PreFundedEscrow.balances(wallet). Same root cause for fetch_positions (returned [] for wallets with real positions) and for fetch_order (forwarded the PMXT-internal task_id straight to Polymarket's CLOB, which surfaced the misleading error Invalid orderID [Polymarket]).
  • All three now dispatch via _hosted_request("fetch_balance" | "fetch_positions" | "fetch_order", ...) in hosted mode, using existing mappers balance_from_v0 / position_from_v0 / order_from_v0 from _hosted_mappers.py. Self-hosted branches preserved unchanged — they continue to use the sidecar, which is correct in self-hosted mode.

Fixed (TypeScript SDK — sdks/typescript/pmxt/client.ts)

  • fetchOrder (line 1189), fetchPositions (line 1319), fetchBalance (line 1345): same bug, same fix. Hosted-mode branches added that dispatch via _tradingRequest with HOSTED_METHOD_ROUTES.get("fetchOrder" | "fetchPositions" | "fetchBalance") and the orderFromV0 / positionFromV0 / balanceFromV0 mappers from hosted-mappers.ts.

Live verification

Same three calls, before and after, against the same wallet on trade.pmxt.dev:

MethodBeforeAfterOn-chain truth
fetch_balance()total=0, available=0, chain=None, venue=Nonetotal=52.094842, available=52.094842PreFundedEscrow.balances(0xcb856…) = 52.094842 USDC.e (verified via direct contract call to 0x3ad326f78b1390b9a5dc5f00e7f62f8632de23e2)
fetch_positions()[]25 real positionsWallet has historical positions; SDK now sees them
fetch_order("364")BadRequest: Invalid orderID [Polymarket] (CLOB rejected an internal task_id it never assigned)HostedTradingError: invalid amount for a marketable BUY order ($0.77), min size: 1 (the real venue error surfaced through the hosted route)GET /v0/orders/364 returns status="failed" with the literal CLOB error string

Docs

  • docs/trading-quickstart.mdx — The "What PMXT abstracts" Note in Step 5 was incomplete. Polymarket has TWO minimums that fire independently: the 5-share minimum (already documented) AND a $1 minimum on marketable BUY orders (previously undocumented). At $0.138/share, the 5-share rule says 0.69 is enough — but the $1 marketable-BUY rule rejects with invalid amount for a marketable BUY order ($0.77), min size: 1. The Note now describes both rules and works the example. Surfaced because the live test placed exactly this rejection.
  • docs/trading-quickstart.mdx Step 6 ("Verify the fill") — added a Note clarifying that the id returned by hosted create_order is a PMXT internal task_id (not a Polymarket order id), must be looked up via client.fetch_order(id) (which now actually works after the SDK fix above), and that fresh orders return status="queued" because venue submission is async. The "queued" wording aligns with the server-side change applied on the trading-api server (branch fix/submit-order-queued-not-accepted on /root/pmxt-trading, commit 1576a03 — not yet pushed to production).
  • docs/guides/hosted-errors.mdx — the OrderSizeTooSmall section was rewritten to split the 5-share rule from the $1 marketable-BUY rule, quote the literal CLOB error string invalid amount for a marketable BUY order ($X), min size: 1, and explicitly note this is the venue's check, not PMXT's.

Investigated but NOT bugs

  • Initial misattribution to a degraded Polygon RPC. A first-pass investigation pointed at lb.drpc.live and 172 "USDC balance call failed in multicall" warnings on the server, claiming a silent-zero on RPC failure. Martin pushed back, correctly: drpc is healthy. Independent verification (direct eth_call to lb.drpc.live) returns 52.094842 USDC.e for the test wallet immediately. The 172 warnings are noise: batch_user_escrow_balances in trading_api/core/multicall3.py is reused across the Polygon HomeEscrow (which implements balances(address)) and the BSC + Base VenueEscrow contracts (which do NOT — every balances() call to them reverts with execution reverted: 0x by design). The caller intentionally discards those sub-call results (escrow.py:73-74) but the wrapper logs WARN on every call. Cosmetic log-level bug, separately tracked, not the cause of the SDK reading 0. The SDK bug was that it never asked the hosted v0 endpoint in the first place — it routed to the sidecar which queried the wrong contract.

2.50.10

Patch

Fixed

A code-correctness audit against the SDK source found 8 HIGH-severity bugs in published docs — code blocks that would TypeError, AttributeError, or silently no-op. Two of them I introduced myself in 2.50.6 by rewriting against the wrong SDK truth. All fixed in parallel with three agents, each verifying against the SDK source line they cite.

  • docs/authentication.mdx:107 + docs/guides/self-hosted.mdx:93: Python create_order(type="limit", ...) would raise TypeError: unexpected keyword argument 'type'. The parameter is order_type=, not type= (sdks/python/pmxt/client.py:2892). Fixed both call sites.
  • docs/guides/escrow-lifecycle.mdx:118,124: Self-introduced regression from 2.50.6. After fixing fetch_balance to return a list, I documented usdc.free / .used / .total — those fields don't exist. The Balance dataclass exposes available / locked / total (sdks/python/pmxt/models.py:596-603; sdks/typescript/pmxt/models.ts:387-393). .free and .used would AttributeError on first access. Updated both Python and TypeScript examples to use the real field names.
  • docs/router/prices.mdx:206: poly.fetch_order_book(market_id=clusters[0].markets[0].market_id)fetch_order_book takes outcome_id, not market_id (sdks/python/pmxt/client.py:1441). The _compat_id resolver at line 1446 would catch the missing required arg and raise TypeError: Missing required argument: 'outcome_id'. Updated to pull an outcome ID from clusters[0].markets[0].outcomes[0].outcome_id.
  • docs/concepts/catalog-uuid-vs-venue-id.mdx:48-56: The "reverse-resolve a venue ID to a catalog UUID" example showed router.fetch_matched_market_clusters(venue="polymarket", venue_market_id="0x...") then read clusters[0].market_id. None of those exist: the method signature (sdks/python/pmxt/router.py:328-350) accepts only market_id, slug, url, query, venues/exclude_venues, and MatchedMarketCluster (models.py:939-964) has cluster_id + markets[] with no top-level market_id. There is no clean SDK reverse-resolution API for the venue→catalog mapping. Replaced the broken example with a POST /v0/sql query against prediction_markets.markets filtered by venue + venue_market_id — which is what the next paragraph of the page already pointed at.
  • docs/guides/signing.mdx## Advanced: bring your own signer: The 2.50.6 rewrite of this section shipped a Python pattern that doesn't work and a TypeScript pattern that throws on the first call. Two distinct bugs:
    • Python: the pattern was built = client.build_order(...); built.signed_order = my_signer.sign_typed_data(...); client.submit_order(built). In hosted mode, submit_order ignores built.signed_order and re-signs internally with self.signer (client.py:875-895 in _call_hosted_signer; client.py:911 reads signer or self.signer; client.py:3203 _hosted_submit_body(..., signer=None) falls back). The custom signature was silently discarded and the user's private_key was used instead. Anyone following the doc would think their custom signer worked when in fact only the private_key path was active.
    • TypeScript: worse — submitOrder is explicitly disabled in hosted mode and throws PmxtError("submitOrder is not available in hosted mode. Use createOrder instead.") (client.ts:1134-1137). The TS recipe could never run.
    • Correct pattern: constructor injection. Python pmxt.Polymarket(..., signer=my_signer) (client.py:336 — constructor accepts signer: Optional[Any] = None). TS new Polymarket({ ..., signer: mySigner }) (client.ts:288,335).
    • Signer protocol: the Python signer must implement sign_typed_data(typed_data: dict) -> hex — taking the WHOLE typed-data dict (client.py:875-895 iterates ("sign_typed_data", "sign") and passes the full dict). The 2.50.6 docs incorrectly showed a split domain=/types=/message= signature; that was wrong. The TS Signer protocol (signers.ts:33-35) is { address: string; signTypedData(typedData: TypedData): Promise<string> }. Rewrote both examples to constructor-inject and call create_order / createOrder. Removed the build/sign/submit dance entirely — it's not a hosted-mode pattern.
  • docs/guides/signing.mdx — TS field names: TS buildOrder/createOrder examples used orderType: and slippagePct:. The CreateOrderParams interface (models.ts:533-545) uses type: (not orderType). Slippage is even more unusual — TS forwards it through the params extension dict as the snake_case key slippage_pct: (client.ts:2429-2430). The TS field name really is snake_case here; that's not a typo. Updated all TS examples.
  • docs/guides/hosted-errors.mdx — TS NoLiquidity recovery (line 214): same orderType:type: fix.
  • docs/guides/hosted-errors.mdxBuiltOrderExpired retry pattern (lines 150-187): same build/sign/submit pattern problem as signing.mdx. The retry example showed built.signed_order = ...; submit_order(built). Same issue: Python hosted ignores the assignment and re-signs; TS hosted throws. Rewrote both examples to call create_order / createOrder directly inside the try/except — the SDK's convenience wrapper handles build → sign → submit atomically, so a BuiltOrderExpired retry is just calling create_order again.
  • docs/api-reference/fetch-ohlcv.mdx:7: Broken internal link /dashboard (an in-docs path that doesn't exist) replaced with the external https://pmxt.dev/dashboard. The only broken internal link in the docs tree per the link-integrity sweep.

2.50.9

Patch

Docs

  • docs/api-reference/openapi-hosted-trading.json: Nine occurrences of the "Available on: Polymarket, Opinion" callout that renders on every hosted trading endpoint reference page (build-order, submit-order, create-order, cancel/build, balances, etc.) were missing Limitless. Bulk-updated to "Polymarket, Opinion, Limitless". This file is the source-of-truth for those endpoint pages in this repo (not synced from hosted-pmxt — only openapi-hosted.json is in docs-sync-check.yml's exclude list), so the change lands directly. Matches the SDK routing and the /concepts/hosted-trading page updated in 2.50.7.
  • docs/rate-limits.mdx: The "Need more?" card at the bottom of the page linked to pmxt.dev/dashboard with the copy "Reach out from the dashboard if you need higher limits." Wrong destination for the question being asked — a builder hitting their rate limit wants to see plans and limits, not the dashboard chrome. Retargeted the card to pmxt.dev/pricing and retitled to "Pricing" with copy "See plans and limits, or reach out for custom quotas." Every other dashboard link in the docs (create key, deposit USDC, rotate key) is correctly targeted and was left alone.

2.50.8

Patch

Docs

  • Canonical "Hosts" section. PMXT serves two hostnames — api.pmxt.dev (reads, Router, MCP, venue passthrough) and trade.pmxt.dev (hosted writes + hosted account state) — and the docs explained that split in different words on docs/introduction.mdx, docs/sdk/server.mdx, and implicitly across hosted-trading/security/migrate. A builder reading any one page in isolation couldn't see the full picture; a builder reading docs/authentication.mdx or docs/rate-limits.mdx (which only mentioned api.pmxt.dev) couldn't tell trade.pmxt.dev existed. Three parallel agents pinned this to one canonical location.
  • docs/authentication.mdx: Added a ## Hosts section at the top of the page (slug #hosts). Nine lines: lists both hosts and what each serves, confirms the same pmxt_api_key authenticates against both, one line on why the split exists (latency and availability profile for the signed-order submission hot path).
  • docs/rate-limits.mdx: Disambiguated which limits apply to which host. Added a line after the intro stating the per-API-key limits apply across both hosts (linking to /authentication#hosts), and clarified below the table that the /v0/* row covers both api.pmxt.dev/v0/* (Router) and trade.pmxt.dev/v0/* (trading) under the same per-key budget.
  • docs/introduction.mdx: Replaced the bullet that listed both hosts inline with a link to the canonical section, and replaced the matching code comment. Two single-line edits.
  • docs/sdk/server.mdx: Replaced one inline mention of both hosts with a link to the canonical section.
  • Files reviewed but not edited (host being USED, not re-explained): quickstart.mdx, mcp.mdx, security.mdx, concepts/hosted-trading.mdx, concepts/catalog-uuid-vs-venue-id.mdx, guides/hosted-errors.mdx, guides/migrate-to-hosted-trading.mdx, guides/self-hosted.mdx, router/search.mdx, router/matching.mdx, router/event-matching.mdx. 24 hostname mentions reviewed across all docs; 3 edited across 2 files (plus the canonical section + rate-limits clarification). Every other mention was the host being called in a code example or named in a flow diagram — those stay.

2.50.7

Patch

Fixed

  • sdks/typescript/pmxt/hosted-routing.ts + sdks/python/pmxt/_hosted_routing.py: HOSTED_TRADING_VENUES includes limitless (since 2.50.3 wired the limitless_buy / limitless_sell_polygon / limitless_sell_base_pull / cancel_limitless_* schemas), but the NotSupported error message thrown to callers attempting hosted trading on an unsupported venue still said "Hosted trading is only supported for Polymarket and Opinion". A caller who hit the error would believe Limitless was unsupported even though it would have worked, and anyone reading the SDK source to figure out which venues are hosted-tradable saw a directly contradictory pair (the set says yes, the error string says no). Updated both SDKs to "Polymarket, Opinion, and Limitless".

Docs

  • Limitless hosted writes documented across the docs surface. The 2.50.3 release wired Limitless through the hosted trading path but no doc page reflected it, so the docs collectively asserted Limitless was read-only / self-hosted-writes-only. Updated nine claim sites across six files:
    • docs/trading-quickstart.mdx:9 — "Hosted writes today: Polymarket, Opinion, and Limitless."
    • docs/authentication.mdx:83 — same.
    • docs/security.mdx:54 — "Hosted (Polymarket, Opinion, Limitless): PMXT never sees the venue private key."
    • docs/guides/escrow-lifecycle.mdx:9 — escrow funding line now lists Polymarket, Opinion, and Limitless as the "today" set.
    • docs/guides/escrow-lifecycle.mdx:20 — "Hosted exchange clients (Polymarket, Opinion, Limitless) expose an escrow namespace."
    • docs/guides/self-hosted.mdx:15 — hosted-writes list updated; Limitless removed from the self-hosted-only list of venues.
    • docs/guides/self-hosted.mdx:20 — "consumer app against Polymarket, Opinion, or Limitless".
    • docs/concepts/hosted-trading.mdx:9 — "trade Polymarket, Opinion, and Limitless" in the funding-once paragraph.
    • docs/concepts/hosted-trading.mdx:18 — "trading across Polymarket, Opinion, and Limitless".
    • docs/concepts/hosted-trading.mdx:51 — added a Limitless settlement sentence: "buys are settled on Polygon directly from escrow; sells use a Base-side pull leg that draws against the user's Polygon escrow" (the Polymarket and Opinion settlement descriptions were already there).
    • docs/concepts/hosted-trading.mdx:119 — Hosted-vs-self-hosted comparison table: "Trading venues | Polymarket, Opinion, Limitless".
    • docs/concepts/hosted-trading.mdx:131 — "What's supported today" table: added a Limitless row (Yes / Yes / "Polygon buy leg, Base pull-sell leg") and removed Limitless from the Kalshi/Smarkets/etc. read-only row.
  • docs/concepts/hosted-trading.mdx:89 — operator multisig wording. The previous wording said "A multisig replacement is planned but not yet deployed" on one line while line 93 said operator is immutable. Read straight through, that reads like an in-place rotation that the contract design forbids. Rewrote to "Because operator is immutable on the deployed contracts, switching to a multisig would mean deploying new escrow contracts and migrating user balances — not an in-place rotation." Same facts, no contradiction.

2.50.6

Patch

Docs

  • Deleted docs/sql.mdx. Removed from Get Started in docs/docs.json and from the API Reference Enterprise group. The POST /v0/sql endpoint reference page stays. One inbound link in docs/concepts/catalog-uuid-vs-venue-id.mdx:56 retargeted from [SQL endpoint](/sql) to a plain POST /v0/sql reference.
  • Contradiction sweep — round 1. Three audit agents (SDK-truth-vs-docs, inter-page contradictions, stale-feature-status) found 60+ candidate findings; fixed every HIGH-severity item that didn't require a product call. Skipped items flagged separately at the end of this note.
  • docs/introduction.mdx: "11 venue integrations" → "15+ venue integrations" — core/src/server/exchange-factory.ts registers 16 (excluding test/demo/mock the customer count is ~14, "15+" is the honest marketing number).
  • docs/concepts/prediction-markets-101.mdx: USDC entry was misleading — said "Polymarket-native USDC". Actual settlement asset is USDC.e (bridged) at 0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174, which is what Polymarket's CTF exchange uses. The migrate-to-hosted-trading guide already had this right; the 101 was contradicting it. Same edit confirms native Polygon USDC and bridged USDC on other chains are not interchangeable with USDC.e.
  • docs/concepts/hosted-trading.mdx: client.escrow.deposit() / .withdraw() — neither method exists. The Escrow class exposes approve_tx / deposit_tx / withdraw_tx / withdrawals (sdks/python/pmxt/escrow.py:114-184). Replaced with the actual call shape including the withdraw_tx("request" | "claim" | "cancel", ...) argument signature.
  • docs/guides/escrow-lifecycle.mdx: fetch_balance returns List[Balance], not a single Balance (sdks/python/pmxt/client.py:1643). The "Confirm the deposit" example unpacked balance.free / .used / .total directly, which would AttributeError on a list. Fixed to balances = client.fetch_balance(); usdc = balances[0]; .... The trading-quickstart already used the list form correctly; this brought escrow-lifecycle in line.
  • docs/guides/signing.mdx: The "Advanced: bring your own signer" example used three attributes that don't exist on the SDK's BuiltOrder dataclass — built.typed_data, built.built_order_id, and a kwargs-style submit_order(built_order_id=, signature=). The actual Python signature is submit_order(built: BuiltOrder) (sdks/python/pmxt/client.py:3183); typed-data lives at built.raw["typed_data"]; the signature attaches as built.signed_order before submit. Rewrote both Python and TypeScript examples to match. The TS submitOrder({ builtOrderId, signature }) shape was also wrong — TS submitOrder accepts the BuiltOrder object too.
  • docs/guides/hosted-errors.mdx (two fixes in one page):
    • The "BuiltOrderExpired retry" example had the same wrong built.typed_data / built.built_order_id / kwargs submit_order shape as the signing guide. Rewrote both Python and TS to use the real attribute paths and the object-shape submit_order(built) / submitOrder(built) call.
    • The stale "fetch_markets returns venue-native IDs, but create_order needs catalog UUIDs" warning was the same lie that 2.50.3 and 2.50.5 fixed in hosted-trading.mdx and prediction-markets-101.mdx. Hosted-errors.mdx was the third place it lived. Rewrote the OutcomeNotFound Quick check to describe the real failure modes — outcome resolved against a different exchange_name, or removed from the catalog — and dropped the venue-native-ID accusation entirely.
    • The NoLiquidity recovery example told callers to fall back to a hosted limit order. Hosted limit orders return 501 (docs/trading-quickstart.mdx:165) — the advice was internally inconsistent and would crash any caller who followed it. Rewrote the recovery to "wait, switch outcome, or run self-hosted for resting limits."
  • Skipped — needs your call before I touch them:
    • Limitless hosted writes. The SDK's hosted routing tables (sdks/typescript/pmxt/hosted-routing.ts:24-28, sdks/python/pmxt/_hosted_routing.py:19) include limitless in HOSTED_TRADING_VENUES, and client.py:829-838 routes the schemas. But the SDK's own NotSupported error string at hosted-routing.ts:75 still says "only supported for Polymarket and Opinion" — and every doc page asserts the same. Either Limitless hosted writes are prod-ready (six doc pages need updating and the error string is wrong) or they're partially wired (no doc change, error string is right). I don't have enough signal to decide.
    • Operator multisig. docs/concepts/hosted-trading.mdx:89 says "A multisig replacement is planned but not yet deployed" while line 93 says the operator address is immutable. Both can be true (replacement = new contract deployment + migration), but the wording reads like an in-place rotation. Wanted your call on the phrasing.
    • trade.pmxt.dev vs api.pmxt.dev terminology drift. Introduction splits hosted into two hostnames (reads on api.pmxt.dev, writes on trade.pmxt.dev); authentication.mdx and rate-limits.mdx only mention api.pmxt.dev. A builder reading auth in isolation won't know trade.pmxt.dev is a thing. Not a wrong claim, just incomplete coverage on auth/rate-limits.

2.50.5

Patch

Docs

  • docs/concepts/prediction-markets-101.mdx: Fixed a third instance of the stale "hosted trading requires the catalog UUID" claim — the same lie that 2.50.3 fixed in hosted-trading.mdx. The 101 glossary still asserted catalog UUID was required for hosted trading and that venue-native IDs only came back from per-venue reads. The SDK has handled both forms since _hosted_build_order_request landed (sdks/python/pmxt/client.py:764): catalog UUIDs go on the wire as outcome_id, venue-native IDs go on as (venue, venue_outcome_id). Reframed the paragraph to say the API accepts either and that the catalog UUID matters for cross-venue identity, not as a hosted-trading requirement. Missed by the strip pass because that pass operated by file ownership and the 101 page wasn't assigned to any of the three agents.

2.50.4

Patch

Docs

  • Strip pass — Documentation tab from 23 pages to 18, hosted-mode story told once instead of four times. Three parallel agents and a cross-reference cleanup pass.
  • Hosted mode merged into one page. docs/concepts/hosted-trading.mdx now absorbs hosted-vs-self-hosted.mdx (as a "When to switch to self-hosted" section, table preserved) and hosted-custody.mdx (as a "Custody: PreFundedEscrow" section, with deployed contract addresses, operator addresses, audit status, and the non-pausable / unilateral-withdrawal trust model preserved). Deleted docs/concepts/hosted-vs-self-hosted.mdx and docs/concepts/hosted-custody.mdx. Softened the "treat as unaudited DeFi primitive, only fund what you can afford to lose" YOLO framing into "non-pausable contract, unilateral withdrawal always available even if PMXT is down" — same facts, builder-readable framing.
  • Router events + markets merged into one page. docs/router/search.mdx replaces docs/router/events.mdx and docs/router/markets.mdx, which were ~80% identical (same filter table, same example shape, only nested-vs-flat differed). One filter table, one example per shape, kept the events-vs-markets decision callout. docs/router/event-matching.mdx was checked against docs/router/matching.mdx and left alone — they document parallel but distinct endpoints (fetchMatchedEventClusters vs fetchMatchedMarketClusters).
  • docs/concepts/catalog-vs-live.mdx deleted. It described an internal fallback decision tree (catalog Postgres vs live venue pass-through) that the SDK abstracts transparently. The two-sentence essence was absorbed into docs/concepts/unified-schema.mdx as a <Note> callout. The four docs/api-reference/fetch-{event,events,market,markets}.mdx pages had their [Learn more](/concepts/catalog-vs-live) link retargeted to /concepts/unified-schema.
  • docs/mcp.mdx trimmed from ~200 lines to 34. The page was a full tool-by-tool walkthrough of an integration surface that ~10% of users hit. Now: one paragraph of what-it-is, the .mcp.json snippet for Claude Code, one-liner pointers for Cursor / custom clients, and a link out. Tooling deep-dive belongs in an MCP reference page, not the core docs landing.
  • docs/sdk/server.mdx trimmed to 21 lines. The start/stop/restart/status/health/logs walkthrough moved to a pointer at the GitHub README. The page exists so the nav link still resolves and so self-hosted users land on a clear hand-off.
  • docs/trading-quickstart.mdx reframed for app builders. The "use a dedicated trading wallet" Warning was framed as personal hot-wallet hygiene ("treat the wallet as compromised if the host is ever compromised"); builders reading that took it as "PMXT is dangerous." Now framed as "non-custodial by construction, users keep their own keys, SDK signs locally" — practical advice about using a small float wallet for prod preserved, fear framing dropped. The Polymarket 5-share minimum and budget-capped market order passages were framed as trader gotchas; now framed as "what PMXT abstracts vs what leaks through from the venue." Trimmed ~60 lines of hosted-mode preamble that duplicated the concept page.
  • docs/security.mdx shortened ~35%. Same wallet-language softening. The venue-credentials table duplicated content in /concepts/venues — replaced with a one-line link. Dropped the "compromised PMXT server" speculation and a stray code block inside a Note. Three /concepts/hosted-custody links retargeted to /concepts/hosted-trading.
  • docs/guides/signing.mdx stripped to the SDK-default flow. The full EIP-712 domain/typed-data spec and the Opinion dual-signature mechanics were removed from the main flow (almost no caller hand-writes typed data — the SDK auto-wraps private_key into a signer). Kept "what the SDK does for you," the reads-without-signature section (load-bearing for the trust model), and a trimmed "Advanced: bring your own signer" with one sentence on Opinion dual-sign for BYO-signer integrators. Deleted the BuiltOrderExpired / InvalidSignature pitfall callouts — runtime errors belong in /guides/hosted-errors.
  • docs/guides/self-hosted.mdx reordered decision-first. Previously led with install steps; now leads with "when self-hosted is the right call" (sub-100ms latency, raw venue creds, regulatory custody, unsupported venues for writes) and "when hosted is still the better choice" (everyone else). Install / construct / per-venue credential content unchanged, just demoted below the decision content. Wallet-language softened. Two /concepts/hosted-vs-self-hosted links retargeted to /concepts/hosted-trading.
  • docs/guides/migrate-to-hosted-trading.mdx demoted from ~293 lines to ~69. Retitled "Coming from Polymarket's official SDK." Leads with a Note telling readers who aren't already using @polymarket/clob-client to skip the page. Kept the two load-bearing tables (credential mapping, signatureType mapping) and a compressed USDC.e / allowance / catalog-UUID / rollback summary. The full prose intro about the Polymarket auth model was the bulk of what got cut.
  • docs/sql.mdx reframed for app builders. Lead paragraph and the OHLCV example were written for quants doing tick-level backfills. Now framed as "historical data backend for your app's charts, dashboards, and analytics" with an in-app chart query (Recharts / lightweight-charts shape) instead of a backfill loop. No new sections.
  • Cross-reference cleanup. docs/docs.json Concepts group lost three entries (catalog-vs-live, hosted-custody, hosted-vs-self-hosted); Router Search group went from two pages (events + markets) to one (search). Six remaining cross-refs across introduction.mdx, quickstart.mdx, router/overview.mdx, concepts/prediction-markets-101.mdx, api-reference/overview.mdx, and api-reference/configuration.mdx were retargeted to the merged destinations (/concepts/hosted-trading#when-to-switch-to-self-hosted, /router/search, /concepts/unified-schema). llms.txt and llms-full.txt deliberately left alone — they'll be regenerated by the docs-sync-check workflow on the next push.

2.50.3

Patch

Docs

  • docs/concepts/hosted-trading.mdx: Replaced the stale Warning that told users fetch_markets against a venue exchange returns venue-native IDs that must be reverse-resolved to catalog UUIDs before trading. The SDK has handled both forms transparently since the _hosted_order_target / _hosted_build_order_request path landed (sdks/python/pmxt/client.py:653-780): catalog UUIDs are forwarded as outcome_id, venue-native IDs are forwarded as (venue, venue_outcome_id), and the backend resolver picks the right wire field. The doc now describes the actual behavior and points at Catalog UUID vs venue ID (which was already correct) instead of describing a workaround that no longer exists. Contradicted Trading Quickstart step 5 ("pass any returned outcome straight to create_order — no UUID lookup required"); builders reading concepts-first would have written and shipped unnecessary reverse-resolution code.

2.50.2

Patch

Rain now actually works end-to-end through both pmxtjs and the Python SDK, and restores hosted-mode createOrder for every other venue (polymarket, opinion, limitless) which had been broken on main since PR #1058. Verified live: new pmxt.Rain().fetchMarkets({ limit: 3 }) and Rain().fetch_markets(limit=3) both round-trip real markets (Khamenei binary, Bond actor 6-way, FIFA 16-way) through the sidecar.

Fixed

  • core/src/exchanges/rain/fetcher.ts + core/src/exchanges/rain/websocket.ts: @buidlrrr/rain-sdk is ESM-only ("type": "module", no CJS export) and tsc with module: "commonjs" silently rewrites await import('@buidlrrr/rain-sdk') into Promise.resolve().then(() => require('@buidlrrr/rain-sdk')), which throws ERR_PACKAGE_PATH_NOT_EXPORTED at runtime. Wrapping the loader in new Function('return import("@buidlrrr/rain-sdk")') keeps the real ESM import() opaque to the downleveller, so Node executes it natively. The same trap applies to the Opinion adapter on paper; its read path appears to escape it via existing bundling, but the Rain path lit it up because the trade-tx builders are called from the cold server start.
  • core/src/exchanges/rain/utils.ts + core/src/exchanges/rain/normalizer.ts: New bigintsToStrings() recursive converter is applied to the spread that feeds buildSourceMetadata. Rain's MarketDetails carries bigint for startTime, endTime, oracleEndTime, allFunds, allVotes, totalLiquidity, numberOfOptions, winner, baseTokenDecimals, etc., and the PMXT sidecar JSON.stringifys the response over HTTP — the unconverted spread threw "Do not know how to serialize a BigInt" and dropped every Rain market on the floor before it reached the SDKs.
  • sdks/typescript/pmxt/client.ts: Restored _hostedSubmitOrder (typed-data sign + economic validation + /v0/trade/submit-order POST) and _hostedTypedDataRoute / _hostedCancelTypedDataRoute helpers. PR #1058 (Limitless hosted wire-up, commit e96801b) removed all three methods but left their call sites and signing imports intact; this made npx tsc fail with TS2551: Property '_hostedSubmitOrder' does not exist on type 'Exchange' and silently broke hosted-mode createOrder on every venue at runtime (it would have thrown "this._hostedSubmitOrder is not a function" the moment a hosted user placed an order). The restored helper now also routes limitless through limitless_buy / limitless_sell_polygon / limitless_sell_base_pull / cancel_limitless_* schemas, matching the PR's stated scope.

2.50.1

Patch

Follow-up to 2.50.0. Rain is now wired through every consumer surface — the openapi enum source, the TS SDK class export, and the Python SDK class export — so pmxtjs.Rain and pmxt.Rain (Python) actually exist instead of being missing from the published packages. The CI exchange-drift check caught this on the 2.50.0 push; ADDING_AN_EXCHANGE.md only documents the core-server registration sites, not these SDK-facing ones.

Fixed

  • core/scripts/generate-openapi.js: Added 'rain' to the path-parameter enum and registered new pmxt.Rain() in the capability-introspection instance map so the generated openapi.yaml describes Rain.
  • sdks/typescript/pmxt/client.ts + sdks/typescript/index.ts: Added a Rain extends Exchange class binding to exchange_name="rain" and exported it from both the package barrel and the default-export pmxt namespace.
  • sdks/python/pmxt/_exchanges.py + sdks/python/pmxt/__init__.py: Added a Rain(Exchange) class binding to exchange_name="rain" and surfaced it on the pmxt package plus __all__.

2.50.0

Minor

Added Rain (rain.one) as the 15th venue — a permissionless AMM-plus-orderbook prediction market on Arbitrum One. Reads (markets, events, outcomes, orderbook, OHLCV, positions, balance) and full on-chain writes (market buys via AMM, limit buys/sells through the order book, cancels) are wired end-to-end via the official @buidlrrr/rain-sdk and viem signing. Verified live against production: markets like "Which Team will win FIFA World Cup 2026?" (16 outcomes) and Trump/Khamenei binary markets round-trip correctly through fetchMarkets, fetchEvents, and fetchOrderBook with prices, liquidity, and statuses populated.

Added

  • core/src/exchanges/rain/: New adapter following the existing 3-layer pattern (fetcher, normalizer, websocket) plus a small auth.ts that derives an EVM signer + Arbitrum public client from a privateKey. The SDK is loaded via ESM dynamic import() (same shape as Opinion) since @buidlrrr/rain-sdk is ESM-only. Multi-option markets (e.g. the 16-team FIFA market) are expanded into one synthetic binary UnifiedMarket per option inside a single UnifiedEvent, matching the Polymarket/Myriad grouping the matching engine expects. Orderbook is a 1-level emulated book at AMM spot (mirrors Myriad). Subgraph-backed methods (fetchOHLCV, fetchTrades, fetchMyTrades, fetchOpenOrders) return [] when no subgraphUrl is configured rather than throwing, since 'emulated' is the honest capability for an on-chain venue with no native list endpoint.
  • core/src/exchanges/rain/index.ts: Full trading path. buildOrder returns a populated BuiltOrder.tx = { to, data, value, chainId: 42161 } from the SDK's transaction builders — buildBuyOptionRawTx for market buys, buildLimitBuyOptionTx for limit buys, buildSellOptionTx for sells (Rain has no AMM market-sell; that path throws NotSupported with a message pointing at the limit branch). submitOrder signs with the viem WalletClient, auto-sends an ERC20 approve(MAX_UINT256) on the market contract before the first buy on that market, and waits for the approval receipt before submitting the order tx. cancelOrder parses an id of the form rain:{contract}:{side}:{option}:{price1e18}:{rainOrderId}:{txHash} and dispatches to buildCancelBuyOrdersTx / buildCancelSellOrdersTx, so cancels round-trip without needing subgraph state.
  • core/src/exchanges/rain/utils.ts: resolveDecimals() helper. The Rain SDK returns baseTokenDecimals as the scale factor (e.g. 1000000n for a 6-decimal token), not the decimal count — passing it straight into a 10 ** n computation produced astronomically large scales and silently zeroed every liquidity and volume reading. The helper detects this (> 36 → log10) and normalizes to a real decimal count; called from every fetcher/normalizer site that touches base-token math.
  • core/src/exchanges/rain/normalizer.ts: Reads from both the list-shape response (getPublicMarkets returns Mongo-style _id, question, options[].percentage as 0-100, and totalLiquidityUSD in base-token wei) and the on-chain details-shape (getMarketDetails returns id, title, options[].currentPrice as 1e18 bigint, totalLiquidity as wei bigint). The list-shape is the source of truth for the catalog and is sufficient on its own — the original implementation called getMarketDetails for every market in an N+1 enrichment loop because the published agent docs describe only the details shape; the loop is kept (bounded parallel, top 25 by default) so on-chain prices override the cached percentage when available, but the adapter still works correctly if every detail call fails.
  • core/src/index.ts, core/src/server/exchange-factory.ts, core/src/server/openapi.yaml: Standard 3-site registration. case "rain" reads RAIN_PRIVATE_KEY, RAIN_WALLET_ADDRESS, RAIN_SUBGRAPH_URL, RAIN_SUBGRAPH_API_KEY, RAIN_WS_RPC_URL, RAIN_ENVIRONMENT from env when no explicit credentials are passed.
  • core/package.json: Added @buidlrrr/rain-sdk ^2.0.0. The SDK declares optional peer deps on @account-kit/* and @alchemy/aa-* for its account-abstraction path; we intentionally do not install them in v1 since PMXT trades from an EOA via viem rather than through Rain's smart-account wrapper.
  • README.md: Rain logo added to the supported-venues row.

Hosted trading works from ESM apps and Opinion orders pass pre-sign validation. Two independent bugs each blocked all hosted writes for affected callers: the ESM build could not lazy-load ethers (bare require is undefined in ESM, and the failure was silently swallowed — the signer was dropped and every write died with "hosted write requires a signer" even when a privateKey was passed), and the client-side economics validator demanded message.opinion_market_id from a trading-API message schema that no longer carries it (the signed economic identity is the outcome tokenId). Both verified live against trade.pmxt.dev from an ESM consumer.

Fixed

  • TS pmxt/signers.ts: New loadEthers() helper used by EthersSigner — native require in the CJS build, process.getBuiltinModule("node:module").createRequire(...) in the ESM build (Node >= 20.16). Previously the ESM build's bare require("ethers") threw ReferenceError, which the lazy-signer bridge in the Exchange constructor caught and swallowed, silently discarding the caller's privateKey.
  • TS pmxt/hosted-typed-data.ts: signature verification now loads ethers via the same helper instead of bare require.
  • TS pmxt/hosted-typed-data.ts + Python pmxt/_hosted_typeddata.py: validateOpinionMarketId / _validate_opinion_market_id now validate message.tokenId against resolved.token_id — the field that is actually signed. The opinion_market_id equality check only applies when the message carries the field (legacy schema); requiring it unconditionally rejected every current-schema Opinion order pre-sign with economic mismatch: message.opinion_market_id missing.
  • Python tests/test_hosted_typeddata.py: opinion economics tests updated to the tokenId-based contract (mismatch rejection on resolved.token_id, params-only quirks no longer block, legacy opinion_market_id mismatch still rejected when present in the message).

2.49.10

Patch

Hosted custody docs now link to the public contract explorer pages instead of private GitHub source paths, and explicitly disclose that explorer source verification/public source publication is still pending.

Fixed

  • docs/concepts/hosted-custody.mdx: Replaced the broken private pmxt-dev/pmxt-trading source links in the deployed-contracts table with public Polygonscan/BscScan explorer links for the current Polygon PreFundedEscrow (0x3ad326f78b1390b9a5dc5f00e7f62f8632de23e2) and BSC VenueEscrow (0x6a273643d84edbb603b808d8a724fb963c7a298a) deployments. Added a warning that the explorers show deployed bytecode but are not source-verified yet, so users should treat the explorer addresses plus the unaudited status as the current public security posture. Removed remaining private-source links from the admin-model and withdrawal-delay copy.

2.49.9

Patch

Kalshi API hostname fix. The api.external-api.kalshi.com domain was decommissioned by Kalshi, causing all Kalshi ingest runs to fail with ENOTFOUND for approximately 2.6 days. Updated all hardcoded URLs to the current hostnames.

Fixed

  • core/src/exchanges/kalshi/config.ts: Updated production REST base URL from https://api.external-api.kalshi.com to https://external-api.kalshi.com, demo REST from https://demo-api.external-api.kalshi.com to https://external-api.demo.kalshi.co, production WebSocket from wss://api.external-api.kalshi.com/trade-api/ws/v2 to wss://external-api-ws.kalshi.com/trade-api/ws/v2, and demo WebSocket to wss://external-api-ws.demo.kalshi.co/trade-api/ws/v2. The KALSHI_BASE_URL and KALSHI_DEMO_BASE_URL env var overrides remain supported and take precedence over these defaults.
  • core/specs/kalshi/Kalshi.yaml: Replaced the {env}.external-api.kalshi.com server-variable entry with two explicit server entries matching the new hostnames. Regenerated core/src/exchanges/kalshi/api.ts from the updated spec.

2.49.8

Patch

Hosted trading quickstart actually works now. The SDKs' client-side economics validator was rejecting every server-built market order before signing (economic mismatch: worst_price expected <= ... got 0.999), because the hosted trading API deliberately pins market-order worst_price to the tick-grid extreme and caps the user with max_cost_usdc / shares_6dec instead ("textbook market semantics"). Verified live against trade.pmxt.dev with a real $5 fill end-to-end.

Fixed

  • TS pmxt/hosted-typed-data.ts + Python pmxt/_hosted_typeddata.py: validateWorstPrice / _validate_worst_price no longer apply the limit-order slippage bound to market orders. For market orders the binding user protection is max_cost_usdc (buys) / shares_6dec (sells) — both already strictly validated — so the validator now only sanity-checks that worst_price lies inside the open (0, 1) price domain. Limit orders keep the existing slippage-bound check. This unblocks create_order / createOrder market orders in hosted mode, which previously failed pre-sign, every time.
  • TS pmxt/hosted-mappers.ts + Python pmxt/_hosted_mappers.py: userTradeFromV0 / user_trade_from_v0 now normalize the v0 wire amount (6-dec micro-shares, e.g. 58139533.0) to decimal shares (58.139533), so UserTrade.amount means shares — consistent with Position.size and the rest of the SDK. The reverse mappers scale back to micros symmetrically.
  • Python client.py: create_order docstring claimed "Not available through the hosted API" — it is; corrected to describe the hosted build → local-sign → submit flow.

Changed

  • docs/trading-quickstart.mdx: Removed the "use aggressive slippage_pct" workaround warning (the validator bug it papered over is fixed); market-order examples no longer pass slippage_pct and note that market orders are budget-capped, not price-capped. Fixed the TypeScript createOrder example to use type (the actual CreateOrderParams field) instead of the nonexistent orderType. Fixed step-6 verification snippets to use real model fields (Position.size / outcome_label, Balance.available) instead of nonexistent quantity / notional / free. Documented that hosted limit orders currently return 501.

2.49.7

Patch

API Reference sidebar reorder: Trading and Orders & Positions move from near the bottom of the tab to immediately under Events & Markets, so the customer's natural path is walkable top-to-bottom — discover (Events & Markets) → act (Trading) → inspect state (Orders & Positions) → niche features.

Changed

  • scripts/generate-mintlify-docs.js: The API-tab assembly now inserts both crossExchangeGroups and the hosted otherExternalGroups after "Events & Markets". Because insertGroupsAfter pushes the existing target's neighbor down, the second insertion (otherExternalGroups) lands between "Events & Markets" and "Cross Exchange", producing the order: Overview → System → Events & Markets → Trading → Orders & Positions → Cross Exchange → Order Book & Trades → Realtime → Data Feeds → Other → Enterprise. Idempotent — re-running the regenerator produces no diff.

2.49.6

Patch

Generator-side hotfix for a regression introduced by the v2.49.5 auto-publish: the generate:mintlify step (scripts/generate-mintlify-docs.js, run by .github/workflows/publish.yml on every release tag) was re-injecting self-hosted Group A endpoints as a duplicate "Trading" group (with "Local Only" badges) into docs.json's API Reference tab, sitting alongside the canonical hosted "Trading" group that this release line had just renamed and cleaned up. Result: every release pushed two same-named groups into the rendered sidebar.

Fixed

  • scripts/generate-mintlify-docs.js: Removed the hardcoded "Trading" and "Orders & Positions" entries from the ENDPOINT_GROUPS matcher array — the generator no longer manufactures those groups from the self-hosted openapi.json. Added the matching self-hosted operation ids (createOrder, buildOrder, submitOrder, cancelOrder, editOrder, fetchOrder, fetchOpenOrders, fetchClosedOrders, fetchAllOrders, fetchMyTrades, fetchPositions, fetchBalance, fetchOrderHistory) to the HIDDEN_OPERATIONS set so they don't fall through to the "Other" bucket either. Net effect: the next auto-publish keeps the manually-curated hosted "Trading" + "Orders & Positions" groups (rendered from openapi-hosted-trading.json) and never re-adds the self-hosted equivalents. Self-hosters who want the full reference still consume openapi.json directly or reach it via /guides/self-hosted.
  • docs/docs.json: Removed two stale duplicate groups ("Trading (Hosted)", "Orders & Positions (Hosted)") that the 2.49.5 auto-publish had left behind after the rename. The generator is now idempotent — regenerating produces a stable group list.

2.49.5

Patch

Sidebar cleanup: drop "(Hosted)" suffixes everywhere and hide the parallel self-hosted Group A reference. The hosted endpoints are now the only Group A surface in the sidebar; the API reference reads as "Trading → Create Order" instead of "Trading (Hosted) → Create Order (Hosted)."

Changed

  • Sidebar group labels (docs.json): "Trading (Hosted)""Trading", "Orders & Positions (Hosted)""Orders & Positions" inside the API Reference tab. The "(Hosted)" disambiguation made sense when a sibling self-hosted group existed in the same sidebar; with that group removed (see below), the suffix is just noise.
  • Operation summaries (openapi-hosted-trading.json): All 9 hosted op summary fields drop the (Hosted) suffix — "Create Order (Hosted)""Create Order", etc. "Cancel Order -- Build (Hosted)" collapses to "Cancel Order" (the build / sign / submit two-step is already explained in the operation description; the user-facing name shouldn't telegraph internal mechanics).
  • Operation tags (openapi-hosted-trading.json): "Trading (Hosted)""Trading", "Orders & Positions (Hosted)""Orders & Positions" on every op. Mintlify derives URL slugs from the tag, so the rendered hosted endpoint URLs go from /api-reference/trading-hosted/create-order to /api-reference/trading/create-order (and equivalent for Orders & Positions). No external links pointed at the previous slugs.

Removed

  • "Self-host API reference" sidebar group (docs.json): The 11-page nested group rendering the self-hosted Group A endpoints from openapi.json (each operation flagged with a "Local Only" badge) is dropped from the Documentation tab sidebar. It duplicated the hosted endpoints' purpose for most readers and made the sidebar feel cluttered (a "Trading" group with "Local Only"–badged entries sitting alongside the prominent hosted "Trading" group on the API Reference tab). The underlying openapi.json file is unchanged — self-hosters who want the full reference can still consume the spec directly or reach the per-method pages via the existing /guides/self-hosted narrative. Net effect on the rendered sidebar: one canonical "Trading" group in the API Reference tab, no parallel "Local Only" entries elsewhere.

2.49.4

Patch

Hosted-trading docs QA pass. This patch also ships the 2.49.2 doc restructure work that had accumulated dirty in the working tree but never landed (Pattern E sidebar split, custody page, prediction-markets-101 glossary, terminology cleanup, etc. — see the 2.49.2 entry below for the full list).

Changed

  • Hosted op descriptions (openapi-hosted-trading.json): All 9 hosted Group A operations (createOrderHosted, buildOrderHosted, submitOrderHosted, cancelOrderHosted, fetchBalanceHosted, fetchPositionsHosted, fetchOpenOrdersHosted, fetchMyTradesHosted, fetchOrderHosted) rewritten in a user-friendly style: lead with what the customer gets, demote internals (EIP-712, build/sign/submit decomposition, custodial-flow alternatives) to a final paragraph or remove entirely. createOrderHosted now opens with an inline code sample showing the natural client.fetch_markets() → client.create_order(outcome=market.yes) chain. No more "EIP-712 typed-data payload" or "PreFundedEscrow balance" in opening sentences.
  • Non-custodial language sweep across customer-facing concept docs: docs/concepts/hosted-trading.mdx, docs/concepts/hosted-custody.mdx (new), docs/concepts/hosted-vs-self-hosted.mdx, docs/concepts/prediction-markets-101.mdx (new), docs/trading-quickstart.mdx, docs/guides/escrow-lifecycle.mdx all reworded so the escrow story reads "USDC sits in a non-custodial PreFundedEscrow smart contract; PMXT cannot move funds without your EIP-712 signature" rather than "PMXT custodies USDC." docs/security.mdx left alone — its credential-handling discussion is intentional and scoped correctly.
  • UI-first deposit / withdraw flow on docs/trading-quickstart.mdx and docs/guides/escrow-lifecycle.mdx: Both pages now lead the deposit and withdraw sections with a "Recommended: use the dashboard" subsection (connect wallet at pmxt.dev/dashboard/wallet → click Deposit/Withdraw → confirm in your wallet). The previous client.escrow.approve_tx(...) / deposit_tx(...) / withdraw_tx(...) <CodeGroup> blocks are preserved verbatim but tucked inside <Accordion title="Advanced: programmatic ..."> so they're one click away for the scripting / treasury-automation case.
  • x-mint.content callouts on all 9 hosted ops (openapi-hosted-trading.json): The funding-prereq <Note> now links directly to pmxt.dev/dashboard/wallet for the one-time deposit (with /guides/escrow-lifecycle as the programmatic-flow fallback), replacing the previous link to the lifecycle guide alone. The EVM-key <Tip> now reads "Your USDC sits in a non-custodial PreFundedEscrow on Polygon — the single funding location for every hosted venue (including Opinion, which PMXT settles cross-chain for you). PMXT cannot move funds without your EIP-712 signature." Previous wording said "PMXT custodies USDC on Polygon for every hosted venue."

Removed

  • BuildOrderHostedRequest.outcome_id (openapi-hosted-trading.json): Following the same logic as market_id (relaxed in 2.49.3), outcome_id is no longer documented as a request property. The schema now only documents venue + venue_outcome_id as the way to identify the target outcome. The hosted trading backend continues to accept outcome_id (UUID) for backward compatibility — existing callers that send it keep working — but the spec stops promising it as a supported input. Net effect: a new reader looking at the create-order page sees one clean way to identify the outcome instead of an "EITHER (a) catalog UUID OR (b) venue + venue_outcome_id" fork. Schema description tightened to a single sentence.
  • Dual-identifier framing across the hosted spec: All "catalog UUID" / "Provide this OR" / "venue-native id from fetch_markets()" / "catalog UUID lookup required" language scrubbed from operation descriptions, schema property descriptions, and x-codeSamples header comments. Net grep -E "catalog UUID|Provide this OR" docs/api-reference/openapi-hosted-trading.json count went from 14 → 0. Response-side outcome_id / market_id on OrderV0, UserTradeV0, PositionV0 now described as plain "Identifier of the outcome / market this row refers to" without the "catalog UUID" / "cross-venue canonical" framing — readers no longer have to learn an ID-architecture taxonomy before reading a position.

Added

  • Per-field descriptions across every hosted-trading spec schema (openapi-hosted-trading.json): ~129 description strings added to bare properties across 16 schemas (BalanceV0, BuildOrderHostedRequest, BuildOrderHostedResponse and nested quote / resolved, CancelBuildHostedRequest, CancelBuildHostedResponse, ErrorResponse, HostedErrorResponse, ListMeta, OrderV0, PositionV0, RateLimitError, SubmitOrderHostedRequest, UnifiedEvent, UnifiedMarket, UnifiedOutcome, UserTradeV0). Mintlify now renders meaningful per-field doc strings instead of bare type | null rows. Descriptions are grounded in the trading backend's models_v0.py Pydantic field semantics — e.g. fee is now documented as a USDC dollar amount net of per-fill venue fees, PositionV0.shares is documented as the ERC-1155 balance held by PreFundedEscrow on behalf of the wallet (not the wallet's on-chain balance), and PositionV0.entry_price is documented as the v1 cost-basis approximation (sum(buy_quote_micros) / sum(buy_shares_micros), ignoring sells) — with the explicit caveat that current_price, current_value, and unrealized_pnl are reserved and currently return null in this release. Verify-grep on bare properties (jq '... .description == null ...') returns zero records.

Not changed (deliberate)

  • docs/security.mdx: The mode-scoped credential-handling table is intentional — its "credentials hit process memory ephemerally" claim is scoped to self-hosted mode and correctly distinguishes from the hosted flow (where the private key never leaves the user's machine). Left alone in the non-custodial sweep.
  • Auto-generated docs/api-reference/openapi.json (self-hosted spec, regenerated by core/scripts/generate-openapi.js): the same description / friendly-rewrite passes were not applied because they'd be lost on the next regeneration. If self-hosted reference docs need the same polish later, the source is the SCHEMAS literal inside core/scripts/generate-openapi.js, not the rendered JSON.
  • Backend BuildOrderV0Req Pydantic model (pmxt-trading): outcome_id remains an accepted field at the API layer for backward compatibility with pre-2.49.3 callers. The spec-side removal is documentation-only — no breaking change on the wire.

2.49.3

Patch

Backend-deploy patch for the venue-native outcome ID acceptance feature described in 2.49.2. Includes a follow-up SDK bug fix discovered during end-to-end verification on Polygon mainnet, plus the doc and test deltas that bring this release to a confirmed working state.

Fixed

  • SDK (sdks/python/pmxt/client.py, sdks/typescript/pmxt/client.ts): When the caller supplies a venue-native outcome identifier (non-UUID string returned by client.fetch_markets() on a venue client), _hosted_build_order_request / _hostedBuildOrderBody now correctly suppress market_id from the wire body instead of forwarding the (also venue-native) value alongside venue + venue_outcome_id. Previous behavior caused the backend to reject the request with a UUID-validation error on market_id (Input should be a valid UUID, invalid length: expected length 32 for simple format, found 6). The catalog-UUID path is unchanged — market_id is still forwarded when both the supplied outcome_id and the supplied market_id are UUID-shaped.

Changed

  • Docs (docs/trading-quickstart.mdx): Step 5 collapsed to the natural single-client chain — client.fetch_markets({"query": "trump 2028"})[0]client.create_order(outcome=market.yes, side="buy", amount=1.0, denom="usdc", order_type="market", slippage_pct=99.9). The previous Router round-trip + <Warning> against pmxt.Polymarket().fetch_markets() results is replaced by a <Note> clarifying that both UUID and venue-native ID forms are accepted by the trading API. Step 7 ("Verify the fill") renumbered to Step 6.
  • Docs (docs/concepts/catalog-uuid-vs-venue-id.mdx): Rewritten (70 lines down from 91). The page no longer frames the two ID spaces as a footgun. New framing: hosted trading accepts either identifier; the catalog UUID matters specifically for cross-venue identity (matched clusters, portfolio analytics that span venues, stability across venue re-listings) and for self-hosted mode where the catalog isn't in the path. The <Warning> against pmxt.Polymarket().fetch_markets() results is removed.

Added

  • SDK tests: Python tests/test_hosted_dispatch.py and TypeScript tests/hosted-dispatch.test.ts gained test_build_order_with_venue_native_id_sends_venue_pair / buildOrder with venue-native outcomeId sends (venue, venue_outcome_id) cases asserting the new wire shape (venue, venue_outcome_id present; outcome_id, market_id absent). Existing test constants upgraded from "market-uuid-1" / "outcome-uuid-1" (which the new UUID detection regex would mis-classify as venue-native) to canonical 8-4-4-4-12 UUID strings so the backcompat-UUID test cases continue to assert the right behavior. Net: Python 26/26, TypeScript 18/18.

Verified

  • End-to-end on Polygon mainnet: Confirmed the full natural workflow client.fetch_markets()[0]client.create_order(outcome=market.yes) → on-chain settlement → fetch_positionsclient.create_order(side="sell") → on-chain settlement → position closed. Round-trip executed against PreFundedEscrow (0x3ad326f78b1390b9a5dc5f00e7f62f8632de23e2) on the Spain WC 2026 YES outcome: buy 1.000000 USDC → 6.25 shares (tx 0x16bcfa7e00c49325bd779b044f71a660899e9d0218b11656e2ca9646a208ba10, block 88201299), sell 6.25 shares → 0.988781 USDC. Net round-trip −0.011 USDC (≈ 1.1%, expected CLOB spread).

Deploy prerequisite met

  • pmxt-trading (BuildOrderV0Req accepts (venue, venue_outcome_id)): deployed in pmxt-dev/pmxt-trading@9983e35.