Skip to main content

py-sdk Compatibility

polysim_polymarket is a paper-mode mirror of Polymarket’s unified py-sdk (polymarket-client, import polymarket). The call shapes and model fields match py-sdk so the port is mechanical, but paper mode is not Polymarket — it has no chain, no wallet, and its own market universe. This page enumerates every place paper deliberately diverges from real Polymarket, up front, so a bot author knows exactly what they get before relying on a surface area. For the install + quickstart + the prefix/host/auth swap, see the py-sdk Mirror overview.
Pinned to polymarket-client==0.1.0b8. Parity is re-locked against this exact pin by the full-surface parity gate; when the pin bumps, this page and the badge bump with it.

The sim→real seam contract

This is a paper-trading mirror. The call shapes and model fields match py-sdk so the port is mechanical, but paper mode is not Polymarket. Every place paper deliberately diverges is enumerated here:
Signing is inert (accepted, never settled on-chain). The authenticated trading write surface IS implemented — create_limit_order / create_market_order / place_limit_order / place_market_order / post_order / post_orders / cancel_order / cancel_orders / cancel_all / cancel_market_orders, with py-sdk’s flat keyword args and SignedOrder / OrderResponse / CancelOrdersResponse return models. But paper trading needs no signer, so signing is accepted-and-inert: the order is produced with an empty signature and the unsigned paper body is submitted. There is no EIP-712 signing, no on-chain settlement, and no Polygon transaction — the fill happens entirely in PolySimulator’s paper matching engine.
Collateral / balances are PAPER. Your balance is PolySimulator’s paper balance (the balance from me() on the native client), not an on-chain USDC balance. get_balance_allowance reports the paper account’s collateral; there is no real USDC, no allowance/approval transaction, no funder wallet.
A settled trade carries the per-fill fee RATE, not a fee amount. PolySimulator’s matching engine charges and records a real (paper) fee on each fill. What it surfaces over the typed read surface is the fee rate: a settled trade read via list_account_trades returns a ClobTrade model carrying the per-fill fee rate as fee_rate_bps (basis points — the category taker rate applied to the fill, 0 for fee-free fills), exactly matching py-sdk’s ClobTrade.
  • What IS surfaced: the per-fill fee_rate_bps.
  • What is NOT surfaced: a realized per-fill fee amount — there is no fee / fee_usdc field on ClobTrade (the only model carrying those is BuilderTrade, whose reads are stubbed), so the sim’s debited fee amount is not exposed as a typed field on the implemented read surface.
  • What is NOT mirrored at all: Polymarket builder fees (the builder-attribution program), and any protocol-side / pUSD v2 fee economics; the builder_code= kwarg on the order builders is accepted for parity but stays inert (no builder fee taken, no builder-revenue ledger).
On maker/taker: the sim classifies a marketable-at-placement GTC (one that crosses the book on submit) as a taker and charges the taker fee, matching Polymarket; only a genuinely-resting GTC that is filled later — when the market moves into it — is booked as a $0-fee maker. This is a fidelity match, not a gap.
The market universe is PolySimulator’s, NOT Polymarket’s live universe. Reads resolve against the markets PolySimulator carries, which is a curated subset of Polymarket plus the platform’s own crypto Up/Down products. A token_id / condition_id / slug that exists on Polymarket but not in PolySimulator’s universe will not resolve here. Do not assume list_markets / get_market returns the same set, count, or ordering as real Polymarket.
On-chain methods are instant PAPER no-ops. approve_erc20 / approve_erc1155_for_all / transfer_erc20 / split_position / merge_positions / redeem_positions / setup_trading_approvals / setup_gasless_wallet carry py-sdk’s exact signatures, but paper mode accepts the call (replicating py-sdk’s pre-chain input guards so a bot hits the SAME UserInputError) and settles nothing: no chain write, no ledger mutation, no balance change. Each returns a paper transaction handle whose .wait() returns instantly with a valid-format placeholder hash (0x + 64 hex, transaction_id=None). Unlike real py-sdk, redeem_positions / merge_positions do not balance-check the position on paper. (Address checks are a 40-hex FORMAT check, not an EIP-55 checksum, and the returned address is not checksum-normalized — moot on paper, where it’s never used on a chain.)
Rewards / builder / RFQ are honest stubs. The rewards/scoring reads return honest empties (get_order_scoringFalse, the list_* rewards reads → empty paginators, get_reward_percentages{}, …) — the paper rewards engine is a separate roadmap item, so a reward-accounting loop runs and truthfully finds nothing. The builder-attribution reads (get_builder_volumes / list_builder_trades / get_builder_fee_rates / list_builder_leaderboard) mirror py-sdk’s signatures but raise NotImplementedError — there is no builder economy on paper. RFQ has no synchronous SecureClient method on py-sdk (so the mirror invents none), the RFQ types re-export for type-hint parity, and any RFQ action (incl. the async open_rfq_session) raises NotImplementedError — RFQ quoting is not simulated.
Streams are core-topics-only, with documented seams. subscribe() is async-only (sync clients have no subscribe, matching py-sdk) and covers py-sdk’s CORE topics: market (MarketSpec), crypto_prices (CryptoPricesSpec), and the authenticated user feed (UserSpec, secure client only). The sports / comments / equity topics are deferred (their specs/events aren’t shipped). The documented stream seams: the market feed is top-of-book, not L2 (it emits price_change / last_trade_price, never a book event — call get_order_book for the ladder); MarketSpec.token_ids are the SDK’s condition_id:LABEL tokens (not raw Polymarket numeric token ids); stream id fields are plain str (not py-sdk’s hex-validated newtypes); and the lifecycle market events (NewMarketEvent, MarketResolvedEvent, …) are defined for match-parity but never emitted on paper. Full detail in Realtime streams.
Read-model fidelity gaps. A handful of implemented reads are present but narrower than py-sdk’s (e.g. Market is a focused identity+state subset, not the full gamma model; list_markets accepts py-sdk’s full gamma keyword set but only forwards the few PolySimulator honours; get_last_trade_price reports a constant side="BUY" from a book snapshot with no trade side; Decimal string-form may carry extra trailing zeros; 0 is the no-quote sentinel). None break the call shape — see Fidelity gaps.
Because the real py-sdk is a superset, swapping back to real Polymarket only ever adds capability over this seam — it never breaks a bot that stuck to the mirrored surface and respected the seams above.

Compat matrix

Support level for every py-sdk surface area, cross-checked against what’s actually implemented in polysim_polymarket: Legend — ✅ Full parity: same behaviour & shape as py-sdk (documented narrowness aside). 🟡 Paper seam: implemented with paper-mode semantics (see the seam contract above). ⚪ Stub: importable & callable, returns an honest empty or raises NotImplementedError — no fabricated data. 🔴 Deferred: not implemented yet; absent from the surface.

Implemented surface — method reference

The CLOB market-data READ core (the foundation, shared by every client and backed by the same proven read path as the v1 mirror): The authenticated SecureClient adds, on top of those reads (see the compat matrix for support level and the seam contract for paper semantics): Develop and paper-test a bot here against PolySimulator (no wallet, no key, no gas), then run the same code on real Polymarket by swapping the prefix back to polymarket, pointing at the real host, and supplying real credentials.
Async twins. Both clients have async counterparts — AsyncPublicClient (the async twin of PublicClient) and AsyncSecureClient (the async twin of SecureClient) — exposing the identical surface with every per-request method an async def coroutine (the list_* reads stay synchronous, returning an AsyncPaginator you then await). Construct with AsyncSecureClient(api_key=…) or await AsyncSecureClient.create(api_key=…), drive with await, and use async with AsyncSecureClient(…) as client: for lifecycle — exactly as py-sdk’s async client does. The async twins share the same transport-free logic as the sync clients (no second copy), so their behaviour is identical by construction.

Seam details — implementation reference

The seam contract above is the high-level “what diverges”; this section is the precise implementation reference for each seam — the exact input guards, return values, and edge behaviours. (A genuinely deferred method — the Gamma/Data reads — is absent and raises AttributeError, which is honest: the surface simply isn’t there yet, rather than a stub that pretends and fails later. The stubs below instead exist, are callable, and return honest empties / raise NotImplementedError.)
  • EIP-712 signing & on-chain machinery — the real-Polymarket signer / builder-auth path. Paper trading needs no signer, so signing is accepted-and-inert: the authenticated SecureClient’s trading write surface IS implemented (create_limit_order / create_market_order / place_limit_order / place_market_order / post_order / post_orders / cancel_order / cancel_orders / cancel_all / cancel_market_orders, with py-sdk’s flat keyword args and SignedOrder / OrderResponse / CancelOrdersResponse return models) — it just produces an inert-signed order (empty signature) and submits the unsigned paper body.
  • On-chain methods are PAPER no-ops. approve_erc20 / approve_erc1155_for_all / transfer_erc20 / split_position / merge_positions / redeem_positions / setup_trading_approvals / setup_gasless_wallet are implemented with py-sdk’s exact signatures, but paper mode accepts the call (validating inputs) and does NOT settle on-chain or mutate any paper state (no ledger write, no balance change): each returns a paper transaction handle whose .wait() returns instantly with a valid-format placeholder TransactionOutcome (0x + 64 hex hash, transaction_id=None). In particular — unlike real py-sdk, which consults on-chain ERC-1155 balances — redeem_positions / merge_positions do NOT balance-check the position on paper, so they always succeed (the paper position ledger isn’t wired into the on-chain methods in this gate). py-sdk’s pre-chain input guards are replicated so a bot hits the SAME UserInputError in paper as in prod (exactly-one of condition_id/legs; exactly-one of condition_id/market_id/position_id; positive amount on a combo split). Address checks are a 40-hex FORMAT check, NOT an EIP-55 checksum. The accepted shape matches py-sdk’s to_checksum_address acceptance set — a 40-hex address with an optional 0x/0X prefix, case-insensitive — so the guard accepts every input py-sdk accepts and rejects the same malformed ones (wrong length, non-hex). Paper trading never touches a chain, so to keep the dependency surface thin the mirror does not pull in eth-utils/web3/eth_account; that means — unlike py-sdk’s to_checksum_address, which normalizes any accepted address to its EIP-55-checksummed form for on-chain use — the mirror does NOT compute the checksum normalization and returns the address unchanged (moot on paper, where the address is never used on a chain).
  • Rewards / scoring reads are honest empty stubs. get_order_scoringFalse, get_orders_scoring → all-False dict, the list_* rewards reads → empty paginators, get_total_earnings_for_user_for_day(), get_reward_percentages{}. The paper rewards engine is a separate roadmap item, so these return honest empties with no fabricated data — a bot’s reward-accounting loop runs and truthfully finds nothing (rewards aren’t earned on paper).
  • Builder attribution raises NotImplementedError. get_builder_volumes / list_builder_trades / get_builder_fee_rates / list_builder_leaderboard mirror py-sdk’s signatures but raise NotImplementedError("Builder attribution is not simulated in PolySimulator paper mode.") — there is no builder fee, no builder revenue ledger, no builder leaderboard on paper. The builder_code= kwarg on the order builders stays inert (no builder fee).
  • Collateral / balances / positions — the account balance/order reads ARE implemented (get_balance_allowance / get_order / list_open_orders / list_account_trades); the wider portfolio/positions reads are deferred (see the Gamma/Data row of the compat matrix).
  • Realtime streams (CORE topics ARE implemented). The async clients’ subscribe() covers py-sdk’s CORE stream topics — market (MarketSpec → book / price_change / last_trade), crypto_prices (CryptoPricesSpec → Binance / Chainlink ticks), and the authenticated user feed (UserSpec → order / trade fills, secure client only) — backed by our polysim_sdk WS + SSE transport. See Realtime streams below for the topic→transport map, the deferred topics, and the seams. Streaming is async-only (the sync PublicClient / SecureClient have no subscribe, mirroring py-sdk). The sports / comments / equity topics are DEFERRED (their specs/events aren’t shipped).
  • RFQ — the RFQ session flow is not simulated. The RFQ types (RfqQuoteRequestEvent, RfqSession, the enums, the rejection errors, …) DO re-export from the package root so a maker bot’s type hints survive the prefix swap; py-sdk has no synchronous SecureClient RFQ method, so the mirror invents none, and any RFQ action raises NotImplementedError("RFQ quoting is not simulated in PolySimulator paper mode.").
  • Paginator dataframe exports.to_pandas / .to_polars / .to_arrow (they need pandas/polars/pyarrow) are not shipped; the paginator’s .first_page() / .iter_items() / iteration surface is here.
Because the real py-sdk is a superset, the swap to real Polymarket only ever adds these — it never breaks a bot that stuck to the implemented surface.

Fidelity gaps

A handful of implemented methods are present but narrower than py-sdk’s. None break the prefix swap (the field names and call shapes match), but a bot that relies on the listed behaviour will see more on real Polymarket than the mirror provides:
Market is a focused subset. The mirror’s Market carries only the identity fields (id, condition_id, question, slug) plus a focused nested .state (active / closed / neg_risk). py-sdk’s Market is a deeply-nested gamma model — the outcomes, per-outcome prices, and the metrics / volume / liquidity blocks are not present here, and market.state omits py-sdk’s wider fields (archived, accepting_orders, enable_order_book, start_date / end_date / closed_time). The mirror also does not hex-validate condition_id and does not reject non-binary markets the way py-sdk’s Market model does — a payload py-sdk would raise UnexpectedResponseError on may bind here. Read only the focused identity+state subset for a clean swap; the rest lands when Market widens.
list_markets accepts py-sdk’s full gamma keyword set but only forwards a few. Only the filters PolySimulator’s GET /v1/markets honours (closed, ordersort, ascending) reach the server; every other gamma keyword (liquidity_num_min/max, volume_num_min/max, tag_id, clob_token_ids, question_ids, start_date_* / end_date_*, …) is accepted for signature parity and silently ignored. The call won’t error, but the result set is not filtered by an ignored knob. (On real Polymarket every one of these filters applies server-side.)
get_last_trade_price always reports side BUY. PolySim’s book snapshot carries last_trade_price but no trade side, so the mirror fills py-sdk’s required side field with a constant "BUY". Don’t branch on the last-trade side against the mirror — it isn’t real provenance; on Polymarket it reflects the actual maker/taker side.
Decimal string-form (trailing zeros) may differ. The mirror computes prices from PolySim’s /v1/book and quantises to the 4-decimal grid, so a midpoint/price/spread can render as Decimal("0.5000") where py-sdk’s server-computed value renders Decimal("0.5"). The numeric value is equal (== and arithmetic match — the behavioural-parity suite asserts this), but str(value) and value.as_tuple() can differ in trailing zeros. Compare by numeric value, never by string form, for a clean swap.
0 is the no-quote sentinel. When a token’s book has no two-sided quote, get_midpoint / get_price / get_spread return Decimal("0") (py-sdk’s “no value available” sentinel) rather than None or an error. A genuine 0 and a missing quote are indistinguishable — treat a 0 result as “no quote”, not as a real price of zero.

Realtime streams

The async clients mirror py-sdk’s subscribe() for the CORE stream topics, backed by our polysim_sdk WS + SSE transport. A consumer iterates a SubscriptionHandle and closes it (or uses it as an async context manager):
The stream surface lives in polysim_polymarket.streams (the specs, the event types, SubscriptionHandle) — exactly as py-sdk keeps it in polymarket.streams. Neither package promotes any stream name to its package root, so nothing stream-related is re-exported from polysim_polymarket (the re-export subset is empty by design).
CORE topics → transport. Each topic maps to one polysim_sdk stream: subscribe() is async-only (the sync clients have no subscribe, mirroring py-sdk). The handle is a bounded queue with drop-oldest backpressure: under a slow consumer it drops the oldest events and counts the losses in handle.dropped. Pass queue_size= (a mirror-only keyword) to size it. Seams — documented, not fabricated. Because PolySimulator’s push surface is leaner than Polymarket’s, the mirror is explicit where a py-sdk field has no real provenance:
Auth handshake (user stream). The user feed mints a short-lived WS JWT from the secure client’s API key (ws_token()) and connects to the authenticated /v1/ws/executions channel — so it’s scoped to that one account. There is no per-subscription credential; UserSpec.markets, when set, is applied as a client-side filter in the adapter (the channel delivers all of the account’s fills).
MarketSpec.token_ids are the SDK’s condition_id:LABEL tokens (the same form get_* and trading use) — not raw Polymarket numeric token ids. The prices WS subscribes by condition_id, so the opener strips each token’s :LABEL suffix (a generic last-colon split, so non-binary UP/DOWN labels strip correctly — not the binary-only _split_token), de-duplicates, and subscribes those condition ids. Each delivered frame carries per-outcome entries with a label (Yes/No/Up/Down) and the Polymarket CLOB-numeric token_id; that raw digit is a different namespace from the SDK token, so the adapter does not filter on it (it would match nothing). Instead the adapter derives each outcome’s SDK token f"{condition_id}:{LABEL}" from the frame’s market_id + outcome label and both filters on and emits that — so a MarketSpec built from the SDK’s own condition_id:LABEL tokens (e.g. ["0xCID:YES", "0xCID:NO"]) subscribes ["0xCID"] AND receives correctly-filtered events for those outcomes, end to end. The emitted event’s token_id is the condition_id:LABEL form a bot uses for reads/trading, never the backend CLOB digit. (A raw pm numeric token id is not a valid MarketSpec.token_id here.)
No book event from the market stream. /v1/ws/prices is a top-of-book cache, not an L2 ladder feed — so the market stream emits price_change and last_trade_price but never a full book event. (Call get_order_book for the ladder.)
Stream id fields are plain str. Token / condition / order id fields on the stream-event models (token_id, market, id, taker_order_id, …) are plain str, consistent with the mirror-wide id-typing decision — they are not py-sdk’s hex-validated TokenId / CtfConditionId newtypes, so no hex-shape validation is applied to ids arriving on the wire.
Custom-feature / lifecycle market events are defined for parity but never emitted. MarketBestBidAskEvent, NewMarketEvent, MarketResolvedEvent, and MarketTickSizeChangeEvent are part of the MarketEvent union (so a ported bot’s imports and match-on-type resolve) but the paper stream produces none of them — MarketSpec(custom_feature_enabled=True) is accepted for signature parity and is otherwise inert.
Lean user frames, honest derivation. The backend fill frame carries market_id + outcome but no CLOB token/asset id, so the event’s token_id is derivedf"{market_id}:{OUTCOME}", the SDK’s canonical token convention (the inverse of _split_token, the same form get_*/trading use), not faked from the order id. id/taker_order_id keep the fill’s real order_id (those are order ids). Remaining py-sdk fields the frame lacks get honest defaults: the order’s original_size == size_matched == the filled quantity with order_event_type="UPDATE", status="MATCHED". A fill missing its outcome yields no event (no honest token could be built).
last_trade_price side + size are read from the frame top level. The prices producer stamps the most-recent trade’s last_trade_side / last_trade_size at the frame top level (not on the per-outcome entry, which carries only the trade price). The adapter reads them there and emits the real side — a SELL trade yields side="SELL", never a fabricated BUY. If a frame ever lacks the top-level side, the last_trade_price event is dropped rather than mislabeled.
Crypto topic routing is by exact source label. The Binance feed’s wire source is polymarket_rtds (with relay_binance as a fallback-relay label); Chainlink’s is chainlink_rtds. The adapter routes by exact membership in those label sets — prices.crypto.binance receives the live Binance ticks and Binance ticks never leak into a prices.crypto.chainlink subscription.
DEFERRED topics. py-sdk’s polymarket.streams also ships sports (SportsSpec / SportsEvent), comments (CommentsSpec / CommentsEvent / reactions), and prices.equity.pyth (EquityPricesSpec / EquityPricesEvent), plus the RtdsSpec / StreamEvent aliases and the merged multi-spec subscribe(Sequence[...]). None of those are shipped here — pass a single CORE spec per subscribe() call. Because real py-sdk is a superset, the swap to real Polymarket only adds them.

Next steps

py-sdk Mirror overview

Install, the prefix/host/auth swap, sync + async quickstart, and re-exports.

CLOB Compatibility (v1)

The py-clob-client v1 drop-in migration map — the sibling mirror.