Skip to content

Commit be3a8cc

Browse files
committed
release: 2.50.11 — SDK hosted-mode routing for fetch_balance/positions/order + $1 min docs
1 parent e2de61a commit be3a8cc

5 files changed

Lines changed: 88 additions & 3 deletions

File tree

changelog.md

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

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

5+
## [2.50.11] - 2026-06-18
6+
7+
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`.
8+
9+
### Fixed (Python SDK — `sdks/python/pmxt/client.py`)
10+
11+
- **`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]`).
12+
- 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.
13+
14+
### Fixed (TypeScript SDK — `sdks/typescript/pmxt/client.ts`)
15+
16+
- **`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`.
17+
18+
### Live verification
19+
20+
Same three calls, before and after, against the same wallet on `trade.pmxt.dev`:
21+
22+
| Method | Before | After | On-chain truth |
23+
| - | - | - | - |
24+
| `fetch_balance()` | `total=0, available=0, chain=None, venue=None` | `total=52.094842, available=52.094842` | `PreFundedEscrow.balances(0xcb856…) = 52.094842 USDC.e` (verified via direct contract call to `0x3ad326f78b1390b9a5dc5f00e7f62f8632de23e2`) |
25+
| `fetch_positions()` | `[]` | 25 real positions | Wallet has historical positions; SDK now sees them |
26+
| `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 |
27+
28+
### Docs
29+
30+
- **`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.
31+
- **`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).
32+
- **`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.
33+
34+
### Investigated but NOT bugs
35+
36+
- **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.
37+
538
## [2.50.10] - 2026-06-18
639

740
### Fixed

docs/guides/hosted-errors.mdx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,14 @@ try {
5959

6060
## OrderSizeTooSmall
6161

62-
**When it fires:** the resolved share count is below the venue's minimum. Polymarket's minimum is **5 shares per order**. A $2 buy at $0.78/share is only 2.5 shares — rejected.
62+
**When it fires:** the resolved order is below one of Polymarket's two independent venue-side minimums on marketable BUY orders. Both rules are enforced by the venue itself (not PMXT), and the **higher of the two binds**:
6363

64-
**Detail string:** `Order size 2.564 below the minimum 5 shares for venue polymarket`.
64+
- **5-share minimum.** A $2 buy at $0.78/share is only 2.5 shares — rejected.
65+
- **$1 notional minimum (marketable BUY).** A 5-share buy at $0.138/share is $0.69 — passes the 5-share rule but is rejected by the venue with the literal error `invalid amount for a marketable BUY order ($0.69), min size: 1`. The effective minimum at that price is ~8 shares (~$1.10).
66+
67+
**Detail string (5-share rule):** `Order size 2.564 below the minimum 5 shares for venue polymarket`.
68+
69+
**Detail string ($1 rule, passed through from venue):** `invalid amount for a marketable BUY order ($X), min size: 1`.
6570

6671
**Parent classes:** `InvalidOrder`, `HostedTradingError`.
6772

docs/trading-quickstart.mdx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,13 +162,18 @@ The hosted trading API accepts either the venue-native identifier (returned by `
162162
</Note>
163163

164164
<Note>
165-
**What PMXT abstracts vs what leaks through from the venue.** PMXT unifies the order shape across venues, but a few Polymarket rules pass through as-is: orders have a **5-share minimum** (a $2 buy at $0.78 is 2.5 shares and is rejected with `OrderSizeTooSmall`), and market orders are **budget-capped** (a buy spends exactly `amount` USDC, a sell sells exactly `amount` shares — `slippage_pct` is ignored). Hosted **limit** orders are not yet available and return `501`; use [self-hosted](/guides/self-hosted) for resting limits.
165+
**What PMXT abstracts vs what leaks through from the venue.** PMXT unifies the order shape across venues, but a few Polymarket rules pass through as-is. Polymarket enforces **two independent minimums on marketable BUY orders** a **5-share minimum** AND a **$1 notional minimum** — and the higher of the two binds. At $0.138/share the 5-share rule alone would only require $0.69, but the $1 rule raises the effective minimum to ~8 shares (~$1.10); a 5-share buy at that price is rejected by the venue with `invalid amount for a marketable BUY order ($0.69), min size: 1` and PMXT surfaces it as `OrderSizeTooSmall`. Market orders are **budget-capped** (a buy spends exactly `amount` USDC, a sell sells exactly `amount` shares — `slippage_pct` is ignored). Hosted **limit** orders are not yet available and return `501`; use [self-hosted](/guides/self-hosted) for resting limits.
166166
</Note>
167167

168168
## 6. Verify the fill
169169

170170
Hosted positions appear immediately on `fetch_positions`. The position's `size` is your share count; the remaining USDC sits in `fetch_balance`.
171171

172+
<Note>
173+
The `id` returned by hosted `create_order` is a **PMXT internal task id**, not a Polymarket (or other venue) order id. Calling Polymarket's own order-lookup with it will return nothing. Use `client.fetch_order(id)` — the hosted SDK knows how to resolve the task and reports the live status (`queued`, `fulfilling`, `fulfilled`, `failed`, `no_fill`). A fresh `create_order` returns `status="queued"` because the venue submit happens asynchronously; poll `fetch_order` (or pass `wait=true`) to get the venue-confirmed outcome.
174+
</Note>
175+
176+
172177
<CodeGroup>
173178
```python Python
174179
positions = client.fetch_positions()

sdks/python/pmxt/client.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1510,6 +1510,12 @@ def cancel_order(self, order_id: str) -> Order:
15101510
raise self._parse_api_exception(e) from None
15111511

15121512
def fetch_order(self, order_id: str) -> Order:
1513+
if self.is_hosted:
1514+
response = self._hosted_request(
1515+
"fetch_order",
1516+
path_params={"order_id": order_id},
1517+
)
1518+
return self._hosted_single(response, "order", order_from_v0)
15131519
try:
15141520
args = []
15151521
args.append(order_id)
@@ -1620,6 +1626,13 @@ def fetch_all_orders(self, params: Optional[dict] = None, **kwargs) -> List[Orde
16201626
raise self._parse_api_exception(e) from None
16211627

16221628
def fetch_positions(self, address: Optional[str] = None) -> List[Position]:
1629+
if self.is_hosted:
1630+
resolved_address = resolve_wallet_address(self, address)
1631+
response = self._hosted_request(
1632+
"fetch_positions",
1633+
path_params={"address": resolved_address},
1634+
)
1635+
return self._hosted_collection(response, "positions", position_from_v0)
16231636
try:
16241637
args = []
16251638
if address is not None:
@@ -1641,6 +1654,13 @@ def fetch_positions(self, address: Optional[str] = None) -> List[Position]:
16411654
raise self._parse_api_exception(e) from None
16421655

16431656
def fetch_balance(self, address: Optional[str] = None) -> List[Balance]:
1657+
if self.is_hosted:
1658+
resolved_address = resolve_wallet_address(self, address)
1659+
response = self._hosted_request(
1660+
"fetch_balance",
1661+
path_params={"address": resolved_address},
1662+
)
1663+
return self._hosted_collection(response, "balances", balance_from_v0)
16441664
try:
16451665
args = []
16461666
if address is not None:

sdks/typescript/pmxt/client.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1188,6 +1188,12 @@ export abstract class Exchange {
11881188

11891189
async fetchOrder(orderId: string): Promise<Order> {
11901190
await this.initPromise;
1191+
if (this.isHosted) {
1192+
const route = HOSTED_METHOD_ROUTES.get("fetchOrder")!;
1193+
const path = formatRoutePath(route, { order_id: orderId });
1194+
const data = await _tradingRequest(this, { method: route.method, path });
1195+
return orderFromV0(data as Record<string, unknown>);
1196+
}
11911197
try {
11921198
const args: any[] = [];
11931199
args.push(orderId);
@@ -1318,6 +1324,14 @@ export abstract class Exchange {
13181324

13191325
async fetchPositions(address?: string): Promise<Position[]> {
13201326
await this.initPromise;
1327+
if (this.isHosted) {
1328+
const resolvedAddress = resolveWalletAddress(this, address);
1329+
const route = HOSTED_METHOD_ROUTES.get("fetchPositions")!;
1330+
const path = formatRoutePath(route, { address: resolvedAddress });
1331+
const data = await _tradingRequest(this, { method: route.method, path });
1332+
const list = Array.isArray(data) ? data : [];
1333+
return list.map((p) => positionFromV0(p as Record<string, unknown>));
1334+
}
13211335
try {
13221336
const args: any[] = [];
13231337
if (address !== undefined) args.push(address);
@@ -1344,6 +1358,14 @@ export abstract class Exchange {
13441358

13451359
async fetchBalance(address?: string): Promise<Balance[]> {
13461360
await this.initPromise;
1361+
if (this.isHosted) {
1362+
const resolvedAddress = resolveWalletAddress(this, address);
1363+
const route = HOSTED_METHOD_ROUTES.get("fetchBalance")!;
1364+
const path = formatRoutePath(route, { address: resolvedAddress });
1365+
const data = await _tradingRequest(this, { method: route.method, path });
1366+
const list = Array.isArray(data) ? data : [];
1367+
return list.map((b) => balanceFromV0(b as Record<string, unknown>));
1368+
}
13471369
try {
13481370
const args: any[] = [];
13491371
if (address !== undefined) args.push(address);

0 commit comments

Comments
 (0)