npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@demo-npm-test/prediction-market-sdk

v0.1.0

Published

Unified TypeScript SDK for prediction markets (Polymarket, Kalshi, predict.fun, Opinion Labs).

Readme

prediction-market-sdk

A TypeScript SDK that exposes a single, normalized interface over multiple prediction-market venues — Polymarket, Kalshi, predict.fun, and Opinion Labs. Write your code once against one contract; never branch on venue.

  • Read-only (v1): market data, orderbooks, trades, balances, positions, open orders.
  • Isomorphic: Node 20+ and modern browsers. Native fetch + Web Crypto only.
  • Zero runtime dependencies.
  • 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).

Status: pre-release (0.0.0). Trading writes and WebSocket streaming are not implemented — see Scope.

Install

npm install prediction-market-sdk
# or: pnpm add prediction-market-sdk

Quick start

import { PolymarketClient, KalshiClient, PredictFunClient } from 'prediction-market-sdk';

// Polymarket market data needs no credentials.
const poly = await PolymarketClient.create();
const markets = await poly.getMarkets({ status: 'open', limit: 5 });
for (const m of markets) {
  const odds = m.outcomes.map((o) => `${o.name} ${(o.probability * 100).toFixed(1)}%`);
  console.log(m.title, '→', odds.join(' / '));
}

// Kalshi market data also needs no credentials.
const kalshi = await KalshiClient.create();
const book = await kalshi.getOrderbook('SOME-TICKER'); // defaults to the YES side
console.log(book.bids[0], book.asks[0]);

// predict.fun mainnet needs an API key — but its testnet is fully keyless.
const predict = await PredictFunClient.create({ testnet: true });
console.log(await predict.getTrendingMarkets({ limit: 5 }));

All clients implement the same abstract base, so you can program against the contract instead of the concrete venue:

import type { PredictionMarketClient } from 'prediction-market-sdk';

async function topMarket(client: PredictionMarketClient) {
  const [first] = await client.getMarkets({ status: 'open', limit: 1 });
  return first; // a normalized Market, identical shape across venues
}

Construction

Every client is created with an async factory (create), never new.

KalshiClient.create(options?)

const kalshi = await KalshiClient.create({
  apiKeyId: process.env.KALSHI_API_KEY_ID,        // required for portfolio methods
  privateKeyPem: process.env.KALSHI_PRIVATE_KEY,  // PKCS#8 PEM ("-----BEGIN PRIVATE KEY-----")
});

| Option | Type | Default | Notes | |---|---|---|---| | apiKeyId | string | — | Kalshi API key id. Required (with privateKeyPem) for getBalance/getPositions/getOpenOrders. | | privateKeyPem | string | — | PKCS#8 PEM. A PKCS#1 key (-----BEGIN RSA PRIVATE KEY-----) is rejected with a conversion hint. | | auth | AuthStrategy | — | Pre-built strategy; overrides the two fields above. Advanced/testing. | | baseUrl | string | https://api.elections.kalshi.com/trade-api/v2 | Set to https://demo-api.kalshi.co/trade-api/v2 for demo. | | timeoutMs | number | 30000 | Per-request timeout. | | fetch | typeof fetch | global fetch | Inject for tests. | | sleep | (ms) => Promise<void> | setTimeout | Inject for tests. | | now | () => number | Date.now | Clock for signing + orderbook timestamps. |

Market-data methods (getMarkets, getMarket, getOrderbook, getTrades) work without credentials. Portfolio methods require apiKeyId + privateKeyPem.

PolymarketClient.create(options?)

const poly = await PolymarketClient.create({
  walletAddress: '0x…', // required for getPositions / getBalance / getPortfolioValue
});

| Option | Type | Default | Notes | |---|---|---|---| | walletAddress | string | — | A public proxy-wallet address. Required for getPositions/getBalance/getPortfolioValue. | | gammaBaseUrl | string | https://gamma-api.polymarket.com | Market-data API. | | clobBaseUrl | string | https://clob.polymarket.com | Orderbook API. | | dataBaseUrl | string | https://data-api.polymarket.com | Trades/positions/value API. | | rpcUrl | string | https://polygon-bor-rpc.publicnode.com | Polygon JSON-RPC, used by getBalance for the on-chain USDC cash balance. | | timeoutMs | number | 30000 | Per-request timeout. | | fetch | typeof fetch | global fetch | Inject for tests. | | sleep | (ms) => Promise<void> | setTimeout | Inject for tests. | | now | () => number | Date.now | Clock for orderbook timestamps. | | 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. | | 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). | | 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. | | 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()). | | exchangeAddress | string | CTF Exchange V2 (CTF_EXCHANGE) | EIP-712 verifying contract; pass the exported NEG_RISK_CTF_EXCHANGE for neg-risk markets. | | chainId | number | 137 | EIP-712 chain id (Polygon). |

Read endpoints are unauthenticated; walletAddress is an address, not a credential — it identifies whose positions/value to read. The trading options above are only needed for placeOrder.

Credentials are automatic — just create a client and trade

The clobApiKey/clobSecret/clobPassphrase are L2 credentials derived from your signer (you don't get them from a dashboard). You don't have to manage them: a client built with only a signer derives them itself on its first authenticated call and caches them, so the whole flow is one step:

import { PolymarketClient, LocalSigner } from 'prediction-market-sdk';

const client = await PolymarketClient.create({
  signer: new LocalSigner('0x…'),
  signatureType: 1, // optional; defaults to 3 (deposit wallet)
});

// Creds are derived + cached under the hood — this just works:
await client.placeOrder({ marketId, outcomeId, side: 'buy', price: 0.5, size: 10, tif: 'gtc' });

deriveApiCreds() (Polymarket only)

Call this only if you want to inspect or persist the creds — e.g. to store them and pass them back into create next time, skipping the one-time derivation round trip:

const poly = await PolymarketClient.create({ signer: new LocalSigner('0x…') });
const creds = await poly.deriveApiCreds(); // → { apiKey, secret, passphrase }

const trading = await PolymarketClient.create({
  signer: new LocalSigner('0x…'),
  signatureType: 1,
  clobApiKey: creds.apiKey,
  clobSecret: creds.secret,
  clobPassphrase: creds.passphrase,
});

The call is idempotent — the same key always maps to the same credentials, so re-running it is safe. Under the hood it is L1-signed (EIP-712 ClobAuth) and create-or-derive: POST /auth/api-key (create) first, falling back to GET /auth/derive-api-key only if create returns no key. This is why a brand-new wallet works — it has no key to derive yet, so the create step runs first. Returns PolymarketApiCreds. Requires only a signer. This is a Polymarket-only helper (not on the base PredictionMarketClient). The playground (pnpm playground) exposes it under its "Setup" group with a one-click "Save to Polymarket credentials" button.

Wallet topology (signatureType): for signatureType: 1 (the common Email/Magic wallet), funderAddress and walletAddress are the same address — the Magic smart-wallet (proxy) — and both differ from the signer (the EOA the signer controls). For signatureType: 0 (EOA) the signer is the funder, so funderAddress can be omitted. For signatureType: 2 (Gnosis-Safe) and signatureType: 3 (deposit wallet), the funder defaults to the EOA's deterministic Safe / deposit-wallet address, so it can be omitted too when the EOA owns that wallet.

signatureType: 3 (deposit wallet / POLY_1271) is LIVE-unverified. The account model Polymarket mandates for new API users. The SDK builds it end-to-end — the deposit-wallet CREATE2 address (verified on-chain) and the Solady ERC-7739 TypedDataSign order envelope, whose signing path is verified byte-for-byte against @polymarket/clob-client-v2 1.0.8. The owner EOA (via LocalSigner or an external Signer) produces the raw ECDSA; maker == signer == depositWallet, while the CLOB credentials bind to the owner EOA (plain ClobAuth, POLY_ADDRESS = EOA) — same as 1.0.8. Whether the live CLOB accepts an end-to-end submit is not yet confirmed against a real response, so treat as experimental until the POLY_SIGTYPE3_SUBMIT=1 LIVE check passes.

PredictFunClient.create(options?)

import { PredictFunClient, LocalSigner } from 'prediction-market-sdk';

const predict = await PredictFunClient.create({
  apiKey: process.env.PREDICTFUN_API_KEY,        // mainnet reads need one
  walletAddress: '0x…',                          // portfolio reads
  signer: new LocalSigner(process.env.PREDICTFUN_PRIVATE_KEY!), // trading
});

// Email/social sign-in account (Predict account / smart wallet): pass the
// deposit address from predict.fun → Account → Settings plus the exported
// Privy wallet key. Orders + auth then act AS the account — its balances and
// allowances apply, so no manual approvals are needed.
const account = await PredictFunClient.create({
  apiKey: process.env.PREDICTFUN_API_KEY,
  predictAccount: '0x…deposit address…',
  signer: new LocalSigner(process.env.PREDICTFUN_PRIVATE_KEY!), // Privy key
});

// Or fully keyless against the BNB testnet:
const test = await PredictFunClient.create({ testnet: true });

| Option | Type | Default | Notes | |---|---|---|---| | apiKey | string | — | Mainnet x-api-key (issued via predict.fun's Discord). Required for every mainnet endpoint; the testnet needs none. | | testnet | boolean | false | Target api-testnet.predict.fun (BNB testnet, chain 97, keyless) — flips the base URL, RPC, collateral, and exchange contracts. | | walletAddress | string | signer's address | Wallet for getBalance/getPositions/getPortfolioValue (read-only, keyed by address). | | 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. | | 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. | | baseUrl | string | per network | REST base override. | | rpcUrl | string | per network | BNB Chain JSON-RPC for the on-chain USDT balance. | | collateralAddress | string | per network | Collateral token getBalance reads (18-decimal USDT). | | timeoutMs / fetch / sleep / now | — | — | Same plumbing as the other venues. |

predict.fun is a BNB Chain fork of Polymarket's V1 CTF-exchange CLOB. Wallet auth is a Bearer JWT the client bootstraps automatically (fetch login message → EIP-191 personal_sign → POST /v1/auth), cached and re-derived once on a 401.

OpinionClient.create(options?)

Opinion Labs (opinion.trade) is an on-chain CLOB on BNB Chain (chainId 56), collateralized in USDT. Reads need only an apiKey; trading uses a two-key model plus an owner-EOA signer on an onboarded Gnosis Safe.

import { OpinionClient, LocalSigner } from 'prediction-market-sdk';

// Read-only:
const opinion = await OpinionClient.create({ apiKey: '…' });

// Trading (maker = the Safe, signed by the owner EOA, relayed by the builder):
const trading = await OpinionClient.create({
  apiKey: '…',            // per-user key: user-scoped reads + cancels
  builderApiKey: '…',     // mints keys, deploys the Safe, relays orders
  signer: new LocalSigner('0x…'), // owner EOA
  safeAddress: '0x…',     // the deposit Safe; auto-resolved via the builder if omitted
});

| Option | Type | Default | Notes | |---|---|---|---| | 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. | | builderApiKey | string | — | Builder key — mints per-user keys, deploys each user's Safe, and relays orders. Required for onboarding and placeOrder. | | signer | Signer | — | Owner-EOA signer (new LocalSigner('0x…') or any external backend). Signs orders (sigType 2 / Gnosis Safe) and the enable-trading Safe tx. | | safeAddress | string | resolved via builder getUser | The maker Safe (asset wallet) where funds live. | | rpcUrl | string | https://bsc-dataseed.bnbchain.org | BSC JSON-RPC for the on-chain balance, Safe nonce, and fee rates. | | chainId | number | 56 | EIP-712 chain id (BNB Chain). |

Orders are signed by the owner EOA but the maker is the Safe (sigType 2); the minimum order value is 1.30 USDT. Onboarding is a separate, idempotent three-step flow (Opinion-specific, not on the base contract): createUser() (deploys the Safe, returns the per-user key once), enableTrading() (a one-time gasless Safe tx approving the exchange), and getUser() (state). See examples/opinionlabs/.

Method support matrix

Legend: ✅ available · 🔜 planned (not yet implemented — see Scope) · ❌ not supported. Every method returns a normalized SDK type, identical in shape across venues.

Market data

| Method | Description (returns) | Kalshi | Polymarket | predict.fun | Opinion Labs | |---|---|---|---|---|---| | getMarkets(query?) | Markets matching a query → Market[] | ✅ | ✅ | ✅ | ✅ | | getMarket(id) | One market by id / conditionId → Market | ✅ | ✅ | ✅ numeric id only | ✅ | | getTrendingMarkets(query?) | Most actively traded markets (24h volume) → Market[] | ✅ | ✅ | ✅ | ✅ | | searchMarkets(text, query?) | Free-text market search → Market[] | ❌ no API¹ | 🔜 | 🔜 | ❌ no API | | getEvents(query?) | Markets grouped into their event hierarchy → Event[] | ✅ | ✅ | ✅ categories | ✅ categorical | | getTrendingEvents(query?) | Most actively traded events (24h volume) → Event[] | ✅ | ✅ | ✅ | ✅ | | getOrderbook(marketId, outcomeId?) | One outcome's bids/asks → Orderbook | ✅ | ✅ | ✅ | ✅ | | getTrades(marketId, opts?) | Public trade tape (all participants) → Trade[] | ✅ | ✅ | ✅ | ❌ no tape³ | | getPriceHistory(marketId, outcomeId?, opts) | OHLC / price time-series → PricePoint[] | 🔜 | 🔜 | 🔜 | 🔜 | | getResolution(marketId) | One market's resolution state (status / outcome / rules) → Resolution | ✅ | ✅ | ✅ | ✅ | | resolutionPolicy() | Venue-static edge-case policy (forfeits) → ResolutionPolicy | ✅ | ✅ | ✅ 'unknown'² | ✅ |

¹ Kalshi exposes no free-text market-search endpoint, so searchMarkets cannot be homogenized across venues yet and is intentionally unimplemented. Use getMarkets / getEvents with category to narrow results in the meantime.

² predict.fun documents no forfeit/postponement policy, so its resolutionPolicy().forfeit is the honest 'unknown' rather than a guess — see ForfeitPolicy.

³ Opinion Labs has no public trade tape, so getTrades throws a typed NotSupportedError. Use getFills for your own executions; a live tape is deferred to a future streaming surface.

Account & positions

| Method | Description (returns) | Kalshi | Polymarket | predict.fun | Opinion Labs | |---|---|---|---|---|---| | getBalance() | Free, withdrawable cash → Balance | ✅ needs credentials | ✅ on-chain USDC, needs walletAddress | ✅ on-chain USDT, needs walletAddress | ✅ on-chain USDT on the Safe | | getPortfolioValue() | Total mark-to-market value → Money | ✅ computed (cash + positions) | ✅ data-api value, needs walletAddress | ✅ computed (cash + venue marks) | ✅ computed (cash + positions) | | getPositions() | Open positions → Position[] | ✅ needs credentials | ✅ needs walletAddress | ✅ needs walletAddress | ✅ needs per-user apiKey | | getFills(query?) | Your own executions (≠ public tape) → Fill[] | ✅ needs credentials | ✅ needs a signer + CLOB creds | ✅ needs a signer | ✅ needs per-user apiKey |

Orders

| Method | Description (returns) | Kalshi | Polymarket | predict.fun | Opinion Labs | |---|---|---|---|---|---| | getOpenOrders(marketId?) | Resting (open / partially filled) orders → Order[] | ✅ needs credentials | ✅ needs a signer + CLOB creds | ✅ needs a signer | ✅ needs per-user apiKey | | getOrder(orderId) | A single order, any status → Order | ✅ needs credentials | ✅ needs a signer + CLOB creds | ✅ needs a signer | ✅ needs per-user apiKey | | getOrderHistory(query?) | Terminal (filled / cancelled) orders → Order[] | ✅ needs credentials | ✅ needs a signer + CLOB creds | ✅ filled only⁴ | ✅ needs per-user apiKey | | placeOrder(req) | Place one order, verified result → OrderResult | ✅ needs credentials | ✅ needs a signer + CLOB creds | ✅ needs a signer | ✅ needs builderApiKey + signer + Safe | | placeOrders(reqs) | Place several orders (not atomic) → OrderResult[] | ✅ needs credentials | ✅ needs a signer + CLOB creds | ✅ needs a signer | ✅ needs builderApiKey + signer + Safe | | modifyOrder(orderId, changes) | Amend price/size (PM: cancel+replace, id may change) → OrderResult | 🔜 | 🔜 | 🔜 | 🔜 | | cancelOrder(orderId) | Cancel one order, verified → CancelResult | ✅ needs credentials | ✅ needs a signer + CLOB creds | ✅ off-chain removal⁵ | ✅ needs per-user apiKey | | cancelAllOrders(marketId?) | Cancel all working orders (optionally per market) → CancelResult[] | ✅ needs credentials | ✅ needs a signer + CLOB creds | ✅ off-chain removal⁵ | ✅ needs per-user apiKey | | estimateFees(req) | Maker + taker fee estimate for an order → FeeEstimate | ✅ | ✅ | ✅ per-market feeRateBps | ✅ on-chain rate |

⁴ predict.fun lists only OPEN and FILLED orders, so its getOrderHistory means filled orders; cancelled/expired ones stay readable individually via getOrder('0x…hash') but are not listable.

⁵ predict.fun cancellation removes the order from the book off-chain; the signed order remains technically valid on-chain until its expiration (a full on-chain cancelOrders transaction is out of the SDK's scope).

Streaming (returns a Subscription handle with .close())

| Method | Description (returns) | Kalshi | Polymarket | predict.fun | Opinion Labs | |---|---|---|---|---|---| | subscribeOrderbook(marketId, outcomeId?, cb) | Live orderbook updates → Subscription | 🔜 | 🔜 | 🔜 | 🔜 | | subscribeTrades(marketId, cb) | Live public trade feed → Subscription | 🔜 | 🔜 | 🔜 | 🔜 | | subscribeTicker(marketId, cb) | Live price / ticker updates → Subscription | 🔜 | 🔜 | 🔜 | 🔜 | | subscribeOrders(cb) | Live updates to your orders → Subscription | 🔜 | 🔜 | 🔜 | 🔜 | | subscribeFills(cb) | Live updates as your orders fill → Subscription | 🔜 | 🔜 | 🔜 | 🔜 |

Cross-venue (on a separate MultiVenueClient facade, not the per-venue client)

| Method | Description (returns) | Kalshi | Polymarket | predict.fun | Opinion Labs | |---|---|---|---|---|---| | getBestPrice(refs) | Best bid/ask across explicit venue refs → best-price result | 🔜 | 🔜 | 🔜 | 🔜 |

getBalance means the same thing on every venue (free cash); getPortfolioValue means total mark-to-market value on all of them. Pick the one you want — no venue branching. The 🔜 rows are on the roadmap and described in Scope; the live contract today is the methods below.

The client contract

Every client exposes one read-only property, twenty async methods, and one synchronous method (resolutionPolicy).

abstract class PredictionMarketClient {
  readonly venue: Venue; // 'polymarket' | 'kalshi' | 'predictfun' | 'opinionlabs'

  getMarkets(query?: MarketQuery): Promise<Market[]>;
  getMarket(id: string): Promise<Market>;
  getTrendingMarkets(query?: TrendingQuery): Promise<Market[]>; // activity-ranked discovery
  getEvents(query?: EventQuery): Promise<Event[]>;
  getTrendingEvents(query?: TrendingQuery): Promise<Event[]>;   // activity-ranked events

  getOrderbook(marketId: string, outcomeId?: string): Promise<Orderbook>;
  getTrades(marketId: string, opts?: TradesQuery): Promise<Trade[]>;
  getResolution(marketId: string): Promise<Resolution>;
  resolutionPolicy(): ResolutionPolicy;    // synchronous; venue-static
  getBalance(): Promise<Balance>;          // free cash
  getPortfolioValue(): Promise<Money>;     // total mark-to-market value
  getPositions(): Promise<Position[]>;
  getFills(query?: FillsQuery): Promise<Fill[]>;          // your own executions
  getOpenOrders(marketId?: string): Promise<Order[]>;     // working orders
  getOrder(orderId: string): Promise<Order>;
  getOrderHistory(query?: OrderHistoryQuery): Promise<Order[]>; // terminal orders
  placeOrder(req: PlaceOrderRequest): Promise<OrderResult>;
  placeOrders(reqs: readonly PlaceOrderRequest[]): Promise<OrderResult[]>;
  cancelOrder(orderId: string): Promise<CancelResult>;
  cancelAllOrders(marketId?: string): Promise<CancelResult[]>;
  estimateFees(req: PlaceOrderRequest): Promise<FeeEstimate>;
}

venue

A readonly string literal: 'polymarket', 'kalshi', 'predictfun', or 'opinionlabs'. Present on the client and stamped onto every normalized object it returns.


getMarkets(query?): Promise<Market[]>

Returns an array of Market. Empty array if none match.

query (MarketQuery, all optional):

| Field | Type | Effect | |---|---|---| | status | MarketStatus | Filter by lifecycle state (see per-venue mapping below). | | category | string | Filtered client-side on the normalized Market.category. | | limit | number | Max markets to request. | | cursor | string | Pagination token. Kalshi: opaque cursor. Polymarket: numeric offset. |

Per-venue:

  • KalshiGET /markets. status maps open→open, closed→closed, resolved→settled; cancelled is ignored (no filter sent).
  • Polymarket → Gamma GET /markets. status maps open→active=true&closed=false, closed/resolvedclosed=true; cancelled sends no status filter. cursor is sent as offset.
  • predict.funGET /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.

getMarket(id): Promise<Market>

Returns a single Market.

  • Kalshi: id is the market ticker (e.g. KXTEMPNYC-…).
  • Polymarket: id is the Gamma numeric id (e.g. "540817") or a 0x… conditionId (resolved via Gamma's condition_ids filter). This lets a Position/Trade round-trip straight back to its market.
  • predict.fun: id is the venue numeric id (e.g. "425") only — the venue exposes no conditionId lookup route, so a 0x… id throws ValidationError code: 'UNSUPPORTED_ID'. Join through Market.conditionId and key follow-up calls by Market.id.

The returned Market carries both id and conditionId — see the venue cheat-sheet on joining positions/trades to markets.

Throws NotFoundError if the venue returns 404.


getTrendingMarkets(query?): Promise<Market[]>

Returns Markets ranked by recent trading activity (24h volume on both venues), restricted to tradeable ones — open, nonzero volume, and at least one outcome marked strictly inside (0, 1). This is the method to build a "browse markets" view on: plain getMarkets({ status: 'open' }) is creation-ordered and (on Kalshi especially) dominated by zero-volume auto-generated markets. A best-effort discovery surface — each venue's own activity ranking — not a precise volume metric.

query (TrendingQuery, all optional): limit (default 25) and category (filtered client-side on Market.category, like getMarkets).

Per-venue:

  • Kalshi → the documented /markets endpoint has no working sort, so the ranking comes from Kalshi's own (undocumented) frontend search API — GET /v1/search/series?order_by=trending (a validated enum; verified live 2026-06-12). It is used only to pick candidate tickers; the markets are then batch-fetched via the documented GET /markets?tickers=…, ranked by volume_24h_fp, filtered, and cut to limit. Being a frontend API it could change without notice; a failure surfaces as a normal VenueError.
  • Polymarket → Gamma GET /markets?order=volume24hr&ascending=false&active=true&closed=false (native server-side ranking), over-fetched 2× then filtered.
  • predict.funGET /v1/markets?sort=VOLUME_24H_DESC&status=OPEN (native server-side ranking), over-fetched 2× then filtered. Markets expose no volume field, so the tradeable filter keeps open markets with a live (0, 1) mark and trusts the venue's volume ordering.

getEvents(query?): Promise<Event[]>

Returns an array of Event — the venue's discovery hierarchy folded into one homogeneous shape. Each Event embeds its fully normalized constituent Markets (no follow-up calls needed), and the parent grouping (where one exists above the event level) rides on seriesKey.

query (EventQuery, all optional): status, category (filtered client-side, like getMarkets), limit, cursor, and seriesKey. limit defaults to 50 on both venues when omitted — each event embeds its full market list, so the call is a single bounded page rather than an unbounded fetch. Pass a larger limit (or page via cursor) for more.

  • KalshiGET /events?with_nested_markets=true (one page). seriesKey maps to the series_ticker filter (the efficient targeted pull) and Event.seriesKey is the series ticker (e.g. KXNBAGAME).
  • Polymarket → Gamma GET /events with statusactive/closed filters. Event.seriesKey is the series slug when the event belongs to one; otherwise absent. seriesKey in the query is ignored (Gamma has no series filter here).
  • predict.funGET /v1/categories — a category is the venue's event grouping (one election, one game) with its markets nested in full. Event.id is the category slug, Event.category the venue tag (e.g. Politics), seriesKey the parentSlug when present (query seriesKey is ignored).

getTrendingEvents(query?): Promise<Event[]>

Returns Events ranked by recent trading activity (24h volume on both venues), restricted to events with at least one tradeable market (open, traded, marked strictly inside (0, 1)). The event-level analog of getTrendingMarkets.

query (TrendingQuery, all optional): limit (default 25) and category (filtered client-side on Event.category).

Per-venue:

  • Kalshi → candidate event tickers come from the same frontend GET /v1/search/series?order_by=trending; each is then fetched through the documented GET /events/{ticker}?with_nested_markets=true (one request per event — Kalshi has no batch event endpoint, so the fan-out is capped) and ranked by its summed 24h market volume. Same frontend-API caveat as getTrendingMarkets.
  • Polymarket → Gamma GET /events?order=volume24hr&ascending=false&active=true&closed=false (native server-side ranking), over-fetched 2× then filtered.
  • predict.funGET /v1/categories?sort=VOLUME_24H_DESC&status=OPEN (native server-side ranking), over-fetched 2× then filtered on live-mark markets (see getTrendingMarkets).

getOrderbook(marketId, outcomeId?): Promise<Orderbook>

Returns an Orderbook for one outcome. bids are sorted descending by price, asks ascending; prices are implied probabilities in [0, 1].

outcomeId:

  • Kalshi: 'YES' or 'NO' (case-insensitive); defaults to 'YES'. Any other value throws ValidationError. The requested side's resting orders become bids; the opposite side is converted to asks via the 1 − price complement.
  • Polymarket: the CLOB token id (this is Outcome.id). If omitted, the client fetches the market and uses outcomes[0].id (one extra request).
  • 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).

Orderbook.timestamp:

  • Kalshi: the client's clock at fetch time (the HTTP orderbook carries no timestamp).
  • Polymarket: the venue's book timestamp when present, else the client's clock.
  • predict.fun: the venue's updateTimestampMs when present, else the client's clock.

getTrades(marketId, opts?): Promise<Trade[]>

Returns an array of Trade — the public trade tape for the market (all participants, not just you). Trade.price is in [0, 1].

opts (TradesQuery, all optional):

| Field | Type | Kalshi | Polymarket | predict.fun | |---|---|---|---|---| | limit | number | sent as limit | sent as limit | sent as first | | since | string (ISO 8601) | converted to min_ts (unix seconds) | ignored | filtered client-side | | cursor | string | sent as cursor | sent as offset | sent as after |

Per-venue: Trade.side means the taker's direction relative to outcomeId: 'buy' = the taker acquired that outcome, 'sell' = disposed of it.

  • KalshiGET /markets/trades?ticker=…. side is always 'buy' because Kalshi models selling YES as buying NO — the tape only ever shows a taker acquiring the outcome named by outcomeId ('YES'/'NO'). marketId and conditionId are both the ticker.
  • 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.
  • predict.funGET /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.

getResolution(marketId): Promise<Resolution>

Returns one market's Resolution state: its status, the winning resolvedOutcomeId (once settled), resolveTime, and rules/source where the venue exposes them. This is per-market data — for the venue-static edge-case policy use resolutionPolicy().

  • Kalshi → read from GET /markets/{ticker}. resolvedOutcomeId is 'YES'/'NO' from the settled result; rules from rules_primary; source from settlement_sources. marketId accepts the ticker.
  • Polymarket → read from the Gamma market (marketId accepts a Gamma id or a 0x… conditionId). Once resolved, resolvedOutcomeId is the token id whose outcomePrices entry is ~1; rules is the market description; source is resolutionSource.
  • predict.fun → read from GET /v1/markets/{id} (numeric id). resolvedOutcomeId is the winning outcome's token id (the venue marks outcomes WON/LOST); rules is the market description.

resolutionPolicy(): ResolutionPolicy

Synchronous (venue-static, no I/O). Returns the venue's ResolutionPolicy for edge cases the two venues resolve differently — most importantly esports/tournament forfeits. Kalshi pays out the official recorded result (forfeit: 'tournament_result'); Polymarket voids 50/50 (forfeit: 'void_50_50'); predict.fun documents no policy, so it reports the honest forfeit: 'unknown' — treat its edge cases as unpriced risk and read the per-market Resolution.rules. Surfacing this prevents the class of cross-venue divergence that silently turns a naive arb into a loss (HANDOFF §4.4).


getBalance(): Promise<Balance>

Returns a Balancefree, withdrawable cash on both venues. available and total are both Money and equal in v1.

  • Kalshi: the account cash balance (GET /portfolio/balance). Requires credentials.
  • Polymarket: free cash, read on-chain via eth_call balanceOf against the Polymarket collateral token pUSD (0xc011a7e1…) on Polygon (uses rpcUrl). Polymarket migrated off bridged USDC.e, which now reads 0. Requires walletAddress (the proxy wallet that custodies the collateral), else throws ValidationError with code: 'NO_WALLET'. A failed RPC call throws a VenueError with code: 'RPC_ERROR'.
  • predict.fun: free cash, read on-chain via eth_call balanceOf against BNB-chain USDT (18 decimals, scaled to canonical micro-dollars; testnet uses the venue's mock USDT). Requires walletAddress (defaults to the signer's address); same NO_WALLET/RPC_ERROR errors as Polymarket.

getPortfolioValue(): Promise<Money>

Returns total mark-to-market portfolio value (cash + open positions) on both venues, as normalized USD Money (read .value for dollars).

  • Kalshi: computed as cash + Σ(position size × the outcome's current price). Kalshi exposes no single portfolio figure, so this is a best-effort estimate that issues one market fetch per open position. Requires credentials.
  • Polymarket: on-chain pUSD cash + data-api GET /value (the venue's positions mark-to-market, which excludes cash). Requires walletAddress.
  • predict.fun: on-chain USDT cash + Σ of the venue's own per-position valueUsd marks. Requires walletAddress (defaults to the signer's address).

getPositions(): Promise<Position[]>

Returns an array of Position. Zero-size positions are omitted. Paginated internally.

  • 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.
  • 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.
  • 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.

getFills(query?): Promise<Fill[]>

Returns an array of Fillyour own executions, each linked to its order with your role (maker/taker). This is not the public getTrades tape. query (all optional): marketId, orderId, since (ISO 8601, exclusive), limit, cursor.

  • 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).
  • 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).
  • 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.

getOpenOrders(marketId?): Promise<Order[]>

Returns an array of Order — working (open / partially filled) orders. The complement of getOrderHistory.

  • Kalshi: requires credentials. GET /portfolio/orders?status=resting, optionally filtered to marketId (ticker). Paginated internally.
  • 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.
  • 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).

getOrder(orderId): Promise<Order>

Returns one Order by id, in any state.

  • Kalshi: requires credentials. GET /portfolio/orders/{id}.
  • Polymarket: requires a signer + CLOB credentials. GET /data/order/{id}.
  • 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.

getOrderHistory(query?): Promise<Order[]>

Returns terminal (filled / cancelled) Orders — the complement of getOpenOrders. query (all optional): marketId, since, limit, cursor.

  • Kalshi: requires credentials. Walks GET /portfolio/orders and keeps the non-working orders.
  • Polymarket: requires a signer + CLOB credentials. Reads GET /data/orders and keeps the terminal ones.
  • 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')).

placeOrder(req): Promise<OrderResult>

Places a single order from a normalized PlaceOrderRequest and returns a verified OrderResultstatus/filledSize already resolve each venue's misleading immediate response, so you can trust them without a follow-up read.

const result = await client.placeOrder({
  marketId: '…',      // Kalshi ticker, or PM Gamma id / 0x… conditionId
  outcomeId: 'YES',   // Kalshi: 'YES'/'NO'; Polymarket: the CLOB token id
  side: 'buy',
  price: 0.62,        // implied probability in [0, 1]
  size: 10,           // contracts (Kalshi) / shares (Polymarket)
  tif: 'ioc',         // default: 'ioc' Kalshi, 'fok' Polymarket
});
  • Kalshi: requires credentials. Uses the V2 endpoint POST /portfolio/events/orders (the legacy POST /portfolio/orders is deprecated and returns 410). V2 quotes a single YES-leg book: side is bid (buy YES) / ask (sell YES), so a NO order is restated as its YES complement — buy NO @ p becomes an ask at 1 − p. Price is a fixed-point dollar string in [0,1], count a fixed-point quantity string. tif maps iocimmediate_or_cancel, gtcgood_till_canceled, fokfill_or_kill (V2 supports fill-or-kill; self_trade_prevention_type defaults to taker_at_cross). The V2 response is flat with fill_count/remaining_count (no status field); when both are zero the client re-reads via GET /portfolio/orders/{id} to confirm the outcome (guarding the legacy fill_count="0" quirk) so filledSize/status are correct.
  • Polymarket: requires a signer (else ValidationError code: 'NO_SIGNER'); the CLOB L2 credentials are derived from the signer and cached automatically on first use, so passing clobApiKey/clobSecret/clobPassphrase is optional. The order is EIP-712-signed locally (secp256k1), submitted with L2 HMAC auth (tif fokFOK, iocFAK, gtcGTC), then polled until terminal (Polymarket's immediate status is usually live/delayed). A CLOB rejection throws VenueError code: 'ORDER_REJECTED'. A fillable order also needs on-chain USDC/CTF allowances set (operator prerequisite). Use PolymarketClient.buildSignedOrder(req) to build + sign without submitting (a dry-run of the signing path).
  • predict.fun: requires a signer (else ValidationError code: 'NO_SIGNER'); the Bearer JWT is bootstrapped and cached automatically. The market is fetched fresh (a non-OPEN tradingStatus throws MARKET_NOT_OPEN), the EIP-712 V1 order is signed against the exchange contract selected by the market's (isNegRisk, isYieldBearing) flags, and submitted as strategy: 'LIMIT' (tif fok → + isFillOrKill, the default; gtc rests; ioc throws UNSUPPORTED_TIF — the venue's MARKET-order semantics are unverified). Every order signs a 30-day expiration (the venue rejects expiration: 0 — minimum 2 minutes out) and must have a value of at least $0.90 (venue minimum, enforced at submit). The submit response carries no status/fill, so the client verifies by re-reading the order before returning. Balance is not checked at submit: an unfunded maker's order is accepted and then auto-cancelled by the venue moments later — a lasting, fillable order needs USDT in the wallet and an on-chain allowance for the exchange (operator prerequisite; the four exchange addresses are exported as PREDICTFUN_EXCHANGES/PREDICTFUN_TESTNET_EXCHANGES).

placeOrders(reqs): Promise<OrderResult[]>

Places several orders, returning one verified OrderResult per request in input order. Not atomic — neither venue offers an all-or-nothing batch, so a later failure does not roll back earlier placements. Inspect each result. Same credentials and per-venue behavior as placeOrder.


cancelOrder(orderId): Promise<CancelResult>

Cancels one order and returns a verified CancelResultcancelled reflects the venue's settled view, the same trust contract as OrderResult.

  • Kalshi: requires credentials. DELETE /portfolio/orders/{id}; the response carries the post-cancel order state.
  • 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.
  • 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.

cancelAllOrders(marketId?): Promise<CancelResult[]>

Cancels every working order, optionally scoped to one marketId. Reads the open orders first, then cancels each — one CancelResult per order. Same credentials and caveats as cancelOrder on every venue (predict.fun batches ids through POST /v1/orders/remove, ≤100 per call).


estimateFees(req): Promise<FeeEstimate>

Returns a FeeEstimate with both the maker and taker fee for a PlaceOrderRequest-shaped order — the role is unknowable until execution, so pick the field for the role you expect. Computed from a pure formula today (no network call), but async to leave room for venues that publish a fee schedule.

  • 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.
  • Polymarket: zero for both roles today (HANDOFF §2.8); gas is paid by the relayer. The hook stays in case that changes.
  • 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.

Types

All types are exported from the package root. Every price/probability/ avgEntryPrice is an implied probability in [0, 1]. Timestamps are ISO 8601 strings.

Venue, Side, Currency

type Venue = 'polymarket' | 'kalshi' | 'predictfun' | 'opinionlabs';
type Side = 'buy' | 'sell';
type Currency = 'USD'; // all money is normalized to USD across venues

Money

All monetary values are normalized to USD and homogeneous across venues. Read value (dollars) for display/comparison; amount is the same figure as a lossless integer in micro-dollars (decimals is always 6).

interface Money {
  amount: bigint;     // exact integer micro-dollars (decimals = 6)
  currency: Currency; // always 'USD'
  decimals: number;   // always 6
  value: number;      // the amount in dollars — use this
}

Example: $20.00{ amount: 20_000_000n, currency: 'USD', decimals: 6, value: 20 }. Kalshi cents and Polymarket USDC are both converted into this shape, so a.value and b.value (or a.amount and b.amount) are directly comparable.

Market

interface Market {
  venue: Venue;
  id: string;
  conditionId?: string;       // settlement/join key — Kalshi: == id (ticker) · Polymarket: 0x… conditionId
  slug?: string;
  title: string;              // distinct per-market label (Kalshi folds yes_sub_title in — see table)
  description?: string;
  status: MarketStatus;       // 'open' | 'closed' | 'resolved' | 'cancelled'
  outcomes: Outcome[];
  closeTime?: string;         // ISO 8601 — expected trading end
  resolveTime?: string;       // ISO 8601 — settlement time, present only once resolved
  volume?: Money;
  category?: string;
  url?: string;
  raw?: unknown;              // the original venue payload
}

Field population by venue:

| Field | Kalshi | Polymarket | predict.fun | |---|---|---|---| | id | ticker | Gamma numeric id | venue numeric id | | conditionId | ticker (== id) | 0x… conditionId (≠ id) | 0x… conditionId (≠ id) | | slug | — | set when present | the category slug | | 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) | | description | rules_primary (the resolution prose), else subtitle | set when present | set when present (the resolution prose) | | outcomes | exactly 2 (YES/NO) | parsed from outcomes/outcomePrices/clobTokenIds | 2, ids = ERC-1155 token ids, names verbatim (Yes/No, Up/Down, …) | | closeTime | expected_expiration_time or close_time | endDate | — (no market-level close time; the category has endsAt) | | resolveTime | settled_time (once resolved) | closedTime (once resolved) | — (venue exposes none) | | 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) | | category | when present | when present (often absent) | — (categories are the event grouping, not a topic) | | url | — (no verified Kalshi web-URL format) | https://polymarket.com/market/<slug> (best-effort) | — (no verified URL format) |

Outcome

interface Outcome {
  id: string;          // Kalshi: 'YES'|'NO'  ·  Polymarket: CLOB token id
  name: string;        // homogeneous binary label: 'Yes' / 'No' on both venues
                       //   (Kalshi selections like 'England' ride on raw, and are
                       //    folded into Market.title — not into the outcome name)
  probability: number; // mark-price implied probability in [0, 1]
  raw?: unknown;
}

MarketStatus

type MarketStatus = 'open' | 'closed' | 'resolved' | 'cancelled';
  • Kalshi: active/initialized → open, closed → closed, settled → resolved (unknown → open).
  • Polymarket: closed → resolved, archived → closed, active → open (else closed). cancelled is not emitted by either venue.

MarketQuery

interface MarketQuery {
  status?: MarketStatus;
  category?: string;
  limit?: number;
  cursor?: string;
}

TrendingQuery

interface TrendingQuery {
  limit?: number;    // max markets to return; defaults to 25
  category?: string; // filtered client-side on Market.category, like MarketQuery
}

The query for getTrendingMarkets. No status/cursor: trending is always open markets, and it is a bounded "what's hot" page, not an enumeration.

Event and EventQuery

interface Event {
  venue: Venue;
  id: string;
  title: string;
  slug?: string;
  category?: string;
  markets: Market[];   // fully normalized, embedded
  seriesKey?: string;  // parent grouping (Kalshi series ticker · Polymarket series slug)
  raw?: unknown;
}

interface EventQuery {
  status?: MarketStatus;
  category?: string;
  limit?: number;      // max events to return; defaults to 50 on both venues when omitted
  cursor?: string;
  seriesKey?: string;  // restrict to one series (Kalshi only; ignored elsewhere)
}

The hierarchy is homogenized to one level: an Event groups markets, and a shared seriesKey is the only thing that links events into a series — there is no separate getSeries.

| Field | Kalshi | Polymarket | predict.fun | |---|---|---|---| | id | event ticker | Gamma event id | category slug | | seriesKey | series ticker (e.g. KXNBAGAME) | series slug when present, else — | parentSlug when present, else — | | markets | nested via with_nested_markets=true | nested in the Gamma event | nested in the category |

Resolution, ResolutionPolicy, and ForfeitPolicy

interface Resolution {           // per-market — getResolution(marketId)
  venue: Venue;
  marketId: string;
  conditionId?: string;
  status: MarketStatus;
  resolvedOutcomeId?: string;    // the winning Outcome.id, once settled
  resolveTime?: string;          // ISO 8601, once settled
  rules?: string;                // resolution criteria, when exposed
  source?: string;               // settlement source / oracle, when exposed
  raw?: unknown;
}

interface ResolutionPolicy {     // venue-static — resolutionPolicy()
  venue: Venue;
  forfeit: ForfeitPolicy;        // how a tournament forfeit resolves
  postponement?: string;         // free-form; undefined until verified
  notes?: string;
}

type ForfeitPolicy = 'void_50_50' | 'tournament_result' | 'unknown';

Resolution is per-market state; ResolutionPolicy is a venue constant that does not vary per market (hence resolutionPolicy() is synchronous). The forfeit value is pinned by a real cross-venue divergence (HANDOFF §4.4): Kalshi tournament_result (pays the recorded result) vs Polymarket void_50_50 (refunds); predict.fun documents no policy, so it reports 'unknown' — an explicit "we refuse to guess", meaning its forfeits carry risk you must assess from the per-market rules. postponement is left undefined rather than guessed — it ships only once verified against a real settlement.

Orderbook and OrderbookLevel

interface OrderbookLevel {
  price: number; // implied probability in [0, 1]
  size: number;  // resting size in contract units
}

interface Orderbook {
  venue: Venue;
  marketId: string;
  outcomeId: string;
  bids: OrderbookLevel[]; // sorted descending by price
  asks: OrderbookLevel[]; // sorted ascending by price
  timestamp: string;      // ISO 8601
}

Trade and TradesQuery

interface Trade {
  venue: Venue;
  id: string;          // Kalshi: trade id  ·  Polymarket: transaction hash
  marketId: string;    // Kalshi: ticker  ·  Polymarket: conditionId
  conditionId: string; // settlement/join key (== Market.conditionId)
  outcomeId: string;
  side: Side;          // taker's direction on outcomeId — Kalshi: always 'buy'
  price: number;       // [0, 1]
  size: number;
  timestamp: string;   // ISO 8601
}

interface TradesQuery {
  limit?: number;
  since?: string;  // ISO 8601 lower bound — Kalshi only (ignored by Polymarket)
  cursor?: string;
}

Balance

interface Balance {
  venue: Venue;
  available: Money; // free cash on both venues (USD)
  total: Money;     // equals `available` for both venues in v1
}

Position

interface Position {
  venue: Venue;
  marketId: string;     // Kalshi: ticker  ·  Polymarket: conditionId
  conditionId: string;  // settlement/join key (== Market.conditionId)
  outcomeId: string;
  size: number;
  avgEntryPrice: number;   // [0, 1]
  currentPrice?: number;   // [0, 1] — Polymarket only
  realizedPnl?: Money;     // USD (both venues when present)
  unrealizedPnl?: Money;   // Polymarket only (USD)
}

Order and OrderStatus

type OrderStatus = 'open' | 'partially_filled' | 'filled' | 'cancelled';

interface Order {
  venue: Venue;
  id: string;
  marketId: string;
  conditionId: string;  // settlement/join key (== Market.conditionId)
  outcomeId: string;
  side: Side;
  price: number;   // limit price, [0, 1]
  size: number;    // filled + remaining
  filled: number;
  status: OrderStatus;
  createdAt: string; // ISO 8601
}

Both venues return Orders — from getOpenOrders (working), getOrder (any state), and getOrderHistory (terminal). On Polymarket these read the authenticated CLOB, so they need a signer + CLOB credentials.

Fill, FillRole, and FillsQuery

The normalized outputs of getFillsyour own executions, distinct from the public getTrades tape.

type FillRole = 'maker' | 'taker';

interface Fill {
  venue: Venue;
  id: string;           // venue fill id (Kalshi trade_id · Polymarket trade id)
  orderId: string;      // the order this execution belongs to
  marketId: string;     // Kalshi: ticker · Polymarket: conditionId
  conditionId: string;  // settlement/join key (== Market.conditionId)
  outcomeId: string;
  side: Side;           // your direction on outcomeId
  price: number;        // [0, 1]
  size: number;         // contracts (Kalshi) / shares (Polymarket)
  role: FillRole;
  fee?: Money;          // present when the venue reports a per-fill fee
  timestamp: string;    // ISO 8601
}

interface FillsQuery {
  limit?: number;
  since?: string;       // ISO 8601 lower bound (exclusive)
  cursor?: string;
  marketId?: string;    // Kalshi ticker / Polymarket conditionId
  orderId?: string;
}

OrderHistoryQuery (for getOrderHistory) has limit, since, cursor, and marketId.

PlaceOrderRequest, OrderResult, and TimeInForce

The normalized inputs/outputs of placeOrder.

type TimeInForce = 'ioc' | 'fok' | 'gtc';

interface PlaceOrderRequest {
  marketId: string;      // Kalshi ticker · Polymarket Gamma id / 0x… conditionId
  outcomeId: string;     // Kalshi: 'YES'/'NO' · Polymarket: CLOB token id
  side: Side;            // 'buy' | 'sell'
  price: number;         // implied probability in [0, 1]
  size: number;          // contracts (Kalshi) / shares (Polymarket)
  tif?: TimeInForce;     // default: 'ioc' Kalshi, 'fok' Polymarket
  clientOrderId?: string;
}

interface OrderResult {
  venue: Venue;
  id: string;
  status: OrderStatus;   // verified — venue's misleading immediate state resolved
  filledSize: number;    // contracts/shares actually filled
  raw: unknown;          // final venue payload (escape hatch)
}

CancelResult

The normalized output of cancelOrder and cancelAllOrders. cancelled is verified — the same trust contract as OrderResult.

interface CancelResult {
  venue: Venue;
  orderId: string;
  cancelled: boolean;    // true once the order is no longer working on the book
  status: OrderStatus;   // resulting order state
  raw: unknown;          // venue payload (escape hatch)
}

FeeEstimate

The normalized output of estimateFees. Both roles are returned because the role is unknowable until execution.

interface FeeEstimate {
  venue: Venue;
  takerFee: Money;       // if you cross the spread (take liquidity)
  makerFee: Money;       // if you rest and are filled by another taker
  raw?: unknown;         // formula inputs / venue payload
}

Errors

Every error thrown by the SDK is an instance of PredictionMarketError (subclass of Error). All carry venue and an optional code.

class PredictionMarketError extends Error {
  venue: Venue;
  code: string | undefined;
}

| Class | When | Extra fields | |---|---|---| | AuthError | 401 / 403, or missing/invalid credentials | — | | RateLimitError | 429 | retryAfterMs?: number | | NotFoundError | 404 | — | | NetworkError | transport failure or timeout (code: 'TIMEOUT' / 'NETWORK') | — | | 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) | — | | VenueError | unrecognized ≥500 response; also code: 'ORDER_REJECTED' (PM CLOB rejection), 'RPC_ERROR' (PM on-chain balance) | status?: number, body?: unknown | | PredictionMarketError | base class of all of the above | — |

import { NotFoundError, RateLimitError } from 'prediction-market-sdk';

try {
  await kalshi.getMarket('does-not-exist');
} catch (err) {
  if (err instanceof NotFoundError) { /* … */ }
  if (err instanceof RateLimitError) { await wait(err.retryAfterMs ?? 1000); }
}

The shared HTTP layer retries idempotent requests up to 3 attempts on 429/500/502/503/504, honoring Retry-After, with a 30s default timeout.

Venue cheat-sheet

| | Kalshi | Polymarket | predict.fun | |---|---|---|---| | Market.id | ticker | Gamma numeric id | venue numeric id | | conditionId (everywhere) | ticker (== id) | 0x… conditionId (≠ id) | 0x… conditionId (≠ id) | | Outcome.id | 'YES' / 'NO' | CLOB token id | ERC-1155 token id | | Orderbook outcomeId | 'YES' / 'NO' (default YES) | CLOB token id | token id (defaults to the primary outcome) | | Position/Trade marketId | ticker | conditionId (≠ Market.id) | venue numeric id (== Market.id) | | getBalance | cash | on-chain pUSD cash | on-chain USDT cash (18-dp) | | getPortfolioValue | cash + positions (computed) | data-api value | cash + venue marks (computed) | | Money | USD Money (.value = dollars) | USD Money (.value = dollars) | USD Money (USDT ≈ USD) | | Auth for market data | none | none | mainnet API key (testnet: none) | | Auth for portfolio | API key + PKCS#8 key | wallet address only | wallet address (+ API key on mainnet) | | placeOrder | API key + PKCS#8 key | secp256k1 key + CLOB L2 creds | secp256k1 key (JWT auto-derived) |

Joining positions/trades to markets: every Market, Position, Trade, and Order carries a conditionId — the universal join key. Match position.conditionId === market.conditionId on any venue (no branching). On Polymarket the conditionId can also be passed straight into getMarket, getOrderbook, and getTrades; on Kalshi every id is just the ticker; on predict.fun key those calls by the numeric Market.id (a raw polymarketConditionIds field even links its mirrored markets to Polymarket's for cross-venue joins). outcomeId (token id / YES·NO) is consistent everywhere.

Scope

Implemented today: the full market-data surface — getMarkets, getMarket, getTrendingMarkets, getEvents, getTrendingEvents, getOrderbook, getTrades, getResolution, and resolutionPolicy — plus account/positions reads and the full order lifecycle on Polymarket, Kalshi, and predict.fun: placeOrder/placeOrders, getOpenOrders/getOrder/getOrderHistory/getFills, cancelOrder/cancelAllOrders, and estimateFees. These are the methods on the client contract. (The authenticated order-read/cancel wire shapes — Kalshi /portfolio/fills and the Polymarket CLOB /data/orders·/data/order/{id}·/data/trades·DELETE /order — ship LIVE=1-gated until verified against a real response.)

Not implemented in the market-data surface (and why):

  • searchMarkets — Kalshi exposes no free-text market-search endpoint, so the method cannot be made homogeneous across venues without a lossy client-side fallback. Deliberately left unimplemented rather than ship an uneven contract; use getMarkets/getEvents with category to narrow.
  • getPriceHistory — deferred. Kalshi candlesticks expose OHLC+volume while Polymarket price-history is single {time, price} points; a homogeneous PricePoint shape is still being settled.

Planned (🔜 in the matrix), roughly in order:

  1. modifyOrder(orderId, changes) — amend price/size. Polymarket has no native amend, so it is cancel+replace and the returned order id may change; the uniform semantics with that caveat are pinned in the doc comment.
  2. getPriceHistory — deferred (see above; PricePoint shape unsettled).
  3. StreamingsubscribeOrderbook / subscribeTrades / subscribeTicker (market) and subscribeOrders / subscribeFills (your account).
  4. Cross-venuegetBestPrice on a separate MultiVenueClient facade.

Not on the roadmap (yet): more venues (Manifold, PredictIt) and order cancellation/streaming in CI by default.

Polymarket placeOrder does not yet set on-chain USDC/CTF allowances — a real order only fills if the operator has set them (planned to be auto-handled internally by placeOrder, not exposed as a method). The Polymarket order wire details (EIP-712 domain/struct, L2 HMAC preimage, poll shape) are gated behind LIVE=1 until confirmed. The same applies to predict.fun: a fillable order needs a USDT allowance on the market's exchange contract, and the order-submit wire details (expiration 0, pricePerShare units, JWT expiry shape, maker fees) stay LIVE=1-gated until the key-gated live blocks confirm them.

Development

pnpm install
pnpm build           # tsup → dist/ (ESM + CJS + .d.ts)
pnpm test            # vitest unit tests
pnpm typecheck       # tsc --noEmit
pnpm lint            # eslint .
pnpm example         # build + run examples/quickstart.ts against live APIs

# Live smoke tests (opt-in). See .env.example for credentials.
LIVE=1 pnpm test:integration

See [examples/](exa