@@ -1047,14 +1047,37 @@ function typeNodeToSchema(node, sourceFile) {
10471047
10481048 case ts . SyntaxKind . UnionType : {
10491049 const members = node . types ;
1050+ // Track whether the union includes `null` (an explicit `null` member
1051+ // means the field is nullable on the wire even if also `?:` optional).
1052+ // We strip `null` / `undefined` before classifying the union shape,
1053+ // then re-apply `nullable: true` to the resulting schema so the JSON
1054+ // shape stays a single type rather than a `oneOf [T, null]`.
1055+ // TS parses `null` inside a union as `LiteralType` wrapping
1056+ // `NullKeyword` (not bare `NullKeyword`), so check both forms.
1057+ const isNullMember = ( t ) =>
1058+ t . kind === ts . SyntaxKind . NullKeyword ||
1059+ ( t . kind === ts . SyntaxKind . LiteralType &&
1060+ t . literal &&
1061+ t . literal . kind === ts . SyntaxKind . NullKeyword ) ;
1062+ const isUndefinedMember = ( t ) =>
1063+ t . kind === ts . SyntaxKind . UndefinedKeyword ;
1064+ const includesNull = members . some ( isNullMember ) ;
10501065 const nonNull = members . filter (
1051- t =>
1052- t . kind !== ts . SyntaxKind . NullKeyword &&
1053- t . kind !== ts . SyntaxKind . UndefinedKeyword
1066+ t => ! isNullMember ( t ) && ! isUndefinedMember ( t )
10541067 ) ;
10551068
10561069 if ( nonNull . length === 0 ) return null ;
10571070
1071+ const withNullable = ( schema ) => {
1072+ if ( ! includesNull || ! schema ) return schema ;
1073+ // OpenAPI 3.0: nullable is a sibling keyword on the schema itself.
1074+ // For $ref values we wrap in allOf so nullable doesn't get dropped.
1075+ if ( schema . $ref ) {
1076+ return { allOf : [ schema ] , nullable : true } ;
1077+ }
1078+ return { ...schema , nullable : true } ;
1079+ } ;
1080+
10581081 // All string literals → enum
10591082 if (
10601083 nonNull . every (
@@ -1063,17 +1086,22 @@ function typeNodeToSchema(node, sourceFile) {
10631086 t . literal . kind === ts . SyntaxKind . StringLiteral
10641087 )
10651088 ) {
1066- return { type : 'string' , enum : nonNull . map ( t => t . literal . text ) } ;
1089+ return withNullable ( {
1090+ type : 'string' ,
1091+ enum : nonNull . map ( t => t . literal . text ) ,
1092+ } ) ;
10671093 }
10681094
1069- if ( nonNull . length === 1 ) return typeNodeToSchema ( nonNull [ 0 ] , sourceFile ) ;
1095+ if ( nonNull . length === 1 ) {
1096+ return withNullable ( typeNodeToSchema ( nonNull [ 0 ] , sourceFile ) ) ;
1097+ }
10701098
10711099 const schemas = nonNull
10721100 . map ( t => typeNodeToSchema ( t , sourceFile ) )
10731101 . filter ( s => s !== null ) ;
10741102 if ( schemas . length === 0 ) return null ;
1075- if ( schemas . length === 1 ) return schemas [ 0 ] ;
1076- return { oneOf : schemas } ;
1103+ if ( schemas . length === 1 ) return withNullable ( schemas [ 0 ] ) ;
1104+ return withNullable ( { oneOf : schemas } ) ;
10771105 }
10781106
10791107 case ts . SyntaxKind . LiteralType : {
@@ -1664,8 +1692,129 @@ const STATIC_SCHEMAS = {
16641692 } ,
16651693 ErrorDetail : {
16661694 type : 'object' ,
1695+ description :
1696+ 'Structured error envelope returned inside `BaseResponse.error` and `ErrorResponse.error`. ' +
1697+ 'Hosted-mode endpoints populate `code`, `retryable`, and optionally `exchange` / `detail`; ' +
1698+ 'legacy local-mode endpoints may still return only `message`.' ,
1699+ properties : {
1700+ message : {
1701+ type : 'string' ,
1702+ description : 'Human-readable error message.' ,
1703+ } ,
1704+ code : {
1705+ type : 'string' ,
1706+ description :
1707+ 'Stable machine-readable error code. Hosted-mode errors use the `HostedTradingError` family ' +
1708+ '(e.g. `INSUFFICIENT_ESCROW_BALANCE`, `BUILT_ORDER_EXPIRED`); pre-hosted local errors use the ' +
1709+ 'legacy family (e.g. `BAD_REQUEST`, `NOT_FOUND`).' ,
1710+ enum : [
1711+ // Hosted-mode error codes (v2.49.0+)
1712+ 'HOSTED_TRADING_ERROR' ,
1713+ 'INSUFFICIENT_ESCROW_BALANCE' ,
1714+ 'ORDER_SIZE_TOO_SMALL' ,
1715+ 'INVALID_API_KEY' ,
1716+ 'OUTCOME_NOT_FOUND' ,
1717+ 'CATALOG_UNAVAILABLE' ,
1718+ 'BUILT_ORDER_EXPIRED' ,
1719+ 'INVALID_SIGNATURE' ,
1720+ 'NO_LIQUIDITY' ,
1721+ 'MISSING_WALLET_ADDRESS' ,
1722+ // Pre-hosted (legacy) error codes
1723+ 'BAD_REQUEST' ,
1724+ 'AUTHENTICATION_ERROR' ,
1725+ 'PERMISSION_DENIED' ,
1726+ 'NOT_FOUND' ,
1727+ 'ORDER_NOT_FOUND' ,
1728+ 'MARKET_NOT_FOUND' ,
1729+ 'EVENT_NOT_FOUND' ,
1730+ 'RATE_LIMIT_EXCEEDED' ,
1731+ 'INVALID_ORDER' ,
1732+ 'INSUFFICIENT_FUNDS' ,
1733+ 'VALIDATION_ERROR' ,
1734+ 'NETWORK_ERROR' ,
1735+ 'EXCHANGE_NOT_AVAILABLE' ,
1736+ 'NOT_SUPPORTED' ,
1737+ ] ,
1738+ } ,
1739+ retryable : {
1740+ type : 'boolean' ,
1741+ description :
1742+ 'Hint for clients: when `true`, the same request may succeed on retry (e.g. transient network ' +
1743+ 'or rate-limit conditions); when `false`, the caller should not retry without modifying the ' +
1744+ 'request.' ,
1745+ } ,
1746+ exchange : {
1747+ type : 'string' ,
1748+ nullable : true ,
1749+ description :
1750+ "Venue the error originated from, when known (e.g. 'polymarket', 'kalshi')." ,
1751+ } ,
1752+ detail : {
1753+ type : 'object' ,
1754+ additionalProperties : { } ,
1755+ nullable : true ,
1756+ description :
1757+ 'Free-form hosted-mode detail blob. Shape depends on `code` — e.g. for ' +
1758+ '`INSUFFICIENT_ESCROW_BALANCE` it may include `{ requested, available }`; for ' +
1759+ '`ORDER_SIZE_TOO_SMALL` it may include `{ min }`; for `BUILT_ORDER_EXPIRED` it may include ' +
1760+ '`{ expiry }`.' ,
1761+ } ,
1762+ } ,
1763+ } ,
1764+ ExchangeOptions : {
1765+ type : 'object' ,
1766+ description :
1767+ 'Constructor-level options for venue clients (Polymarket, Kalshi, Opinion, etc.).\n' +
1768+ 'Hosted mode is the default when pmxtApiKey is set; otherwise the SDK runs against\n' +
1769+ 'a local sidecar with venue credentials.' ,
16671770 properties : {
1668- message : { type : 'string' } ,
1771+ pmxtApiKey : {
1772+ type : 'string' ,
1773+ description :
1774+ 'PMXT customer API key. When set, the SDK routes to api.pmxt.dev (catalog) and ' +
1775+ 'trade.pmxt.dev (trading). Get one at pmxt.dev/dashboard.' ,
1776+ } ,
1777+ walletAddress : {
1778+ type : 'string' ,
1779+ nullable : true ,
1780+ description :
1781+ 'EVM wallet address for hosted reads/writes. Required for endpoints that operate on a wallet ' +
1782+ '(balances, positions, trades, open orders).' ,
1783+ } ,
1784+ signer : {
1785+ type : 'object' ,
1786+ nullable : true ,
1787+ description :
1788+ 'Optional pre-built signer for hosted writes. If absent and privateKey is set, the SDK ' +
1789+ 'auto-wraps privateKey into a signer.' ,
1790+ } ,
1791+ privateKey : {
1792+ type : 'string' ,
1793+ nullable : true ,
1794+ description :
1795+ 'Private key. In hosted mode, used to derive an EIP-712 signer for writes (wraps into ' +
1796+ 'EthAccountSigner/EthersSigner). In self-hosted mode, used as the venue credential directly.' ,
1797+ } ,
1798+ baseUrl : {
1799+ type : 'string' ,
1800+ nullable : true ,
1801+ description :
1802+ 'Explicit base URL override. When unset, the SDK uses api.pmxt.dev when pmxtApiKey is set, ' +
1803+ 'or the local sidecar otherwise.' ,
1804+ } ,
1805+ apiKey : {
1806+ type : 'string' ,
1807+ nullable : true ,
1808+ description :
1809+ 'Venue-side API key (e.g. Polymarket CLOB key). Only relevant for self-hosted mode.' ,
1810+ } ,
1811+ autoStartServer : {
1812+ type : 'boolean' ,
1813+ nullable : true ,
1814+ description :
1815+ 'Auto-start the local sidecar when running self-hosted. Defaults to true when no pmxtApiKey ' +
1816+ 'is set, false when hosted.' ,
1817+ } ,
16691818 } ,
16701819 } ,
16711820 BaseRequest : {
0 commit comments