Changelog

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

2.54.0

Minor

Added

  • feat(probable): unified Web3 authentication lifecycle (#1614). Adds getAuthNonce, loginWithSignature, logout, isSessionActive, and verifyL1/verifyL2 to the Probable exchange, with in-memory session storage on the base class, exposed through both SDKs.

Security

  • Session credentials are never exposed over the network. #1614 as submitted exposed getSession as an HTTP/RPC endpoint that returned the full session credential set (apiKey/apiSecret/passphrase) keyed by a public wallet address — on a shared/hosted deployment any caller could retrieve another user's trading secrets. Before landing, getSession was added to EXCLUDED_METHODS in generate-openapi.js, so it is omitted from method-verbs.json, the OpenAPI spec, and the generated SDK clients; it remains an in-process helper only. The signature-gated loginWithSignature (which returns the caller's own freshly minted credentials, only after they prove wallet ownership) and the boolean isSessionActive remain available.

2.53.1

Patch

Fixed

  • fix(core): type compareMarketPrices with CompareMarketPricesParams (#1419). Introduce a precise CompareMarketPricesParams type and use it for the base compareMarketPrices signature instead of the loose FetchMatchesParams. Type-only refinement — the base method is a stub and the concrete Router.ts behavior is unchanged. Only the buildable core of #1419 was applied; its broken SDK-generator import and a private_key=wallet_address bug were dropped.

2.53.0

Minor

Second wave of community PR triage. Each of these was reworked before landing — the original branches carried stale reverts, file-encoding corruption, or compile errors, so only the intended net change was applied on top of current main, then re-verified. All Core (816), Python SDK (267), and TypeScript SDK (93) tests pass. Minor bump for the new opt-in Router and WebSocket capabilities.

Added

  • feat(sdk): Router.sql(query) (#1129). Both SDKs gain a Router.sql() / Router.sql() method that POSTs a query to the server's existing /v0/sql endpoint (read-only ClickHouse, error-scrubbed) with the SDK's normal auth headers, returning a typed SqlResult (SqlMeta/SqlColumn) exported from each package root. The original PR's UTF-16 file corruption and unrelated reverts were dropped.
  • feat(sdk): cross-venue Router.fetchOrderBook(outcomeId) (#1670). Both SDKs expose a Router-level order-book fetch that routes through the existing core Router.fetchOrderBook surface and returns a single merged OrderBook. Reimplemented as a proper Router class method (the original defined it at module level, which gutted the Python Router class).
  • feat(sdk): expose WebSocket configuration for Polymarket and Kalshi (#1126). A websocket options object (reconnect interval, max reconnect attempts, ping interval, connect timeout) is threaded through the SDK exchange constructors into the WebSocket clients in both SDKs. Reconciled with the #1659 handshake-timeout/retry work already on main, and the fast default reconnect backoff is preserved (no 5s-default regression).

Fixed

  • fix(baozi): correct program error-code mappings (#1118). Remap 6018/6040/6020/60416013/6014/6023/6024 to match the current on-chain IDL. Applied without the stale hosted-trading reverts the original branch carried.
  • fix(limitless): paginate all markets (#1119). Replace fixed-page parallel fetching with sequential pagination driven by totalMarketsCount (with a 50-page safety cap) plus a token-filter over-fetch factor, so large limit requests return the full set. Applied without the stale reverts.
  • fix(ts-sdk): align MatchResult / PriceComparison nullable fields (#1578). Widen reasoning, bestBid, bestAsk to T | null to match openapi.yaml (nullable: true). The half-wired parity CI from that PR was intentionally not included.

Not merged

  • #1614 (feat(probable)) was rejected: it exposed session credentials (apiKey/apiSecret/passphrase) over an HTTP endpoint keyed by a public wallet address. Held out pending a fix that stops returning secrets over the server surface.
  • #1419 (replace any with proper types) was superseded: the remaining any usages are raw wire-JSON in converter helpers with no genuine importable type to substitute; the public Router signatures are already typed.

2.52.0

Minor

Maintenance release rolling up the community and internal fixes accumulated since 2.51.4, plus two additive venue capabilities. All Core (816), Python SDK (260), and TypeScript SDK (86) tests pass. Backward-compatible; minor bump for the new opt-in features.

Added

  • feat(rain): fetchOrderBooks batch order-book fetch (#1693). RainExchange now implements the batch fetchOrderBooks(outcomeIds) surface (emulated by fanning out to the existing single-outcome fetchOrderBook) and declares the emulated capability, so Rain participates in the cross-venue batch order-book path.
  • feat(python): SuiBets wallet options (#1657). The Python SDK gains a SuiBetsOptions typed dict and threads wallet_address through the SuiBets constructor into the sidecar credential dict, matching the wallet-option pattern used by other venues.

Fixed

  • fix(hyperliquid): builder order attribution (#1658). createOrder accepts optional builder/builderFee params, appended to the signed payload in the correct key order (after grouping) with address/fee validation. Purely opt-in; existing order flows are unchanged.
  • fix(typescript): WS client connection timeout and retry (#1659). The TS SDK WebSocket client now applies a 10s handshake timeout and a 3-attempt retry loop with backoff, bringing it to parity with the Python client.
  • fix(limitless): wire unwatchOrderBook to WebSocket unsubscribe (#1692). unwatchOrderBook now forwards to LimitlessWebSocket.unsubscribe, mirroring the watchOrderBook override and the Polymarket pattern.
  • fix(kalshi): expose event metadata endpoint (#1611); sync Trade schema with v2 API drift (#1187, #1282); OHLCV normalizer + docs outcomeId param (#1429).
  • fix(suibets): model onchainState to restore accurate order-book liquidity (#1583).
  • fix(polymarket-us): prevent WebSocket connect hangs from a garbage-collected timeout (#1580).
  • fix(limitless): throw on watchOrderBook failure instead of silently swallowing it (#1581).
  • fix(gemini-titan): add terms-acceptance flow before order submission (#1326).
  • fix(smarkets): resolutionDate fallback (#1367).
  • fix(sdk): compute detailed execution prices locally (#1308); expose venue filters in fetch params (#1325); align order book field types (#1417).
  • fix(ts-sdk): export Polymarket_us alias (#1290); raise hosted embedded errors (#1307); require wallet for hosted build orders (#1314); forward apiSecret credentials (#1375); make hosted errors catchable by the base class (#1416); export public SDK option types (#1453); export ExchangeOptions from the package root (#1493); add documented event helper methods (#1492); make Position fields nullable to match Core (#1418); export version constant (#1579).
  • fix(python-sdk): route order-book unwatch via HTTP (#1324); accept wallet address in the Hunch client (#1376); route Myriad wallet address correctly (#1452); type api_secret in exchange options (#1511); export filter function types (#1546).

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.