Skip to content

Releases: pmxt-dev/pmxt

v2.51.4

Choose a tag to compare

@github-actions github-actions released this 24 Jun 09:51

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.

Installation

npm:

npm install pmxtjs@2.51.4
npm install -g @pmxt/cli@2.51.4

PyPI:

pip install pmxt==2.51.4

Links

Full Changelog: v2.51.3f...v2.51.4f

v2.51.3

Choose a tag to compare

@github-actions github-actions released this 24 Jun 09:19

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.

Installation

npm:

npm install pmxtjs@2.51.3
npm install -g @pmxt/cli@2.51.3

PyPI:

pip install pmxt==2.51.3

Links

What's Changed

Full Changelog: v2.51.2f...v2.51.3f

v2.51.2

Choose a tag to compare

@github-actions github-actions released this 23 Jun 09:53

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.

Installation

npm:

npm install pmxtjs@2.51.2
npm install -g @pmxt/cli@2.51.2

PyPI:

pip install pmxt==2.51.2

Links

What's Changed

  • feat(hunch): surface live odds + 24h volume + Crypto taxonomy + cursor draining by @rajkaria in #1293

Full Changelog: v2.51.1f...v2.51.2f

v2.51.1

Choose a tag to compare

@github-actions github-actions released this 23 Jun 06:13

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.

Installation

npm:

npm install pmxtjs@2.51.1
npm install -g @pmxt/cli@2.51.1

PyPI:

pip install pmxt==2.51.1

Links

Full Changelog: v2.51.0f...v2.51.1f

v2.51.0

Choose a tag to compare

@github-actions github-actions released this 22 Jun 13:12

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

v2.50.16

Choose a tag to compare

@github-actions github-actions released this 18 Jun 14:40

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.

Installation

npm:

npm install pmxtjs@2.50.16
npm install -g @pmxt/cli@2.50.16

PyPI:

pip install pmxt==2.50.16

Links

Full Changelog: v2.50.15f...v2.50.16f

v2.50.15

Choose a tag to compare

@github-actions github-actions released this 18 Jun 14:24

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

Installation

npm:

npm install pmxtjs@2.50.15
npm install -g @pmxt/cli@2.50.15

PyPI:

pip install pmxt==2.50.15

Links

Full Changelog: v2.50.14f...v2.50.15f

v2.50.14

Choose a tag to compare

@github-actions github-actions released this 18 Jun 13:51

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

Installation

npm:

npm install pmxtjs@2.50.14
npm install -g @pmxt/cli@2.50.14

PyPI:

pip install pmxt==2.50.14

Links

Full Changelog: v2.50.13f...v2.50.14f

v2.50.13

Choose a tag to compare

@github-actions github-actions released this 18 Jun 13:15

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.

Installation

npm:

npm install pmxtjs@2.50.13
npm install -g @pmxt/cli@2.50.13

PyPI:

pip install pmxt==2.50.13

Links

Full Changelog: v2.50.12f...v2.50.13f

v2.50.12

Choose a tag to compare

@github-actions github-actions released this 18 Jun 12:33

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.


Installation

npm:

npm install pmxtjs@2.50.12
npm install -g @pmxt/cli@2.50.12

PyPI:

pip install pmxt==2.50.12

Links

Full Changelog: v2.50.11f...v2.50.12f