diff --git a/changelog.md b/changelog.md index 99b9f3eb..ac3e5ad9 100644 --- a/changelog.md +++ b/changelog.md @@ -2,6 +2,38 @@ All notable changes to this project will be documented in this file. +## [2.49.1] - 2026-06-08 + +Positioning-shift patch on top of 2.49.0 — the hosted trading mode shipped in 2.49.0 but the docs, READMEs, and OpenAPI schemas still defaulted to the self-hosted sidecar path. This release flips the default everywhere the SDK + docs surface a customer hits: hosted PMXT is the primary experience; self-hosting becomes the advanced escape hatch. No SDK runtime behavior changes — pure documentation, schema, and copy work. Marketing-site changes ship separately in a sibling pmxt-website PR. + +### Added + +- **Docs**: 11 new MDX pages on the Mintlify site covering the hosted trading mode end-to-end — `trading-quickstart` (60-second walkthrough), `concepts/hosted-trading` (feature landing), `concepts/hosted-vs-self-hosted` (one-pager comparison), `concepts/catalog-uuid-vs-venue-id` (the UUID/venue-id gotcha), `guides/escrow-lifecycle` (PreFundedEscrow walkthrough), `guides/signing` (EthAccountSigner / EthersSigner + EIP-712), `guides/hosted-errors` (the 5 most-common subclasses with `try/except` cookbook), `guides/migrate-to-hosted-trading` (ported `MIGRATION.md` content with language tabs), `guides/self-hosted` (consolidated local-sidecar story), `api-reference/errors` (full `HostedTradingError` tree with dual-parent semantic-map), `api-reference/configuration` (`ExchangeOptions` + env vars + base-URL resolution). +- **Docs**: New "Hosted Trading" and "Self-host" sidebar groups in `docs.json`, plus a "Reference" group at the top of the API Reference tab. `sdk/server` moved out of the previous "SDK" group into "Self-host" (without slug rename — link-stability preserved for this release). +- **Core**: New `ExchangeOptions` component schema in `core/src/server/openapi.yaml` documenting constructor-level options (`pmxtApiKey`, `walletAddress`, `signer`, `privateKey`, `baseUrl`, etc.) — previously only `ExchangeCredentials` (per-request body credentials) existed at the schema level. +- **Core**: `BuiltOrder.expiry` field added to the OpenAPI schema (the TTL that triggers `BuiltOrderExpired` at submit time was implicit in the SDK and undocumented at the spec level). + +### Changed + +- **Docs**: `introduction.mdx` "It runs two ways" bullet order inverted — hosted listed first as the default, self-hosted second as the advanced path. First code block swapped from a dual-variant local/hosted snippet to a single hosted-default `pmxt.Polymarket(pmxt_api_key=...)` constructor. +- **Docs**: `authentication.mdx` venue-credentials section reframed — "Hosted writes (recommended)" subsection added on top showing the `pmxt_api_key + wallet_address + private_key` constructor with a one-line `client.escrow.deposit()` example. The raw-private-key prose was preserved but relabeled as "Self-hosted / direct venue credentials (advanced)". Status/body/meaning error table picked up a fourth "SDK exception" column cross-linking to the new `/api-reference/errors` page. +- **Docs**: `security.mdx` "Run pmxt locally" callout downgraded from `` to `` and reworded — self-hosting is positioned as one option among several rather than the implicit "safer choice". PreFundedEscrow custody surfaced as the hosted alternative. +- **Docs**: `sdk/server.mdx` got a top `` banner clarifying that the page applies to self-hosted mode only and hosted-mode users can skip it. (File location and slug intentionally not renamed in this release to preserve external links.) +- **Docs**: `concepts/venues.mdx` gained a third table at the bottom — "Hosted-trading venues" — listing Polymarket and Opinion with custody type, cross-chain support, and minimum-order-size columns. +- **READMEs (root, Python, TypeScript)**: All three flipped to hosted-default. Subtitles, "Why pmxt?" bullets, Quick Start, and Trading sections now lead with `pmxt.Polymarket(pmxt_api_key=...)`. Per-venue raw-credentials blocks preserved verbatim but moved into "Self-hosted trading (advanced)" subsections. Root README's "No API key required" bullet (actively anti-hosted-positioning) replaced with a "Hosted API" lead bullet. Net +176 lines across the three files. + +### Fixed + +- **Core**: `Order` schema in `core/src/server/openapi.yaml` (and the generated `docs/api-reference/openapi.json`) now includes the nullable `txHash`, `chain`, and `blockNumber` fields the SDK has been returning in hosted mode since 2.49.0. Previous spec was silent on these and downstream codegen consumers missed them. +- **Core**: `UserTrade` schema gained the same `txHash` / `chain` / `blockNumber` nullable trio. +- **Core**: `Position` schema — `required` list trimmed from `[marketId, outcomeId, outcomeLabel, size, entryPrice, currentPrice, unrealizedPnL]` to `[marketId, outcomeId, size]`. The other four became optional in 2.49.0 when the SDK stopped fabricating mark-to-market defaults for positions without a known current price; the schema kept claiming they were required, so generated clients with strict-null checking were rejecting valid responses. New `currentValue` field added (`size * currentPrice` when available). `txHash` / `chain` / `blockNumber` enrichment added. +- **Core**: `Balance` schema gained the optional `venue` field that hosted-mode responses already carry on multi-venue queries. +- **Core**: `ErrorDetail` schema expanded from `{ message: string }` to the full envelope shipping in production responses — `code` (with a populated enum covering all `HostedTradingError` codes plus the pre-existing tree), `retryable: boolean`, optional `exchange`, optional free-form `detail` object. Downstream codegen can now branch on `code`. + +### Docs + +- **`docs.json`**: First-time `redirects` array added (empty for this release; reserves the structure for future slug renames). + ## [2.49.0] - 2026-06-08 ### Added diff --git a/docs/api-reference/configuration.mdx b/docs/api-reference/configuration.mdx new file mode 100644 index 00000000..d41aa107 --- /dev/null +++ b/docs/api-reference/configuration.mdx @@ -0,0 +1,168 @@ +--- +title: Configuration +description: "Exchange constructor arguments, environment variables, and base-URL resolution rules." +--- + +The PMXT SDKs share one configuration surface across hosted and self-hosted modes. The same `ExchangeOptions` object selects mode, supplies credentials, and overrides defaults. Hosted is the default; self-hosted is the explicit fallback when no `pmxt_api_key` is present. + +## ExchangeOptions + +Constructor arguments, Python kwargs alongside TypeScript option keys. + +### Hosted (the default) + +| Python kwarg | TypeScript key | Type | Required? | Description | +| ------------ | -------------- | ---- | --------- | ----------- | +| `pmxt_api_key` | `pmxtApiKey` | `str` | Required for hosted | PMXT API key from [pmxt.dev/dashboard](https://pmxt.dev/dashboard). Triggers hosted mode. | +| `wallet_address` | `walletAddress` | `str` | Required for hosted writes + escrow | Address that owns the escrow balance and signs orders. | +| `private_key` | `privateKey` | `str` | Required for hosted writes | EVM private key used to sign EIP-712 typed-data locally. Auto-wrapped into a signer. | +| `signer` | `signer` | `Signer` | Optional (alt to `private_key`) | Custom signer implementing `sign_typed_data` / `signTypedData`. For hardware wallets, MPC, remote signing. | +| `base_url` | `baseUrl` | `str` | Optional | Override the default `https://api.pmxt.dev`. Rarely needed. | +| `trade_base_url` | `tradeBaseUrl` | `str` | Optional | Override the default `https://trade.pmxt.dev`. Rarely needed. | +| `timeout` | `timeout` | `float` (s) | Optional | Per-request timeout in seconds. Default 30. | + + +```python Python +import pmxt + +# Read-only: no private_key +read_only = pmxt.Polymarket( + pmxt_api_key="pmxt_live_...", + wallet_address="0xYourWallet...", +) + +# Trading-capable: private_key auto-wraps into EthAccountSigner +trader = pmxt.Polymarket( + pmxt_api_key="pmxt_live_...", + wallet_address="0xYourWallet...", + private_key="0xYourPrivateKey...", +) + +# Custom signer (hardware wallet, MPC, etc.) +trader = pmxt.Polymarket( + pmxt_api_key="pmxt_live_...", + wallet_address="0xYourWallet...", + signer=my_ledger_signer, +) +``` + +```typescript TypeScript +import { Polymarket } from "pmxtjs"; + +// Read-only +const readOnly = new Polymarket({ + pmxtApiKey: "pmxt_live_...", + walletAddress: "0xYourWallet...", +}); + +// Trading-capable +const trader = new Polymarket({ + pmxtApiKey: "pmxt_live_...", + walletAddress: "0xYourWallet...", + privateKey: "0xYourPrivateKey...", +}); + +// Custom signer +const traderCustom = new Polymarket({ + pmxtApiKey: "pmxt_live_...", + walletAddress: "0xYourWallet...", + signer: myLedgerSigner, +}); +``` + + +### Self-hosted (fallback) + +When `pmxt_api_key` is absent, the SDK enters self-hosted mode and routes through the local `pmxt-core` server. Venue-native credentials apply here. + +| Python kwarg | TypeScript key | Type | Description | +| ------------ | -------------- | ---- | ----------- | +| `private_key` | `privateKey` | `str` | EVM private key, for Polymarket / Limitless / Probable / Opinion / etc. | +| `api_key_id`, `private_key_pem` | `apiKeyId`, `privateKeyPem` | `str` | Kalshi RSA credentials. | +| `email`, `password` | `email`, `password` | `str` | Smarkets session credentials. | +| `local_port` | `localPort` | `int` | Override the default pmxt-core port (`3847`). | + +See [Self-hosted](/guides/self-hosted) for per-venue credential examples. + +## Environment variables + +| Variable | Effect | +| -------- | ------ | +| `PMXT_API_KEY` | Default `pmxt_api_key` for all SDK clients. Explicit constructor arg wins. | +| `PMXT_BASE_URL` | Override `api.pmxt.dev`. Useful for staging environments. | +| `PMXT_TRADE_BASE_URL` | Override `trade.pmxt.dev`. Useful for staging hosted-trading environments. | +| `PMXT_LOCAL_PORT` | Override the local pmxt-core port (default `3847`). Self-hosted only. | +| `PMXT_ALWAYS_RESTART=1` | Force-restart the local pmxt-core on every `ensure_server_running` call. Useful during local SDK development. | +| `PMXT_TIMEOUT` | Default per-request timeout in seconds. | +| `PMXT_LOG_LEVEL` | Log verbosity for SDK + local server: `debug`, `info`, `warn`, `error`. | + + +Constructor arguments always take precedence over environment variables. The env-var path is for ergonomics in deploy environments where you set `PMXT_API_KEY` once. + + +## Base URL resolution + +When the SDK makes a request, it picks one of three base URLs based on the operation and configuration: + +1. **`trade.pmxt.dev` (hosted trading)** — used for `/v0/trade/*`, `/v0/user/*`, and `/v0/escrow/*` whenever `pmxt_api_key` is set. Override with `trade_base_url` or `PMXT_TRADE_BASE_URL`. +2. **`api.pmxt.dev` (hosted catalog + reads)** — used for `/v0/markets`, `/v0/events`, `/api/{venue}/*` whenever `pmxt_api_key` is set and the operation is not a hosted-trading write. Override with `base_url` or `PMXT_BASE_URL`. +3. **`http://localhost:3847` (self-hosted)** — used for **every** operation when `pmxt_api_key` is absent. Override port with `local_port` or `PMXT_LOCAL_PORT`. + +### Precedence (highest to lowest) + +1. Explicit constructor `base_url` / `trade_base_url`. +2. `PMXT_BASE_URL` / `PMXT_TRADE_BASE_URL` env var. +3. Default (`https://api.pmxt.dev` / `https://trade.pmxt.dev` / `http://localhost:3847`). + +### Worked example + +```python +import os +os.environ["PMXT_BASE_URL"] = "https://api-staging.pmxt.dev" + +# Hosted mode (because pmxt_api_key is set); base_url comes from env +client = pmxt.Polymarket(pmxt_api_key="pmxt_test_...") +# → reads go to https://api-staging.pmxt.dev +# → trade writes still go to https://trade.pmxt.dev (PMXT_TRADE_BASE_URL not set) + +# Self-hosted mode (no pmxt_api_key); env var ignored +local = pmxt.Polymarket(private_key="0x...") +# → reads + writes go to http://localhost:3847 +``` + +## Hosted as default + +The configuration philosophy: + +- Setting `pmxt_api_key` (or `PMXT_API_KEY`) is the **single switch** that opts in to hosted mode. No other flag is required. +- All hosted-specific URLs default to PMXT's production endpoints — no setup beyond the API key. +- Self-hosted is the explicit fallback path. The SDK only attempts to spawn `pmxt-core` when no API key is configured. + +The result: the **shortest correct config is hosted**. A single env var (`PMXT_API_KEY`) plus a constructor with a wallet and private key is everything you need to trade. + +```python +# Minimum hosted trading config: +import os +os.environ["PMXT_API_KEY"] = "pmxt_live_..." + +client = pmxt.Polymarket( + wallet_address="0x...", + private_key="0x...", +) +``` + +## Self-hosted fallback (advanced) + +If you have specific reasons to run local — sub-100ms latency, raw venue credentials, regulatory custody — drop the API key. See [Self-hosted](/guides/self-hosted) for the full guide and [Server Management](/sdk/server) for lifecycle controls. + +```python +# Self-hosted: no PMXT_API_KEY, no pmxt_api_key arg +client = pmxt.Polymarket(private_key="0x...") +``` + +## See also + +- [Authentication](/authentication) — API keys and venue credentials. +- [Self-hosted](/guides/self-hosted) — running pmxt-core locally. +- [Server Management](/sdk/server) — local server lifecycle. +- [Hosted vs self-hosted](/concepts/hosted-vs-self-hosted) — when each mode applies. diff --git a/docs/api-reference/errors.mdx b/docs/api-reference/errors.mdx new file mode 100644 index 00000000..7b7dade3 --- /dev/null +++ b/docs/api-reference/errors.mdx @@ -0,0 +1,211 @@ +--- +title: Error Reference +description: "Every error class the SDK can raise, with status codes, detail shapes, parent classes, and catch examples." +--- + +PMXT's error hierarchy is designed so the **same `except`/`catch` clause works in both hosted and self-hosted modes**. Hosted errors descend from `HostedTradingError` *and* from a semantic parent (`InsufficientFunds`, `InvalidOrder`, `AuthenticationError`, etc.). Self-hosted errors raise the semantic parent directly. Catch the parent and you cover both paths. + +For recovery patterns and the five most-common errors with code, see [Handling hosted errors](/guides/hosted-errors). + +## Hierarchy + +``` +PmxtError (root for the whole SDK) +├── ValidationError (local-only — bad arguments) +│ └── MissingWalletAddress (escrow call without wallet_address) +├── AuthenticationError +│ ├── InvalidApiKey ◄── hosted +│ └── InvalidSignature ◄── hosted +├── InsufficientFunds +│ └── InsufficientEscrowBalance ◄── hosted +├── InvalidOrder +│ ├── OrderSizeTooSmall ◄── hosted +│ ├── BuiltOrderExpired ◄── hosted +│ └── NoLiquidity ◄── hosted +├── NotFoundError +│ └── OutcomeNotFound ◄── hosted +├── ExchangeNotAvailable +│ └── CatalogUnavailable ◄── hosted (retryable) +├── NotSupported +└── HostedTradingError (any 4xx/5xx from trade.pmxt.dev) + │ + └── (all classes marked ◄── hosted above are also HostedTradingError) +``` + +In Python this is implemented via multi-inheritance: `class InsufficientEscrowBalance(InsufficientFunds, HostedTradingError)`. In TypeScript, JS single-inheritance means each hosted leaf extends only its semantic parent, and the `HostedTradingError` membership is carried by a `static isHostedError = true` flag. Use the `isHostedError(e)` helper to test for membership. + +## `HostedTradingError` + +**Root of all errors returned by `trade.pmxt.dev`.** + +| | | +| - | - | +| Status code | Any 4xx or 5xx from the hosted trading API | +| Detail body | `{ "error": "..." }` or `{ "detail": "..." }` or `{ "success": false, "error": "..." }` | +| Parents (Python) | `PmxtError` | +| TS membership | `e instanceof HostedTradingError` or `isHostedError(e) === true` | +| Retryable | 5xx → yes; 4xx → no (unless a leaf overrides) | +| Fields | `e.status` (int), `e.detail` (str) | + + +```python Python +from pmxt._hosted_errors import HostedTradingError + +try: + client.create_order(...) +except HostedTradingError as e: + print(e.status, e.detail) +``` + +```typescript TypeScript +import { isHostedError, HostedTradingError } from "pmxtjs"; + +try { + await client.createOrder({ ... }); +} catch (e) { + if (e instanceof HostedTradingError) { + console.log((e as any).status, (e as any).detail); + } else if (isHostedError(e)) { + // Subclass without the direct extends-chain — still hosted + } +} +``` + + +## `InsufficientEscrowBalance` + +**Escrow free balance is below the order's USDC requirement.** + +| | | +| - | - | +| Status code | `400` | +| Detail starts with | `Insufficient escrow balance` | +| Parents (Python) | `InsufficientFunds`, `HostedTradingError` | +| TS extends | `InsufficientFunds`; `static isHostedError = true` | +| Retryable | No (deposit and retry) | + +Recovery: top up via `client.escrow.deposit_tx(...)`. + +## `OrderSizeTooSmall` + +**Resolved share count is below the venue's minimum (Polymarket: 5 shares).** + +| | | +| - | - | +| Status code | `400` | +| Detail contains | `below the minimum` | +| Parents (Python) | `InvalidOrder`, `HostedTradingError` | +| TS extends | `InvalidOrder`; `static isHostedError = true` | +| Retryable | No (resize and retry) | + +## `InvalidApiKey` + +**`pmxt_api_key` is missing, malformed, revoked, or expired.** + +| | | +| - | - | +| Status code | `401` (always) | +| Detail | `invalid api key` or `missing api key` | +| Parents (Python) | `AuthenticationError`, `HostedTradingError` | +| TS extends | `AuthenticationError`; `static isHostedError = true` | +| Retryable | No | + +Recovery: rotate the key in the [dashboard](https://pmxt.dev/dashboard). Do not retry with the same key. + +## `OutcomeNotFound` + +**Catalog could not resolve the requested `outcome_id` (or `market_id`).** + +| | | +| - | - | +| Status code | `404` | +| Detail contains | `catalog: no outcome` | +| Parents (Python) | `NotFoundError`, `HostedTradingError` | +| TS extends | `NotFoundError`; `static isHostedError = true` | +| Retryable | No | + +Most often: you passed a **venue-native ID** to a hosted endpoint that expects a **catalog UUID**. See [Catalog UUID vs venue ID](/concepts/catalog-uuid-vs-venue-id). + +## `CatalogUnavailable` + +**The hosted catalog is temporarily unavailable.** + +| | | +| - | - | +| Status code | `502` / `503` | +| Detail starts with | `catalog:` | +| Parents (Python) | `ExchangeNotAvailable`, `HostedTradingError` | +| TS extends | `ExchangeNotAvailable`; `static isHostedError = true` | +| Retryable | **Yes** (override on `DEFAULT_RETRYABLE`) | + +Recovery: retry with exponential backoff. + +## `BuiltOrderExpired` + +**The `built_order_id` or `cancel_id` TTL elapsed before submit/cancel.** + +| | | +| - | - | +| Status code | `400` | +| Detail contains | `built_order_id expired` or `cancel_id expired` | +| Parents (Python) | `InvalidOrder`, `HostedTradingError` | +| TS extends | `InvalidOrder`; `static isHostedError = true` | +| Retryable | No (re-build and re-sign) | + +Re-call `build_order`, re-sign, re-submit. Common cause: slow hardware-wallet confirmations. + +## `InvalidSignature` + +**The hosted trading API rejected the EIP-712 signature or typed-data shape.** + +| | | +| - | - | +| Status code | `400` / `401` | +| Detail contains | `Invalid signature` | +| Parents (Python) | `AuthenticationError`, `HostedTradingError` | +| TS extends | `AuthenticationError`; `static isHostedError = true` | +| Retryable | No | + +Most often: signing the wrong domain (chain ID, verifying contract). See [Signing](/guides/signing). + +## `NoLiquidity` + +**Empty book on the side you're crossing.** + +| | | +| - | - | +| Status code | `400` | +| Detail contains | `book has no resting asks` or `book has no resting bids` | +| Parents (Python) | `InvalidOrder`, `HostedTradingError` | +| TS extends | `InvalidOrder`; `static isHostedError = true` | +| Retryable | No (post a limit instead, or wait) | + +## `MissingWalletAddress` + +**Local validation — `client.escrow.*` was called without a `wallet_address`.** + +| | | +| - | - | +| Status code | n/a (local) | +| Parents (Python) | `ValidationError` | +| TS extends | `ValidationError` | +| Hosted member? | **No** — this is a local pre-flight check, not a hosted-API response | + +Pass `wallet_address` to the exchange constructor. + +## Catching by intent + +| Intent | Catch (Python) | Catch (TypeScript) | +| ------ | -------------- | ------------------ | +| Any hosted error | `except HostedTradingError` | `if (isHostedError(e))` | +| Any auth problem | `except AuthenticationError` | `e instanceof AuthenticationError` | +| Any insufficient-funds | `except InsufficientFunds` | `e instanceof InsufficientFunds` | +| Any invalid order | `except InvalidOrder` | `e instanceof InvalidOrder` | +| Anything PMXT | `except PmxtError` | `e instanceof PmxtError` | + +## See also + +- [Handling hosted errors](/guides/hosted-errors) — recovery cookbook with code for the five most-common errors. +- [Hosted trading concept](/concepts/hosted-trading) — where these errors come from. +- Python source: [`sdks/python/pmxt/_hosted_errors.py`](https://github.com/pmxt-dev/pmxt/blob/main/sdks/python/pmxt/_hosted_errors.py) +- TypeScript source: [`sdks/typescript/pmxt/hosted-errors.ts`](https://github.com/pmxt-dev/pmxt/blob/main/sdks/typescript/pmxt/hosted-errors.ts) diff --git a/docs/authentication.mdx b/docs/authentication.mdx index 043c4ce9..a3195adf 100644 --- a/docs/authentication.mdx +++ b/docs/authentication.mdx @@ -56,9 +56,38 @@ Explicit argument takes precedence over the environment variable. ## Venue credentials -Your PMXT API key authenticates you to **PMXT**. Venue credentials -(Polymarket private key, Kalshi API key, etc.) authenticate you to the -**venue** and are passed in the request body for calls that need them: +### Hosted writes (recommended) + +In hosted mode, trade writes use **PMXT's PreFundedEscrow custody** plus a +locally-signed EIP-712 payload. You pass your `pmxt_api_key`, the wallet +address you trade from, and a private key used only for local signing — +the private key never leaves your machine, and PMXT custodies USDC in +the escrow contract on your behalf. + +```python +import pmxt + +client = pmxt.Polymarket( + pmxt_api_key="pmxt_live_...", + wallet_address="0xYourWallet...", + private_key="0xYourPrivateKey...", +) + +# One-time: approve + deposit USDC into PreFundedEscrow +deposit_tx = client.escrow.deposit_tx(amount=10.0) +# (sign + broadcast deposit_tx with your wallet library) +``` + +See [Trading quickstart](/trading-quickstart) for the full 60-second flow +and [Escrow lifecycle](/guides/escrow-lifecycle) for the approve / deposit / +withdraw mechanics. Hosted writes today: Polymarket and Opinion. + +### Self-hosted / direct venue credentials (advanced) + +When you run [pmxt-core locally](/guides/self-hosted), or when you need a +venue that hosted writes don't yet support, you pass venue-native +credentials (Polymarket private key, Kalshi RSA, Smarkets session, etc.) +directly: ```python order = poly.create_order( @@ -70,14 +99,14 @@ order = poly.create_order( ) ``` -PMXT never stores venue credentials. They are forwarded to the venue -and dropped when the call completes. +PMXT never stores venue credentials. They are forwarded to the venue and +dropped when the call completes. Some venues (Polymarket, Limitless, Probable, Opinion, Baozi) require raw wallet private keys with full fund control. **Use a dedicated trading wallet** and read [Security & Credential Handling](/security) -before passing private keys to the hosted API. +before passing private keys to PMXT — hosted or self-hosted. ## Rotating keys @@ -96,8 +125,11 @@ All revocations are logged under **Settings > Audit Log**. ## Errors -| Status | Body | Meaning | -| ------ | ---------------------------------------- | ----------------------------------------- | -| `401` | `{"error": "missing api key"}` | No `Authorization` header on the request. | -| `401` | `{"error": "invalid api key"}` | Key unknown, revoked, or expired. | -| `429` | `{"error": "rate_limit_exceeded", ... }` | See [Plans & Limits](/rate-limits). | +| Status | Body | Meaning | SDK exception | +| ------ | ---------------------------------------- | ----------------------------------------- | ------------- | +| `401` | `{"error": "missing api key"}` | No `Authorization` header on the request. | [`InvalidApiKey`](/api-reference/errors#invalidapikey) | +| `401` | `{"error": "invalid api key"}` | Key unknown, revoked, or expired. | [`InvalidApiKey`](/api-reference/errors#invalidapikey) | +| `429` | `{"error": "rate_limit_exceeded", ... }` | See [Plans & Limits](/rate-limits). | `RateLimitExceeded` | + +See [API Reference / Errors](/api-reference/errors) for the full error +class hierarchy. diff --git a/docs/concepts/catalog-uuid-vs-venue-id.mdx b/docs/concepts/catalog-uuid-vs-venue-id.mdx new file mode 100644 index 00000000..486a4547 --- /dev/null +++ b/docs/concepts/catalog-uuid-vs-venue-id.mdx @@ -0,0 +1,90 @@ +--- +title: Catalog UUID vs Venue ID +description: "Two market identifier spaces coexist in PMXT — when each is used, and how to convert." +--- + +PMXT speaks two market identifier languages. The **catalog UUID** is PMXT's own stable identifier — a UUID assigned to every row in `prediction_markets.markets` and `prediction_markets.outcomes`. The **venue-native ID** is whatever the underlying venue uses — a Polymarket condition ID hex string, a Kalshi ticker, an Opinion market hash, a Limitless market address. + +Both exist. They are not interchangeable. Picking the wrong one is the single most common cause of `OutcomeNotFound` errors. + +## When each is used + +| Endpoint family | Identifier expected | +| --------------- | ------------------- | +| `trade.pmxt.dev/v0/trade/*` (hosted writes) | **Catalog UUID** | +| `trade.pmxt.dev/v0/user/*` (hosted reads) | Wallet address (UUIDs not required) | +| `api.pmxt.dev/v0/markets`, `/v0/events` (Router) | **Catalog UUID** in responses | +| `api.pmxt.dev/api/{venue}/fetchMarkets` (per-venue reads) | Returns **venue-native ID** | +| `api.pmxt.dev/api/{venue}/fetchOrderBook` (per-venue reads) | Accepts **venue-native ID** | +| Direct venue API (self-hosted) | **Venue-native ID** only | + +The hosted trading API was designed to be venue-agnostic. To do that, every market reference needs a stable PMXT-owned identifier rather than a venue-native one that may collide across venues or change shape. + +## Examples + +A single Polymarket binary market has both: + +- Catalog `market_id`: `2eeb03dc-404b-41d5-bc57-6aeb37927ae6` +- Polymarket `conditionId`: `0xc704f74e2f9dfae70f770cb253ffadde10768eeab41233098bf5ac67995a94b5` + +Each outcome has both: + +- Catalog `outcome_id`: `a114f052-1fd1-4bcd-b9cf-de019db81b67` +- Polymarket `tokenId`: `104932610032177696635191871147557737718087870958469629338467406422339967452218` + +`client.create_order(market_id="2eeb03dc-...", outcome_id="a114f052-...")` works against `trade.pmxt.dev`. `client.create_order(market_id="0xc704...", outcome_id="10493...")` will fail with `OutcomeNotFound`. + +## The `fetch_markets` quirk + + +**`fetch_markets` against a venue exchange returns venue-native IDs, not catalog UUIDs.** This is the quirk you will hit first. + +If you call `pmxt.Polymarket(...).fetch_markets(query="election")`, the `market_id` and `outcome_id` fields on every returned row are Polymarket-native. Feeding those into hosted-mode `create_order` will fail because `trade.pmxt.dev` resolves only catalog UUIDs. + + +This is a known gap. Three workarounds, ordered by ergonomics: + +### Workaround 1 — Use the Router + +The [Router](/router/overview) endpoints (`fetch_markets`, `fetch_events`) speak catalog UUIDs natively. The same query against the Router gives you UUIDs you can feed directly into `create_order`. + +```python +router = pmxt.Router(pmxt_api_key="pmxt_live_...") +markets = router.fetch_markets(query="knicks 2026 nba champion", venue="polymarket") +# markets[0].market_id is already a catalog UUID +``` + +This is the path you should use for hosted trading. The Router is the hosted-mode address-book. + +### Workaround 2 — Reverse-resolve through the catalog + +If you already have a venue-native ID and need the catalog UUID, query the catalog via the hosted SQL endpoint or fetch the matched cluster: + +```python +# Look up by venue-native condition_id +clusters = router.fetch_matched_market_clusters( + venue="polymarket", + venue_market_id="0xc704f74e2f9dfae70f770cb253ffadde10768eeab41233098bf5ac67995a94b5", +) +catalog_uuid = clusters[0].market_id +``` + +### Workaround 3 — Catalog DB lookup (Enterprise) + +For batch workflows, query the `prediction_markets.markets` and `prediction_markets.outcomes` tables directly via the [SQL endpoint](/sql). Each row has the canonical UUID alongside the venue-native fields (`venue_market_id`, `venue_outcome_id` / `token_id`). + +```sql +SELECT id AS market_id, venue_market_id, venue +FROM prediction_markets.markets +WHERE venue = 'polymarket' + AND venue_market_id = '0xc704...'; +``` + +## A simple rule of thumb + +- Discovering markets? **Use the Router** — UUIDs everywhere. +- Trading via hosted? **UUIDs only.** +- Reading order books / OHLCV? **Either works** at the per-venue endpoints, but venue-native is canonical there. +- Self-hosted trading? **Venue-native** only — there's no PMXT catalog in the path. + +When in doubt: if the URL contains `trade.pmxt.dev`, it's UUIDs. If the URL contains `/api/{venue}/`, it's venue-native. diff --git a/docs/concepts/hosted-trading.mdx b/docs/concepts/hosted-trading.mdx new file mode 100644 index 00000000..062834e2 --- /dev/null +++ b/docs/concepts/hosted-trading.mdx @@ -0,0 +1,105 @@ +--- +title: Hosted Trading +description: "The hosted-default execution path — how PMXT routes orders, why custody lives in PreFundedEscrow, and when to choose self-host instead." +--- + +Hosted trading is PMXT's default execution path. You provide an API key and a wallet; PMXT custodies USDC in a `PreFundedEscrow` contract; orders are built server-side, signed in your browser/server with your private key, and submitted by PMXT to the underlying venue. You never run a local server, never integrate with venue-specific signature schemes, and never expose your private key over the wire. + +This page explains what hosted mode actually is, what flows through where, and when it's the right choice. + +## When to use hosted mode + +Hosted is the right default for: + +- **Web and mobile apps** — your backend holds the `pmxt_api_key`; users keep their own private keys on their devices. +- **Trading bots that don't need sub-100ms latency** — typical hosted round-trip is ~150–300ms including the venue submit. +- **Multi-venue strategies** — you stay on one HTTP surface even when you're trading across Polymarket and Opinion. +- **Anyone who doesn't want to operate infrastructure.** + +Choose [self-hosted](/concepts/hosted-vs-self-hosted) instead when you need sub-100ms latency, want to use raw venue credentials (Polymarket L2 API keys, Kalshi RSA, etc.), or have regulatory custody constraints. + +## The mental model + +A hosted trade has three actors: + +1. **Your client** — your application code, holding the `pmxt_api_key` and (for writes) the user's `private_key`. +2. **`trade.pmxt.dev`** — PMXT's hosted trading API. Routes orders, owns the escrow contract, talks to the venue. +3. **The venue** — Polymarket's CLOB, Opinion's matching engine, etc. Sees PMXT as the submitter. + +Reads (`fetch_balance`, `fetch_positions`, `fetch_my_trades`, etc.) only need the API key and a wallet address. Writes (`create_order`, `cancel_order`) require the user to sign an EIP-712 typed-data payload locally. + +## The trade flow + +``` +┌──────────┐ 1. POST /v0/trade/build-order ┌──────────────────┐ +│ client │ ───────────────────────────────► │ trade.pmxt.dev │ +│ │ │ │ +│ │ 2. unsigned typed-data + id │ │ +│ │ ◄─────────────────────────────── │ │ +│ │ │ │ +│ signs │ ─── EIP-712 (local only) ────► │ │ +│ │ │ │ +│ │ 3. POST /v0/trade/submit-order │ │ +│ │ { built_order_id, signature }│ │ +│ │ ───────────────────────────────► │ │ +│ │ │ 4. Venue submit │ +│ │ 5. order id + status │ ───────────────► │ +│ │ ◄─────────────────────────────── │ (Polygon / │ +└──────────┘ │ BSC chain) │ + └──────────────────┘ +``` + +Step-by-step: + +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. +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. +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. +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. + +`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. + +## Catalog UUIDs are the address space + +Every hosted endpoint speaks in **catalog UUIDs**, not venue-native IDs. A Polymarket condition ID, a Kalshi ticker, an Opinion market hash — none of those work directly against `trade.pmxt.dev`. Instead, PMXT's catalog assigns a stable UUID to every `prediction_markets.markets` row and every `prediction_markets.outcomes` row, and the trading API uses those UUIDs as the addressable identifiers. + +The catalog UUIDs are the same ones returned by the Router (`/v0/markets`, `/v0/events`). They are stable across venue API changes and survive venue re-listings. + + +`fetch_markets` against a venue exchange (`pmxt.Polymarket(...).fetch_markets(...)`) currently returns **venue-native IDs**, not catalog UUIDs. To trade against those rows, you need to reverse-resolve to the UUID. See [Catalog UUID vs venue ID](/concepts/catalog-uuid-vs-venue-id) for the workaround. + + +## Custody: PreFundedEscrow + +PMXT does not custody USDC in a hot wallet, an exchange-style omnibus account, or a multisig. It custodies in a `PreFundedEscrow` smart contract: + +- **Polymarket** uses the escrow contract on **Polygon**. The escrow holds USDC and acts as the operator that submits orders on behalf of the user. +- **Opinion** uses the escrow contract on **BSC** as the settlement leg of a cross-chain flow. + +The user's wallet retains beneficial ownership at all times. Funds enter via `client.escrow.deposit()`, exit via `client.escrow.withdraw()`. Both are unsigned-tx builders — your wallet signs and submits them. See [Escrow Lifecycle](/guides/escrow-lifecycle). + +## Trust model + +The `pmxt_api_key` is a **service-role credential**: + +- The key holder can read any user's escrow data and forward signed orders for any wallet. +- Writes are still gated by the user's EIP-712 signature against their own wallet. **The key alone cannot move funds.** +- Reads (`fetch_balance`, `fetch_positions`) are NOT gated by a signature — anyone holding the key can read any associated wallet's hosted positions. + +Keep `pmxt_api_key` on a server. Never ship it to a browser bundle. Never log it. + +## What's supported today + +| Venue | Hosted writes | Hosted reads | Notes | +| ----- | ------------- | ------------ | ----- | +| Polymarket | Yes | Yes | Polygon escrow, CLOB exchange | +| Opinion | Yes | Yes | Cross-chain (BSC settlement, dual-signature) | +| Kalshi, Limitless, Smarkets, Probable, Myriad, Metaculus, etc. | No | Read-only via catalog | Use [self-hosted](/guides/self-hosted) for writes | + +If a venue you need isn't here, run pmxt-core locally and pass raw venue credentials — see [Self-hosted](/guides/self-hosted). + +## Next + +- [Trading quickstart](/trading-quickstart) — 60-second guided path. +- [Escrow lifecycle](/guides/escrow-lifecycle) — deposits, withdrawals, the timelock. +- [Hosted errors](/guides/hosted-errors) — every error class and how to recover. +- [Hosted vs self-hosted](/concepts/hosted-vs-self-hosted) — one-page comparison. diff --git a/docs/concepts/hosted-vs-self-hosted.mdx b/docs/concepts/hosted-vs-self-hosted.mdx new file mode 100644 index 00000000..818b28bd --- /dev/null +++ b/docs/concepts/hosted-vs-self-hosted.mdx @@ -0,0 +1,77 @@ +--- +title: Hosted vs Self-hosted +description: "One-page comparison of the two PMXT execution paths." +--- + +PMXT runs two ways. **Hosted** is the default — it's what `pmxt.Polymarket(pmxt_api_key=...)` does when an API key is set. **Self-hosted** is the advanced escape hatch — you run `pmxt-core` on your own machine and the SDK talks to `localhost`. Both expose the same SDK surface; the difference is where execution happens and who holds keys. + +## At a glance + +| | Hosted (`pmxt_api_key` set) | Self-hosted (no API key) | +| - | --------------------------- | ------------------------ | +| **Install footprint** | `pip install pmxt` / `npm i pmxtjs` | SDK + pmxt-core local server | +| **Who holds the API key?** | You (backend secret) | Not used | +| **Who holds venue credentials?** | PMXT escrow contract (USDC custody only) | You, on your machine | +| **Trading auth (writes)** | Your EIP-712 signature, signed locally | Raw venue credential (private key, Kalshi RSA, etc.) | +| **Reads** | `trade.pmxt.dev/v0/user/...` | Direct to the venue API | +| **Writes** | `trade.pmxt.dev/v0/trade/{build,submit}-order` | Direct to the venue API | +| **Custody** | USDC in PMXT `PreFundedEscrow` | You retain venue-native custody | +| **Latency** | ~150–300ms round-trip | Limited by venue + your network | +| **Trading venues** | Polymarket, Opinion | Every venue PMXT supports | +| **Read-only venues** | All catalog venues via Router | All catalog venues via Router | +| **Infra to run** | None | One local process | +| **Regulatory custody** | PMXT escrow as counterparty | You as direct counterparty to the venue | + +## When hosted is the right choice + +- You're building a **web or mobile app** and want to keep `pmxt_api_key` server-side while end users keep their private keys client-side. +- You want **one HTTP surface** across Polymarket and Opinion without learning each venue's order schema. +- You want **PreFundedEscrow custody** instead of managing venue-native accounts. +- You don't want to operate the pmxt-core process, manage upgrades, or run a sidecar. + +## When self-hosted is the right choice + +- You need **sub-100ms latency** — colocated arbitrage, market-making, latency-sensitive HFT-style strategies. +- You **prefer raw venue credentials** — Polymarket L2 API keys, Kalshi RSA keys, Smarkets sessions — and don't want PMXT in the custody path. +- You have **regulatory custody requirements** that mandate direct counterparty status with the venue. +- You want to **trade on venues not yet supported by hosted** (Kalshi, Limitless, Smarkets, Probable, Myriad, etc.). +- You're an **OSS contributor** developing or debugging pmxt-core itself. + +See [Self-hosted](/guides/self-hosted) for setup. + +## What's identical either way + +The SDK surface is the same. Code written against hosted will run against self-hosted with no changes except dropping the `pmxt_api_key` argument: + +```python +# Hosted (default) +client = pmxt.Polymarket(pmxt_api_key="pmxt_live_...", wallet_address="0x...", private_key="0x...") + +# Self-hosted (drop the API key — talks to http://localhost:3847) +client = pmxt.Polymarket(private_key="0x...") +``` + +`fetch_markets`, `fetch_order_book`, `create_order`, `fetch_positions` — same signatures, same response shapes. + +## What differs subtly + +- **`fetch_balance`**: hosted returns escrow USDC; self-hosted returns the venue-native balance (e.g. Polymarket's CLOB-proxy USDC). Same shape, different source. See the [migration guide](/guides/migrate-to-hosted-trading) for the implications. +- **`Position` fields**: hosted may surface `None`/`undefined` on `entry_price`, `current_price`, `unrealized_pnl`, `outcome_label` when the server doesn't yet have the data. Self-hosted always populates them. +- **Catalog UUIDs vs venue IDs**: hosted-mode `create_order` requires catalog UUIDs (`market_id`, `outcome_id`). Self-hosted accepts the venue-native ID directly. See [Catalog UUID vs venue ID](/concepts/catalog-uuid-vs-venue-id). +- **Error classes**: hosted produces `HostedTradingError` subclasses with semantic parents (`InsufficientEscrowBalance` is also `InsufficientFunds`). Self-hosted produces the parent classes directly. Catch the parent and you handle both paths. See [Hosted errors](/guides/hosted-errors). + +## Mixing modes + +You can run **hosted reads with self-hosted writes** (or vice versa) — they're independent SDK instances. A common pattern is: + +```python +# Hosted client for cross-venue research +hosted = pmxt.Polymarket(pmxt_api_key="pmxt_live_...") +markets = hosted.fetch_markets(query="election") + +# Self-hosted client for trading with raw venue creds +local = pmxt.Polymarket(private_key="0x...") +order = local.create_order(...) +``` + +This is useful if you trust PMXT enough for catalog and price data but want custody to stay with the venue for execution. diff --git a/docs/concepts/venues.mdx b/docs/concepts/venues.mdx index b57a63bb..ac022a32 100644 --- a/docs/concepts/venues.mdx +++ b/docs/concepts/venues.mdx @@ -69,3 +69,19 @@ Not every venue supports every method. Broadly: See the [API Reference](/api-reference/overview) for the per-method matrix (inferred from the OpenAPI `operationId`s). + +## Hosted-trading venues + +A subset of venues support **hosted trading** — writes routed through +`trade.pmxt.dev` with PMXT's PreFundedEscrow custody and locally-signed +EIP-712 orders. Every other venue is read-only via the catalog or +requires [self-hosted](/guides/self-hosted) for writes. + +| Venue | Custody | Cross-chain | Min order size | Hosted writes supported | +| ----- | ------- | ----------- | -------------- | ----------------------- | +| Polymarket | `PreFundedEscrow` on Polygon | No (Polygon-only) | 5 shares | Yes | +| Opinion | `PreFundedEscrow` on BSC | Yes (dual-signature settlement) | Venue minimum | Yes | + +See [Hosted trading](/concepts/hosted-trading) for the build → sign → +submit flow, and [Trading quickstart](/trading-quickstart) for the +end-to-end path. diff --git a/docs/docs.json b/docs/docs.json index f8d57d9d..06d86f36 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -45,6 +45,7 @@ "href": "https://pmxt.dev/dashboard" } }, + "redirects": [], "navigation": { "tabs": [ { @@ -55,6 +56,7 @@ "pages": [ "introduction", "quickstart", + "trading-quickstart", "authentication", "mcp", "security", @@ -67,12 +69,25 @@ "pages": [ "concepts/unified-schema", "concepts/catalog-vs-live", - "concepts/venues" + "concepts/venues", + "concepts/hosted-trading", + "concepts/hosted-vs-self-hosted", + "concepts/catalog-uuid-vs-venue-id" ] }, { - "group": "SDK", + "group": "Hosted Trading", "pages": [ + "guides/escrow-lifecycle", + "guides/signing", + "guides/hosted-errors", + "guides/migrate-to-hosted-trading" + ] + }, + { + "group": "Self-host", + "pages": [ + "guides/self-hosted", "sdk/server" ] } @@ -107,6 +122,13 @@ { "tab": "API Reference", "groups": [ + { + "group": "Reference", + "pages": [ + "api-reference/errors", + "api-reference/configuration" + ] + }, { "group": "Overview", "pages": [ diff --git a/docs/guides/escrow-lifecycle.mdx b/docs/guides/escrow-lifecycle.mdx new file mode 100644 index 00000000..85461357 --- /dev/null +++ b/docs/guides/escrow-lifecycle.mdx @@ -0,0 +1,209 @@ +--- +title: Escrow Lifecycle +description: "Deposit, trade, withdraw — how USDC moves through PMXT's PreFundedEscrow custody." +--- + +Hosted trading on PMXT settles through a `PreFundedEscrow` smart contract. Your wallet retains beneficial ownership; PMXT operates the contract and submits orders to the venue on your behalf. This page walks through the full lifecycle: approve, deposit, trade, withdraw. + +The escrow contract lives on **Polygon** for Polymarket trades and on **BSC** for Opinion's cross-chain settlement. + +## Why escrow at all? + +Polymarket's CLOB exchange expects the submitter to be the operator of the user's CLOB proxy wallet. That proxy is created by Polymarket's USDC.e adapter and is non-trivial to operate from a third-party context. PMXT's `PreFundedEscrow` solves this by acting as a pre-funded operator: the user deposits USDC once, PMXT routes orders against that balance, and the user can withdraw at any time. The user signs every order with EIP-712; the escrow contract can only spend USDC against signed orders, never unilaterally. + +For Opinion, the escrow plays a different role — it's the on-chain settlement leg of a dual-signature cross-chain flow. Same custody story, different mechanics. + +## The `client.escrow` namespace + +Hosted exchange clients (Polymarket, Opinion) expose an `escrow` namespace. Every method **builds an unsigned transaction**. Your wallet — MetaMask, ethers `Wallet`, viem, web3.py, etc. — is responsible for signing and broadcasting it. PMXT never holds your private key. + +| Method (Python / TypeScript) | Description | +| ---------------------------- | ----------- | +| `escrow.approve_tx(token, amount_wei=None)` / `escrow.approveTx(token, amountWei?)` | Build an unsigned ERC-20 approval for USDC or CTF. | +| `escrow.deposit_tx(amount)` / `escrow.depositTx(amount)` | Build an unsigned USDC deposit into PreFundedEscrow. | +| `escrow.withdraw_tx(action, amount=None)` / `escrow.withdrawTx(action, amount?)` | Build an unsigned `request` / `claim` / `cancel` withdrawal. | +| `escrow.withdrawals(include="pending,events")` / `escrow.withdrawals({ include })` | Read pending withdrawal state and historical events. | + +See the source: [`sdks/python/pmxt/escrow.py`](https://github.com/pmxt-dev/pmxt/blob/main/sdks/python/pmxt/escrow.py) and [`sdks/typescript/pmxt/escrow.ts`](https://github.com/pmxt-dev/pmxt/blob/main/sdks/typescript/pmxt/escrow.ts). + +## 1. Approve + +Before the first deposit, the escrow contract needs permission to pull USDC (and, for some flows, the Polymarket CTF token) from your wallet. This is a standard ERC-20 approval. + + +```python Python +# Unlimited approval for USDC (most common) +tx = client.escrow.approve_tx("usdc") +# tx is a dict like: +# { "to": "0x...", "data": "0x...", "value": "0", "chain_id": 137 } +# Sign and broadcast it with your preferred wallet library. + +# Or scope the approval to a specific wei amount +tx = client.escrow.approve_tx("usdc", amount_wei=1_000_000_000) # 1,000 USDC + +# Approve the Polymarket CTF token (only needed for direct CTF transfers) +tx = client.escrow.approve_tx("ctf") +``` + +```typescript TypeScript +// Unlimited approval for USDC (most common) +const tx = await client.escrow.approveTx("usdc"); +// tx is an object like: +// { to: "0x...", data: "0x...", value: "0", chainId: 137 } + +// Or scope the approval to a specific wei amount (bigint) +const txScoped = await client.escrow.approveTx("usdc", 1_000_000_000n); + +// Approve the CTF token +const ctfTx = await client.escrow.approveTx("ctf"); +``` + + + +Approval is one-time per token + spender. If you ever rotate the escrow contract (rare, gated on a PMXT migration announcement), you'll need to re-approve. + + +## 2. Deposit + +Once approval is in place, deposit USDC. Amounts are in **whole USDC** (6 decimals); the SDK validates precision and rejects values like `0.0000001`. + + +```python Python +# Deposit 10 USDC +tx = client.escrow.deposit_tx(amount=10.0) +# Sign and send with your wallet library. + +# Decimals up to 6 places are supported +tx = client.escrow.deposit_tx(amount=10.5) +tx = client.escrow.deposit_tx(amount=10.123456) +``` + +```typescript TypeScript +// Deposit 10 USDC — number, decimal string, or wei BigInt all accepted +const tx = await client.escrow.depositTx(10); +const tx2 = await client.escrow.depositTx("10.5"); +const tx3 = await client.escrow.depositTx(10_500_000n); // wei micro-USDC +``` + + + +USDC precision is 6 decimals. The SDK rejects `0.0000001` with `ValidationError`. Pre-round before passing the value in. + + +## 3. Confirm the deposit + +After the deposit transaction confirms on-chain, the balance shows up in escrow. `fetch_balance` is the canonical check. + + +```python Python +balance = client.fetch_balance() +print(f"Free: {balance.free}, Used: {balance.used}, Total: {balance.total}") +``` + +```typescript TypeScript +const balance = await client.fetchBalance(); +console.log(`Free: ${balance.free}, Used: ${balance.used}, Total: ${balance.total}`); +``` + + + +On-chain confirmation usually takes 2–5 seconds on Polygon. If `fetch_balance` still reads zero 30 seconds after broadcast, check the tx on Polygonscan — a failed deposit (e.g. due to missing approval) will not update escrow state. + + +## 4. Trade + +With escrow funded, you can trade. `create_order` and `submit_order` debit the escrow balance; `cancel_order` releases the reservation. No additional escrow calls are needed during normal trading — the balance just gets spent. + +```python +order = client.create_order( + market_id="2eeb03dc-404b-41d5-bc57-6aeb37927ae6", + outcome_id="a114f052-1fd1-4bcd-b9cf-de019db81b67", + side="buy", + order_type="market", + amount=5.0, + denom="usdc", + slippage_pct=30.0, +) +``` + +## 5. Withdraw + +Withdrawals are a **two-step timelock**. You first `request` a withdrawal; after a contract-enforced delay (~1 hour in production), you `claim` it. You can also `cancel` a pending request. + +The timelock is a security feature — it gives the user a window to detect and abort an unauthorized withdrawal even if the operator key were compromised. + +### Request + + +```python Python +tx = client.escrow.withdraw_tx("request", amount=10.0) +# Sign and send. After the timelock, the funds become claimable. +``` + +```typescript TypeScript +const tx = await client.escrow.withdrawTx("request", 10); +``` + + +### Inspect pending withdrawals + + +```python Python +state = client.escrow.withdrawals(include="pending,events") +# state.pending is a list of pending requests with their `claimable_at` timestamps. +for req in state["pending"]: + print(req["amount"], "claimable at", req["claimable_at"]) +``` + +```typescript TypeScript +const state = await client.escrow.withdrawals({ include: "pending,events" }); +// state.pending lists requests with claimable_at timestamps +for (const req of state.pending) { + console.log(req.amount, "claimable at", req.claimable_at); +} +``` + + +### Claim + +Once `claimable_at` has passed, claim the funds — they move from escrow to your wallet. + + +```python Python +tx = client.escrow.withdraw_tx("claim") +``` + +```typescript TypeScript +const tx = await client.escrow.withdrawTx("claim"); +``` + + + +`claim` does not take an `amount`. It claims all matured requests at once. The escrow tracks individual request maturities; only matured ones settle. + + +### Cancel + +If you change your mind during the timelock window, cancel a pending request and the funds remain in escrow as free balance. + + +```python Python +tx = client.escrow.withdraw_tx("cancel") +``` + +```typescript TypeScript +const tx = await client.escrow.withdrawTx("cancel"); +``` + + +## Errors you might hit + +- `MissingWalletAddress` — `client.escrow.*` requires `wallet_address` on the exchange constructor. Pass it explicitly. +- `ValidationError: amount precision exceeds 6 decimals` — round before passing. +- `InsufficientEscrowBalance` (during a trade) — deposit more before retrying, or wait for matured withdrawals to clear pending positions. +- `HostedTradingError` (5xx) — transient server error; retry with backoff. See [Hosted errors](/guides/hosted-errors). + +## Source references + +- Python: [`sdks/python/pmxt/escrow.py`](https://github.com/pmxt-dev/pmxt/blob/main/sdks/python/pmxt/escrow.py) +- TypeScript: [`sdks/typescript/pmxt/escrow.ts`](https://github.com/pmxt-dev/pmxt/blob/main/sdks/typescript/pmxt/escrow.ts) diff --git a/docs/guides/hosted-errors.mdx b/docs/guides/hosted-errors.mdx new file mode 100644 index 00000000..414eb9b8 --- /dev/null +++ b/docs/guides/hosted-errors.mdx @@ -0,0 +1,269 @@ +--- +title: Handling Hosted Errors +description: "Every common hosted-trading error, when it fires, and how to recover." +--- + +Hosted trading errors all descend from `HostedTradingError`. Each subclass also inherits from a **semantic parent** — `InsufficientEscrowBalance` is also an `InsufficientFunds`, `OrderSizeTooSmall` is also an `InvalidOrder`. Catch the parent and you handle both hosted and self-hosted paths with the same code. Catch the leaf and you can branch on the specific recovery action. + +In **Python**, this is true multi-inheritance — `isinstance(e, InsufficientFunds)` and `isinstance(e, HostedTradingError)` both work. In **TypeScript**, the same effect is achieved with a `static isHostedError = true` flag and the `isHostedError()` helper, since JS only allows single-extends. + +For the full class reference, see [API Reference / Errors](/api-reference/errors). + +This page covers the five errors you'll hit most often. + +## InsufficientEscrowBalance + +**When it fires:** the order would draw more USDC than your escrow free balance. PMXT debits escrow when an order is submitted; if the requested `amount` exceeds `balance.free`, the build phase rejects the order. + +**Detail string:** `Insufficient escrow balance: requested 50.0 USDC, available 12.34 USDC`. + +**Parent classes:** `InsufficientFunds`, `HostedTradingError`. + +**Recovery:** deposit more, or shrink the order. See [Escrow lifecycle](/guides/escrow-lifecycle). + + +```python Python +from pmxt.errors import InsufficientFunds +from pmxt._hosted_errors import InsufficientEscrowBalance + +try: + client.create_order(...) +except InsufficientEscrowBalance as e: + print(f"Need to deposit more. {e.detail}") + # Build a deposit tx for the shortfall + tx = client.escrow.deposit_tx(amount=20.0) + # ... sign and broadcast, then retry the order +except InsufficientFunds: + # Self-hosted path also lands here + ... +``` + +```typescript TypeScript +import { InsufficientEscrowBalance, isHostedError } from "pmxtjs"; + +try { + await client.createOrder({ ... }); +} catch (e) { + if (e instanceof InsufficientEscrowBalance) { + console.log(`Need to deposit. ${(e as any).detail}`); + const tx = await client.escrow.depositTx(20); + // sign and broadcast, retry + } else if (isHostedError(e)) { + // any other hosted-error parent + } else { + throw e; + } +} +``` + + +## OrderSizeTooSmall + +**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. + +**Detail string:** `Order size 2.564 below the minimum 5 shares for venue polymarket`. + +**Parent classes:** `InvalidOrder`, `HostedTradingError`. + +**Recovery:** size up the order or pick a cheaper outcome. + + +The 5-share minimum is enforced **after** PMXT resolves your USDC amount into shares using the current price. If the price moves between price-check and submit, a borderline-sized order may flip from accepted to rejected. Add a buffer for marginal sizes. + + + +```python Python +from pmxt._hosted_errors import OrderSizeTooSmall + +try: + client.create_order(amount=2.0, ...) +except OrderSizeTooSmall as e: + # Resize: at $0.78/share, 5 shares ≈ $3.90. Round up with buffer. + client.create_order(amount=5.0, ...) +``` + +```typescript TypeScript +import { OrderSizeTooSmall } from "pmxtjs"; + +try { + await client.createOrder({ amount: 2, ... }); +} catch (e) { + if (e instanceof OrderSizeTooSmall) { + await client.createOrder({ amount: 5, ... }); + } else { + throw e; + } +} +``` + + +## InvalidApiKey + +**When it fires:** the `pmxt_api_key` is missing, malformed, revoked, or expired. Surface is `HTTP 401` from `trade.pmxt.dev`. + +**Detail string:** `invalid api key` or `missing api key`. + +**Parent classes:** `AuthenticationError`, `HostedTradingError`. + +**Recovery:** rotate the key from [pmxt.dev/dashboard](https://pmxt.dev/dashboard). Update your deployed config. **Do not retry** with the same key. + + +```python Python +from pmxt.errors import AuthenticationError +from pmxt._hosted_errors import InvalidApiKey + +try: + client.fetch_balance() +except InvalidApiKey: + # Rotate the key; don't retry with the same one + raise SystemExit("PMXT_API_KEY invalid — rotate from dashboard") +``` + +```typescript TypeScript +import { InvalidApiKey } from "pmxtjs"; + +try { + await client.fetchBalance(); +} catch (e) { + if (e instanceof InvalidApiKey) { + throw new Error("PMXT_API_KEY invalid — rotate from dashboard"); + } + throw e; +} +``` + + +## BuiltOrderExpired + +**When it fires:** between `build_order` and `submit_order`, the built-order TTL elapsed (typically 30 seconds). Also fires for `cancel_id expired` in the cancel flow. + +**Detail string:** `built_order_id expired` or `cancel_id expired`. + +**Parent classes:** `InvalidOrder`, `HostedTradingError`. + +**Recovery:** re-build, then re-sign, then submit. Don't reuse the old `built_order_id`. + + +Hardware-wallet signing is the most common cause — Ledger confirmations can take 10–60 seconds, blowing past the TTL. If you sign with a hardware wallet, expect to retry once on `BuiltOrderExpired`. + + + +```python Python +from pmxt._hosted_errors import BuiltOrderExpired + +def submit_with_retry(client, *, market_id, outcome_id, **kwargs): + for attempt in range(2): + built = client.build_order(market_id=market_id, outcome_id=outcome_id, **kwargs) + sig = signer.sign_typed_data(built.typed_data) + try: + return client.submit_order( + built_order_id=built.built_order_id, + signature=sig, + ) + except BuiltOrderExpired: + if attempt == 1: + raise + continue +``` + +```typescript TypeScript +import { BuiltOrderExpired } from "pmxtjs"; + +async function submitWithRetry(client, params) { + for (let attempt = 0; attempt < 2; attempt++) { + const built = await client.buildOrder(params); + const sig = await signer.signTypedData( + built.typedData.domain, + built.typedData.types, + built.typedData.message, + ); + try { + return await client.submitOrder({ + builtOrderId: built.builtOrderId, + signature: sig, + }); + } catch (e) { + if (e instanceof BuiltOrderExpired && attempt === 0) continue; + throw e; + } + } +} +``` + + +## NoLiquidity + +**When it fires:** the side of the book you're crossing is empty — there are no resting asks for a market buy, or no resting bids for a market sell. + +**Detail string:** `book has no resting asks` or `book has no resting bids`. + +**Parent classes:** `InvalidOrder`, `HostedTradingError`. + +**Recovery:** wait for liquidity, post a limit order instead of a market order, or pick a different outcome. + + +```python Python +from pmxt._hosted_errors import NoLiquidity + +try: + client.create_order(order_type="market", ...) +except NoLiquidity: + # Fall back to a limit order at a price you'd accept + client.create_order(order_type="limit", price=0.50, ...) +``` + +```typescript TypeScript +import { NoLiquidity } from "pmxtjs"; + +try { + await client.createOrder({ orderType: "market", ... }); +} catch (e) { + if (e instanceof NoLiquidity) { + await client.createOrder({ orderType: "limit", price: 0.5, ... }); + } else { + throw e; + } +} +``` + + +## Workaround warnings + + +**Use aggressive `slippage_pct` until the upstream economic validator tightens its `worst_price` checks.** Pragmatic defaults: `slippage_pct=30` for buys, `slippage_pct=99.9` for sells. Lower values frequently trip a precision check that has nothing to do with actual slippage. This will tighten once the validator ships its fix. + + + +**`fetch_markets` returns venue-native IDs**, but `create_order` needs catalog UUIDs. If you see `OutcomeNotFound` despite having a valid-looking ID, you are almost certainly passing a venue-native ID to a hosted endpoint. See [Catalog UUID vs venue ID](/concepts/catalog-uuid-vs-venue-id). + + +## Catching everything hosted + +If you only want to know "did the hosted layer reject this?", catch `HostedTradingError` (Python) or use `isHostedError(e)` (TS): + + +```python Python +from pmxt._hosted_errors import HostedTradingError + +try: + client.create_order(...) +except HostedTradingError as e: + log.error("hosted trade failed", status=e.status, detail=e.detail) +``` + +```typescript TypeScript +import { isHostedError } from "pmxtjs"; + +try { + await client.createOrder({ ... }); +} catch (e) { + if (isHostedError(e)) { + log.error("hosted trade failed", e); + } else { + throw e; + } +} +``` + + +For the full error class reference, parent classes, and status codes, see [API Reference / Errors](/api-reference/errors). diff --git a/docs/guides/migrate-to-hosted-trading.mdx b/docs/guides/migrate-to-hosted-trading.mdx new file mode 100644 index 00000000..48d52e99 --- /dev/null +++ b/docs/guides/migrate-to-hosted-trading.mdx @@ -0,0 +1,197 @@ +--- +title: Migrate to Hosted Trading +description: "Move existing pmxt / pmxtjs code from venue-direct to hosted-mode trading." +--- + +If you've been using PMXT in **venue-direct** mode — passing raw venue credentials to a local `pmxt-core` server — moving to hosted mode is mostly additive. The SDK surface is unchanged; the constructor takes new arguments; a few read methods change their data source. This guide enumerates every observable difference so you can audit your code before flipping the switch. + +This is a port of `sdks/python/MIGRATION.md` and `sdks/typescript/MIGRATION.md` with extra context. The original notes are tagged with the SDK version that introduced each change (`2.18.0` and later). + +## TL;DR + +1. Get a `pmxt_api_key` from [pmxt.dev/dashboard](https://pmxt.dev/dashboard). +2. Pass `pmxt_api_key`, `wallet_address`, `private_key` to the exchange constructor. +3. Approve + deposit USDC into PreFundedEscrow once (`client.escrow.approve_tx`, `client.escrow.deposit_tx`). +4. Audit `fetch_balance()` consumers — the source changed. +5. Audit `Position` field consumers — four fields are now Optional. +6. Catch the new `HostedTradingError` tree where you used to catch broad `Exception`. + +## 1. Constructor — new arguments + +In venue-direct mode you'd construct a Polymarket client with a raw private key only. In hosted mode you also pass `pmxt_api_key` and an explicit `wallet_address`. + + +```python Python +# Before (venue-direct) +client = pmxt.Polymarket(private_key="0xYourPrivateKey...") + +# After (hosted) +client = pmxt.Polymarket( + pmxt_api_key="pmxt_live_...", + wallet_address="0xYourWallet...", + private_key="0xYourPrivateKey...", +) +``` + +```typescript TypeScript +// Before (venue-direct) +const client = new Polymarket({ privateKey: "0xYourPrivateKey..." }); + +// After (hosted) +const client = new Polymarket({ + pmxtApiKey: "pmxt_live_...", + walletAddress: "0xYourWallet...", + privateKey: "0xYourPrivateKey...", +}); +``` + + +The `wallet_address` argument is new in hosted mode and is **required** for any `client.escrow.*` call — the SDK uses it to scope all escrow operations to a single user. If you've been deriving the address from the private key implicitly, make it explicit. + +## 2. Trust model — `pmxt_api_key` is a service-role credential + +`pmxt_api_key` is not app-scoped per-user auth. The key holder can: + +- Read any user's escrow data (balance, positions, trades) by passing a different `wallet_address`. +- Forward signed orders for any wallet (the signature still gates the write). + +Writes are still gated by the user's EIP-712 signature against their own wallet — the key alone cannot move funds. Reads (`fetch_balance`, `fetch_positions`, etc.) are NOT gated by signature. + +**Treat `pmxt_api_key` as a backend secret.** Keep it on a server. Never ship it to a browser bundle. Never log it. + +If your previous architecture had end-user private keys on the server, hosted mode lets you flip that — keep `pmxt_api_key` on the server and push private keys to the client, where they're signed locally and never leave the device. + +## 3. `fetch_balance` — semantic change + +The shape of the `Balance` object is unchanged, but the source is different. + +| Mode | Source | +| ---- | ------ | +| Venue-direct (`pmxt_api_key=None`) | Wallet's CLOB-proxy USDC balance at the venue | +| Hosted (`pmxt_api_key` set) | Wallet's USDC balance in PMXT's PreFundedEscrow on Polygon | + +If your code relied on venue-direct semantics — for example, comparing the balance against the raw on-chain USDC.e balance — audit it. The hosted balance reflects **available trading capital in escrow**, not the wallet's total USDC. + + +```python Python +balance = client.fetch_balance() +# Hosted: `free` is the deposit-side escrow balance available for trading +# Venue-direct: `free` is the wallet's CLOB-proxy USDC balance +``` + +```typescript TypeScript +const balance = await client.fetchBalance(); +// Hosted: `free` is the escrow balance +// Venue-direct: `free` is the CLOB-proxy USDC balance +``` + + +## 4. `Position` fields now Optional + +Four fields on `Position` became Optional in 2.18.0. Venue-direct mode still populates them; hosted mode returns `None` / `undefined` when the server doesn't have the data yet. + + +```python Python +# Python: these are now Optional[...] +position.outcome_label # Optional[str] +position.entry_price # Optional[float] +position.current_price # Optional[float] +position.unrealized_pnl # Optional[float] + +# If your code does math on these, guard the None +if position.current_price is not None: + pnl = position.current_price * position.quantity +``` + +```typescript TypeScript +// TypeScript: these are now `| undefined` +position.outcomeLabel // string | undefined +position.entryPrice // number | undefined +position.currentPrice // number | undefined +position.unrealizedPnL // number | undefined + +if (position.currentPrice !== undefined) { + const pnl = position.currentPrice * position.quantity; +} +``` + + +`market_id`, `outcome_id`, `quantity`, `side`, `notional` are still required and always populated. + +## 5. New error tree + +Hosted mode introduces `HostedTradingError` and a suite of semantic-parent subclasses. The parents are the same classes used in venue-direct mode (`InsufficientFunds`, `InvalidOrder`, `AuthenticationError`, etc.), so existing `except`/`catch` clauses keep working. + +```python +# Before: catch the venue-direct error +try: + client.create_order(...) +except InsufficientFunds: + ... + +# After: same catch still works for hosted (InsufficientEscrowBalance extends InsufficientFunds) +try: + client.create_order(...) +except InsufficientFunds: + ... +``` + +If you want hosted-specific handling, catch the leaf: + +```python +from pmxt._hosted_errors import InsufficientEscrowBalance +try: + client.create_order(...) +except InsufficientEscrowBalance as e: + # Specifically the hosted-escrow shortfall + ... +``` + +See [Hosted errors](/guides/hosted-errors) for the full cookbook. + +## 6. Escrow setup — new one-time step + +Before your first hosted trade, USDC has to be in the PreFundedEscrow contract. + + +```python Python +# Once per wallet, once per token +tx = client.escrow.approve_tx("usdc") +# sign and broadcast via your wallet library + +# Once per top-up +tx = client.escrow.deposit_tx(amount=100.0) +# sign and broadcast +``` + +```typescript TypeScript +const approve = await client.escrow.approveTx("usdc"); +// sign + broadcast + +const deposit = await client.escrow.depositTx(100); +// sign + broadcast +``` + + +There is no equivalent in venue-direct mode — direct venue trading uses the wallet's native balance. See [Escrow lifecycle](/guides/escrow-lifecycle) for details. + +## 7. Catalog UUIDs replace venue-native IDs in trading calls + +Venue-direct trading lets you pass venue-native IDs (Polymarket condition IDs, token IDs). Hosted trading requires **catalog UUIDs**. The simplest path: discover markets via the [Router](/router/overview) instead of via `client.fetch_markets`, since the Router returns UUIDs natively. + +See [Catalog UUID vs venue ID](/concepts/catalog-uuid-vs-venue-id) for the reverse-resolution workaround if you already have venue-native IDs in your database. + +## 8. Network surface + +Venue-direct mode talks to a local `pmxt-core` server, which talks to venue APIs directly. Hosted mode talks to `trade.pmxt.dev` and `api.pmxt.dev`. Make sure both are reachable from your runtime if you flip — corporate firewalls that allow only `api.pmxt.dev` will need `trade.pmxt.dev` added. + +## Rollback plan + +If you migrate and need to roll back, drop the `pmxt_api_key` argument. The SDK falls back to venue-direct mode and routes to the local pmxt-core server. No other changes needed. You'll lose the escrow funds until you withdraw them via `client.escrow.withdraw_tx`, but the venue-native trading flow is unchanged. + +## See also + +- [Trading quickstart](/trading-quickstart) — 60-second guided path. +- [Escrow lifecycle](/guides/escrow-lifecycle) — deposit, withdraw, claim. +- [Hosted errors](/guides/hosted-errors) — error class reference and recovery. +- [Self-hosted](/guides/self-hosted) — when not to migrate. diff --git a/docs/guides/self-hosted.mdx b/docs/guides/self-hosted.mdx new file mode 100644 index 00000000..492a72fe --- /dev/null +++ b/docs/guides/self-hosted.mdx @@ -0,0 +1,168 @@ +--- +title: Self-hosted (Advanced) +description: "Run pmxt-core on your own machine when hosted mode isn't the right fit." +--- + +Self-hosted PMXT runs the open-source `pmxt-core` server on your machine. The SDK detects the absence of a `pmxt_api_key` and routes every request through `http://localhost:3847` instead of `trade.pmxt.dev`. Venue credentials stay on your machine; you talk to venue APIs directly. + +This is the **advanced escape hatch**. Most users should use [hosted mode](/concepts/hosted-trading). Read on if you have specific reasons to run local. + +## When to choose self-hosted + +- **Sub-100ms latency.** Hosted trading adds a network hop. If you're running latency-sensitive arbitrage or market-making, the local server colocates the venue submission step. +- **Raw venue credentials.** You want to use Polymarket L2 API keys, Kalshi RSA keys, Smarkets session cookies — credentials that hosted mode doesn't accept. +- **Regulatory custody requirements.** You need to be the direct counterparty to the venue with no third-party intermediary in the custody path. +- **Venues hosted mode doesn't trade yet.** Hosted writes are Polymarket + Opinion. Self-hosted writes work on every venue PMXT supports (Kalshi, Limitless, Smarkets, Probable, Myriad, Metaculus, etc.). +- **OSS contribution.** You're developing or debugging `pmxt-core` itself. + +If none of these apply, [hosted](/concepts/hosted-trading) is simpler. + +## 1. Install pmxt-core + +The SDK installs and supervises `pmxt-core` for you. You don't run a separate binary; the SDK spawns it as a child process on first use. + + +```bash Python +pip install pmxt +``` + +```bash TypeScript +npm install pmxtjs +``` + + +That's it — `pmxt-core` is bundled. + +## 2. Construct without an API key + +Drop the `pmxt_api_key` argument. The SDK detects the absence and starts the local server. + + +```python Python +import pmxt + +# No pmxt_api_key → SDK spawns and supervises pmxt-core locally +poly = pmxt.Polymarket() + +markets = poly.fetch_markets(query="election", limit=3) +``` + +```typescript TypeScript +import { Polymarket } from "pmxtjs"; + +// No pmxtApiKey → SDK spawns and supervises pmxt-core locally +const poly = new Polymarket({}); + +const markets = await poly.fetchMarkets({ query: "election", limit: 3 }); +``` + + +The first call blocks briefly while the local server warms up; subsequent calls reuse the process. + +## 3. Manage the local server + +For lifecycle control (start, stop, restart, status, logs), use the `server` namespace. See [Server Management](/sdk/server) for the full reference. + +```python +pmxt.server.start() # idempotent +pmxt.server.health() # bool +pmxt.server.status() # structured snapshot +pmxt.server.stop() +pmxt.server.restart() +pmxt.server.logs(20) +``` + +Most users never call these — creating an exchange instance auto-starts the server. + +## 4. Per-venue raw credentials + +Self-hosted unlocks the full venue-credential surface. Pass credentials directly to the exchange constructor. + +### Polymarket — EVM private key + +```python +import pmxt + +poly = pmxt.Polymarket(private_key="0xYourPrivateKey...") + +order = poly.create_order( + outcome=market.yes, + side="buy", + type="limit", + price=0.42, + amount=100, +) +``` + +### Kalshi — RSA key pair + +```python +import pmxt + +kalshi = pmxt.Kalshi( + api_key_id="...", + private_key_pem="-----BEGIN RSA PRIVATE KEY-----\n...", +) +``` + +### Limitless / Probable / Opinion (self-hosted) — EVM private key + +```python +limitless = pmxt.Limitless(private_key="0x...") +probable = pmxt.Probable(private_key="0x...") +opinion = pmxt.Opinion(private_key="0x...") +``` + +### Smarkets — email + password + +```python +smarkets = pmxt.Smarkets(email="you@example.com", password="...") +``` + +### Baozi — Solana keypair + +```python +baozi = pmxt.Baozi(private_key="") +``` + +See [Supported Venues](/concepts/venues) for the full credential matrix. + + +**Use a dedicated trading wallet** for any venue that requires a private key with full fund control (Polymarket, Limitless, Probable, Opinion, Baozi). Self-hosted keeps the key on your machine — but a compromised host still compromises the wallet. Limit balances accordingly. + + +## 5. Environment overrides + +| Variable | Effect | +| -------- | ------ | +| `PMXT_LOCAL_PORT=3847` | Override the local server port (default `3847`). | +| `PMXT_ALWAYS_RESTART=1` | Force-restart the local server on every `ensure_server_running` call. Useful during development. | +| `PMXT_BASE_URL` | Point the SDK at a custom server URL (advanced — defaults to `http://localhost:3847`). | + +See [API Reference / Configuration](/api-reference/configuration) for the full env-var matrix. + +## 6. Running multiple exchanges + +A single pmxt-core process can serve every venue. Construct multiple exchange clients against the same server — they all share the local process. + +```python +poly = pmxt.Polymarket(private_key="0x...") +kalshi = pmxt.Kalshi(api_key_id="...", private_key_pem="...") +# Both clients reuse the same pmxt-core process at http://localhost:3847 +``` + +## 7. Production deployment + +For production self-hosted, the SDK + bundled server is sufficient — there's no separate daemon to manage. If you want to run `pmxt-core` as a long-lived process with systemd or similar, see the [pmxt-core README](https://github.com/pmxt-dev/pmxt) for standalone server commands. + +## What you give up vs hosted + +- **Cross-venue search via Router** — still works, but requires the hosted `pmxt_api_key` for the catalog. If you want Router without trading-hosted, construct a separate `Router` client with the API key alongside your local trading clients. See [Hosted vs self-hosted / mixing modes](/concepts/hosted-vs-self-hosted#mixing-modes). +- **Hosted error tree** — you'll get the parent classes (`InvalidOrder`, `InsufficientFunds`) but not the hosted leaves. Catching parents keeps your code portable. +- **PreFundedEscrow custody** — irrelevant in self-hosted; you custody at the venue. + +## Next + +- [Server Management](/sdk/server) — start/stop/status/logs. +- [Hosted vs self-hosted](/concepts/hosted-vs-self-hosted) — full comparison. +- [Supported Venues](/concepts/venues) — what each venue accepts. diff --git a/docs/guides/signing.mdx b/docs/guides/signing.mdx new file mode 100644 index 00000000..557a0ad7 --- /dev/null +++ b/docs/guides/signing.mdx @@ -0,0 +1,242 @@ +--- +title: Signing Orders +description: "EIP-712 typed-data signing for hosted trading — what the SDK does, what the protocol looks like, and how to bring your own signer." +--- + +Hosted trading writes are authenticated by an **EIP-712 typed-data signature** produced by the user's wallet. The signature is computed locally on your machine; PMXT never sees your private key. The `pmxt_api_key` authenticates you to PMXT; the signature authenticates the order to the venue's on-chain settlement contract. + +This page covers (1) what the SDK does automatically, (2) the EIP-712 payload shape for each venue, and (3) how to bring your own signer (hardware wallet, remote signer, MPC, etc.). + +## What the SDK does for you + +If you pass `private_key` to the exchange constructor, the SDK auto-wraps it into an internal signer and signs every order for you. You never see the typed-data shape. + + +```python Python +import pmxt + +# Python: private_key is wrapped into an EthAccountSigner internally +client = pmxt.Polymarket( + pmxt_api_key="pmxt_live_...", + wallet_address="0xYourWallet...", + private_key="0xYourPrivateKey...", +) + +# create_order signs locally with EthAccountSigner before submitting +order = client.create_order( + market_id="2eeb03dc-404b-41d5-bc57-6aeb37927ae6", + outcome_id="a114f052-1fd1-4bcd-b9cf-de019db81b67", + side="buy", + order_type="market", + amount=5.0, + denom="usdc", + slippage_pct=30.0, +) +``` + +```typescript TypeScript +import { Polymarket } from "pmxtjs"; + +// TypeScript: privateKey is wrapped into an EthersSigner internally +const client = new Polymarket({ + pmxtApiKey: "pmxt_live_...", + walletAddress: "0xYourWallet...", + privateKey: "0xYourPrivateKey...", +}); + +// createOrder signs with EthersSigner before submitting +const order = await client.createOrder({ + marketId: "2eeb03dc-404b-41d5-bc57-6aeb37927ae6", + outcomeId: "a114f052-1fd1-4bcd-b9cf-de019db81b67", + side: "buy", + orderType: "market", + amount: 5, + denom: "usdc", + slippagePct: 30, +}); +``` + + +In Python, the wrapper is `EthAccountSigner` (uses `eth_account` under the hood). In TypeScript, it's `EthersSigner` (uses ethers v6 `Wallet._signTypedData`). Both implement a tiny protocol: a single async `signTypedData(domain, types, message) -> hex` method. + +## Why reads only need a wallet address + +For reads (`fetch_balance`, `fetch_positions`, `fetch_my_trades`), no signature is required — the `pmxt_api_key` is enough. You can construct a hosted client with only `pmxt_api_key` and `wallet_address` and read all hosted state for that wallet: + +```python +read_only = pmxt.Polymarket( + pmxt_api_key="pmxt_live_...", + wallet_address="0xSomeOtherWallet...", + # no private_key +) +balance = read_only.fetch_balance() # works +``` + + +Because reads don't require a signature, anyone with the `pmxt_api_key` can read any wallet's hosted state. Keep the key on the server. See the [trust model](/concepts/hosted-trading#trust-model). + + +## The EIP-712 payload — Polymarket + +Polymarket orders use the `Order` primary type on the Polygon CTF exchange domain. + +**Domain** + +```json +{ + "name": "Polymarket CTF Exchange", + "version": "1", + "chainId": 137, + "verifyingContract": "0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E" +} +``` + +**Types** + +```json +{ + "Order": [ + { "name": "salt", "type": "uint256" }, + { "name": "maker", "type": "address" }, + { "name": "signer", "type": "address" }, + { "name": "taker", "type": "address" }, + { "name": "tokenId", "type": "uint256" }, + { "name": "makerAmount", "type": "uint256" }, + { "name": "takerAmount", "type": "uint256" }, + { "name": "expiration", "type": "uint256" }, + { "name": "nonce", "type": "uint256" }, + { "name": "feeRateBps", "type": "uint256" }, + { "name": "side", "type": "uint8" }, + { "name": "signatureType", "type": "uint8" } + ] +} +``` + +The `Order.maker` is the user's wallet. `signer` is also the user's wallet (no operator-on-behalf-of for hosted PMXT — the operator role is played by the escrow contract). `taker` is `0x000...` (open order). + +The full `message` is what `build_order` returns; you sign it and pass the result back to `submit_order`. + +## The EIP-712 payload — Opinion + +Opinion uses a **dual-signature cross-chain** flow. Two typed-data payloads must be signed: + +1. **`typed_data`** — the Opinion order itself, signed against the Opinion settlement contract on BSC. +2. **`pull_typed_data`** — a USDC pull authorization, signed against the BSC USDC contract for the cross-chain settlement leg. + +Both are returned by `build_order` and both must be signed before `submit_order` is called. The SDK signs both with the same private key by default. + +```python +built = client.build_order(...) +# built has two typed-data payloads +sig_order = signer.sign_typed_data(built.typed_data) +sig_pull = signer.sign_typed_data(built.pull_typed_data) +client.submit_order( + built_order_id=built.built_order_id, + signature=sig_order, + pull_signature=sig_pull, +) +``` + +## Bringing your own signer + +If your key lives in a hardware wallet, an HSM, an MPC service, or anything else that isn't a raw hex private key string, skip `private_key` and use the lower-level `build_order` / `submit_order` flow. + + +```python Python +import pmxt + +client = pmxt.Polymarket( + pmxt_api_key="pmxt_live_...", + wallet_address="0xYourWallet...", + # NO private_key — we'll sign with a custom signer +) + +# 1. Build the order. Returns the typed-data payload plus a built_order_id. +built = client.build_order( + market_id="2eeb03dc-...", + outcome_id="a114f052-...", + side="buy", + order_type="market", + amount=5.0, + denom="usdc", + slippage_pct=30.0, +) + +# 2. Sign with your own signer (Ledger, HSM, MPC, remote, etc.) +signature = my_custom_signer.sign_typed_data( + domain=built.typed_data["domain"], + types=built.typed_data["types"], + message=built.typed_data["message"], +) + +# 3. Submit +order = client.submit_order( + built_order_id=built.built_order_id, + signature=signature, +) +``` + +```typescript TypeScript +import { Polymarket } from "pmxtjs"; + +const client = new Polymarket({ + pmxtApiKey: "pmxt_live_...", + walletAddress: "0xYourWallet...", + // NO privateKey +}); + +const built = await client.buildOrder({ + marketId: "2eeb03dc-...", + outcomeId: "a114f052-...", + side: "buy", + orderType: "market", + amount: 5, + denom: "usdc", + slippagePct: 30, +}); + +// Sign with your own signer +const signature = await myCustomSigner.signTypedData( + built.typedData.domain, + built.typedData.types, + built.typedData.message, +); + +const order = await client.submitOrder({ + builtOrderId: built.builtOrderId, + signature, +}); +``` + + +## The signer protocol + +Any signer that satisfies this interface works: + +```python +class Signer(Protocol): + def sign_typed_data(self, domain: dict, types: dict, message: dict) -> str: + """Return a 0x-prefixed hex signature.""" +``` + +```typescript +interface Signer { + signTypedData( + domain: Eip712Domain, + types: Record, + message: Record, + ): Promise; +} +``` + +The SDK's `EthAccountSigner` / `EthersSigner` are reference implementations of this protocol. For hardware wallets, ethers v6 supports Ledger out of the box. For MPC, see Fireblocks / Privy / Turnkey docs. + +## Common pitfalls + + +**Don't sign the wrong domain.** Each venue (Polymarket / Opinion) has its own EIP-712 domain with a specific `chainId` and `verifyingContract`. The server constructs these in `build_order`. Signing against the wrong domain produces `InvalidSignature`. + + + +**Built orders expire.** A `built_order_id` is single-use and time-bound (typically 30 seconds). If you sign slowly — e.g. waiting on a hardware wallet confirmation — you may hit `BuiltOrderExpired` on submit. Re-build to get a fresh payload. + diff --git a/docs/introduction.mdx b/docs/introduction.mdx index 90cea464..52578b6c 100644 --- a/docs/introduction.mdx +++ b/docs/introduction.mdx @@ -10,22 +10,21 @@ talking to. It runs two ways: -- **Locally** — the open-source [pmxt](https://github.com/pmxt-dev/pmxt) - local server runs on your machine. No API key, no external dependency. Your - requests go directly to the venues. -- **Hosted** — `api.pmxt.dev` adds a shared catalog, cross-venue search, - and you skip running infrastructure. Set an API key and the SDK - switches automatically. +- **Hosted (default)** — `api.pmxt.dev` and `trade.pmxt.dev` give you a + shared catalog, cross-venue search, and end-to-end [hosted + trading](/concepts/hosted-trading) with PreFundedEscrow custody. Set + an API key and the SDK is fully operational. +- **Self-hosted (advanced)** — for users who run + [pmxt-core](https://github.com/pmxt-dev/pmxt) on their own machine. + No API key, no external dependency. Your requests go directly to the + venues. See [self-hosted](/guides/self-hosted). The code is identical either way: ```python import pmxt -# Local: talks directly to Polymarket -poly = pmxt.Polymarket() - -# Hosted: talks to api.pmxt.dev (add an API key and that's it) +# Hosted (default): talks to api.pmxt.dev / trade.pmxt.dev poly = pmxt.Polymarket(pmxt_api_key="pmxt_live_...") markets = poly.fetch_markets(query="fed rate cut", limit=5) @@ -33,6 +32,15 @@ for m in markets: print(m.title, m.outcomes[0].price) ``` +If you'd rather run pmxt-core yourself, drop the API key: + +```python +# Self-hosted: SDK spawns pmxt-core on localhost +poly = pmxt.Polymarket() +``` + +See [self-hosted](/guides/self-hosted) for when that's the right choice. + Swap the venue class — `pmxt.Kalshi(...)`, `pmxt.Limitless(...)` — and the methods and response shapes stay the same. diff --git a/docs/sdk/server.mdx b/docs/sdk/server.mdx index eca9490a..00b96cba 100644 --- a/docs/sdk/server.mdx +++ b/docs/sdk/server.mdx @@ -3,6 +3,15 @@ title: Server Management description: "Start, stop, restart, and inspect the PMXT local server from your SDK code." --- + +This page covers the **self-hosted** local server. If you're using a +`pmxt_api_key`, you can ignore this page — the SDK talks to +`api.pmxt.dev` / `trade.pmxt.dev` directly and never spawns a local +process. See [Hosted trading](/concepts/hosted-trading) for the default +path, or [Self-hosted](/guides/self-hosted) for when running local is +the right choice. + + The PMXT SDKs manage a local server automatically. When you create an exchange instance without a hosted API key, the SDK spawns a local server process, waits for it to become healthy, and routes every diff --git a/docs/security.mdx b/docs/security.mdx index e1973ab8..f6c02529 100644 --- a/docs/security.mdx +++ b/docs/security.mdx @@ -51,20 +51,20 @@ wallet, fund it with only what you need, and use that wallet's private key with PMXT. - -**For maximum security, run pmxt locally.** The hosted API exists for -convenience. If you are not comfortable sending your private key to a -third-party server — even over HTTPS — you can run pmxt-core locally and -retain full custody of your credentials. They will never leave your machine. + +**Self-host pmxt-core if you want to keep venue credentials entirely on +your own machine.** Most users should use hosted with PMXT's +PreFundedEscrow custody — see [Hosted trading](/concepts/hosted-trading). +Self-hosting is the right fit when you need sub-100ms latency, want raw +venue credentials, or have regulatory custody requirements. See +[Self-hosted](/guides/self-hosted) for setup. ```bash -npm install pmxt-core +npm install pmxtjs # or pip install pmxt ``` - -See the [quickstart](/quickstart) for local setup. - + ### Best practices diff --git a/docs/trading-quickstart.mdx b/docs/trading-quickstart.mdx new file mode 100644 index 00000000..38d09d30 --- /dev/null +++ b/docs/trading-quickstart.mdx @@ -0,0 +1,177 @@ +--- +title: Trading Quickstart +description: "Place your first hosted trade in 60 seconds — API key, escrow deposit, market order." +--- + +This page walks you from a fresh `pmxt_api_key` to a confirmed position on Polymarket in under a minute. Hosted mode is the default: PMXT custodies USDC in [PreFundedEscrow](/guides/escrow-lifecycle) and your wallet only ever signs EIP-712 typed-data payloads — no private key leaves your machine when you trade. + + +Hosted trading currently supports **Polymarket** (Polygon) and **Opinion** (cross-chain). Other venues are read-only via the hosted catalog. To trade on those, run the [self-hosted server](/guides/self-hosted) with raw venue credentials. + + +## 1. Get an API key + +Go to [pmxt.dev/dashboard](https://pmxt.dev/dashboard), create a key, and copy it. It looks like `pmxt_live_...` and works immediately. + +```bash +export PMXT_API_KEY="pmxt_live_..." +``` + +## 2. Install the SDK + + +```bash Python +pip install pmxt +``` + +```bash TypeScript +npm install pmxtjs +``` + + +## 3. Construct a hosted client + +A hosted trading client takes three things: your PMXT API key, the wallet address you'll trade from, and the private key for that wallet (used only to sign EIP-712 messages locally — it is never sent to PMXT). The SDK auto-wraps `private_key` into an `EthAccountSigner` / `EthersSigner` for you. + + +```python Python +import pmxt + +client = pmxt.Polymarket( + pmxt_api_key="pmxt_live_...", + wallet_address="0xYourWallet...", + private_key="0xYourPrivateKey...", +) +``` + +```typescript TypeScript +import { Polymarket } from "pmxtjs"; + +const client = new Polymarket({ + pmxtApiKey: "pmxt_live_...", + walletAddress: "0xYourWallet...", + privateKey: "0xYourPrivateKey...", +}); +``` + +```bash curl +# Trading via curl is possible but tedious — you have to build the +# EIP-712 payload, sign it locally, and POST it yourself. The SDK +# does all of this for you. See /guides/signing for the protocol. +echo "Use an SDK for the quickstart." +``` + + + +Use a **dedicated trading wallet**. The private key only stays on your machine for local signing, but treat the wallet as compromised if the host is ever compromised. Read [Security](/security) before funding production wallets. + + +## 4. Approve and deposit USDC into escrow + +Hosted trades execute against your balance in PMXT's `PreFundedEscrow` contract. The first time you trade, you need to (a) approve the escrow to pull USDC from your wallet, and (b) deposit. Both methods on `client.escrow` build **unsigned** transactions — you submit them with your wallet (web3, ethers, viem, MetaMask, etc.). + + +```python Python +# 1. Build an unsigned ERC-20 approval tx for USDC +approve_tx = client.escrow.approve_tx("usdc") +# Sign and send approve_tx with your wallet library... + +# 2. Build an unsigned deposit tx for 10 USDC +deposit_tx = client.escrow.deposit_tx(amount=10.0) +# Sign and send deposit_tx with your wallet library... + +# 3. Confirm the deposit landed +balance = client.fetch_balance() +print(f"Escrow USDC: {balance.free}") +``` + +```typescript TypeScript +// 1. Build an unsigned ERC-20 approval tx for USDC +const approveTx = await client.escrow.approveTx("usdc"); +// Sign and send approveTx with your wallet library... + +// 2. Build an unsigned deposit tx for 10 USDC +const depositTx = await client.escrow.depositTx(10); +// Sign and send depositTx with your wallet library... + +// 3. Confirm the deposit landed +const balance = await client.fetchBalance(); +console.log(`Escrow USDC: ${balance.free}`); +``` + + + +Approval and deposit are one-time setup. Subsequent trades just spend from escrow until you withdraw. See [Escrow Lifecycle](/guides/escrow-lifecycle) for the full deposit / withdraw flow. + + +## 5. Place your first market order + +Let's buy 5 USDC of the YES outcome on **"Will the Knicks win the 2026 NBA Championship?"**. These UUIDs come straight from the live hosted catalog. + + +```python Python +order = client.create_order( + market_id="2eeb03dc-404b-41d5-bc57-6aeb37927ae6", + outcome_id="a114f052-1fd1-4bcd-b9cf-de019db81b67", + side="buy", + order_type="market", + amount=5.0, + denom="usdc", + slippage_pct=30.0, +) +print(f"Order {order.id}: {order.status}") +``` + +```typescript TypeScript +const order = await client.createOrder({ + marketId: "2eeb03dc-404b-41d5-bc57-6aeb37927ae6", + outcomeId: "a114f052-1fd1-4bcd-b9cf-de019db81b67", + side: "buy", + orderType: "market", + amount: 5, + denom: "usdc", + slippagePct: 30, +}); +console.log(`Order ${order.id}: ${order.status}`); +``` + +```bash curl +# create_order is a convenience wrapper. The hosted API exposes a +# build → sign → submit flow. See /guides/signing for the raw protocol. +echo "Use an SDK for create_order." +``` + + + +**Polymarket has a 5-share minimum per order.** At ~$0.78/share, a 5 USDC buy is ~6.4 shares — fine. But $2 at the same price is only 2.5 shares and will be rejected with `OrderSizeTooSmall`. Size up or switch to a cheaper outcome. + + + +**Use aggressive `slippage_pct`** until the upstream economic validator tightens its `worst_price` checks. Pragmatic defaults: `slippage_pct=30` for buys, `slippage_pct=99.9` for sells. Lower values frequently trip a precision check that has nothing to do with actual slippage. + + +## 6. Verify the fill + +Hosted positions appear immediately on `fetch_positions`. The position's `quantity` and `notional` reflect the escrow-side accounting. + + +```python Python +positions = client.fetch_positions() +for p in positions: + print(f"{p.market_id} qty={p.quantity} notional={p.notional}") +``` + +```typescript TypeScript +const positions = await client.fetchPositions(); +for (const p of positions) { + console.log(`${p.marketId} qty=${p.quantity} notional=${p.notional}`); +} +``` + + +That's it — you placed a real trade through hosted PMXT. From here: + +- [Escrow lifecycle](/guides/escrow-lifecycle) — deposits, withdrawals, the request/claim timelock. +- [Hosted errors](/guides/hosted-errors) — what each error means and how to recover. +- [Signing](/guides/signing) — the EIP-712 protocol underneath `create_order`. +- [Self-hosted](/guides/self-hosted) — when to skip hosted and run pmxt-core yourself. diff --git a/readme.md b/readme.md index cefe0617..abffa973 100644 --- a/readme.md +++ b/readme.md @@ -1,7 +1,7 @@ # pmxt [![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)](https://twitter.com/intent/tweet?text=The%20ccxt%20for%20prediction%20markets.&url=https://github.com/pmxt-dev/pmxt&hashtags=predictionmarkets,trading) [![DOI](https://zenodo.org/badge/1130657894.svg)](https://doi.org/10.5281/zenodo.19111315) -**The [ccxt](https://github.com/ccxt/ccxt) for prediction markets.** A unified, language-agnostic API for accessing prediction market data across multiple exchanges — works from Python, TypeScript, or any HTTP client. +**The [ccxt](https://github.com/ccxt/ccxt) for prediction markets.** Hosted unified API for prediction markets — trade Polymarket, Kalshi, Opinion, and more from one API key. Open-source SDK and self-host option included. plot @@ -79,8 +79,9 @@ Different prediction market platforms have different APIs, data formats, and conventions. pmxt provides a single, consistent interface to work with all of them. +- **Hosted API.** Get a key at [pmxt.dev/dashboard](https://pmxt.dev/dashboard), construct a client, trade. PMXT handles custody, signing infrastructure, and on-chain settlement. +- **Open source (MIT).** Self-host the local server for full control — your keys, your machine, no PMXT in the loop. See [Self-hosted](#self-hosted). - **Language-agnostic.** Python and TypeScript SDKs today, with HTTP access for any other language. No lock-in to a single ecosystem. -- **Open source (MIT).** No API key required, no vendor dependency, no rate-limit business model. Self-host and run locally, or hit our hosted api. - **Drop-in Dome API replacement.** Automatic codemod (`dome-to-pmxt`) for teams migrating after the Polymarket acquisition. - **Unified trading, not just data.** Place orders across Polymarket, Kalshi, and Limitless with a single interface. - **[MCP-native](https://pmxt.dev/mcp).** Use pmxt directly from Claude, Cursor, and other AI agents. @@ -135,52 +136,119 @@ npx dome-to-pmxt ./src ## Quickstart -Prediction markets are structured in a hierarchy to group related information. - -* **Event**: The broad topic (e.g., *"Who will Trump nominate as Fed Chair?"*) -* **Market**: A specific tradeable question (e.g., *"Will Trump nominate Kevin Warsh as the next Fed Chair?"*) -* **Outcome**: The actual share you buy (e.g., *"Yes"* or *"No"*) +Get your API key at [pmxt.dev/dashboard](https://pmxt.dev/dashboard). For reads, only `pmxt_api_key` and `wallet_address` are required. For trading, also pass `private_key` — the SDK auto-wraps it into an EIP-712 signer. ### Python ```python import pmxt -api = pmxt.Exchange() - -# 1. Search for the broad Event -events = api.fetch_events(query='Who will Trump nominate as Fed Chair?') -fed_event = events[0] +# Reads — pmxt_api_key + wallet_address only +client = pmxt.Polymarket( + pmxt_api_key="pmxt_live_...", + wallet_address="0xYourWalletAddress", +) -# 2. Find the specific Market within that event -warsh = fed_event.markets.match('Kevin Warsh') +positions = client.fetch_positions() +balance = client.fetch_balance() +markets = client.fetch_markets(query="nba") -print(f"Price: {warsh.yes.price}") +# Trading — also pass private_key +trader = pmxt.Polymarket( + pmxt_api_key="pmxt_live_...", + wallet_address="0xYourWalletAddress", + private_key="0xYourPrivateKey", +) +order = trader.create_order( + market_id="market-uuid", + outcome_id="outcome-uuid", + side="buy", + order_type="market", + amount=5.0, + denom="usdc", + slippage_pct=30.0, +) ``` ### TypeScript -> **Note:** Named imports do not work in ESM. Use `import pmxt from 'pmxtjs'` (default import), not `import { Polymarket } from 'pmxtjs'`. +> **Note:** Named imports do not work in ESM. Use `import pmxt from 'pmxtjs'` (default import) for the namespaced form, or import `Polymarket` from `pmxtjs` only via the CJS build. ```typescript -import pmxt from 'pmxtjs'; +import { Polymarket } from "pmxtjs"; + +// Reads — pmxtApiKey + walletAddress only +const client = new Polymarket({ + pmxtApiKey: "pmxt_live_...", + walletAddress: "0xYourWalletAddress", +}); + +const positions = await client.fetchPositions(); +const balance = await client.fetchBalance(); + +// Trading — also pass privateKey +const trader = new Polymarket({ + pmxtApiKey: "pmxt_live_...", + walletAddress: "0xYourWalletAddress", + privateKey: "0xYourPrivateKey", +}); +const order = await trader.createOrder({ + marketId: "market-uuid", + outcomeId: "outcome-uuid", + side: "buy", + type: "market", + amount: 5.0, + denom: "usdc", + slippage_pct: 30.0, +} as any); +``` + +### Prediction market hierarchy + +Prediction markets are structured in a hierarchy to group related information. + +* **Event**: The broad topic (e.g., *"Who will Trump nominate as Fed Chair?"*) +* **Market**: A specific tradeable question (e.g., *"Will Trump nominate Kevin Warsh as the next Fed Chair?"*) +* **Outcome**: The actual share you buy (e.g., *"Yes"* or *"No"*) + +## Trading +pmxt supports unified trading across exchanges. The hosted API is the default — see Quickstart above for the basic flow. + +### Hosted trading (recommended) + +With a PMXT API key, you only need your wallet address and a private key to sign orders. PMXT handles custody, signer infrastructure, and on-chain settlement. + +```python +import pmxt -const api = new pmxt.Exchange(); +trader = pmxt.Polymarket( + pmxt_api_key="pmxt_live_...", + wallet_address="0xYourWalletAddress", + private_key="0xYourPrivateKey", +) -// 1. Search for the broad Event -const events = await api.fetchEvents({ query: 'Who will Trump nominate as Fed Chair?' }); -const fedEvent = events[0]; +# 1. Check balance +balance = trader.fetch_balance() +print(f"Available balance: {balance[0].available}") -// 2. Find the specific Market within that event -const warsh = fedEvent.markets.match('Kevin Warsh'); +# 2. Fetch markets +markets = trader.fetch_markets(query='Trump') -console.log(`Price: ${warsh.yes?.price}`); +# 3. Place an order +order = trader.create_order( + market_id=markets[0].market_id, + outcome_id=markets[0].yes.outcome_id, + side='buy', + order_type='market', + amount=5.0, + denom='usdc', + slippage_pct=30.0, +) +print(f"Order status: {order.status}") ``` -## Trading -pmxt supports unified trading across exchanges. +### Self-hosted trading (advanced) -### Setup -To trade, you must provide your private credentials during initialization. For detailed credential setup instructions, see the exchange-specific guides: [Polymarket](core/docs/SETUP_POLYMARKET.md), [Kalshi](core/docs/SETUP_KALSHI.md), [Limitless](core/docs/SETUP_LIMITLESS.md). +Use this when you self-host the local server. See [Self-hosted](#self-hosted) for setup. You provide venue credentials directly — no `pmxt_api_key` required. For detailed credential setup instructions, see the exchange-specific guides: [Polymarket](core/docs/SETUP_POLYMARKET.md), [Kalshi](core/docs/SETUP_KALSHI.md), [Limitless](core/docs/SETUP_LIMITLESS.md). #### Polymarket ```python @@ -207,35 +275,9 @@ exchange = pmxt.Limitless( ) ``` -### Trading Example (Python) - -```python -import pmxt -import os - -# Initialize with credentials (e.g., Polymarket) -exchange = pmxt.Polymarket( - private_key=os.getenv('POLYMARKET_PRIVATE_KEY'), - proxy_address=os.getenv('POLYMARKET_PROXY_ADDRESS') -) - -# 1. Check Balance -balance = exchange.fetch_balance() -print(f"Available balance: {balance[0].available}") +## Self-hosted -# 2. Fetch markets -markets = exchange.fetch_markets(query='Trump') - -# 3. Place an Order (using outcome shorthand) -order = exchange.create_order( - outcome=markets[0].yes, - side='buy', - type='limit', - price=0.33, - amount=100 -) -print(f"Order Status: {order.status}") -``` +To self-host pmxt-core on your own machine: `pip install pmxt-core` (Python) or `npm install pmxt-core` (Node.js), then construct any venue client without `pmxt_api_key`. The SDK spawns a local sidecar process; you supply venue credentials directly. See the [self-hosted guide](https://pmxt.dev/docs/guides/self-hosted) for details. ## Documentation diff --git a/sdks/python/README.md b/sdks/python/README.md index b298d58f..94a0716a 100644 --- a/sdks/python/README.md +++ b/sdks/python/README.md @@ -1,8 +1,8 @@ # PMXT Python SDK -A unified Python interface for interacting with multiple prediction market exchanges (Kalshi, Polymarket). +A unified Python interface for prediction market exchanges (Polymarket, Kalshi, Limitless, Opinion, and more). -> **Note**: This SDK requires the PMXT sidecar server to be running. See [Installation](#installation) below. +> **Note**: Use with a PMXT API key (hosted, recommended) or self-host the sidecar locally. Get a key at [pmxt.dev/dashboard](https://pmxt.dev/dashboard). ## Installation @@ -10,19 +10,23 @@ A unified Python interface for interacting with multiple prediction market excha pip install pmxt ``` -**Requirements**: Python >= 3.8. The sidecar server is bundled automatically via the `pmxt-core` dependency -- no separate install needed. +**Requirements**: Python >= 3.8. The sidecar server is bundled automatically via the `pmxt-core` dependency — only needed when self-hosting. ## Quick Start +Get your API key at [pmxt.dev/dashboard](https://pmxt.dev/dashboard). For reads, only `pmxt_api_key` and `wallet_address` are required. + ```python import pmxt -# Initialize exchanges (server starts automatically!) -poly = pmxt.Polymarket() -kalshi = pmxt.Kalshi() +# Reads — pmxt_api_key + wallet_address only +client = pmxt.Polymarket( + pmxt_api_key="pmxt_live_...", + wallet_address="0xYourWalletAddress", +) # Search for markets -markets = poly.fetch_markets(query="Trump") +markets = client.fetch_markets(query="Trump") print(markets[0].title) # Get outcome details @@ -30,28 +34,41 @@ outcome = markets[0].outcomes[0] print(f"{outcome.label}: {outcome.price * 100:.1f}%") # Fetch historical data (use outcome.outcome_id!) -candles = poly.fetch_ohlcv( +candles = client.fetch_ohlcv( outcome.outcome_id, resolution="1d", - limit=30 + limit=30, ) # Get current order book -order_book = poly.fetch_order_book(outcome.outcome_id) +order_book = client.fetch_order_book(outcome.outcome_id) spread = order_book.asks[0].price - order_book.bids[0].price print(f"Spread: {spread * 100:.2f}%") + +# Account reads +positions = client.fetch_positions() +balance = client.fetch_balance() ``` -### How It Works +### How it works (hosted) -The Python SDK automatically manages the PMXT sidecar server: +When you pass `pmxt_api_key`, the SDK talks to the PMXT hosted services: + +1. Catalog requests go to `api.pmxt.dev` (markets, events, order books, OHLCV, trades). +2. Trading requests go to `trade.pmxt.dev` (orders, positions, balances). +3. The SDK does **not** spawn a local process. +4. For Polymarket and Opinion, PMXT's PreFundedEscrow handles custody — you sign orders with your own key, PMXT settles on-chain. + +### How it works (self-hosted) + +When you omit `pmxt_api_key`, the Python SDK manages the PMXT sidecar server for you: 1. **First API call**: Checks if server is running 2. **Auto-start**: Starts server if needed (takes ~1-2 seconds) 3. **Reuse**: Multiple Python processes share the same server 4. **Zero config**: Just import and use! -### Manual Server Control (Optional) +#### Manual server control (optional) If you prefer to manage the server yourself: @@ -63,9 +80,57 @@ poly = pmxt.Polymarket(auto_start_server=False) # $ pmxt-server ``` -## Authentication (for Trading) +## Trading + +### Hosted trading (recommended) + +With a PMXT API key, pass `pmxt_api_key`, `wallet_address`, and `private_key`. The SDK auto-wraps your key into an EIP-712 signer and PMXT settles the order on-chain. + +#### Polymarket + +```python +import pmxt + +trader = pmxt.Polymarket( + pmxt_api_key="pmxt_live_...", + wallet_address="0xYourWalletAddress", + private_key="0xYourPrivateKey", +) + +balance = trader.fetch_balance() +print(f"Available: ${balance[0].available}") + +order = trader.create_order( + market_id="market-uuid", + outcome_id="outcome-uuid", + side="buy", + order_type="market", + amount=5.0, + denom="usdc", + slippage_pct=30.0, +) +print(f"Order status: {order.status}") +``` + +#### Opinion + +```python +import pmxt + +trader = pmxt.Opinion( + pmxt_api_key="pmxt_live_...", + wallet_address="0xYourWalletAddress", + private_key="0xYourPrivateKey", +) +``` + +See the full [hosted trading guide](https://pmxt.dev/docs/concepts/hosted-trading) for venue support, custody model, and limits. + +### Self-hosted trading (advanced) + +When self-hosting, you supply venue credentials directly — no `pmxt_api_key`. The SDK spawns a local sidecar process. -### Polymarket +#### Polymarket Requires your **Polygon Private Key**: @@ -94,7 +159,7 @@ order = poly.create_order( ) ``` -### Kalshi +#### Kalshi Requires **API Key** and **Private Key**: @@ -113,7 +178,7 @@ for pos in positions: print(f"{pos.outcome_label}: ${pos.unrealized_pnl:.2f}") ``` -### Limitless +#### Limitless Requires **Private Key**: diff --git a/sdks/typescript/README.md b/sdks/typescript/README.md index bebcabdc..9311d3c9 100644 --- a/sdks/typescript/README.md +++ b/sdks/typescript/README.md @@ -1,6 +1,8 @@ # pmxtjs -A unified TypeScript/Node.js SDK for prediction markets - The ccxt for prediction markets. +A unified TypeScript/Node.js SDK for prediction markets — The ccxt for prediction markets. + +> **Note**: Use with a PMXT API key (hosted, recommended) or self-host the sidecar locally. Get a key at [pmxt.dev/dashboard](https://pmxt.dev/dashboard). ## Installation @@ -10,15 +12,19 @@ npm install pmxtjs ## Quick Start +Get your API key at [pmxt.dev/dashboard](https://pmxt.dev/dashboard). For reads, only `pmxtApiKey` and `walletAddress` are required. + ```typescript -import pmxt from 'pmxtjs'; +import { Polymarket } from "pmxtjs"; -// Initialize exchanges -const poly = new pmxt.Polymarket(); -const kalshi = new pmxt.Kalshi(); +// Reads — pmxtApiKey + walletAddress only +const client = new Polymarket({ + pmxtApiKey: "pmxt_live_...", + walletAddress: "0xYourWalletAddress", +}); // Search for markets -const markets = await poly.fetchMarkets({ query: 'Trump' }); +const markets = await client.fetchMarkets({ query: "Trump" }); console.log(markets[0].title); // Get outcome details @@ -26,17 +32,29 @@ const outcome = markets[0].outcomes[0]; console.log(`${outcome.label}: ${(outcome.price * 100).toFixed(1)}%`); // Fetch historical data (use outcome.outcomeId!) -const candles = await poly.fetchOHLCV(outcome.outcomeId, { +const candles = await client.fetchOHLCV(outcome.outcomeId, { resolution: '1d', - limit: 30 + limit: 30, }); // Get current order book -const orderBook = await poly.fetchOrderBook(outcome.outcomeId); +const orderBook = await client.fetchOrderBook(outcome.outcomeId); const spread = orderBook.asks[0].price - orderBook.bids[0].price; console.log(`Spread: ${(spread * 100).toFixed(2)}%`); + +// Account reads +const positions = await client.fetchPositions(); +const balance = await client.fetchBalance(); ``` +### How it works (hosted) + +When you pass `pmxtApiKey`, the SDK talks to PMXT's hosted services: catalog requests go to `api.pmxt.dev`, trading requests go to `trade.pmxt.dev`. The SDK does **not** spawn a local process. For Polymarket and Opinion, PMXT's PreFundedEscrow handles custody — you sign orders with your own key, PMXT settles on-chain. + +### How it works (self-hosted) + +Omit `pmxtApiKey` to use the local sidecar. Install `pmxt-core` from npm and supply venue credentials directly. See [Self-hosted trading (advanced)](#self-hosted-trading-advanced) below. + ## Core Methods ### Market Data @@ -77,7 +95,51 @@ console.log(`Spread: ${(spread * 100).toFixed(2)}%`); ## Trading -### Authentication +### Hosted trading (recommended) + +With a PMXT API key, pass `pmxtApiKey`, `walletAddress`, and `privateKey`. The SDK auto-wraps your key into an `EthersSigner` and PMXT settles the order on-chain. + +**Polymarket:** +```typescript +import { Polymarket } from "pmxtjs"; + +const trader = new Polymarket({ + pmxtApiKey: "pmxt_live_...", + walletAddress: "0xYourWalletAddress", + privateKey: "0xYourPrivateKey", +}); + +const balance = await trader.fetchBalance(); +console.log(`Available: $${balance[0].available}`); + +const order = await trader.createOrder({ + marketId: "market-uuid", + outcomeId: "outcome-uuid", + side: "buy", + type: "market", + amount: 5.0, + denom: "usdc", + slippage_pct: 30.0, +} as any); +console.log(`Order status: ${order.status}`); +``` + +**Opinion:** +```typescript +import { Opinion } from "pmxtjs"; + +const trader = new Opinion({ + pmxtApiKey: "pmxt_live_...", + walletAddress: "0xYourWalletAddress", + privateKey: "0xYourPrivateKey", +}); +``` + +See the full [hosted trading guide](https://pmxt.dev/docs/concepts/hosted-trading) for venue support, custody model, and limits. + +### Self-hosted trading (advanced) + +When self-hosting, supply venue credentials directly — no `pmxtApiKey`. The SDK spawns a local sidecar process. **Polymarket:** ```typescript