Skip to content

Commit 1ad0aab

Browse files
committed
release: 2.50.13 — settlement architecture rewrite + 5 SDK fixes + tier-aware rate limits
1 parent 988b27e commit 1ad0aab

13 files changed

Lines changed: 215 additions & 52 deletions

File tree

changelog.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,46 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
## [2.50.13] - 2026-06-18
6+
7+
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.
8+
9+
### Fixed (Python SDK)
10+
11+
- **`sdks/python/pmxt/client.py:1492``cancel_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.
12+
- **`sdks/python/pmxt/_exchanges.py:78-110``Limitless` 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.
13+
- **`sdks/python/pmxt/errors.py:66-73``MarketNotFound.__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.
14+
- **`sdks/python/pmxt/client.py:1395-1401``fetch_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.
15+
- **`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.
16+
17+
### Fixed (TypeScript SDK)
18+
19+
- **`sdks/typescript/pmxt/client.ts:1163-1170,2344``cancelOrder` 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.
20+
21+
### Docs — settlement architecture rewrite (Opinion + Limitless)
22+
23+
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.
24+
25+
- **`docs/concepts/hosted-trading.mdx` — Settle step (line 51).** Rewrote to per-venue model with correct trust gates:
26+
- **Polymarket** — same-chain Polygon, single EIP-712 `OrderParams` on `PreFundedEscrow`, CTF exchange.
27+
- **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.
28+
- **Opinion SELL** — BSC→Polygon, dual-signed parallel (`CrossChainSellPayParams` on Polygon + `CrossChainSellPullParams` on BSC `VenueEscrow`). `settleCrossChainSellUSDC``settleCrossChainSellTokens`. 2×2 outcome matrix: row 3 (pay✗, pull✓) is the operator-trusted gap today, bond-backstopped later.
29+
- **Limitless** — Polygon→Base, ERC-7683 single sig, v1 signature-gated front-and-reimburse with explicit trust asterisk; v2 will use delivery proof.
30+
- **`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.
31+
- **`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.
32+
33+
### Docs — operational fixes from the verification round
34+
35+
- **`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.
36+
- **`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.
37+
- **`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.
38+
- **`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", ...)`.
39+
- **`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.
40+
41+
### Skipped / in flight
42+
43+
- **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.
44+
545
## [2.50.12] - 2026-06-18
646

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

docs/authentication.mdx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,9 +138,16 @@ All revocations are logged under **Settings > Audit Log**.
138138

139139
| Status | Body | Meaning | SDK exception |
140140
| ------ | ---------------------------------------- | ----------------------------------------- | ------------- |
141-
| `401` | `{"error": "missing api key"}` | No `Authorization` header on the request. | [`InvalidApiKey`](/api-reference/errors#invalidapikey) |
142-
| `401` | `{"error": "invalid api key"}` | Key unknown, revoked, or expired. | [`InvalidApiKey`](/api-reference/errors#invalidapikey) |
141+
| `401` | `{"error": "missing api key"}` | No `Authorization` header on the request. | `PmxtError` (message: `missing api key`) |
142+
| `401` | `{"error": "invalid api key"}` | Key unknown, revoked, or expired. | `PmxtError` (message: `invalid api key`) |
143143
| `429` | `{"error": "rate_limit_exceeded", ... }` | See [Plans & Limits](/rate-limits). | `RateLimitExceeded` |
144144

145+
<Note>
146+
Today the SDKs raise the base `pmxt.errors.PmxtError` for both 401 cases
147+
— match on the message string. A dedicated `InvalidApiKey` subclass may
148+
be added in a future SDK release; this doc will be updated when that
149+
ships.
150+
</Note>
151+
145152
See [API Reference / Errors](/api-reference/errors) for the full error
146153
class hierarchy.

docs/concepts/hosted-trading.mdx

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,12 @@ Step-by-step:
4848
1. **Build.** The SDK calls `POST /v0/trade/build-order` with the catalog UUIDs (`market_id`, `outcome_id`), side, and amount. The server resolves the venue-native fields (token IDs, salt, expiry, fees), packages them into the venue's EIP-712 typed-data shape, and returns a `built_order_id` plus the payload to sign.
4949
2. **Sign.** The SDK signs the typed-data payload locally with your `private_key`. This step never leaves your process. See [Signing](/guides/signing) for the exact shape.
5050
3. **Submit.** The SDK calls `POST /v0/trade/submit-order` with the `built_order_id` and the signature. The server attaches the signature to the prepared order and submits to the venue.
51-
4. **Settle.** The venue matches the order. On Polymarket, fills come from the CLOB and are settled on Polygon via the CTF exchange. On Opinion, settlement uses a dual-signature cross-chain flow into a BSC-side `VenueEscrow`. On Limitless, buys are settled on Polygon directly from escrow; sells use a Base-side pull leg that draws against the user's Polygon escrow.
51+
4. **Settle.** The venue matches the order. Settlement model varies by venue:
52+
53+
- **Polymarket** — same-chain on Polygon. The user signs one EIP-712 `OrderParams` against the Polygon `PreFundedEscrow` domain. Settlement is direct against the Polymarket CTF exchange. No oracle, no cross-chain hop.
54+
- **Opinion BUY** — cross-chain Polygon → BSC, **single signature + oracle-attested delivery-versus-payment (DvP)**. The user signs one `CrossChainOrderParams` payload on the Polygon `PreFundedEscrow` domain, authorizing a capped spend of their USDC. The operator fronts BSC working capital, buys on Opinion's CLOB, and calls `depositForUser` on the BSC `VenueEscrow` to credit the user with the real outcome tokens. An independent **settlement oracle** observes the BSC delivery and signs a `DeliveryAttestation`. The operator submits the attestation to `settleCrossChainBuy` on Polygon, which verifies the user signature, the attestation binding (order, user, token, destination escrow), the oracle signature, a non-zero `tokensDelivered`, and a per-token worst-price check before releasing the user's USDC. The operator cannot pull USDC until it has provably delivered tokens. Trust assumption: honest oracle.
55+
- **Opinion SELL** — cross-chain BSC → Polygon, **dual-signed parallel, no oracle**. The user signs two EIP-712 payloads at build time: a Polygon pay leg (`CrossChainSellPayParams` on the `PreFundedEscrow` domain) and a BSC pull leg (`CrossChainSellPullParams` on the `VenueEscrow` domain). The operator fires both in parallel: `settleCrossChainSellUSDC` credits the user's USDC on Polygon, `settleCrossChainSellTokens` pulls the outcome tokens from the user's BSC escrow. Time-to-tokens is roughly one block. The 2×2 outcome matrix's row 3 (pay failed, pull succeeded) is the operator-trusted gap today; a future bond backstop will close it.
56+
- **Limitless** — cross-chain Polygon → Base, **v1 signature-gated front-and-reimburse** (ERC-7683). The user signs one ERC-7683 order via EIP-712 on the Polygon escrow domain: "buy ≥Y tokens, debit ≤X USDC, worstPrice, deadline, nonce". The operator fronts Base working capital, buys on Limitless' CLOB, delivers the real tokens to the user's Base escrow, then pulls the capped USDC on Polygon against the signature. **v1 trust asterisk:** the pull is gated by the signature alone, so reimbursement carries a delivery-performance trust point on the operator. v2 will gate the pull on a Base → Polygon delivery proof (UMA optimistic or ZK).
5257

5358
`create_order` is a convenience wrapper that chains build → sign → submit in one call. `build_order` and `submit_order` are the lower-level primitives if you want to inspect or modify the typed-data before signing.
5459

@@ -64,12 +69,15 @@ The SDK accepts either form. Outcomes returned by `pmxt.Polymarket(...).fetch_ma
6469

6570
## Custody: PreFundedEscrow
6671

67-
**Non-custodial by construction** — the `PreFundedEscrow` contract enforces that funds can only move against an EIP-712 signature from the user's wallet. PMXT does not hold USDC in a hot wallet, an exchange-style omnibus account, or a multisig. USDC sits in the `PreFundedEscrow` smart contract on **Polygon** (chainId 137, sometimes called `HomeEscrow`). The same contract backs every hosted venue:
72+
**Non-custodial by construction** — escrow contracts enforce that funds can only move against an EIP-712 signature from the user's wallet (plus, for cross-chain buys, an oracle attestation). PMXT does not hold USDC in a hot wallet, an exchange-style omnibus account, or a multisig. The user's wallet retains beneficial ownership at all times.
73+
74+
USDC custody lives on **Polygon** in the `PreFundedEscrow` contract (chainId 137, sometimes called `HomeEscrow`). Outcome-token custody on non-Polygon venues lives on the venue's native chain:
6875

69-
- **Polymarket** is on Polygon. Escrow-held USDC is spent directly via Polymarket's CTF exchange — same-chain settlement.
70-
- **Opinion** is on BSC (chainId 56). Escrow-held USDC funds a cross-chain settlement leg into a BSC-side `VenueEscrow` that holds Opinion [outcome](/concepts/prediction-markets-101#event-market-outcome) tokens. The user never deposits to or signs on BSC — PMXT operates the cross-chain hop, still gated by the user's signature on the Polygon side.
76+
- **Polygon `PreFundedEscrow`** holds the user's USDC (USDC.e, `0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174`) and serves every venue's payment leg. Polymarket CTF tokens are custodied here too — same-chain.
77+
- **BSC `VenueEscrow`** (chainId 56) holds Opinion [outcome](/concepts/prediction-markets-101#event-market-outcome) tokens. For Opinion BUYs the operator deposits tokens here on the user's behalf (no user signature on BSC). For Opinion SELLs the user signs a BSC-domain pull leg authorizing the operator to debit this escrow.
78+
- **Base escrow** holds Limitless outcome tokens. The operator delivers tokens here after buying on Limitless' CLOB; the user funds and signs only on Polygon.
7179

72-
All EIP-712 order signatures use the Polygon `PreFundedEscrow` domain regardless of venue. The user's wallet retains beneficial ownership at all times. Funds enter via `client.escrow.deposit_tx(amount)`, exit via `client.escrow.withdraw_tx("request" | "claim" | "cancel", ...)`. Both build unsigned txs — your wallet signs and submits them. See [Escrow Lifecycle](/guides/escrow-lifecycle).
80+
USDC enters via `client.escrow.deposit_tx(amount)` on Polygon, exits via `client.escrow.withdraw_tx("request" | "claim" | "cancel", ...)`. Both build unsigned txs — your wallet signs and submits them. See [Escrow Lifecycle](/guides/escrow-lifecycle).
7381

7482
### Deployed contracts
7583

0 commit comments

Comments
 (0)