{"_id":"@demo-npm-test/prediction-market-sdk","name":"@demo-npm-test/prediction-market-sdk","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@demo-npm-test/prediction-market-sdk","version":"0.1.0","description":"Unified TypeScript SDK for prediction markets (Polymarket, Kalshi, predict.fun, Opinion Labs).","type":"module","main":"./dist/index.cjs","module":"./dist/index.mjs","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.cjs"}},"publishConfig":{"access":"public"},"repository":{"type":"git","url":"git+https://github.com/arbsxyz/prediction-market-sdk.git"},"homepage":"https://github.com/arbsxyz/prediction-market-sdk#readme","bugs":{"url":"https://github.com/arbsxyz/prediction-market-sdk/issues"},"engines":{"node":">=20"},"scripts":{"build":"tsup","typecheck":"tsc --noEmit","test":"vitest run","test:watch":"vitest","test:integration":"vitest run --config vitest.integration.config.ts","example":"pnpm build && tsx examples/quickstart.ts","playground":"pnpm build && tsx playground/server.ts","lint":"eslint .","format":"prettier --write .","prepublishOnly":"pnpm typecheck && pnpm test && pnpm build"},"keywords":["prediction-market","polymarket","kalshi","predict.fun","opinion-labs","trading","sdk","typescript"],"license":"MIT","devDependencies":{"@types/node":"^20.12.0","@typescript-eslint/eslint-plugin":"^7.7.0","@typescript-eslint/parser":"^7.7.0","eslint":"^8.57.0","eslint-config-prettier":"^9.1.0","ethers":"^6.17.0","prettier":"^3.2.5","tsup":"^8.0.2","tsx":"^4.19.0","typescript":"^5.4.5","vitest":"^1.5.0"},"dependencies":{"@noble/curves":"^2.2.0","@noble/hashes":"^2.2.0"},"_id":"@demo-npm-test/prediction-market-sdk@0.1.0","gitHead":"2335e77cd0fec12af329953bf7a86a416ffdd8a3","_nodeVersion":"20.15.1","_npmVersion":"10.7.0","dist":{"integrity":"sha512-54mrQjsPIQdPiHr9ZN6tYEk7NRrrjQhev5lECntAP+kFlid0kfnqW/KogrA1xx3vlM8F/6uwMujBi54AIpez/w==","shasum":"050b691d95a5c5817e0c8c11417dcc554ff601ac","tarball":"https://registry.npmjs.org/@demo-npm-test/prediction-market-sdk/-/prediction-market-sdk-0.1.0.tgz","fileCount":9,"unpackedSize":1833803,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIDta5Mu4EkdIRfyYW+XUzDS5tDpVi+RlRnkgMNBiLXImAiAOjtqXe6obTQqCk05+CuAA5wZJ5GUXEdDaOjHnYjs+pQ=="}]},"_npmUser":{"name":"dash87","email":"divyeshlalwani13@gmail.com"},"directories":{},"maintainers":[{"name":"dash87","email":"divyeshlalwani13@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/prediction-market-sdk_0.1.0_1784713256976_0.3721773685022316"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-22T09:40:56.717Z","0.1.0":"2026-07-22T09:40:57.215Z","modified":"2026-07-22T09:40:57.458Z"},"maintainers":[{"name":"dash87","email":"divyeshlalwani13@gmail.com"}],"description":"Unified TypeScript SDK for prediction markets (Polymarket, Kalshi, predict.fun, Opinion Labs).","homepage":"https://github.com/arbsxyz/prediction-market-sdk#readme","keywords":["prediction-market","polymarket","kalshi","predict.fun","opinion-labs","trading","sdk","typescript"],"repository":{"type":"git","url":"git+https://github.com/arbsxyz/prediction-market-sdk.git"},"bugs":{"url":"https://github.com/arbsxyz/prediction-market-sdk/issues"},"license":"MIT","readme":"# prediction-market-sdk\n\nA TypeScript SDK that exposes a single, normalized interface over multiple\nprediction-market venues — **Polymarket**, **Kalshi**, **predict.fun**, and\n**Opinion Labs**.\nWrite your code once against one contract; never branch on venue.\n\n- **Read-only (v1):** market data, orderbooks, trades, balances, positions, open orders.\n- **Isomorphic:** Node 20+ and modern browsers. Native `fetch` + Web Crypto only.\n- **Zero runtime dependencies.**\n- **Strict, normalized types** — prices are always implied probabilities in `[0, 1]`; money is always USD with a ready-to-use dollar `value` (and a lossless integer `amount`).\n\n> Status: pre-release (`0.0.0`). Trading writes and WebSocket streaming are not\n> implemented — see [Scope](#scope).\n\n## Install\n\n```sh\nnpm install prediction-market-sdk\n# or: pnpm add prediction-market-sdk\n```\n\n## Quick start\n\n```ts\nimport { PolymarketClient, KalshiClient, PredictFunClient } from 'prediction-market-sdk';\n\n// Polymarket market data needs no credentials.\nconst poly = await PolymarketClient.create();\nconst markets = await poly.getMarkets({ status: 'open', limit: 5 });\nfor (const m of markets) {\n  const odds = m.outcomes.map((o) => `${o.name} ${(o.probability * 100).toFixed(1)}%`);\n  console.log(m.title, '→', odds.join(' / '));\n}\n\n// Kalshi market data also needs no credentials.\nconst kalshi = await KalshiClient.create();\nconst book = await kalshi.getOrderbook('SOME-TICKER'); // defaults to the YES side\nconsole.log(book.bids[0], book.asks[0]);\n\n// predict.fun mainnet needs an API key — but its testnet is fully keyless.\nconst predict = await PredictFunClient.create({ testnet: true });\nconsole.log(await predict.getTrendingMarkets({ limit: 5 }));\n```\n\nAll clients implement the same abstract base, so you can program against the\ncontract instead of the concrete venue:\n\n```ts\nimport type { PredictionMarketClient } from 'prediction-market-sdk';\n\nasync function topMarket(client: PredictionMarketClient) {\n  const [first] = await client.getMarkets({ status: 'open', limit: 1 });\n  return first; // a normalized Market, identical shape across venues\n}\n```\n\n## Construction\n\nEvery client is created with an **async factory** (`create`), never `new`.\n\n### `KalshiClient.create(options?)`\n\n```ts\nconst kalshi = await KalshiClient.create({\n  apiKeyId: process.env.KALSHI_API_KEY_ID,        // required for portfolio methods\n  privateKeyPem: process.env.KALSHI_PRIVATE_KEY,  // PKCS#8 PEM (\"-----BEGIN PRIVATE KEY-----\")\n});\n```\n\n| Option | Type | Default | Notes |\n|---|---|---|---|\n| `apiKeyId` | `string` | — | Kalshi API key id. Required (with `privateKeyPem`) for `getBalance`/`getPositions`/`getOpenOrders`. |\n| `privateKeyPem` | `string` | — | **PKCS#8** PEM. A PKCS#1 key (`-----BEGIN RSA PRIVATE KEY-----`) is rejected with a conversion hint. |\n| `auth` | `AuthStrategy` | — | Pre-built strategy; overrides the two fields above. Advanced/testing. |\n| `baseUrl` | `string` | `https://api.elections.kalshi.com/trade-api/v2` | Set to `https://demo-api.kalshi.co/trade-api/v2` for demo. |\n| `timeoutMs` | `number` | `30000` | Per-request timeout. |\n| `fetch` | `typeof fetch` | global `fetch` | Inject for tests. |\n| `sleep` | `(ms) => Promise<void>` | `setTimeout` | Inject for tests. |\n| `now` | `() => number` | `Date.now` | Clock for signing + orderbook timestamps. |\n\nMarket-data methods (`getMarkets`, `getMarket`, `getOrderbook`, `getTrades`) work\n**without credentials**. Portfolio methods require `apiKeyId` + `privateKeyPem`.\n\n### `PolymarketClient.create(options?)`\n\n```ts\nconst poly = await PolymarketClient.create({\n  walletAddress: '0x…', // required for getPositions / getBalance / getPortfolioValue\n});\n```\n\n| Option | Type | Default | Notes |\n|---|---|---|---|\n| `walletAddress` | `string` | — | A public proxy-wallet address. Required for `getPositions`/`getBalance`/`getPortfolioValue`. |\n| `gammaBaseUrl` | `string` | `https://gamma-api.polymarket.com` | Market-data API. |\n| `clobBaseUrl` | `string` | `https://clob.polymarket.com` | Orderbook API. |\n| `dataBaseUrl` | `string` | `https://data-api.polymarket.com` | Trades/positions/value API. |\n| `rpcUrl` | `string` | `https://polygon-bor-rpc.publicnode.com` | Polygon JSON-RPC, used by `getBalance` for the on-chain USDC cash balance. |\n| `timeoutMs` | `number` | `30000` | Per-request timeout. |\n| `fetch` | `typeof fetch` | global `fetch` | Inject for tests. |\n| `sleep` | `(ms) => Promise<void>` | `setTimeout` | Inject for tests. |\n| `now` | `() => number` | `Date.now` | Clock for orderbook timestamps. |\n| `signer` | `Signer` | — | Signs orders + ClobAuth. Use `new LocalSigner('0x…')` for a local secp256k1 key, or any external `Signer` (Privy/Turnkey/viem/ethers). Both are exported. Required for `placeOrder`/`buildSignedOrder`. |\n| `funderAddress` | `string` | derived from `signatureType` | Funds owner (proxy/funder). Defaults to the signer's EOA (sigType 0), its Safe (sigType 2), or its deposit wallet (sigType 3). |\n| `signatureType` | `0 \\| 1 \\| 2 \\| 3` | `3` | `0` EOA, `1` Polymarket proxy, `2` Gnosis-Safe proxy, `3` deposit wallet (POLY_1271). **Defaults to `3`** — Polymarket mandates it for all *new* API accounts; pre-existing EOA/proxy/Safe users must set `0`/`1`/`2` explicitly. sigType 3 is implemented but **LIVE-unverified** — see the note below. |\n| `clobApiKey` / `clobSecret` / `clobPassphrase` | `string` | auto-derived | CLOB L2 API credentials. **Optional** — a client with just a `signer` derives and caches these itself on the first authenticated call. Pass them only to skip that one-time round trip (see [`deriveApiCreds()`](#deriveapicreds-polymarket-only)). |\n| `exchangeAddress` | `string` | CTF Exchange V2 (`CTF_EXCHANGE`) | EIP-712 verifying contract; pass the exported `NEG_RISK_CTF_EXCHANGE` for neg-risk markets. |\n| `chainId` | `number` | `137` | EIP-712 chain id (Polygon). |\n\nRead endpoints are unauthenticated; `walletAddress` is an address, not a\ncredential — it identifies whose positions/value to read. The trading options\nabove are only needed for `placeOrder`.\n\n#### Credentials are automatic — just create a client and trade\n\nThe `clobApiKey`/`clobSecret`/`clobPassphrase` are L2 credentials derived from\nyour `signer` (you don't get them from a dashboard). **You don't have to manage\nthem:** a client built with only a `signer` derives them itself on its first\nauthenticated call and caches them, so the whole flow is one step:\n\n```ts\nimport { PolymarketClient, LocalSigner } from 'prediction-market-sdk';\n\nconst client = await PolymarketClient.create({\n  signer: new LocalSigner('0x…'),\n  signatureType: 1, // optional; defaults to 3 (deposit wallet)\n});\n\n// Creds are derived + cached under the hood — this just works:\nawait client.placeOrder({ marketId, outcomeId, side: 'buy', price: 0.5, size: 10, tif: 'gtc' });\n```\n\n#### `deriveApiCreds()` (Polymarket only)\n\nCall this only if you want to **inspect or persist** the creds — e.g. to store\nthem and pass them back into `create` next time, skipping the one-time derivation\nround trip:\n\n```ts\nconst poly = await PolymarketClient.create({ signer: new LocalSigner('0x…') });\nconst creds = await poly.deriveApiCreds(); // → { apiKey, secret, passphrase }\n\nconst trading = await PolymarketClient.create({\n  signer: new LocalSigner('0x…'),\n  signatureType: 1,\n  clobApiKey: creds.apiKey,\n  clobSecret: creds.secret,\n  clobPassphrase: creds.passphrase,\n});\n```\n\nThe call is **idempotent** — the same key always maps to the same credentials,\nso re-running it is safe. Under the hood it is L1-signed (EIP-712 `ClobAuth`) and\n**create-or-derive**: `POST /auth/api-key` (create) first, falling back to\n`GET /auth/derive-api-key` only if create returns no key. This is why a\nbrand-new wallet works — it has no key to *derive* yet, so the create step runs\nfirst. Returns `PolymarketApiCreds`. Requires\nonly a `signer`. This is a Polymarket-only helper (not on the base\n`PredictionMarketClient`). The **playground** (`pnpm playground`) exposes it under\nits \"Setup\" group with a one-click \"Save to Polymarket credentials\" button.\n\n> **Wallet topology (`signatureType`):** for `signatureType: 1` (the common\n> Email/Magic wallet), `funderAddress` and `walletAddress` are the **same**\n> address — the Magic smart-wallet (proxy) — and both differ from the signer\n> (the EOA the `signer` controls). For `signatureType: 0` (EOA) the signer\n> *is* the funder, so `funderAddress` can be omitted. For `signatureType: 2`\n> (Gnosis-Safe) and `signatureType: 3` (deposit wallet), the funder defaults to\n> the EOA's deterministic Safe / deposit-wallet address, so it can be omitted\n> too when the EOA owns that wallet.\n\n> **`signatureType: 3` (deposit wallet / POLY_1271) is LIVE-unverified.** The\n> account model Polymarket mandates for *new* API users. The SDK builds it\n> end-to-end — the deposit-wallet CREATE2 address (verified on-chain) and the\n> Solady ERC-7739 `TypedDataSign` **order** envelope, whose signing path is\n> verified byte-for-byte against `@polymarket/clob-client-v2` 1.0.8. The owner EOA\n> (via `LocalSigner` or an external `Signer`) produces the raw ECDSA;\n> `maker == signer == depositWallet`, while the **CLOB credentials bind to the\n> owner EOA** (plain ClobAuth, `POLY_ADDRESS = EOA`) — same as 1.0.8. Whether the\n> live CLOB **accepts** an end-to-end submit is not yet confirmed against a real\n> response, so treat as experimental until the `POLY_SIGTYPE3_SUBMIT=1` LIVE check\n> passes.\n\n### `PredictFunClient.create(options?)`\n\n```ts\nimport { PredictFunClient, LocalSigner } from 'prediction-market-sdk';\n\nconst predict = await PredictFunClient.create({\n  apiKey: process.env.PREDICTFUN_API_KEY,        // mainnet reads need one\n  walletAddress: '0x…',                          // portfolio reads\n  signer: new LocalSigner(process.env.PREDICTFUN_PRIVATE_KEY!), // trading\n});\n\n// Email/social sign-in account (Predict account / smart wallet): pass the\n// deposit address from predict.fun → Account → Settings plus the exported\n// Privy wallet key. Orders + auth then act AS the account — its balances and\n// allowances apply, so no manual approvals are needed.\nconst account = await PredictFunClient.create({\n  apiKey: process.env.PREDICTFUN_API_KEY,\n  predictAccount: '0x…deposit address…',\n  signer: new LocalSigner(process.env.PREDICTFUN_PRIVATE_KEY!), // Privy key\n});\n\n// Or fully keyless against the BNB testnet:\nconst test = await PredictFunClient.create({ testnet: true });\n```\n\n| Option | Type | Default | Notes |\n|---|---|---|---|\n| `apiKey` | `string` | — | Mainnet `x-api-key` (issued via predict.fun's Discord). Required for **every** mainnet endpoint; the testnet needs none. |\n| `testnet` | `boolean` | `false` | Target `api-testnet.predict.fun` (BNB testnet, chain 97, keyless) — flips the base URL, RPC, collateral, and exchange contracts. |\n| `walletAddress` | `string` | signer's address | Wallet for `getBalance`/`getPositions`/`getPortfolioValue` (read-only, keyed by address). |\n| `signer` | `Signer` | — | Required for trading + `getFills`/order reads. A plain EOA must custody the USDT collateral (`maker == signer`); with `predictAccount` set it is the account's owner (Privy) key instead. JWT auth personal-signs (EIP-191), so the signer needs raw-digest capability — `LocalSigner` provides it. |\n| `predictAccount` | `string` | — | Kernel smart-account address for an email/social sign-in account (the **deposit address** in predict.fun settings). Orders and the JWT then act as the account — `maker == signer == predictAccount`, Kernel-envelope signatures (`signatureType` stays 0; the exchange verifies via ERC-1271), and portfolio reads default to the account. |\n| `baseUrl` | `string` | per network | REST base override. |\n| `rpcUrl` | `string` | per network | BNB Chain JSON-RPC for the on-chain USDT balance. |\n| `collateralAddress` | `string` | per network | Collateral token `getBalance` reads (18-decimal USDT). |\n| `timeoutMs` / `fetch` / `sleep` / `now` | — | — | Same plumbing as the other venues. |\n\npredict.fun is a BNB Chain fork of Polymarket's V1 CTF-exchange CLOB. Wallet\nauth is a Bearer JWT the client bootstraps automatically (fetch login message →\nEIP-191 personal_sign → `POST /v1/auth`), cached and re-derived once on a 401.\n\n### `OpinionClient.create(options?)`\n\nOpinion Labs (opinion.trade) is an on-chain CLOB on **BNB Chain (chainId 56)**,\ncollateralized in **USDT**. Reads need only an `apiKey`; trading uses a\n**two-key model** plus an owner-EOA signer on an onboarded Gnosis Safe.\n\n```ts\nimport { OpinionClient, LocalSigner } from 'prediction-market-sdk';\n\n// Read-only:\nconst opinion = await OpinionClient.create({ apiKey: '…' });\n\n// Trading (maker = the Safe, signed by the owner EOA, relayed by the builder):\nconst trading = await OpinionClient.create({\n  apiKey: '…',            // per-user key: user-scoped reads + cancels\n  builderApiKey: '…',     // mints keys, deploys the Safe, relays orders\n  signer: new LocalSigner('0x…'), // owner EOA\n  safeAddress: '0x…',     // the deposit Safe; auto-resolved via the builder if omitted\n});\n```\n\n| Option | Type | Default | Notes |\n|---|---|---|---|\n| `apiKey` | `string` | — | Per-user key. Public market data works with any key; user-scoped reads (`getPositions`/`getOpenOrders`/`getFills`/`getOrder`) and `cancelOrder` need the caller's own per-user key. |\n| `builderApiKey` | `string` | — | Builder key — mints per-user keys, deploys each user's Safe, and relays orders. Required for onboarding and `placeOrder`. |\n| `signer` | `Signer` | — | Owner-EOA signer (`new LocalSigner('0x…')` or any external backend). Signs orders (sigType 2 / Gnosis Safe) and the enable-trading Safe tx. |\n| `safeAddress` | `string` | resolved via builder `getUser` | The maker Safe (asset wallet) where funds live. |\n| `rpcUrl` | `string` | `https://bsc-dataseed.bnbchain.org` | BSC JSON-RPC for the on-chain balance, Safe nonce, and fee rates. |\n| `chainId` | `number` | `56` | EIP-712 chain id (BNB Chain). |\n\nOrders are signed by the **owner EOA** but the maker is the **Safe** (sigType 2);\nthe minimum order value is **1.30 USDT**. Onboarding is a separate, idempotent\nthree-step flow (Opinion-specific, not on the base contract): `createUser()`\n(deploys the Safe, returns the per-user key **once**), `enableTrading()` (a\none-time gasless Safe tx approving the exchange), and `getUser()` (state). See\n[`examples/opinionlabs/`](./examples/opinionlabs).\n\n## Method support matrix\n\nLegend: ✅ available · 🔜 planned (not yet implemented — see [Scope](#scope)) ·\n❌ not supported. Every method returns a normalized SDK type, identical in shape\nacross venues.\n\n**Market data**\n\n| Method | Description (returns) | Kalshi | Polymarket | predict.fun | Opinion Labs |\n|---|---|---|---|---|---|\n| `getMarkets(query?)` | Markets matching a query → [`Market[]`](#market) | ✅ | ✅ | ✅ | ✅ |\n| `getMarket(id)` | One market by id / conditionId → [`Market`](#market) | ✅ | ✅ | ✅ numeric id only | ✅ |\n| `getTrendingMarkets(query?)` | Most actively traded markets (24h volume) → [`Market[]`](#market) | ✅ | ✅ | ✅ | ✅ |\n| `searchMarkets(text, query?)` | Free-text market search → [`Market[]`](#market) | ❌ no API¹ | 🔜 | 🔜 | ❌ no API |\n| `getEvents(query?)` | Markets grouped into their event hierarchy → [`Event[]`](#event-and-eventquery) | ✅ | ✅ | ✅ categories | ✅ categorical |\n| `getTrendingEvents(query?)` | Most actively traded events (24h volume) → [`Event[]`](#event-and-eventquery) | ✅ | ✅ | ✅ | ✅ |\n| `getOrderbook(marketId, outcomeId?)` | One outcome's bids/asks → [`Orderbook`](#orderbook-and-orderbooklevel) | ✅ | ✅ | ✅ | ✅ |\n| `getTrades(marketId, opts?)` | Public trade tape (all participants) → [`Trade[]`](#trade-and-tradesquery) | ✅ | ✅ | ✅ | ❌ no tape³ |\n| `getPriceHistory(marketId, outcomeId?, opts)` | OHLC / price time-series → `PricePoint[]` | 🔜 | 🔜 | 🔜 | 🔜 |\n| `getResolution(marketId)` | One market's resolution state (status / outcome / rules) → [`Resolution`](#resolution-resolutionpolicy-and-forfeitpolicy) | ✅ | ✅ | ✅ | ✅ |\n| `resolutionPolicy()` | Venue-static edge-case policy (forfeits) → [`ResolutionPolicy`](#resolution-resolutionpolicy-and-forfeitpolicy) | ✅ | ✅ | ✅ `'unknown'`² | ✅ |\n\n¹ Kalshi exposes no free-text market-search endpoint, so `searchMarkets` cannot\nbe homogenized across venues yet and is intentionally unimplemented. Use\n[`getMarkets`](#getmarketsquery-promisemarket) / [`getEvents`](#geteventsquery-promiseevent)\nwith `category` to narrow results in the meantime.\n\n² predict.fun documents no forfeit/postponement policy, so its\n`resolutionPolicy().forfeit` is the honest `'unknown'` rather than a guess —\nsee [`ForfeitPolicy`](#resolution-resolutionpolicy-and-forfeitpolicy).\n\n³ Opinion Labs has no public trade tape, so `getTrades` throws a typed\n`NotSupportedError`. Use [`getFills`](#fill-fillrole-and-fillsquery) for your own\nexecutions; a live tape is deferred to a future streaming surface.\n\n**Account & positions**\n\n| Method | Description (returns) | Kalshi | Polymarket | predict.fun | Opinion Labs |\n|---|---|---|---|---|---|\n| `getBalance()` | Free, withdrawable cash → [`Balance`](#balance) | ✅ needs credentials | ✅ on-chain USDC, needs `walletAddress` | ✅ on-chain USDT, needs `walletAddress` | ✅ on-chain USDT on the Safe |\n| `getPortfolioValue()` | Total mark-to-market value → [`Money`](#money) | ✅ computed (cash + positions) | ✅ data-api value, needs `walletAddress` | ✅ computed (cash + venue marks) | ✅ computed (cash + positions) |\n| `getPositions()` | Open positions → [`Position[]`](#position) | ✅ needs credentials | ✅ needs `walletAddress` | ✅ needs `walletAddress` | ✅ needs per-user `apiKey` |\n| `getFills(query?)` | **Your own** executions (≠ public tape) → [`Fill[]`](#fill-fillrole-and-fillsquery) | ✅ needs credentials | ✅ needs a `signer` + CLOB creds | ✅ needs a `signer` | ✅ needs per-user `apiKey` |\n\n**Orders**\n\n| Method | Description (returns) | Kalshi | Polymarket | predict.fun | Opinion Labs |\n|---|---|---|---|---|---|\n| `getOpenOrders(marketId?)` | Resting (open / partially filled) orders → [`Order[]`](#order-and-orderstatus) | ✅ needs credentials | ✅ needs a `signer` + CLOB creds | ✅ needs a `signer` | ✅ needs per-user `apiKey` |\n| `getOrder(orderId)` | A single order, any status → [`Order`](#order-and-orderstatus) | ✅ needs credentials | ✅ needs a `signer` + CLOB creds | ✅ needs a `signer` | ✅ needs per-user `apiKey` |\n| `getOrderHistory(query?)` | Terminal (filled / cancelled) orders → [`Order[]`](#order-and-orderstatus) | ✅ needs credentials | ✅ needs a `signer` + CLOB creds | ✅ filled only⁴ | ✅ needs per-user `apiKey` |\n| `placeOrder(req)` | Place one order, **verified** result → [`OrderResult`](#placeorderrequest-orderresult-and-timeinforce) | ✅ needs credentials | ✅ needs a `signer` + CLOB creds | ✅ needs a `signer` | ✅ needs `builderApiKey` + `signer` + Safe |\n| `placeOrders(reqs)` | Place several orders (not atomic) → [`OrderResult[]`](#placeorderrequest-orderresult-and-timeinforce) | ✅ needs credentials | ✅ needs a `signer` + CLOB creds | ✅ needs a `signer` | ✅ needs `builderApiKey` + `signer` + Safe |\n| `modifyOrder(orderId, changes)` | Amend price/size (PM: cancel+replace, id may change) → `OrderResult` | 🔜 | 🔜 | 🔜 | 🔜 |\n| `cancelOrder(orderId)` | Cancel one order, verified → [`CancelResult`](#cancelresult) | ✅ needs credentials | ✅ needs a `signer` + CLOB creds | ✅ off-chain removal⁵ | ✅ needs per-user `apiKey` |\n| `cancelAllOrders(marketId?)` | Cancel all working orders (optionally per market) → [`CancelResult[]`](#cancelresult) | ✅ needs credentials | ✅ needs a `signer` + CLOB creds | ✅ off-chain removal⁵ | ✅ needs per-user `apiKey` |\n| `estimateFees(req)` | Maker + taker fee estimate for an order → [`FeeEstimate`](#feeestimate) | ✅ | ✅ | ✅ per-market `feeRateBps` | ✅ on-chain rate |\n\n⁴ predict.fun lists only OPEN and FILLED orders, so its `getOrderHistory` means\n**filled** orders; cancelled/expired ones stay readable individually via\n`getOrder('0x…hash')` but are not listable.\n\n⁵ predict.fun cancellation removes the order from the book off-chain; the\nsigned order remains technically valid on-chain until its expiration (a full\non-chain `cancelOrders` transaction is out of the SDK's scope).\n\n**Streaming** (returns a `Subscription` handle with `.close()`)\n\n| Method | Description (returns) | Kalshi | Polymarket | predict.fun | Opinion Labs |\n|---|---|---|---|---|---|\n| `subscribeOrderbook(marketId, outcomeId?, cb)` | Live orderbook updates → `Subscription` | 🔜 | 🔜 | 🔜 | 🔜 |\n| `subscribeTrades(marketId, cb)` | Live public trade feed → `Subscription` | 🔜 | 🔜 | 🔜 | 🔜 |\n| `subscribeTicker(marketId, cb)` | Live price / ticker updates → `Subscription` | 🔜 | 🔜 | 🔜 | 🔜 |\n| `subscribeOrders(cb)` | Live updates to **your** orders → `Subscription` | 🔜 | 🔜 | 🔜 | 🔜 |\n| `subscribeFills(cb)` | Live updates as **your** orders fill → `Subscription` | 🔜 | 🔜 | 🔜 | 🔜 |\n\n**Cross-venue** (on a separate `MultiVenueClient` facade, not the per-venue client)\n\n| Method | Description (returns) | Kalshi | Polymarket | predict.fun | Opinion Labs |\n|---|---|---|---|---|---|\n| `getBestPrice(refs)` | Best bid/ask across explicit venue refs → best-price result | 🔜 | 🔜 | 🔜 | 🔜 |\n\n`getBalance` means the **same thing on every venue** (free cash); `getPortfolioValue`\nmeans total mark-to-market value on all of them. Pick the one you want — no venue branching.\nThe 🔜 rows are on the roadmap and described in [Scope](#scope); the live contract\ntoday is the [methods](#the-client-contract) below.\n\n## The client contract\n\nEvery client exposes one read-only property, twenty async methods, and one\nsynchronous method (`resolutionPolicy`).\n\n```ts\nabstract class PredictionMarketClient {\n  readonly venue: Venue; // 'polymarket' | 'kalshi' | 'predictfun' | 'opinionlabs'\n\n  getMarkets(query?: MarketQuery): Promise<Market[]>;\n  getMarket(id: string): Promise<Market>;\n  getTrendingMarkets(query?: TrendingQuery): Promise<Market[]>; // activity-ranked discovery\n  getEvents(query?: EventQuery): Promise<Event[]>;\n  getTrendingEvents(query?: TrendingQuery): Promise<Event[]>;   // activity-ranked events\n\n  getOrderbook(marketId: string, outcomeId?: string): Promise<Orderbook>;\n  getTrades(marketId: string, opts?: TradesQuery): Promise<Trade[]>;\n  getResolution(marketId: string): Promise<Resolution>;\n  resolutionPolicy(): ResolutionPolicy;    // synchronous; venue-static\n  getBalance(): Promise<Balance>;          // free cash\n  getPortfolioValue(): Promise<Money>;     // total mark-to-market value\n  getPositions(): Promise<Position[]>;\n  getFills(query?: FillsQuery): Promise<Fill[]>;          // your own executions\n  getOpenOrders(marketId?: string): Promise<Order[]>;     // working orders\n  getOrder(orderId: string): Promise<Order>;\n  getOrderHistory(query?: OrderHistoryQuery): Promise<Order[]>; // terminal orders\n  placeOrder(req: PlaceOrderRequest): Promise<OrderResult>;\n  placeOrders(reqs: readonly PlaceOrderRequest[]): Promise<OrderResult[]>;\n  cancelOrder(orderId: string): Promise<CancelResult>;\n  cancelAllOrders(marketId?: string): Promise<CancelResult[]>;\n  estimateFees(req: PlaceOrderRequest): Promise<FeeEstimate>;\n}\n```\n\n---\n\n### `venue`\n\nA readonly string literal: `'polymarket'`, `'kalshi'`, `'predictfun'`, or\n`'opinionlabs'`.\nPresent on the client and stamped onto every normalized object it returns.\n\n---\n\n### `getMarkets(query?): Promise<Market[]>`\n\nReturns an array of [`Market`](#market). Empty array if none match.\n\n**`query` ([`MarketQuery`](#marketquery), all optional):**\n\n| Field | Type | Effect |\n|---|---|---|\n| `status` | `MarketStatus` | Filter by lifecycle state (see per-venue mapping below). |\n| `category` | `string` | Filtered **client-side** on the normalized `Market.category`. |\n| `limit` | `number` | Max markets to request. |\n| `cursor` | `string` | Pagination token. **Kalshi:** opaque cursor. **Polymarket:** numeric offset. |\n\n**Per-venue:**\n- **Kalshi** → `GET /markets`. `status` maps `open→open`, `closed→closed`, `resolved→settled`; `cancelled` is ignored (no filter sent).\n- **Polymarket** → Gamma `GET /markets`. `status` maps `open→active=true&closed=false`, `closed`/`resolved`→`closed=true`; `cancelled` sends no status filter. `cursor` is sent as `offset`.\n- **predict.fun** → `GET /v1/markets` (`first`/`after` cursor paging). The server filter only knows `OPEN`/`RESOLVED`, so `status` is sent when it maps and always re-applied client-side (making `closed`/`cancelled` exact too). Markets carry no venue category field, so a `category` filter matches nothing.\n\n---\n\n### `getMarket(id): Promise<Market>`\n\nReturns a single [`Market`](#market).\n\n- **Kalshi:** `id` is the market **ticker** (e.g. `KXTEMPNYC-…`).\n- **Polymarket:** `id` is the **Gamma numeric id** (e.g. `\"540817\"`) **or** a `0x…`\n  conditionId (resolved via Gamma's `condition_ids` filter). This lets a\n  `Position`/`Trade` round-trip straight back to its market.\n- **predict.fun:** `id` is the venue **numeric id** (e.g. `\"425\"`) only — the venue\n  exposes no conditionId lookup route, so a `0x…` id throws\n  [`ValidationError`](#errors) `code: 'UNSUPPORTED_ID'`. Join through\n  `Market.conditionId` and key follow-up calls by `Market.id`.\n\nThe returned `Market` carries both `id` and [`conditionId`](#market) — see the\n[venue cheat-sheet](#venue-cheat-sheet) on joining positions/trades to markets.\n\nThrows [`NotFoundError`](#errors) if the venue returns 404.\n\n---\n\n### `getTrendingMarkets(query?): Promise<Market[]>`\n\nReturns [`Market`](#market)s ranked by **recent trading activity** (24h volume\non both venues), restricted to tradeable ones — open, nonzero volume, and at\nleast one outcome marked strictly inside `(0, 1)`. This is the method to build a\n\"browse markets\" view on: plain `getMarkets({ status: 'open' })` is\ncreation-ordered and (on Kalshi especially) dominated by zero-volume\nauto-generated markets. A best-effort discovery surface — each venue's own\nactivity ranking — not a precise volume metric.\n\n**`query` ([`TrendingQuery`](#trendingquery), all optional):** `limit` (default\n25) and `category` (filtered client-side on `Market.category`, like\n`getMarkets`).\n\n**Per-venue:**\n- **Kalshi** → the documented `/markets` endpoint has no working sort, so the\n  ranking comes from Kalshi's own (undocumented) frontend search API —\n  `GET /v1/search/series?order_by=trending` (a validated enum; verified live\n  2026-06-12). It is used **only to pick candidate tickers**; the markets are\n  then batch-fetched via the documented `GET /markets?tickers=…`, ranked by\n  `volume_24h_fp`, filtered, and cut to `limit`. Being a frontend API it could\n  change without notice; a failure surfaces as a normal [`VenueError`](#errors).\n- **Polymarket** → Gamma `GET /markets?order=volume24hr&ascending=false&active=true&closed=false`\n  (native server-side ranking), over-fetched 2× then filtered.\n- **predict.fun** → `GET /v1/markets?sort=VOLUME_24H_DESC&status=OPEN` (native\n  server-side ranking), over-fetched 2× then filtered. Markets expose no volume\n  field, so the tradeable filter keeps open markets with a live `(0, 1)` mark\n  and trusts the venue's volume ordering.\n\n---\n\n### `getEvents(query?): Promise<Event[]>`\n\nReturns an array of [`Event`](#event-and-eventquery) — the venue's discovery\nhierarchy folded into one homogeneous shape. Each `Event` embeds its fully\nnormalized constituent [`Market`](#market)s (no follow-up calls needed), and the\nparent grouping (where one exists above the event level) rides on `seriesKey`.\n\n**`query` ([`EventQuery`](#event-and-eventquery), all optional):** `status`,\n`category` (filtered client-side, like `getMarkets`), `limit`, `cursor`, and\n`seriesKey`. `limit` **defaults to 50** on both venues when omitted — each event\nembeds its full market list, so the call is a single bounded page rather than an\nunbounded fetch. Pass a larger `limit` (or page via `cursor`) for more.\n\n- **Kalshi** → `GET /events?with_nested_markets=true` (one page). `seriesKey` maps\n  to the `series_ticker` filter (the efficient targeted pull) and `Event.seriesKey`\n  is the series ticker (e.g. `KXNBAGAME`).\n- **Polymarket** → Gamma `GET /events` with `status`→`active`/`closed` filters.\n  `Event.seriesKey` is the series slug when the event belongs to one; otherwise\n  absent. `seriesKey` in the query is ignored (Gamma has no series filter here).\n- **predict.fun** → `GET /v1/categories` — a *category* is the venue's event\n  grouping (one election, one game) with its markets nested in full. `Event.id`\n  is the category slug, `Event.category` the venue tag (e.g. `Politics`),\n  `seriesKey` the `parentSlug` when present (query `seriesKey` is ignored).\n\n---\n\n### `getTrendingEvents(query?): Promise<Event[]>`\n\nReturns [`Event`](#event-and-eventquery)s ranked by **recent trading activity**\n(24h volume on both venues), restricted to events with **at least one tradeable\nmarket** (open, traded, marked strictly inside `(0, 1)`). The event-level analog\nof [`getTrendingMarkets`](#gettrendingmarketsquery-promisemarket).\n\n**`query` ([`TrendingQuery`](#trendingquery), all optional):** `limit` (default\n25) and `category` (filtered client-side on `Event.category`).\n\n**Per-venue:**\n- **Kalshi** → candidate event tickers come from the same frontend\n  `GET /v1/search/series?order_by=trending`; each is then fetched through the\n  documented `GET /events/{ticker}?with_nested_markets=true` (**one request per\n  event** — Kalshi has no batch event endpoint, so the fan-out is capped) and\n  ranked by its summed 24h market volume. Same frontend-API caveat as\n  `getTrendingMarkets`.\n- **Polymarket** → Gamma `GET /events?order=volume24hr&ascending=false&active=true&closed=false`\n  (native server-side ranking), over-fetched 2× then filtered.\n- **predict.fun** → `GET /v1/categories?sort=VOLUME_24H_DESC&status=OPEN`\n  (native server-side ranking), over-fetched 2× then filtered on live-mark\n  markets (see `getTrendingMarkets`).\n\n---\n\n### `getOrderbook(marketId, outcomeId?): Promise<Orderbook>`\n\nReturns an [`Orderbook`](#orderbook) for one outcome. `bids` are sorted\ndescending by price, `asks` ascending; prices are implied probabilities in\n`[0, 1]`.\n\n**`outcomeId`:**\n- **Kalshi:** `'YES'` or `'NO'` (case-insensitive); **defaults to `'YES'`**. Any other value throws [`ValidationError`](#errors). The requested side's resting orders become `bids`; the opposite side is converted to `asks` via the `1 − price` complement.\n- **Polymarket:** the **CLOB token id** (this is `Outcome.id`). If omitted, the client fetches the market and uses `outcomes[0].id` (one extra request).\n- **predict.fun:** the **ERC-1155 token id** (`Outcome.id`). The venue serves one book per market, quoted on its primary (`indexSet === 1`) outcome — the default when `outcomeId` is omitted; requesting the complement outcome returns the mirrored `1 − price` book (like Kalshi's NO side).\n\n**`Orderbook.timestamp`:**\n- **Kalshi:** the client's clock at fetch time (the HTTP orderbook carries no timestamp).\n- **Polymarket:** the venue's book timestamp when present, else the client's clock.\n- **predict.fun:** the venue's `updateTimestampMs` when present, else the client's clock.\n\n---\n\n### `getTrades(marketId, opts?): Promise<Trade[]>`\n\nReturns an array of [`Trade`](#trade) — the public trade tape for the market\n(all participants, not just you). `Trade.price` is in `[0, 1]`.\n\n**`opts` ([`TradesQuery`](#tradesquery), all optional):**\n\n| Field | Type | Kalshi | Polymarket | predict.fun |\n|---|---|---|---|---|\n| `limit` | `number` | sent as `limit` | sent as `limit` | sent as `first` |\n| `since` | `string` (ISO 8601) | converted to `min_ts` (unix seconds) | **ignored** | filtered client-side |\n| `cursor` | `string` | sent as `cursor` | sent as `offset` | sent as `after` |\n\n**Per-venue:**\n`Trade.side` means the **taker's direction relative to `outcomeId`**: `'buy'` = the\ntaker acquired that outcome, `'sell'` = disposed of it.\n\n- **Kalshi** → `GET /markets/trades?ticker=…`. `side` is always `'buy'` because Kalshi\n  models selling YES as buying NO — the tape only ever shows a taker *acquiring* the\n  outcome named by `outcomeId` (`'YES'`/`'NO'`). `marketId` and `conditionId` are both the ticker.\n- **Polymarket** → data-api `GET /trades?market=<conditionId>`. If `marketId` starts with `0x` it is used as the conditionId directly; otherwise the client resolves it via `getMarket` (one extra request). `side` is the real `'buy'`/`'sell'`; `id` is the transaction hash; `marketId` and `conditionId` are both the conditionId, `outcomeId` the token id.\n- **predict.fun** → `GET /v1/orders/matches?marketId=…` (settled order-match events, newest first). The trade is the **taker's** slice of each match: `side` from the taker quote (`Bid`→buy, `Ask`→sell), `id` the settlement id, prices/sizes normalized from the venue's 1e18 wei strings.\n\n---\n\n### `getResolution(marketId): Promise<Resolution>`\n\nReturns one market's [`Resolution`](#resolution-resolutionpolicy-and-forfeitpolicy)\nstate: its `status`, the winning `resolvedOutcomeId` (once settled), `resolveTime`,\nand `rules`/`source` where the venue exposes them. This is **per-market** data —\nfor the venue-static edge-case policy use [`resolutionPolicy()`](#resolutionpolicy-resolutionpolicy).\n\n- **Kalshi** → read from `GET /markets/{ticker}`. `resolvedOutcomeId` is `'YES'`/`'NO'`\n  from the settled `result`; `rules` from `rules_primary`; `source` from\n  `settlement_sources`. `marketId` accepts the ticker.\n- **Polymarket** → read from the Gamma market (`marketId` accepts a Gamma id **or**\n  a `0x…` conditionId). Once resolved, `resolvedOutcomeId` is the token id whose\n  `outcomePrices` entry is ~1; `rules` is the market description; `source` is\n  `resolutionSource`.\n- **predict.fun** → read from `GET /v1/markets/{id}` (numeric id).\n  `resolvedOutcomeId` is the winning outcome's token id (the venue marks\n  outcomes `WON`/`LOST`); `rules` is the market description.\n\n### `resolutionPolicy(): ResolutionPolicy`\n\n**Synchronous** (venue-static, no I/O). Returns the venue's\n[`ResolutionPolicy`](#resolution-resolutionpolicy-and-forfeitpolicy) for edge\ncases the two venues resolve **differently** — most importantly esports/tournament\nforfeits. Kalshi pays out the official recorded result (`forfeit: 'tournament_result'`);\nPolymarket voids 50/50 (`forfeit: 'void_50_50'`); predict.fun documents no policy,\nso it reports the honest `forfeit: 'unknown'` — treat its edge cases as unpriced\nrisk and read the per-market `Resolution.rules`. Surfacing this prevents the class\nof cross-venue divergence that silently turns a naive arb into a loss (HANDOFF §4.4).\n\n---\n\n### `getBalance(): Promise<Balance>`\n\nReturns a [`Balance`](#balance) — **free, withdrawable cash** on both venues.\n`available` and `total` are both [`Money`](#money) and equal in v1.\n\n- **Kalshi:** the account cash balance (`GET /portfolio/balance`). Requires credentials.\n- **Polymarket:** free cash, read **on-chain** via `eth_call balanceOf` against the\n  Polymarket collateral token `pUSD` (`0xc011a7e1…`) on Polygon (uses `rpcUrl`).\n  Polymarket migrated off bridged USDC.e, which now reads 0. Requires\n  `walletAddress` (the proxy wallet that\n  custodies the collateral), else throws [`ValidationError`](#errors) with\n  `code: 'NO_WALLET'`. A failed RPC call throws a [`VenueError`](#errors) with\n  `code: 'RPC_ERROR'`.\n- **predict.fun:** free cash, read **on-chain** via `eth_call balanceOf` against\n  BNB-chain USDT (**18 decimals**, scaled to canonical micro-dollars; testnet\n  uses the venue's mock USDT). Requires `walletAddress` (defaults to the\n  signer's address); same `NO_WALLET`/`RPC_ERROR` errors as Polymarket.\n\n---\n\n### `getPortfolioValue(): Promise<Money>`\n\nReturns total **mark-to-market portfolio value** (cash + open positions) on both\nvenues, as normalized USD [`Money`](#money) (read `.value` for dollars).\n\n- **Kalshi:** computed as cash + Σ(position size × the outcome's current price). Kalshi\n  exposes no single portfolio figure, so this is a best-effort estimate that issues one\n  market fetch per open position. Requires credentials.\n- **Polymarket:** on-chain `pUSD` cash + data-api `GET /value` (the venue's positions\n  mark-to-market, which excludes cash). Requires `walletAddress`.\n- **predict.fun:** on-chain USDT cash + Σ of the venue's own per-position\n  `valueUsd` marks. Requires `walletAddress` (defaults to the signer's address).\n\n---\n\n### `getPositions(): Promise<Position[]>`\n\nReturns an array of [`Position`](#position). Zero-size positions are omitted.\nPaginated internally.\n\n- **Kalshi:** requires credentials. `marketId` and `conditionId` are both the ticker; `outcomeId` is `'YES'`/`'NO'`; `realizedPnl` is USD when present; `currentPrice` and `unrealizedPnl` are **not** set.\n- **Polymarket:** requires `walletAddress` (else `ValidationError` `code: 'NO_WALLET'`). `marketId` and `conditionId` are both the **conditionId**; `outcomeId` is the token id; `avgEntryPrice` and `currentPrice` are in `[0, 1]`; `realizedPnl` and `unrealizedPnl` are USD [`Money`](#money).\n- **predict.fun:** requires `walletAddress` (an API-key-only read — no signer needed). `GET /v1/positions/{address}`, cursor-paged. `marketId` is the numeric id, `conditionId` rides from the embedded market, `outcomeId` is the token id; sizes normalize from 1e18 wei strings; `currentPrice` is the outcome's best bid; `unrealizedPnl` from the venue's `pnlUsd`.\n\n---\n\n### `getFills(query?): Promise<Fill[]>`\n\nReturns an array of [`Fill`](#fill-fillrole-and-fillsquery) — **your own** executions,\neach linked to its order with your `role` (maker/taker). This is **not** the\npublic `getTrades` tape. `query` (all optional): `marketId`, `orderId`, `since`\n(ISO 8601, exclusive), `limit`, `cursor`.\n\n- **Kalshi:** requires credentials. `GET /portfolio/fills`, cursor-paged. Kalshi does not report a per-fill `fee` on this payload, so it is omitted (use [`estimateFees`](#estimatefeesreq-promisefeeestimate)).\n- **Polymarket:** requires a `signer` + CLOB credentials (else `ValidationError` `code: 'NO_SIGNER'`). `GET /data/trades` (L2-authed); `fee` is derived from `fee_rate_bps` (zero today).\n- **predict.fun:** requires a `signer` (else `ValidationError` `code: 'NO_SIGNER'`). `GET /v1/orders/matches?signerAddress=…` — your `role`/`side`/`orderId` come from whether you were the match's taker or one of its makers; a share-denominated fee (`type: 'SHARES'`) is valued at the execution price into USD [`Money`](#money).\n\n---\n\n### `getOpenOrders(marketId?): Promise<Order[]>`\n\nReturns an array of [`Order`](#order-and-orderstatus) — working (open / partially\nfilled) orders. The complement of [`getOrderHistory`](#getorderhistoryquery-promiseorder).\n\n- **Kalshi:** requires credentials. `GET /portfolio/orders?status=resting`, optionally filtered to `marketId` (ticker). Paginated internally.\n- **Polymarket:** requires a `signer` + CLOB credentials (else `ValidationError` `code: 'NO_SIGNER'`). `GET /data/orders` (L2-authed), filtered to working orders; `marketId` (Gamma id or `0x…` conditionId) scopes by market.\n- **predict.fun:** requires a `signer`. `GET /v1/orders?status=OPEN` (JWT-authed), cursor-paged; `marketId` (numeric id) filters client-side. `Order.id` is the venue's numeric order id (the `0x…` order hash rides on `raw`).\n\n---\n\n### `getOrder(orderId): Promise<Order>`\n\nReturns one [`Order`](#order-and-orderstatus) by id, in any state.\n\n- **Kalshi:** requires credentials. `GET /portfolio/orders/{id}`.\n- **Polymarket:** requires a `signer` + CLOB credentials. `GET /data/order/{id}`.\n- **predict.fun:** requires a `signer`. A `0x…` order **hash** reads `GET /v1/orders/{hash}` directly (works in any state); a numeric id is looked up by walking the OPEN and FILLED lists — cancelled/expired orders are only reachable by hash.\n\n---\n\n### `getOrderHistory(query?): Promise<Order[]>`\n\nReturns terminal (filled / cancelled) [`Order`](#order-and-orderstatus)s — the\ncomplement of [`getOpenOrders`](#getopenordersmarketid-promiseorder). `query`\n(all optional): `marketId`, `since`, `limit`, `cursor`.\n\n- **Kalshi:** requires credentials. Walks `GET /portfolio/orders` and keeps the non-working orders.\n- **Polymarket:** requires a `signer` + CLOB credentials. Reads `GET /data/orders` and keeps the terminal ones.\n- **predict.fun:** requires a `signer`. Walks `GET /v1/orders?status=FILLED` — the venue lists only OPEN and FILLED, so history here means **filled** orders (cancelled/expired ones stay readable individually via `getOrder('0x…hash')`).\n\n---\n\n### `placeOrder(req): Promise<OrderResult>`\n\nPlaces a single order from a normalized [`PlaceOrderRequest`](#placeorderrequest)\nand returns a **verified** [`OrderResult`](#orderresult) — `status`/`filledSize`\nalready resolve each venue's misleading immediate response, so you can trust\nthem without a follow-up read.\n\n```ts\nconst result = await client.placeOrder({\n  marketId: '…',      // Kalshi ticker, or PM Gamma id / 0x… conditionId\n  outcomeId: 'YES',   // Kalshi: 'YES'/'NO'; Polymarket: the CLOB token id\n  side: 'buy',\n  price: 0.62,        // implied probability in [0, 1]\n  size: 10,           // contracts (Kalshi) / shares (Polymarket)\n  tif: 'ioc',         // default: 'ioc' Kalshi, 'fok' Polymarket\n});\n```\n\n- **Kalshi:** requires credentials. Uses the **V2** endpoint `POST\n  /portfolio/events/orders` (the legacy `POST /portfolio/orders` is deprecated and\n  returns `410`). V2 quotes a single YES-leg book: `side` is `bid` (buy YES) /\n  `ask` (sell YES), so a `NO` order is restated as its YES complement — buy NO @ p\n  becomes an ask at `1 − p`. Price is a fixed-point dollar string in `[0,1]`,\n  `count` a fixed-point quantity string. `tif` maps `ioc`→`immediate_or_cancel`,\n  `gtc`→`good_till_canceled`, `fok`→`fill_or_kill` (V2 supports fill-or-kill;\n  `self_trade_prevention_type` defaults to `taker_at_cross`). The V2 response is\n  flat with `fill_count`/`remaining_count` (no `status` field); when both are zero\n  the client re-reads via `GET /portfolio/orders/{id}` to confirm the outcome\n  (guarding the legacy `fill_count=\"0\"` quirk) so `filledSize`/`status` are\n  correct.\n- **Polymarket:** requires a `signer` (else `ValidationError` `code:\n  'NO_SIGNER'`); the CLOB L2 credentials are derived from the signer and cached\n  automatically on first use, so passing `clobApiKey`/`clobSecret`/`clobPassphrase`\n  is optional. The order is EIP-712-signed locally (secp256k1), submitted with L2 HMAC auth\n  (`tif` `fok`→`FOK`, `ioc`→`FAK`, `gtc`→`GTC`), then polled until terminal\n  (Polymarket's immediate status is usually `live`/`delayed`). A CLOB rejection\n  throws `VenueError` `code: 'ORDER_REJECTED'`. **A fillable order also needs\n  on-chain USDC/CTF allowances set** (operator prerequisite). Use\n  `PolymarketClient.buildSignedOrder(req)` to build + sign **without** submitting\n  (a dry-run of the signing path).\n- **predict.fun:** requires a `signer` (else `ValidationError` `code:\n  'NO_SIGNER'`); the Bearer JWT is bootstrapped and cached automatically. The\n  market is fetched fresh (a non-`OPEN` `tradingStatus` throws\n  `MARKET_NOT_OPEN`), the EIP-712 **V1** order is signed against the exchange\n  contract selected by the market's `(isNegRisk, isYieldBearing)` flags, and\n  submitted as `strategy: 'LIMIT'` (`tif` `fok` → + `isFillOrKill`, the\n  default; `gtc` rests; `ioc` throws `UNSUPPORTED_TIF` — the venue's\n  MARKET-order semantics are unverified). Every order signs a **30-day\n  expiration** (the venue rejects `expiration: 0` — minimum 2 minutes out)\n  and must have a **value of at least $0.90** (venue minimum, enforced at\n  submit). The submit response carries no status/fill, so the client\n  **verifies by re-reading the order** before returning. Balance is **not**\n  checked at submit: an unfunded maker's order is accepted and then\n  auto-cancelled by the venue moments later — **a lasting, fillable order\n  needs USDT in the wallet and an on-chain allowance for the exchange**\n  (operator prerequisite; the four exchange addresses are exported as\n  `PREDICTFUN_EXCHANGES`/`PREDICTFUN_TESTNET_EXCHANGES`).\n\n---\n\n### `placeOrders(reqs): Promise<OrderResult[]>`\n\nPlaces several orders, returning one **verified** [`OrderResult`](#placeorderrequest-orderresult-and-timeinforce)\nper request **in input order**. Not atomic — neither venue offers an\nall-or-nothing batch, so a later failure does not roll back earlier placements.\nInspect each result. Same credentials and per-venue behavior as\n[`placeOrder`](#placeorderreq-promiseorderresult).\n\n---\n\n### `cancelOrder(orderId): Promise<CancelResult>`\n\nCancels one order and returns a **verified** [`CancelResult`](#cancelresult) —\n`cancelled` reflects the venue's settled view, the same trust contract as\n`OrderResult`.\n\n- **Kalshi:** requires credentials. `DELETE /portfolio/orders/{id}`; the response carries the post-cancel order state.\n- **Polymarket:** requires a `signer` + CLOB credentials. `DELETE /order` with the order id; `cancelled` is set when the id appears in the venue's `canceled` list.\n- **predict.fun:** requires a `signer`. `POST /v1/orders/remove` — an **off-chain book removal**: matching stops immediately, but the signed order stays technically valid on-chain until its expiration (full invalidation needs an on-chain `cancelOrders` transaction, out of the SDK's scope). A `noop` response (already filled/removed) is resolved by re-reading the order for its true `status`.\n\n---\n\n### `cancelAllOrders(marketId?): Promise<CancelResult[]>`\n\nCancels every working order, optionally scoped to one `marketId`. Reads the open\norders first, then cancels each — one [`CancelResult`](#cancelresult) per order.\nSame credentials and caveats as `cancelOrder` on every venue (predict.fun\nbatches ids through `POST /v1/orders/remove`, ≤100 per call).\n\n---\n\n### `estimateFees(req): Promise<FeeEstimate>`\n\nReturns a [`FeeEstimate`](#feeestimate) with **both** the maker and taker fee for\na [`PlaceOrderRequest`](#placeorderrequest-orderresult-and-timeinforce)-shaped\norder — the role is unknowable until execution, so pick the field for the role\nyou expect. Computed from a pure formula today (no network call), but async to\nleave room for venues that publish a fee schedule.\n\n- **Kalshi:** `rate × P(1−P) × contracts`, rounded **up** to the cent — taker `0.07`, maker `0.0175` (HANDOFF §3.9). The price-shaped fee is materially more expensive at mid-prices than near the extremes.\n- **Polymarket:** zero for both roles today (HANDOFF §2.8); gas is paid by the relayer. The hook stays in case that changes.\n- **predict.fun:** per-market — one market fetch reads its `feeRateBps` (2% on testnet), applied as `rate × min(p, 1−p) × size` (the CTF-exchange formula, symmetric around 0.5). **Takers only**: makers pay zero (verified on the live match tape of both networks), so `makerFee` is always `0`.\n\n---\n\n## Types\n\nAll types are exported from the package root. Every `price`/`probability`/\n`avgEntryPrice` is an implied probability in `[0, 1]`. Timestamps are ISO 8601\nstrings.\n\n### `Venue`, `Side`, `Currency`\n\n```ts\ntype Venue = 'polymarket' | 'kalshi' | 'predictfun' | 'opinionlabs';\ntype Side = 'buy' | 'sell';\ntype Currency = 'USD'; // all money is normalized to USD across venues\n```\n\n### `Money`\n\nAll monetary values are normalized to **USD** and homogeneous across venues.\nRead `value` (dollars) for display/comparison; `amount` is the same figure as a\nlossless integer in micro-dollars (`decimals` is always `6`).\n\n```ts\ninterface Money {\n  amount: bigint;     // exact integer micro-dollars (decimals = 6)\n  currency: Currency; // always 'USD'\n  decimals: number;   // always 6\n  value: number;      // the amount in dollars — use this\n}\n```\n\nExample: `$20.00` → `{ amount: 20_000_000n, currency: 'USD', decimals: 6, value: 20 }`.\nKalshi cents and Polymarket USDC are both converted into this shape, so\n`a.value` and `b.value` (or `a.amount` and `b.amount`) are directly comparable.\n\n### `Market`\n\n```ts\ninterface Market {\n  venue: Venue;\n  id: string;\n  conditionId?: string;       // settlement/join key — Kalshi: == id (ticker) · Polymarket: 0x… conditionId\n  slug?: string;\n  title: string;              // distinct per-market label (Kalshi folds yes_sub_title in — see table)\n  description?: string;\n  status: MarketStatus;       // 'open' | 'closed' | 'resolved' | 'cancelled'\n  outcomes: Outcome[];\n  closeTime?: string;         // ISO 8601 — expected trading end\n  resolveTime?: string;       // ISO 8601 — settlement time, present only once resolved\n  volume?: Money;\n  category?: string;\n  url?: string;\n  raw?: unknown;              // the original venue payload\n}\n```\n\nField population by venue:\n\n| Field | Kalshi | Polymarket | predict.fun |\n|---|---|---|---|\n| `id` | ticker | Gamma numeric id | venue numeric id |\n| `conditionId` | ticker (== `id`) | `0x…` conditionId (≠ `id`) | `0x…` conditionId (≠ `id`) |\n| `slug` | — | set when present | the category slug |\n| `title` | `title` + `yes_sub_title` folded in (e.g. `England vs Ghana Winner? — England`), so multi-outcome sub-markets are distinct; bare `title`/ticker when no sub-title | `question` or id | `question` (distinct per market; the venue's short `title` like `<$2,500` rides on `raw`) |\n| `description` | `rules_primary` (the resolution prose), else `subtitle` | set when present | set when present (the resolution prose) |\n| `outcomes` | exactly 2 (`YES`/`NO`) | parsed from `outcomes`/`outcomePrices`/`clobTokenIds` | 2, ids = ERC-1155 token ids, names verbatim (`Yes`/`No`, `Up`/`Down`, …) |\n| `closeTime` | `expected_expiration_time` or `close_time` | `endDate` | — (no market-level close time; the category has `endsAt`) |\n| `resolveTime` | `settled_time` (once resolved) | `closedTime` (once resolved) | — (venue exposes none) |\n| `volume` | `volume_fp` as USD `Money` (the figure Kalshi's frontend shows in dollars) | `volumeNum` as USD `Money` | from `stats` when populated (null on testnet) |\n| `category` | when present | when present (often absent) | — (categories are the event grouping, not a topic) |\n| `url` | — (no verified Kalshi web-URL format) | `https://polymarket.com/market/<slug>` (best-effort) | — (no verified URL format) |\n\n### `Outcome`\n\n```ts\ninterface Outcome {\n  id: string;          // Kalshi: 'YES'|'NO'  ·  Polymarket: CLOB token id\n  name: string;        // homogeneous binary label: 'Yes' / 'No' on both venues\n                       //   (Kalshi selections like 'England' ride on raw, and are\n                       //    folded into Market.title — not into the outcome name)\n  probability: number; // mark-price implied probability in [0, 1]\n  raw?: unknown;\n}\n```\n\n### `MarketStatus`\n\n```ts\ntype MarketStatus = 'open' | 'closed' | 'resolved' | 'cancelled';\n```\n\n- **Kalshi:** `active`/`initialized → open`, `closed → closed`, `settled → resolved` (unknown → `open`).\n- **Polymarket:** `closed → resolved`, `archived → closed`, `active → open` (else `closed`). `cancelled` is not emitted by either venue.\n\n### `MarketQuery`\n\n```ts\ninterface MarketQuery {\n  status?: MarketStatus;\n  category?: string;\n  limit?: number;\n  cursor?: string;\n}\n```\n\n### `TrendingQuery`\n\n```ts\ninterface TrendingQuery {\n  limit?: number;    // max markets to return; defaults to 25\n  category?: string; // filtered client-side on Market.category, like MarketQuery\n}\n```\n\nThe query for [`getTrendingMarkets`](#gettrendingmarketsquery-promisemarket).\nNo `status`/`cursor`: trending is always open markets, and it is a bounded\n\"what's hot\" page, not an enumeration.\n\n### `Event` and `EventQuery`\n\n```ts\ninterface Event {\n  venue: Venue;\n  id: string;\n  title: string;\n  slug?: string;\n  category?: string;\n  markets: Market[];   // fully normalized, embedded\n  seriesKey?: string;  // parent grouping (Kalshi series ticker · Polymarket series slug)\n  raw?: unknown;\n}\n\ninterface EventQuery {\n  status?: MarketStatus;\n  category?: string;\n  limit?: number;      // max events to return; defaults to 50 on both venues when omitted\n  cursor?: string;\n  seriesKey?: string;  // restrict to one series (Kalshi only; ignored elsewhere)\n}\n```\n\nThe hierarchy is homogenized to one level: an `Event` groups `markets`, and a\nshared `seriesKey` is the only thing that links events into a series — there is\nno separate `getSeries`.\n\n| Field | Kalshi | Polymarket | predict.fun |\n|---|---|---|---|\n| `id` | event ticker | Gamma event id | category slug |\n| `seriesKey` | series ticker (e.g. `KXNBAGAME`) | series slug when present, else — | `parentSlug` when present, else — |\n| `markets` | nested via `with_nested_markets=true` | nested in the Gamma event | nested in the category |\n\n### `Resolution`, `ResolutionPolicy`, and `ForfeitPolicy`\n\n```ts\ninterface Resolution {           // per-market — getResolution(marketId)\n  venue: Venue;\n  marketId: string;\n  conditionId?: string;\n  status: MarketStatus;\n  resolvedOutcomeId?: string;    // the winning Outcome.id, once settled\n  resolveTime?: string;          // ISO 8601, once settled\n  rules?: string;                // resolution criteria, when exposed\n  source?: string;               // settlement source / oracle, when exposed\n  raw?: unknown;\n}\n\ninterface ResolutionPolicy {     // venue-static — resolutionPolicy()\n  venue: Venue;\n  forfeit: ForfeitPolicy;        // how a tournament forfeit resolves\n  postponement?: string;         // free-form; undefined until verified\n  notes?: string;\n}\n\ntype ForfeitPolicy = 'void_50_50' | 'tournament_result' | 'unknown';\n```\n\n`Resolution` is per-market state; `ResolutionPolicy` is a venue constant that\ndoes **not** vary per market (hence `resolutionPolicy()` is synchronous). The\n`forfeit` value is pinned by a real cross-venue divergence (HANDOFF §4.4):\nKalshi `tournament_result` (pays the recorded result) vs Polymarket `void_50_50`\n(refunds); predict.fun documents no policy, so it reports `'unknown'` — an\nexplicit \"we refuse to guess\", meaning its forfeits carry risk you must assess\nfrom the per-market `rules`. `postponement` is left `undefined` rather than\nguessed — it ships only once verified against a real settlement.\n\n### `Orderbook` and `OrderbookLevel`\n\n```ts\ninterface OrderbookLevel {\n  price: number; // implied probability in [0, 1]\n  size: number;  // resting size in contract units\n}\n\ninterface Orderbook {\n  venue: Venue;\n  marketId: string;\n  outcomeId: string;\n  bids: OrderbookLevel[]; // sorted descending by price\n  asks: OrderbookLevel[]; // sorted ascending by price\n  timestamp: string;      // ISO 8601\n}\n```\n\n### `Trade` and `TradesQuery`\n\n```ts\ninterface Trade {\n  venue: Venue;\n  id: string;          // Kalshi: trade id  ·  Polymarket: transaction hash\n  marketId: string;    // Kalshi: ticker  ·  Polymarket: conditionId\n  conditionId: string; // settlement/join key (== Market.conditionId)\n  outcomeId: string;\n  side: Side;          // taker's direction on outcomeId — Kalshi: always 'buy'\n  price: number;       // [0, 1]\n  size: number;\n  timestamp: string;   // ISO 8601\n}\n\ninterface TradesQuery {\n  limit?: number;\n  since?: string;  // ISO 8601 lower bound — Kalshi only (ignored by Polymarket)\n  cursor?: string;\n}\n```\n\n### `Balance`\n\n```ts\ninterface Balance {\n  venue: Venue;\n  available: Money; // free cash on both venues (USD)\n  total: Money;     // equals `available` for both venues in v1\n}\n```\n\n### `Position`\n\n```ts\ninterface Position {\n  venue: Venue;\n  marketId: string;     // Kalshi: ticker  ·  Polymarket: conditionId\n  conditionId: string;  // settlement/join key (== Market.conditionId)\n  outcomeId: string;\n  size: number;\n  avgEntryPrice: number;   // [0, 1]\n  currentPrice?: number;   // [0, 1] — Polymarket only\n  realizedPnl?: Money;     // USD (both venues when present)\n  unrealizedPnl?: Money;   // Polymarket only (USD)\n}\n```\n\n### `Order` and `OrderStatus`\n\n```ts\ntype OrderStatus = 'open' | 'partially_filled' | 'filled' | 'cancelled';\n\ninterface Order {\n  venue: Venue;\n  id: string;\n  marketId: string;\n  conditionId: string;  // settlement/join key (== Market.conditionId)\n  outcomeId: string;\n  side: Side;\n  price: number;   // limit price, [0, 1]\n  size: number;    // filled + remaining\n  filled: number;\n  status: OrderStatus;\n  createdAt: string; // ISO 8601\n}\n```\n\nBoth venues return `Order`s — from `getOpenOrders` (working), `getOrder` (any\nstate), and `getOrderHistory` (terminal). On Polymarket these read the\nauthenticated CLOB, so they need a `signer` + CLOB credentials.\n\n### `Fill`, `FillRole`, and `FillsQuery`\n\nThe normalized outputs of [`getFills`](#getfillsquery-promisefill) — **your own**\nexecutions, distinct from the public `getTrades` tape.\n\n```ts\ntype FillRole = 'maker' | 'taker';\n\ninterface Fill {\n  venue: Venue;\n  id: string;           // venue fill id (Kalshi trade_id · Polymarket trade id)\n  orderId: string;      // the order this execution belongs to\n  marketId: string;     // Kalshi: ticker · Polymarket: conditionId\n  conditionId: string;  // settlement/join key (== Market.conditionId)\n  outcomeId: string;\n  side: Side;           // your direction on outcomeId\n  price: number;        // [0, 1]\n  size: number;         // contracts (Kalshi) / shares (Polymarket)\n  role: FillRole;\n  fee?: Money;          // present when the venue reports a per-fill fee\n  timestamp: string;    // ISO 8601\n}\n\ninterface FillsQuery {\n  limit?: number;\n  since?: string;       // ISO 8601 lower bound (exclusive)\n  cursor?: string;\n  marketId?: string;    // Kalshi ticker / Polymarket conditionId\n  orderId?: string;\n}\n```\n\n`OrderHistoryQuery` (for [`getOrderHistory`](#getorderhistoryquery-promiseorder))\nhas `limit`, `since`, `cursor`, and `marketId`.\n\n### `PlaceOrderRequest`, `OrderResult`, and `TimeInForce`\n\nThe normalized inputs/outputs of [`placeOrder`](#placeorderreq-promiseorderresult).\n\n```ts\ntype TimeInForce = 'ioc' | 'fok' | 'gtc';\n\ninterface PlaceOrderRequest {\n  marketId: string;      // Kalshi ticker · Polymarket Gamma id / 0x… conditionId\n  outcomeId: string;     // Kalshi: 'YES'/'NO' · Polymarket: CLOB token id\n  side: Side;            // 'buy' | 'sell'\n  price: number;         // implied probability in [0, 1]\n  size: number;          // contracts (Kalshi) / shares (Polymarket)\n  tif?: TimeInForce;     // default: 'ioc' Kalshi, 'fok' Polymarket\n  clientOrderId?: string;\n}\n\ninterface OrderResult {\n  venue: Venue;\n  id: string;\n  status: OrderStatus;   // verified — venue's misleading immediate state resolved\n  filledSize: number;    // contracts/shares actually filled\n  raw: unknown;          // final venue payload (escape hatch)\n}\n```\n\n### `CancelResult`\n\nThe normalized output of [`cancelOrder`](#cancelorderorderid-promisecancelresult)\nand [`cancelAllOrders`](#cancelallordersmarketid-promisecancelresult). `cancelled`\nis **verified** — the same trust contract as `OrderResult`.\n\n```ts\ninterface CancelResult {\n  venue: Venue;\n  orderId: string;\n  cancelled: boolean;    // true once the order is no longer working on the book\n  status: OrderStatus;   // resulting order state\n  raw: unknown;          // venue payload (escape hatch)\n}\n```\n\n### `FeeEstimate`\n\nThe normalized output of [`estimateFees`](#estimatefeesreq-promisefeeestimate).\nBoth roles are returned because the role is unknowable until execution.\n\n```ts\ninterface FeeEstimate {\n  venue: Venue;\n  takerFee: Money;       // if you cross the spread (take liquidity)\n  makerFee: Money;       // if you rest and are filled by another taker\n  raw?: unknown;         // formula inputs / venue payload\n}\n```\n\n## Errors\n\nEvery error thrown by the SDK is an instance of `PredictionMarketError`\n(subclass of `Error`). All carry `venue` and an optional `code`.\n\n```ts\nclass PredictionMarketError extends Error {\n  venue: Venue;\n  code: string | undefined;\n}\n```\n\n| Class | When | Extra fields |\n|---|---|---|\n| `AuthError` | 401 / 403, or missing/invalid credentials | — |\n| `RateLimitError` | 429 | `retryAfterMs?: number` |\n| `NotFoundError` | 404 | — |\n| `NetworkError` | transport failure or timeout (`code: 'TIMEOUT'` / `'NETWORK'`) | — |\n| `ValidationError` | bad input or 4xx client error; also `code: 'NO_WALLET'` (PM portfolio), `'NO_SIGNER'` (PM order placement / authenticated order reads without key+CLOB creds), `'INVALID_PRICE'`/`'INVALID_SIZE'`/`'UNSUPPORTED_TIF'` (bad order) | — |\n| `VenueError` | unrecognized ≥500 response; also `code: 'ORDER_REJECTED'` (PM CLOB rejection), `'RPC_ERROR'` (PM on-chain balance) | `status?: number`, `body?: unknown` |\n| `PredictionMarketError` | base class of all of the above | — |\n\n```ts\nimport { NotFoundError, RateLimitError } from 'prediction-market-sdk';\n\ntry {\n  await kalshi.getMarket('does-not-exist');\n} catch (err) {\n  if (err instanceof NotFoundError) { /* … */ }\n  if (err instanceof RateLimitError) { await wait(err.retryAfterMs ?? 1000); }\n}\n```\n\nThe shared HTTP layer retries idempotent requests up to 3 attempts on\n`429/500/502/503/504`, honoring `Retry-After`, with a 30s default timeout.\n\n## Venue cheat-sheet\n\n| | Kalshi | Polymarket | predict.fun |\n|---|---|---|---|\n| `Market.id` | ticker | Gamma numeric id | venue numeric id |\n| `conditionId` (everywhere) | ticker (== `id`) | `0x…` conditionId (≠ `id`) | `0x…` conditionId (≠ `id`) |\n| `Outcome.id` | `'YES'` / `'NO'` | CLOB token id | ERC-1155 token id |\n| Orderbook `outcomeId` | `'YES'` / `'NO'` (default `YES`) | CLOB token id | token id (defaults to the primary outcome) |\n| `Position`/`Trade` `marketId` | ticker | conditionId (≠ `Market.id`) | venue numeric id (== `Market.id`) |\n| `getBalance` | cash | on-chain `pUSD` cash | on-chain USDT cash (18-dp) |\n| `getPortfolioValue` | cash + positions (computed) | data-api value | cash + venue marks (computed) |\n| Money | USD `Money` (`.value` = dollars) | USD `Money` (`.value` = dollars) | USD `Money` (USDT ≈ USD) |\n| Auth for market data | none | none | mainnet API key (testnet: none) |\n| Auth for portfolio | API key + PKCS#8 key | wallet address only | wallet address (+ API key on mainnet) |\n| `placeOrder` | API key + PKCS#8 key | secp256k1 key + CLOB L2 creds | secp256k1 key (JWT auto-derived) |\n\n> **Joining positions/trades to markets:** every `Market`, `Position`, `Trade`,\n> and `Order` carries a `conditionId` — the universal join key. Match\n> `position.conditionId === market.conditionId` on any venue (no branching).\n> On Polymarket the `conditionId` can also be passed straight into `getMarket`,\n> `getOrderbook`, and `getTrades`; on Kalshi every id is just the ticker; on\n> predict.fun key those calls by the numeric `Market.id` (a raw `polymarketConditionIds`\n> field even links its mirrored markets to Polymarket's for cross-venue joins).\n> `outcomeId` (token id / `YES`·`NO`) is consistent everywhere.\n\n## Scope\n\n**Implemented today:** the full **market-data** surface — `getMarkets`,\n`getMarket`, `getTrendingMarkets`, `getEvents`, `getTrendingEvents`,\n`getOrderbook`, `getTrades`, `getResolution`, and\n`resolutionPolicy` — plus account/positions reads and the full **order\nlifecycle** on Polymarket, Kalshi, and predict.fun: `placeOrder`/`placeOrders`,\n`getOpenOrders`/`getOrder`/`getOrderHistory`/`getFills`,\n`cancelOrder`/`cancelAllOrders`, and `estimateFees`. These are the\n[methods](#the-client-contract) on the client contract. (The authenticated\norder-read/cancel wire shapes — Kalshi `/portfolio/fills` and the Polymarket\nCLOB `/data/orders`·`/data/order/{id}`·`/data/trades`·`DELETE /order` — ship\n`LIVE=1`-gated until verified against a real response.)\n\n**Not implemented in the market-data surface (and why):**\n\n- **`searchMarkets`** — Kalshi exposes no free-text market-search endpoint, so\n  the method cannot be made homogeneous across venues without a lossy\n  client-side fallback. Deliberately left unimplemented rather than ship an\n  uneven contract; use `getMarkets`/`getEvents` with `category` to narrow.\n- **`getPriceHistory`** — deferred. Kalshi candlesticks expose OHLC+volume while\n  Polymarket price-history is single `{time, price}` points; a homogeneous\n  `PricePoint` shape is still being settled.\n\n**Planned (🔜 in the matrix), roughly in order:**\n\n1. **`modifyOrder(orderId, changes)`** — amend price/size. Polymarket has no\n   native amend, so it is cancel+replace and the returned order id may change;\n   the uniform semantics with that caveat are pinned in the doc comment.\n2. **`getPriceHistory`** — deferred (see above; `PricePoint` shape unsettled).\n3. **Streaming** — `subscribeOrderbook` / `subscribeTrades` / `subscribeTicker`\n   (market) and `subscribeOrders` / `subscribeFills` (your account).\n4. **Cross-venue** — `getBestPrice` on a separate `MultiVenueClient` facade.\n\n**Not on the roadmap (yet):** more venues (Manifold, PredictIt) and order\ncancellation/streaming in CI by default.\n\nPolymarket `placeOrder` does not yet set on-chain USDC/CTF allowances — a real\norder only fills if the operator has set them (planned to be auto-handled\ninternally by `placeOrder`, not exposed as a method). The Polymarket order wire\ndetails (EIP-712 domain/struct, L2 HMAC preimage, poll shape) are gated behind\n`LIVE=1` until confirmed. The same applies to predict.fun: a fillable order\nneeds a USDT allowance on the market's exchange contract, and the order-submit\nwire details (expiration `0`, `pricePerShare` units, JWT expiry shape, maker\nfees) stay `LIVE=1`-gated until the key-gated live blocks confirm them.\n\n## Development\n\n```sh\npnpm install\npnpm build           # tsup → dist/ (ESM + CJS + .d.ts)\npnpm test            # vitest unit tests\npnpm typecheck       # tsc --noEmit\npnpm lint            # eslint .\npnpm example         # build + run examples/quickstart.ts against live APIs\n\n# Live smoke tests (opt-in). See .env.example for credentials.\nLIVE=1 pnpm test:integration\n```\n\nSee [`examples/`](exa","readmeFilename":"README.md","_rev":"1-d24d6abf812c24a3318481a61d82ff26"}