> ## Documentation Index
> Fetch the complete documentation index at: https://docs.polysimulator.com/llms.txt
> Use this file to discover all available pages before exploring further.

# py-sdk Compatibility

> The deep reference for the polysim_polymarket py-sdk mirror: the sim→real seam contract, the full compat matrix, the implemented method surface, fidelity gaps, and realtime-stream seams.

# py-sdk Compatibility

`polysim_polymarket` is a **paper-mode mirror** of Polymarket's unified py-sdk
([`polymarket-client`](https://pypi.org/project/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](/sdk-polymarket).

<Note>
  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.
</Note>

## 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:

<Warning>
  **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.
</Warning>

<Info>
  **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.
</Info>

<Info>
  **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.
</Info>

<Warning>
  **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.
</Warning>

<Info>
  **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.)
</Info>

<Info>
  **Rewards / builder / RFQ are honest stubs.** The rewards/scoring reads return
  honest **empties** (`get_order_scoring` → `False`, 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.
</Info>

<Info>
  **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](#realtime-streams).
</Info>

<Info>
  **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](#fidelity-gaps).
</Info>

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`:

| Surface area                                                                                                                          | Support                                  | Notes                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| ------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Public reads** (CLOB market data)                                                                                                   | ✅ **Full parity**                        | `get_order_book(s)` / `get_midpoint(s)` / `get_price(s)` / `get_spread(s)` / `get_last_trade_price(s)` / `get_price_history` / `estimate_market_price` / `get_market` / `list_markets` — sync + async, identical call shapes & model fields (subset/sentinel gaps documented; see [Fidelity gaps](#fidelity-gaps) for the `Market` subset + the ignored `list_markets` filters).                                                                                                                                                                                                                                                                                  |
| **Secure reads** (CLOB market data)                                                                                                   | ✅ **Full parity**                        | The secure client shares the public reads, identical signatures.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| **Account / auth**                                                                                                                    | 🟡 **Paper seam**                        | `fetch_api_keys` / `delete_api_key` / `get_balance_allowance` / `is_gasless_ready` / `get_closed_only_mode` / `get_notifications` / `get_order` / `list_open_orders` / `list_account_trades` implemented over paper auth (single `ps_live_*` key). Note: `is_gasless_ready` / `get_closed_only_mode` / `get_notifications` are **constant honest stubs** (return `True` / `False` / `()` with **no network call**), distinct from `get_balance_allowance` / `get_order` / `list_open_orders` / `list_account_trades`, which are genuinely backed by the paper API. py-sdk's wallet-auth `end_authentication` and the `drop_notifications` write are **deferred**. |
| **Trading** (orders)                                                                                                                  | 🟡 **Paper seam**                        | Full write surface (`create_*` / `place_*` / `post_order(s)` / `cancel_*`) with py-sdk's flat kwargs + `SignedOrder` / `OrderResponse` / `CancelOrdersResponse`. **Signing inert, settled in paper engine, not on-chain.**                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| **On-chain** (ERC-20/1155, split/merge/redeem, approvals)                                                                             | 🟡 **Paper seam (no-op)**                | All methods implemented with exact signatures; each is an **instant no-op** returning a placeholder tx handle — no chain write, no balance/ledger mutation, no balance-check.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| **Rewards / scoring**                                                                                                                 | ⚪ **Stub (honest empties)**              | `get_order(s)_scoring` / `list_*` rewards / `get_total_earnings_*` / `get_reward_percentages` implemented but return honest empties — no paper rewards engine yet.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| **Builder attribution**                                                                                                               | ⚪ **Stub (NotImplementedError)**         | `get_builder_volumes` / `list_builder_trades` / `get_builder_fee_rates` / `list_builder_leaderboard` mirror signatures, raise `NotImplementedError` — no builder economy on paper.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| **RFQ**                                                                                                                               | ⚪ **Stub (types + NotImplementedError)** | RFQ **types** re-export for type-hint parity; py-sdk has no sync `SecureClient` RFQ method (mirror invents none); the async `open_rfq_session` and any RFQ action raise `NotImplementedError`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| **Market stream**                                                                                                                     | 🟡 **Paper seam**                        | `subscribe(MarketSpec)` → `price_change` / `last_trade_price`. **TOB, not L2** (no `book` event); `condition_id:LABEL` token keying; lifecycle events defined-but-never-emitted.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| **User stream**                                                                                                                       | 🟡 **Paper seam**                        | `subscribe(UserSpec)` (secure only) → order/trade fills over the authenticated executions channel; `UserSpec.markets` is a client-side filter; lean frames with honest derivations.                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| **Crypto stream**                                                                                                                     | 🟡 **Paper seam**                        | `subscribe(CryptoPricesSpec)` → Binance / Chainlink ticks, routed by exact source label, over the spot SSE feed.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| **Sports / comments / equity streams**                                                                                                | 🔴 **Deferred**                          | `SportsSpec` / `CommentsSpec` / `EquityPricesSpec` (+ the `RtdsSpec` base alias) and their events are **not shipped**; pass a single CORE spec per `subscribe()`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| **Gamma / Data reads** (events, series, tags, comments, sports, portfolio, positions, leaderboards, combo, search, accounting export) | 🔴 **Deferred**                          | Not implemented on `PublicClient` / `SecureClient` (calling one raises `AttributeError`). Real py-sdk is a superset, so the swap to real Polymarket only *adds* them.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |

**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):

| Method                                                                      | Returns                                                                                                                                                                          |
| --------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `get_order_book(*, token_id)` / `get_order_books(*, token_ids)`             | `OrderBook` / `tuple[OrderBook, …]`                                                                                                                                              |
| `get_midpoint(*, token_id)` / `get_midpoints(*, token_ids)`                 | `Decimal` / `dict[str, Decimal]`                                                                                                                                                 |
| `get_price(*, token_id, side)` / `get_prices(*, requests)`                  | `Decimal` / `dict[str, dict[side, Decimal]]`                                                                                                                                     |
| `get_spread(*, token_id)` / `get_spreads(*, token_ids)`                     | `Decimal` / `dict[str, Decimal]`                                                                                                                                                 |
| `get_last_trade_price(*, token_id)` / `get_last_trade_prices(*, token_ids)` | `LastTradePrice` / `tuple[LastTradePriceForToken, …]`                                                                                                                            |
| `get_price_history(*, token_id, …)`                                         | `tuple[PriceHistoryPoint, …]` (a bare tuple, like py-sdk)                                                                                                                        |
| `estimate_market_price(*, token_id, side, …)`                               | `Decimal` (marginal/limit price — the worst book level touched to fill, like py-sdk; `FOK` underfill raises `InsufficientLiquidityError`, `FAK` falls back to the deepest level) |
| `get_market(*, id=…/slug=…)`                                                | `Market` (with a nested `.state`)                                                                                                                                                |
| `list_markets(*, …)`                                                        | `Paginator[Market]` — drive with `.first_page()` / `.iter_items()`                                                                                                               |

The **authenticated `SecureClient`** adds, on top of those reads (see the
[compat matrix](#compat-matrix) for support level and the
[seam contract](#the-simreal-seam-contract) for paper semantics):

| Group                            | Methods                                                                                                                                                                                                                                 |
| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Account / auth                   | `fetch_api_keys` / `delete_api_key` / `get_balance_allowance` / `is_gasless_ready` / `get_closed_only_mode` / `get_notifications` / `get_order` / `list_open_orders` / `list_account_trades`                                            |
| Trading                          | `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`                                     |
| On-chain (paper no-ops)          | `approve_erc20` / `approve_erc1155_for_all` / `transfer_erc20` / `split_position` / `merge_positions` / `redeem_positions` / `setup_trading_approvals` / `setup_gasless_wallet`                                                         |
| Rewards / scoring (honest stubs) | `get_order_scoring` / `get_orders_scoring` / `list_current_rewards` / `list_market_rewards` / `list_user_earnings_for_day` / `get_total_earnings_for_user_for_day` / `list_user_earnings_and_markets_config` / `get_reward_percentages` |
| Builder (NotImplementedError)    | `get_builder_volumes` / `list_builder_trades` / `get_builder_fee_rates` / `list_builder_leaderboard`                                                                                                                                    |

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.

<Info>
  **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.
</Info>

## Seam details — implementation reference

The [seam contract](#the-simreal-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_scoring` →
  `False`, `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](#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](#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:

<Info>
  **`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.
</Info>

<Info>
  **`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`,
  `order`→`sort`, `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.)
</Info>

<Info>
  **`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.
</Info>

<Info>
  **`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.
</Info>

<Info>
  **`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.
</Info>

## 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):

```python theme={null}
from polysim_polymarket import AsyncSecureClient
from polysim_polymarket.streams import MarketSpec, CryptoPricesSpec, UserSpec

async with await AsyncSecureClient.create(api_key="ps_live_…") as client:
    async with await client.subscribe(MarketSpec(token_ids=["…"])) as h:
        async for ev in h:          # MarketPriceChangeEvent / MarketLastTradePriceEvent
            print(ev.payload.market, ev.type)
```

<Info>
  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).
</Info>

**CORE topics → transport.** Each topic maps to one `polysim_sdk` stream:

| Spec                       | Topic                               | Events emitted                                            | Transport                                     |
| -------------------------- | ----------------------------------- | --------------------------------------------------------- | --------------------------------------------- |
| `MarketSpec`               | `market`                            | `MarketPriceChangeEvent`, `MarketLastTradePriceEvent`     | `ws.aprices_stream` (`/v1/ws/prices`)         |
| `CryptoPricesSpec`         | `prices.crypto.{binance,chainlink}` | `CryptoPricesBinanceEvent` / `CryptoPricesChainlinkEvent` | `sse.aspot_stream` (`/prices/stream`)         |
| `UserSpec` *(secure only)* | `user`                              | `UserTradeEvent`, `UserOrderEvent`                        | `ws.aexecutions_stream` (`/v1/ws/executions`) |

`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:

<Info>
  **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).
</Info>

<Info>
  **`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.)
</Info>

<Info>
  **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.)
</Info>

<Info>
  **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.
</Info>

<Info>
  **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.
</Info>

<Info>
  **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 *derived* — `f"{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).
</Info>

<Info>
  **`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.
</Info>

<Info>
  **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.
</Info>

<Warning>
  **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.
</Warning>

## Next steps

<CardGroup cols={2}>
  <Card title="py-sdk Mirror overview" icon="python" href="/sdk-polymarket">
    Install, the prefix/host/auth swap, sync + async quickstart, and re-exports.
  </Card>

  <Card title="CLOB Compatibility (v1)" icon="arrows-left-right" href="/concepts/clob-compatibility">
    The `py-clob-client` v1 drop-in migration map — the sibling mirror.
  </Card>
</CardGroup>
