From 4e0f3b7d34c989ecd5a9fef739696fcbe2836301 Mon Sep 17 00:00:00 2001 From: Adam Boudjemaa Date: Thu, 12 Mar 2026 01:51:00 +0100 Subject: [PATCH 1/2] feat(ts-sdk): add buildOrder method and BuiltOrder type --- sdks/typescript/pmxt/client.ts | 32 +++++++++++++++++++++++++++ sdks/typescript/pmxt/models.ts | 22 ++++++++++++++++++ sdks/typescript/tests/surface.test.ts | 2 ++ 3 files changed, 56 insertions(+) diff --git a/sdks/typescript/pmxt/client.ts b/sdks/typescript/pmxt/client.ts index 6f16b3a..8173814 100644 --- a/sdks/typescript/pmxt/client.ts +++ b/sdks/typescript/pmxt/client.ts @@ -24,6 +24,7 @@ import { Trade, UserTrade, Order, + BuiltOrder, Position, Balance, SearchIn, @@ -926,6 +927,37 @@ export abstract class Exchange { // Trading Methods (require authentication) + async buildOrder(params: any): Promise { + await this.initPromise; + try { + let marketId = params.marketId; + let outcomeId = params.outcomeId; + if (params.outcome) { + if (marketId !== undefined || outcomeId !== undefined) { + throw new Error('Provide either outcome or marketId/outcomeId, not both'); + } + marketId = params.outcome.marketId; + outcomeId = params.outcome.outcomeId; + } + const paramsDict: any = { marketId, outcomeId, side: params.side, type: params.type, amount: params.amount }; + if (params.price !== undefined) paramsDict.price = params.price; + if (params.fee !== undefined) paramsDict.fee = params.fee; + const response = await fetch(`${this.config.basePath}/api/${this.exchangeName}/buildOrder`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, + body: JSON.stringify({ args: [paramsDict], credentials: this.getCredentials() }), + }); + if (!response.ok) { + const error = await response.json().catch(() => ({})); + throw new Error(error.error?.message || response.statusText); + } + const json = await response.json(); + return this.handleResponse(json) as BuiltOrder; + } catch (error) { + throw new Error(`Failed to buildOrder: ${error}`); + } + } + /** * Create a new order. * diff --git a/sdks/typescript/pmxt/models.ts b/sdks/typescript/pmxt/models.ts index 95db590..1d27762 100644 --- a/sdks/typescript/pmxt/models.ts +++ b/sdks/typescript/pmxt/models.ts @@ -368,6 +368,28 @@ export interface CreateOrderParams { /** Optional fee rate (e.g., 1000 for 0.1%) */ fee?: number; } + +/** + * An order payload built but not yet submitted to the exchange. + */ +export interface BuiltOrder { + /** The exchange name this order was built for. */ + exchange: string; + /** The original params used to build this order. */ + params: CreateOrderParams; + /** For CLOB exchanges (Polymarket): the EIP-712 signed order. */ + signedOrder?: Record; + /** For on-chain AMM exchanges: the EVM transaction payload. */ + tx?: { + to: string; + data: string; + value: string; + chainId: number; + }; + /** The raw, exchange-native payload. Always present. */ + raw: unknown; +} + /** * A list of UnifiedMarket objects with a convenience match() method. * Extends Array so all standard array operations work unchanged. diff --git a/sdks/typescript/tests/surface.test.ts b/sdks/typescript/tests/surface.test.ts index 66e7437..7e0c8d6 100644 --- a/sdks/typescript/tests/surface.test.ts +++ b/sdks/typescript/tests/surface.test.ts @@ -19,6 +19,8 @@ const PUBLIC_METHODS = [ 'fetchOrderBook', 'fetchTrades', 'createOrder', + 'buildOrder', + 'submitOrder', 'cancelOrder', 'fetchOrder', 'fetchOpenOrders', From 45384ca529dc41a20d1adf56b9888fd6054200a5 Mon Sep 17 00:00:00 2001 From: "Samuel EF. Tinnerholm" Date: Fri, 13 Mar 2026 15:10:44 +0200 Subject: [PATCH 2/2] feat(sdk): add buildOrder/submitOrder to Python SDK, improve typing and docs in TypeScript SDK --- sdks/python/pmxt/__init__.py | 2 + sdks/python/pmxt/client.py | 166 ++++++++++++++++++++++++++++++ sdks/python/pmxt/models.py | 20 ++++ sdks/python/tests/test_surface.py | 2 + sdks/typescript/pmxt/client.ts | 157 +++++++++++++++++++++------- 5 files changed, 308 insertions(+), 39 deletions(-) diff --git a/sdks/python/pmxt/__init__.py b/sdks/python/pmxt/__init__.py index 72a6c74..ed0329e 100644 --- a/sdks/python/pmxt/__init__.py +++ b/sdks/python/pmxt/__init__.py @@ -31,6 +31,7 @@ UserTrade, PaginatedMarketsResult, Order, + BuiltOrder, Position, Balance, ) @@ -78,6 +79,7 @@ def restart_server(): "UserTrade", "PaginatedMarketsResult", "Order", + "BuiltOrder", "Position", "Balance", ] diff --git a/sdks/python/pmxt/client.py b/sdks/python/pmxt/client.py index fdf1e6d..918b630 100644 --- a/sdks/python/pmxt/client.py +++ b/sdks/python/pmxt/client.py @@ -34,6 +34,7 @@ UserTrade, PaginatedMarketsResult, Order, + BuiltOrder, Position, Balance, ExecutionPriceResult, @@ -177,6 +178,17 @@ def _convert_order(raw: Dict[str, Any]) -> Order: ) +def _convert_built_order(raw: Dict[str, Any]) -> BuiltOrder: + """Convert raw API response to BuiltOrder.""" + return BuiltOrder( + exchange=raw.get("exchange", ""), + params=raw.get("params", {}), + raw=raw.get("raw"), + signed_order=raw.get("signedOrder"), + tx=raw.get("tx"), + ) + + def _convert_position(raw: Dict[str, Any]) -> Position: """Convert raw API response to Position.""" return Position( @@ -1378,6 +1390,160 @@ def create_order( except ApiException as e: raise Exception(f"Failed to create order: {self._extract_api_error(e)}") from None + def build_order( + self, + market_id: Optional[str] = None, + outcome_id: Optional[str] = None, + side: Literal["buy", "sell"] = "buy", + type: Literal["market", "limit"] = "market", + amount: float = 0, + price: Optional[float] = None, + fee: Optional[int] = None, + outcome: Optional[MarketOutcome] = None, + ) -> BuiltOrder: + """ + Build an order payload without submitting it to the exchange. + + Returns the exchange-native signed order or transaction payload for + inspection, forwarding through a middleware layer, or deferred + submission via submit_order(). + + You can specify the market either with explicit market_id/outcome_id, + or by passing an outcome object directly (e.g., market.yes). + + Args: + market_id: Market ID (or use outcome instead) + outcome_id: Outcome ID (or use outcome instead) + side: Order side (buy/sell) + type: Order type (market/limit) + amount: Number of contracts + price: Limit price (required for limit orders, 0.0-1.0) + fee: Optional fee rate (e.g., 1000 for 0.1%) + outcome: A MarketOutcome object (e.g., market.yes). Extracts market_id and outcome_id automatically. + + Returns: + A BuiltOrder containing the exchange-native payload + + Example: + >>> # Build, inspect, then submit: + >>> built = exchange.build_order( + ... market_id="663583", + ... outcome_id="10991849...", + ... side="buy", + ... type="limit", + ... amount=10, + ... price=0.55 + ... ) + >>> print(built.signed_order) # inspect before submitting + >>> order = exchange.submit_order(built) + >>> + >>> # Using outcome shorthand: + >>> built = exchange.build_order( + ... outcome=market.yes, + ... side="buy", + ... type="market", + ... amount=10 + ... ) + """ + try: + # Resolve outcome shorthand + if outcome is not None: + if market_id is not None or outcome_id is not None: + raise ValueError( + "Cannot specify both 'outcome' and 'market_id'/'outcome_id'. Use one or the other." + ) + if not outcome.market_id: + raise ValueError( + "outcome.market_id is not set. Ensure the outcome comes from a fetched market." + ) + market_id = outcome.market_id + outcome_id = outcome.outcome_id + elif market_id is None or outcome_id is None: + raise ValueError( + "Either provide 'outcome' or both 'market_id' and 'outcome_id'." + ) + + params_dict = { + "marketId": market_id, + "outcomeId": outcome_id, + "side": side, + "type": type, + "amount": amount, + } + if price is not None: + params_dict["price"] = price + if fee is not None: + params_dict["fee"] = fee + + request_body_dict = {"args": [params_dict]} + + # Add credentials if available + creds = self._get_credentials_dict() + if creds: + request_body_dict["credentials"] = creds + + request_body = internal_models.BuildOrderRequest.from_dict(request_body_dict) + + response = self._api.build_order( + exchange=self.exchange_name, + build_order_request=request_body, + ) + + data = self._handle_response(response.to_dict()) + return _convert_built_order(data) + except ApiException as e: + raise Exception(f"Failed to build order: {self._extract_api_error(e)}") from None + + def submit_order(self, built: BuiltOrder) -> Order: + """ + Submit a pre-built order returned by build_order(). + + Args: + built: The BuiltOrder payload from build_order() + + Returns: + The submitted order + + Example: + >>> built = exchange.build_order( + ... outcome=market.yes, + ... side="buy", + ... type="limit", + ... amount=10, + ... price=0.55 + ... ) + >>> order = exchange.submit_order(built) + >>> print(order.id, order.status) + """ + try: + built_dict = { + "exchange": built.exchange, + "params": built.params, + "raw": built.raw, + } + if built.signed_order is not None: + built_dict["signedOrder"] = built.signed_order + if built.tx is not None: + built_dict["tx"] = built.tx + + request_body_dict = {"args": [built_dict]} + + creds = self._get_credentials_dict() + if creds: + request_body_dict["credentials"] = creds + + request_body = internal_models.SubmitOrderRequest.from_dict(request_body_dict) + + response = self._api.submit_order( + exchange=self.exchange_name, + submit_order_request=request_body, + ) + + data = self._handle_response(response.to_dict()) + return _convert_order(data) + except ApiException as e: + raise Exception(f"Failed to submit order: {self._extract_api_error(e)}") from None + def get_execution_price( self, order_book: OrderBook, diff --git a/sdks/python/pmxt/models.py b/sdks/python/pmxt/models.py index ffbd68c..a4e7cd3 100644 --- a/sdks/python/pmxt/models.py +++ b/sdks/python/pmxt/models.py @@ -337,6 +337,26 @@ class Order: """Trading fee""" +@dataclass +class BuiltOrder: + """An order payload built but not yet submitted to the exchange.""" + + exchange: str + """The exchange name this order was built for.""" + + params: Dict[str, Any] + """The original params used to build this order.""" + + raw: Any + """The raw, exchange-native payload. Always present.""" + + signed_order: Optional[Dict[str, Any]] = None + """For CLOB exchanges (Polymarket): the EIP-712 signed order.""" + + tx: Optional[Dict[str, Any]] = None + """For on-chain AMM exchanges: the EVM transaction payload.""" + + @dataclass class Position: """A current position in a market.""" diff --git a/sdks/python/tests/test_surface.py b/sdks/python/tests/test_surface.py index 75723c1..28f6e14 100644 --- a/sdks/python/tests/test_surface.py +++ b/sdks/python/tests/test_surface.py @@ -19,6 +19,8 @@ "fetch_order_book", "fetch_trades", "create_order", + "build_order", + "submit_order", "cancel_order", "fetch_order", "fetch_open_orders", diff --git a/sdks/typescript/pmxt/client.ts b/sdks/typescript/pmxt/client.ts index 8173814..d6b4b72 100644 --- a/sdks/typescript/pmxt/client.ts +++ b/sdks/typescript/pmxt/client.ts @@ -11,6 +11,8 @@ import { FetchOHLCVRequest, FetchTradesRequest, CreateOrderRequest, + BuildOrderRequest, + SubmitOrderRequest, ExchangeCredentials, } from "../generated/src/index.js"; @@ -25,6 +27,7 @@ import { UserTrade, Order, BuiltOrder, + CreateOrderParams, Position, Balance, SearchIn, @@ -518,27 +521,6 @@ export abstract class Exchange { } } - async submitOrder(built: any): Promise { - await this.initPromise; - try { - const args: any[] = []; - args.push(built); - const response = await fetch(`${this.config.basePath}/api/${this.exchangeName}/submitOrder`, { - method: 'POST', - headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, - body: JSON.stringify({ args, credentials: this.getCredentials() }), - }); - if (!response.ok) { - const error = await response.json().catch(() => ({})); - throw new Error(error.error?.message || response.statusText); - } - const json = await response.json(); - const data = this.handleResponse(json); - return convertOrder(data); - } catch (error) { - throw new Error(`Failed to submitOrder: ${error}`); - } - } async cancelOrder(orderId: string): Promise { await this.initPromise; @@ -927,34 +909,131 @@ export abstract class Exchange { // Trading Methods (require authentication) - async buildOrder(params: any): Promise { + /** + * Build an order payload without submitting it to the exchange. + * Returns the exchange-native signed order or transaction payload for + * inspection, forwarding through a middleware layer, or deferred + * submission via {@link submitOrder}. + * + * You can specify the market either with explicit marketId/outcomeId, + * or by passing an outcome object directly (e.g., market.yes). + * + * @param params - Order parameters (same as createOrder) + * @returns A BuiltOrder containing the exchange-native payload + * + * @example + * ```typescript + * // Build, inspect, then submit: + * const built = await exchange.buildOrder({ + * marketId: "663583", + * outcomeId: "10991849...", + * side: "buy", + * type: "limit", + * amount: 10, + * price: 0.55 + * }); + * + * console.log(built.signedOrder); // inspect before submitting + * const order = await exchange.submitOrder(built); + * + * // Using outcome shorthand: + * const built2 = await exchange.buildOrder({ + * outcome: market.yes, + * side: "buy", + * type: "market", + * amount: 10 + * }); + * ``` + */ + async buildOrder(params: CreateOrderParams & { outcome?: MarketOutcome }): Promise { await this.initPromise; try { let marketId = params.marketId; let outcomeId = params.outcomeId; + if (params.outcome) { if (marketId !== undefined || outcomeId !== undefined) { - throw new Error('Provide either outcome or marketId/outcomeId, not both'); + throw new Error( + "Cannot specify both 'outcome' and 'marketId'/'outcomeId'. Use one or the other." + ); + } + const outcome: MarketOutcome = params.outcome; + if (!outcome.marketId) { + throw new Error( + "outcome.marketId is not set. Ensure the outcome comes from a fetched market." + ); } - marketId = params.outcome.marketId; - outcomeId = params.outcome.outcomeId; + marketId = outcome.marketId; + outcomeId = outcome.outcomeId; } - const paramsDict: any = { marketId, outcomeId, side: params.side, type: params.type, amount: params.amount }; - if (params.price !== undefined) paramsDict.price = params.price; - if (params.fee !== undefined) paramsDict.fee = params.fee; - const response = await fetch(`${this.config.basePath}/api/${this.exchangeName}/buildOrder`, { - method: 'POST', - headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, - body: JSON.stringify({ args: [paramsDict], credentials: this.getCredentials() }), - }); - if (!response.ok) { - const error = await response.json().catch(() => ({})); - throw new Error(error.error?.message || response.statusText); + + const paramsDict: any = { + marketId, + outcomeId, + side: params.side, + type: params.type, + amount: params.amount, + }; + if (params.price !== undefined) { + paramsDict.price = params.price; } - const json = await response.json(); - return this.handleResponse(json) as BuiltOrder; + if (params.fee !== undefined) { + paramsDict.fee = params.fee; + } + + const requestBody: BuildOrderRequest = { + args: [paramsDict], + credentials: this.getCredentials() + }; + + const response = await this.api.buildOrder({ + exchange: this.exchangeName as any, + buildOrderRequest: requestBody, + }); + + const data = this.handleResponse(response); + return data as BuiltOrder; + } catch (error) { + throw new Error(`Failed to build order: ${error}`); + } + } + + /** + * Submit a pre-built order returned by {@link buildOrder}. + * + * @param built - The BuiltOrder payload from buildOrder() + * @returns The submitted order + * + * @example + * ```typescript + * const built = await exchange.buildOrder({ + * outcome: market.yes, + * side: "buy", + * type: "limit", + * amount: 10, + * price: 0.55 + * }); + * const order = await exchange.submitOrder(built); + * console.log(order.id, order.status); + * ``` + */ + async submitOrder(built: BuiltOrder): Promise { + await this.initPromise; + try { + const requestBody: SubmitOrderRequest = { + args: [built as any], + credentials: this.getCredentials() + }; + + const response = await this.api.submitOrder({ + exchange: this.exchangeName as any, + submitOrderRequest: requestBody, + }); + + const data = this.handleResponse(response); + return convertOrder(data); } catch (error) { - throw new Error(`Failed to buildOrder: ${error}`); + throw new Error(`Failed to submit order: ${error}`); } }