{
  "markdown": "# pmq\n\n<!-- mcp-name: io.github.crp4222/pmq -->\n\n[![PyPI](https://img.shields.io/pypi/v/pmquant)](https://pypi.org/project/pmquant/)\n[![tests](https://github.com/crp4222/pmq/actions/workflows/test.yml/badge.svg)](https://github.com/crp4222/pmq/actions/workflows/test.yml)\n[![canary](https://github.com/crp4222/pmq/actions/workflows/canary.yml/badge.svg)](https://github.com/crp4222/pmq/actions/workflows/canary.yml)\n[![coverage gate](https://img.shields.io/badge/coverage-%E2%89%A585%25%20enforced%20in%20CI-blue)](.github/workflows/test.yml)\n[![typed](https://img.shields.io/badge/types-mypy%20strict-blue)](pyproject.toml)\n[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/crp4222/pmq/badge)](https://scorecard.dev/viewer/?uri=github.com/crp4222/pmq)\n[![license](https://img.shields.io/badge/license-MIT-green)](LICENSE)\n\nFail-closed execution and market data for **Polymarket CLOB V2**, in Python,\nbuilt agent-first. Local signing (your keys never leave your process),\nexchange-confirmed fills only, fee-correct math, deposit-wallet\n(`POLY_1271`) support that actually works in production, order-attribution\nregistries (several bots can share one wallet, each with its own\nexchange-truth accounting), and a bundled\n**MCP server**: plug any LLM or agent framework that speaks MCP (Claude,\nChatGPT, LangChain, your own loop) on top and it can read every market and,\nif and only if the operator enables it, trade under hard rails: tools that\ndo not exist until you create them, a cap per order, a daily buy budget.\nThe model cannot widen any of this from inside a session.\n\n```bash\npip install pmquant        # Python >= 3.10; distribution pmquant, import pmq\n```\n\n(PyPI's similarity check reserves the bare name; the module you import is\n`pmq`, same pattern as beautifulsoup4/bs4.)\n\n## Try it in 30 seconds, no keys\n\nPoint any MCP client at `uvx`; it installs the MCP extra in an isolated\nenvironment and starts `pmq-mcp` with one environment variable:\n\n```json\n{\n  \"mcpServers\": {\n    \"pmq\": {\n      \"command\": \"uvx\",\n      \"args\": [\"--from\", \"pmquant[mcp]\", \"pmq-mcp\"],\n      \"env\": { \"PMQ_MCP_PAPER\": \"1\" }\n    }\n  }\n}\n```\n\n`PMQ_MCP_PAPER=1` registers the same trading tools as live, but fills are\n**simulated against the real live order books** using the displayed best\nquote, venue minimums, and a documented crypto-rate fee estimate. The first\npaper ledger starts at 1000 USD, configurable with `PMQ_MCP_PAPER_USD`; it\nthen persists locally across server restarts. No keys are needed and no order\ncan reach the exchange. A real session, captured 2026-07-04, quoted verbatim:\n\n```text\n> find_markets(query=\"fed decision july\")\n    12 markets, among them \"How many dissent at the July Fed meeting?\"\n> market(slug=\"will-no-one-dissent-the-july-fed-decision-20260616001928666\")\n    condition_id 0x50ba...7967, token ids for Yes and No, closes 2026-07-29\n> book(token_id=<Yes>)\n    bid 0.54 x 592.75 | ask 0.56 x 21 | min order 5 shares | tick 0.01\n> fak_buy(token_id=<Yes>, price_cap=0.58, usd=10)\n    paper fill: 17.8571 shares at 0.56 (the real ask, not the cap),\n    fee 0.308, cash left 989.69\n> account_collateral()\n    989.69 paper USD\n```\n\nFive calls: discover, resolve, read the live book, buy with simulated\nmoney at the real ask, check the balance. The same session rendered as a\nstep-by-step page: [docs/demo.html](docs/demo.html) (one self-contained\nHTML file, no JavaScript, no external requests; download and open it).\nTrading real money additionally requires keys and an explicit\n`PMQ_MCP_LIVE=1`, under the rails in\n[the agents section](#agents-the-mcp-server).\n\nAs of 2026-07-03 this is, to our knowledge, the **only maintained Python\nlayer combining local CLOB V2 signing, an exchange-confirmed fill contract,\nand working deposit-wallet (POLY_1271) auth**. That claim is dated and\nfalsifiable: [docs/comparison.md](docs/comparison.md) names the\nalternatives and what each does instead; open an issue if it goes stale.\n\n## Quickstart\n\nMarket data needs no keys:\n\n```python\nimport pmq\n\nm = pmq.parse_market(pmq.get_market(\"btc-updown-15m-1783062000\"))\nbook = pmq.get_book(m[\"token_a\"])\nbid, bid_sz, ask, ask_sz = pmq.best_bid_ask(book)\nprint(ask, pmq.band_ask_depth_usd(book, 0.90, 0.97))\nprint(pmq.fee(price=0.95, shares=100))          # taker fee in $, crypto rate\n```\n\nExecution (reads `POLY_PRIVATE_KEY`, `POLY_FUNDER`, `POLY_SIG_TYPE` from the\nenvironment):\n\n```python\nfrom pmq import PolymarketExecutor, OrderUncertain\n\nex = PolymarketExecutor()                        # signature_type=3 for the app's deposit wallet\nex.require_collateral(5.0)                       # fail fast, with a diagnostic that names sig_type\n\ntry:\n    fill = ex.buy_fak(token_id=m[\"token_a\"], price_cap=0.95, usd=5.00)\nexcept OrderUncertain:\n    ex.reconcile(m[\"condition_id\"], m[\"token_a\"])   # exchange truth before anything else\nelse:\n    if fill:                                     # book ONLY what matched\n        print(fill.matched_shares, \"shares at\", fill.price, \"order\", fill.order_id)\n```\n\n`sell_fak` and `limit_gtc` follow the same contract, and all three paths\nhave carried production volume: a FAK round trip (buy 5.149 @ 0.94, sell\nback 5.14 @ 0.94, cross-checked via `get_trades`, 2026-07-03) and a GTC\nmaker fill (posted above the bid, matched as MAKER at zero fee,\n2026-07-04, settlement tx in the production section below).\n\n## Scope, latency, requirements\n\nPython 3.10 to 3.14 (the CI matrix runs all five). Plain REST round trips,\nmeasured 2026-07-04 (medians of 5, residential fiber, Western Europe):\nresolve a market 76 ms, fetch a book 85 ms, sign + POST an order and get\nthe exchange's answer 73 ms. Sub-second everywhere, built for second-scale\nstrategies (the maintainer's bot polls 15-minute windows every 2.5 s); it\nis not a microsecond market-making stack: no websockets, no co-location,\none HTTP call per action.\n\n## Why this exists\n\nPolymarket cut over to CLOB V2 on 2026-04-28. V1-signed orders are rejected in\nproduction, the fee schedule is decided at match time, and the official client\nexamples leave several traps undocumented. Every line of pmq was paid for with\na real error in live trading:\n\n* `invalid amounts, the market buy orders maker amount supports a max accuracy\n  of 2 decimals, taker amount a max of 4 decimals`: the CLOB treats FAK/FOK\n  buys as **market orders** and caps their signed amounts at 2 decimals\n  (maker) / 4 decimals (taker) whatever the tick size. The official client's\n  rounding table allows 5-6 taker decimals on markets whose tick is finer\n  than 0.01 (any book trading past 0.96 or under 0.04), so market orders\n  there are rejected wholesale (reported upstream:\n  [py-clob-client-v2#99](https://github.com/Polymarket/py-clob-client-v2/issues/99)).\n  pmq clamps the signed\n  pair to the exchange caps before signing and refuses at startup any\n  client build that would still sign a rejectable pair, so the trap cannot\n  reach your orders. Measurements in\n  [docs/rounding-study.md](docs/rounding-study.md).\n* `no orders found to match with FAK order` (HTTP 400, yet with an `orderID`):\n  a clean no-fill, not an error. pmq returns an empty `Fill` instead of crashing\n  or, worse, retrying blindly.\n* CLOB shows `balance: 0` while your pUSD sits on-chain: the balance endpoint\n  ignores your `funder` parameter and derives the wallet from your EOA and\n  `signature_type`. Funds in the Polymarket app's default wallet (an ERC-1271\n  deposit wallet) are only visible with `signature_type=3`.\n\nThe full write-up with reproduction details: [docs/war-story.md](docs/war-story.md).\n\n## Runs in production: my own money, daily\n\nI built pmq for my own trading. It executes real volume with my funds every\nday, and it has never booked a fill the exchange did not confirm. If you\nwant to see it on-chain, here is a settlement from one of my wallets\n(2026-07-03):\n[`0x387f5f09...100d88a8`](https://polygonscan.com/tx/0x387f5f09c031bb36a71c54adc978b1ed4d50c67f6dd3f0c2c8068391100d88a8)\non the CTF Exchange V2: a FAK market buy built by this library, matched and\nsettled, with the builder code visible in the calldata. The maker path has\nits own receipt (2026-07-04): a `limit_gtc` posted one tick above the bid,\nmatched as MAKER at zero fee and settled in\n[`0x1b60f19a...c35d09`](https://polygonscan.com/tx/0x1b60f19a6f089624f27babb58bf82538c49f044ee83778783195e26a33c35d09),\nwhere the `maker_orders` slice accounting that release 0.4.6 encodes is\nvisible in the raw trade record. A weekly\n[canary workflow](.github/workflows/canary.yml) exercises the real endpoints\nand the installed client surface, and opens an issue by itself if Polymarket\ndrifts.\n\n## pmq-doctor: diagnose your setup in one command\n\n```bash\npip install pmquant && pmq-doctor --market <slug>\n```\n\nIt checks, in order: the installed client surface (introspection), your\nderived EOA, the funder wallet on-chain (`owner()` and bytecode: is it a\ndeposit wallet?), whether `POLY_SIG_TYPE` matches the wallet type, whether\nthe CLOB actually sees your collateral (and if not, WHICH sig_type does),\nand the target market's minimum size and tick. Real output on a real\ndeposit-wallet account:\n\n![pmq-doctor output](docs/assets/pmq-doctor.svg)\n\nIf you landed here from \"the order signer address has to be the address of\nthe API KEY\" or a CLOB balance of 0 with funds on-chain: this is the tool.\n\n## The contract: nothing is booked without exchange confirmation\n\n| Situation | What pmq does |\n|---|---|\n| Response is a dict with `orderID`, not flagged failed | `Fill` with the **matched** size read from the response |\n| Error dict on HTTP 200, string body, `success: false` | `Fill(rejected=True)`, zero booked |\n| HTTP 4xx (incl. FAK no-match) | `Fill(rejected=True)`, zero booked |\n| Timeout, 5xx, exception after send | raises `OrderUncertain`: the order MAY exist. Call `reconcile()` before trading that market again |\n| Unparseable matched amounts | zero booked (fail closed) |\n\n`reconcile(condition_id)` cancels anything resting, verifies nothing stayed\nopen, and returns `(shares, usd, fees)` from `get_trades`: the exchange truth.\n\nAt startup pmq **introspects the installed py-clob-client-v2** against the API\nsurface it was verified on, and refuses to trade on drift instead of sending\norders through changed semantics. The whole table is pinned by an executable\ntest per row plus a hypothesis fuzz suite (hundreds of generated adversarial\nresponses per run, including NaN/Infinity and negative amounts, which book\nzero).\n\n## Several bots, one wallet\n\n`get_trades` is account-level: run two bots on the same wallet and each\none's exchange-truth totals silently include the other's fills. Since\n0.5.0 every order-sender can keep an **attribution registry**: an\nappend-only file of its own order ids, written on every confirmed post.\n\n```python\nex = PolymarketExecutor(order_log=\"botA.orders\",\n                        foreign_order_logs=[\"botB.orders\"])\n# or per process: POLY_ORDER_LOG=botA.orders POLY_FOREIGN_ORDER_LOGS=botB.orders\n```\n\nWith a registry configured, `trades_totals()` counts only trades whose\n`taker_order_id` (taker role) or `maker_orders[].order_id` slice (maker\nrole) belongs to OUR registry (both fields verified present and populated\non real V2 trade records), and `reconcile()` additionally claims trades\nunknown to EVERY registry, so a fill posted during an uncertainty window\nis recovered by the bot that was uncertain and by nobody else. Sound only\nif every sender on the wallet keeps a registry\n(`POLY_FOREIGN_ORDER_LOGS` is colon-separated). The MCP server inherits\nthe registries through the same environment variables. Fully opt-in:\nwithout `POLY_ORDER_LOG` the behavior is unchanged.\n\n## Streaming the resolution prices\n\nThe updown markets resolve on the Chainlink stream, and\n`wss://ws-live-data.polymarket.com` republishes that exact stream (plus a\nBinance spot mirror). `pmq.stream.PriceStream` consumes it with the\nstandard library only:\n\n```python\nfrom pmq.stream import PriceStream\n\nps = PriceStream(assets=(\"btc\", \"eth\")).start()\nps.last(\"btc\")               # (unix_seconds, value) from the Chainlink feed\nps.age(\"btc\")                # seconds since the freshest tick\nps.last(\"btc\", \"binance\")    # the spot mirror, for comparison\n```\n\nDesign note, measured 2026-07 from two unrelated egresses: the edge serves\nthe sustained push only to browser connections; a plain client gets the\ninitial tick batch after subscribing, then silence. `PriceStream` therefore\nre-polls short connections (about one per second); the freshest tick is\ntypically 1.2 to 2.8 seconds old. Treat the feed as advisory and fail\nclosed on `age()`: the exchange resolves with its own copy.\n\n## The signature_type decoder table\n\n| `signature_type` | Wallet | When it is yours |\n|---|---|---|\n| 0 | the EOA itself | you trade from a bare private key |\n| 1 | `POLY_PROXY` | email/Magic accounts (legacy) |\n| 2 | `POLY_GNOSIS_SAFE` | browser-wallet proxy |\n| 3 | `POLY_1271` deposit wallet | **the Polymarket app's default wallet** |\n\nIf `collateral()` returns 0 while the funds are visible on-chain on your funder\naddress, your `signature_type` is wrong. Debug trick: `eth_call` `owner()`\n(`0x8da5cb5b`) on the funder; if it returns your EOA and the wallet bytecode is\nan ERC-1167 proxy, you want `signature_type=3`.\n\n## Alternatives\n\nNautilusTrader if you want a full backtesting and trading framework; pmxt\nif you accept routing writes through a hosted backend; raw\npy-clob-client-v2 if you want no opinion layered on the official client.\nThe dated feature-by-feature table (written by an interested party, every\nrow checkable) lives in [docs/comparison.md](docs/comparison.md).\n\n## Builder code disclosure\n\npmq ships with the maintainer's public Polymarket **builder code** as default\nattribution inside signed orders (`pmq.executor.DEFAULT_BUILDER_CODE`). Its\ncommission is set to **0/0: it never adds any fee to your orders**. Attribution\nfeeds Polymarket's builder program and funds this project at zero cost to you.\n\n## Agents: the MCP server\n\nFor an installed server, run `pip install \"pmquant[mcp]\"` then `pmq-mcp`\n(stdio). For a clean MCP-client configuration, use\n`uvx --from \"pmquant[mcp]\" pmq-mcp` as in the paper example above. Listed in\nthe [official MCP registry](https://registry.modelcontextprotocol.io) as\n`io.github.crp4222/pmq`, it works with Claude Desktop or Code, ChatGPT,\nLangChain, and a bare SDK loop.\n\n**What an agent can do, exactly:**\n\n| Tool | Needs | What it does |\n|---|---|---|\n| `pmq_status` | nothing | mode, registered trading surface, caps, daily headroom, and durable-state health without constructing a signer |\n| `find_markets` | nothing | discover active markets, any category, full-text search |\n| `event` | nothing | all binary markets of a multi-outcome event (elections, tournaments) |\n| `market` | nothing | slug to condition id, outcome names, token ids, close time, winner |\n| `market_snapshot` | nothing | resolve a market and read a top-of-book summary for every outcome in one call |\n| `book` | nothing | real-time bid/ask with sizes, depth in a price range, exchange minimums |\n| `order_preview` | nothing | non-mutating top-of-book FAK estimate with rails and a crypto-rate fee estimate; it never creates a signer, submits an order, or reserves budget |\n| `taker_fee` | nothing | official fee formula per category, cost per share including fee |\n| `account_collateral` | paper mode, or keys | paper cash or the CLOB-visible live balance with a sig_type diagnostic |\n| `account_trades` | paper mode, or keys | paper totals or exchange-truth BUY totals on one market |\n| `account_portfolio` | paper mode, or public wallet | durable paper positions, or public Data API positions for `wallet` or `POLY_FUNDER` |\n| `fak_buy` | `PMQ_MCP_PAPER=1`, or keys plus `PMQ_MCP_LIVE=1` | open a position with a fill-and-kill buy; nothing rests |\n| `fak_sell` | `PMQ_MCP_PAPER=1`, or keys plus `PMQ_MCP_LIVE=1` | close a position with a fill-and-kill sell under the same contract |\n| `cancel_and_reconcile` | `PMQ_MCP_PAPER=1`, or keys plus `PMQ_MCP_LIVE=1` | cancel resting orders and return reconciliation truth; paper has nothing resting |\n\nWith `PMQ_MCP_PAPER=1` (the [30-second demo](#try-it-in-30-seconds-no-keys)\nabove) the same trading and account tools are registered **keyless**:\nfills are simulated at the displayed best quote, capped by the displayed\nsize, refused under the exchange minimum, and the account tools report the\ndurable paper ledger. `order_preview` remains read-only in every mode.\nPaper responses are flagged `paper: true`, and no order reaches the exchange.\n\n**The rails, all operator-set (server environment, invisible to and\nuntouchable by the model):**\n\n| Variable | Effect | Default |\n|---|---|---|\n| `PMQ_MCP_LIVE` | unset: the three trading tools are never REGISTERED; an agent cannot call a tool that does not exist | read-only |\n| `PMQ_MCP_PAPER` | trading tools simulate fills against the real live books, keyless, nothing sent to the exchange; wins over `PMQ_MCP_LIVE` when both are set | off |\n| `PMQ_MCP_PAPER_USD` | initial paper balance when a new state file is created | 1000 |\n| `PMQ_MCP_MAX_USD` | hard cap per single order, live and paper alike | 10 |\n| `PMQ_MCP_DAILY_USD` | durable cumulative BUY budget per UTC day; unknown live results retain their requested reservation through that UTC day | off |\n| `PMQ_MCP_STATE_FILE` | local file for the durable paper ledger and daily budget | `$XDG_STATE_HOME/pmq/mcp-state.json`, otherwise `~/.local/state/pmq/mcp-state.json` |\n| `POLY_*` keys | omit them entirely for a data-only server | absent |\n\nStructural rails on top: only FAK orders exist (nothing rests unattended on\nthe book), every uncertain outcome is surfaced for reconciliation, and fills\nare booked only from exchange confirmations, never from optimism.\n\nThe state file contains paper cash, positions, fills, and the daily budget,\nnever key material. It is atomically replaced on update. Use a distinct state\nfile for each concurrently running server. A live buy reserves its requested\namount before the client call, then settles that reservation to an\nexchange-confirmed amount. A clean rejection releases it; an unknown outcome\nkeeps the full reservation through the UTC day. `pmq_status` exposes state\nhealth without exposing secrets. If a required durable write fails, the\naffected buy is refused rather than proceeding without its rail.\n\n```json\n{\n  \"mcpServers\": {\n    \"pmq\": {\n      \"command\": \"uvx\",\n      \"args\": [\"--from\", \"pmquant[mcp]\", \"pmq-mcp\"],\n      \"env\": {\n        \"PMQ_MCP_LIVE\": \"1\",\n        \"PMQ_MCP_MAX_USD\": \"10\",\n        \"PMQ_MCP_DAILY_USD\": \"25\",\n        \"POLY_PRIVATE_KEY\": \"...\",\n        \"POLY_FUNDER\": \"0x...\",\n        \"POLY_SIG_TYPE\": \"3\"\n      }\n    }\n  }\n}\n```\n\nRemove `PMQ_MCP_LIVE` and the `POLY_*` variables entirely for a read-only\nmarket-data server.\n\n## Bot template\n\n[bot-template/](bot-template/) is a complete bot minus the strategy, for ANY\nmarket (politics, sports, crypto, culture): paper mode against real books\nwith real fees, per-market budgets with fee headroom, poisoned-market\nreconciliation, consecutive-failure halt, disk-persisted daily loss halt, a\nsystemd unit with `RestartPreventExitStatus=42` so halts stay halted, and a\nlightweight phone dashboard. You implement `watchlist()` and `decide()`; the\nshipped demo strategy is an API illustration meant to be replaced.\n\n## Security posture\n\n* Keys are read from the environment, used to instantiate the signer, and\n  never logged. No custody, no backend, no telemetry, zero network calls\n  besides Polymarket endpoints.\n* A documented wave of fake \"polymarket bot\" repositories steals private\n  keys; pmq is deliberately small so the entire execution path stays\n  readable in minutes by anyone who wants to look.\n* Fund the trading wallet with what you can afford to lose. Nothing here is\n  financial advice; prediction-market access is restricted in some\n  jurisdictions and compliance is on you.\n\n## If you feel like checking any of it\n\nNone of the claims above require taking my word; each one comes with a\nhandle you can pull, whenever you care to:\n\n* **Egress.** `PMQ_CANARY=1 pytest tests/test_canary_live.py -k egress -s`\n  records every DNS resolution during a full session (market data, auth\n  derivation, one signed order) and fails on any host outside\n  `polymarket.com`. Last observed list: `clob.polymarket.com`,\n  `gamma-api.polymarket.com`, nothing else. The weekly\n  [canary](../../actions/workflows/canary.yml) prints that list in public\n  CI logs. One designed exception: `pmq-doctor`'s optional on-chain checks\n  use the public Polygon RPCs named in its source.\n* **Provenance.** Releases carry a signed PEP 740 attestation (Sigstore,\n  via PyPI trusted publishing): click \"provenance\" next to any file on the\n  [PyPI files page](https://pypi.org/project/pmquant/#files), or fetch it\n  raw from PyPI's integrity API. The signing identity is this repository's\n  `publish.yml` workflow.\n* **Dependencies.** Dependabot files weekly bump PRs (Python and\n  SHA-pinned GitHub Actions), and the weekly canary runs `pip-audit`; a\n  hit opens an issue by itself.\n* **The source.** Five small modules; the whole execution path reads in\n  minutes. The grep targets that answer the important questions fastest\n  are listed in [SECURITY.md](SECURITY.md).\n\n## Stability and maintenance\n\nPre-1.0 SemVer with a written deprecation window and a stated bar for 1.0;\none maintainer, trading his own money through this exact code daily. The\noperational rule worth knowing: if the canary badge goes red and stays\nred, treat the project as unmaintained and pin your last known-good\nversion. Full policy and the precisely scoped help-wanted:\n[docs/stability.md](docs/stability.md).\n\n## License\n\nMIT\n",
  "bytes": 21749,
  "sha": "22f158b8cd712438e94d9a65e42c0fee74cf729ac2cebaffef0d0b40fba917a3",
  "repo_slug": "crp4222/pmq",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_crp4222_pmq_8e9e9966/readme"
}