Skip to main content

Fail-closed execution and market-data layer for Polymarket CLOB V2: local signing, confirmed fills only, fee-correct math, working deposit-wallet (POLY_1271) support.

Project description

pmq

PyPI tests canary coverage gate typed OpenSSF Scorecard license

Fail-closed execution and market data for Polymarket CLOB V2, in Python, built agent-first. Local signing (your keys never leave your process), exchange-confirmed fills only, fee-correct math, deposit-wallet (POLY_1271) support that actually works in production, and a bundled MCP server: plug any LLM or agent framework that speaks MCP (Claude, ChatGPT, LangChain, your own loop) on top and it can read every market and, if and only if the operator enables it, trade under hard rails: tools that do not exist until you create them, a cap per order, a daily buy budget. The model cannot widen any of this from inside a session.

pip install pmquant        # Python >= 3.10; distribution pmquant, import pmq

(PyPI's similarity check reserves the bare name; the module you import is pmq, same pattern as beautifulsoup4/bs4.)

Try it in 30 seconds, no keys

Point any MCP client at pmq-mcp with one environment variable:

{ "mcpServers": { "pmq": { "command": "pmq-mcp", "env": { "PMQ_MCP_PAPER": "1" } } } }

PMQ_MCP_PAPER=1 registers the same trading tools as live, but fills are simulated against the real live order books (real best ask, real exchange minimums, the official taker fee formula) from a paper balance (default 1000, set PMQ_MCP_PAPER_USD). No keys anywhere, and no order can reach the exchange. A real session, captured 2026-07-04, quoted verbatim:

> find_markets(query="fed decision july")
    12 markets, among them "How many dissent at the July Fed meeting?"
> market(slug="will-no-one-dissent-the-july-fed-decision-20260616001928666")
    condition_id 0x50ba...7967, token ids for Yes and No, closes 2026-07-29
> book(token_id=<Yes>)
    bid 0.54 x 592.75 | ask 0.56 x 21 | min order 5 shares | tick 0.01
> fak_buy(token_id=<Yes>, price_cap=0.58, usd=10)
    paper fill: 17.8571 shares at 0.56 (the real ask, not the cap),
    fee 0.308, cash left 989.69
> account_collateral()
    989.69 paper USD

Five calls: discover, resolve, read the live book, buy with simulated money at the real ask, check the balance. The same session rendered as a step-by-step page: docs/demo.html (one self-contained HTML file, no JavaScript, no external requests; download and open it). Trading real money additionally requires keys and an explicit PMQ_MCP_LIVE=1, under the rails in the agents section.

As of 2026-07-03 this is, to our knowledge, the only maintained Python layer combining local CLOB V2 signing, an exchange-confirmed fill contract, and working deposit-wallet (POLY_1271) auth. That claim is dated and falsifiable: docs/comparison.md names the alternatives and what each does instead; open an issue if it goes stale.

Quickstart

Market data needs no keys:

import pmq

m = pmq.parse_market(pmq.get_market("btc-updown-15m-1783062000"))
book = pmq.get_book(m["token_a"])
bid, bid_sz, ask, ask_sz = pmq.best_bid_ask(book)
print(ask, pmq.band_ask_depth_usd(book, 0.90, 0.97))
print(pmq.fee(price=0.95, shares=100))          # taker fee in $, crypto rate

Execution (reads POLY_PRIVATE_KEY, POLY_FUNDER, POLY_SIG_TYPE from the environment):

from pmq import PolymarketExecutor, OrderUncertain

ex = PolymarketExecutor()                        # signature_type=3 for the app's deposit wallet
ex.require_collateral(5.0)                       # fail fast, with a diagnostic that names sig_type

try:
    fill = ex.buy_fak(token_id=m["token_a"], price_cap=0.95, usd=5.00)
except OrderUncertain:
    ex.reconcile(m["condition_id"], m["token_a"])   # exchange truth before anything else
else:
    if fill:                                     # book ONLY what matched
        print(fill.matched_shares, "shares at", fill.price, "order", fill.order_id)

sell_fak and limit_gtc follow the same contract, and all three paths have carried production volume: a FAK round trip (buy 5.149 @ 0.94, sell back 5.14 @ 0.94, cross-checked via get_trades, 2026-07-03) and a GTC maker fill (posted above the bid, matched as MAKER at zero fee, 2026-07-04, settlement tx in the production section below).

Scope, latency, requirements

Python 3.10 to 3.14 (the CI matrix runs all five). Plain REST round trips, measured 2026-07-04 (medians of 5, residential fiber, Western Europe): resolve a market 76 ms, fetch a book 85 ms, sign + POST an order and get the exchange's answer 73 ms. Sub-second everywhere, built for second-scale strategies (the maintainer's bot polls 15-minute windows every 2.5 s); it is not a microsecond market-making stack: no websockets, no co-location, one HTTP call per action.

Why this exists

Polymarket cut over to CLOB V2 on 2026-04-28. V1-signed orders are rejected in production, the fee schedule is decided at match time, and the official client examples leave several traps undocumented. Every line of pmq was paid for with a real error in live trading:

  • invalid amounts, the market buy orders maker amount supports a max accuracy of 2 decimals, taker amount a max of 4 decimals: the CLOB treats FAK/FOK buys as market orders and caps their signed amounts at 2 decimals (maker) / 4 decimals (taker) whatever the tick size. The official client's rounding table allows 5-6 taker decimals on markets whose tick is finer than 0.01 (any book trading past 0.96 or under 0.04), so market orders there are rejected wholesale (reported upstream: py-clob-client-v2#99). pmq clamps the signed pair to the exchange caps before signing and refuses at startup any client build that would still sign a rejectable pair, so the trap cannot reach your orders. Measurements in docs/rounding-study.md.
  • no orders found to match with FAK order (HTTP 400, yet with an orderID): a clean no-fill, not an error. pmq returns an empty Fill instead of crashing or, worse, retrying blindly.
  • CLOB shows balance: 0 while your pUSD sits on-chain: the balance endpoint ignores your funder parameter and derives the wallet from your EOA and signature_type. Funds in the Polymarket app's default wallet (an ERC-1271 deposit wallet) are only visible with signature_type=3.

The full write-up with reproduction details: docs/war-story.md.

Runs in production: my own money, daily

I built pmq for my own trading. It executes real volume with my funds every day, and it has never booked a fill the exchange did not confirm. If you want to see it on-chain, here is a settlement from one of my wallets (2026-07-03): 0x387f5f09...100d88a8 on the CTF Exchange V2: a FAK market buy built by this library, matched and settled, with the builder code visible in the calldata. The maker path has its own receipt (2026-07-04): a limit_gtc posted one tick above the bid, matched as MAKER at zero fee and settled in 0x1b60f19a...c35d09, where the maker_orders slice accounting that release 0.4.6 encodes is visible in the raw trade record. A weekly canary workflow exercises the real endpoints and the installed client surface, and opens an issue by itself if Polymarket drifts.

pmq-doctor: diagnose your setup in one command

pip install pmquant && pmq-doctor --market <slug>

It checks, in order: the installed client surface (introspection), your derived EOA, the funder wallet on-chain (owner() and bytecode: is it a deposit wallet?), whether POLY_SIG_TYPE matches the wallet type, whether the CLOB actually sees your collateral (and if not, WHICH sig_type does), and the target market's minimum size and tick. Real output on a real deposit-wallet account:

pmq-doctor output

If you landed here from "the order signer address has to be the address of the API KEY" or a CLOB balance of 0 with funds on-chain: this is the tool.

The contract: nothing is booked without exchange confirmation

Situation What pmq does
Response is a dict with orderID, not flagged failed Fill with the matched size read from the response
Error dict on HTTP 200, string body, success: false Fill(rejected=True), zero booked
HTTP 4xx (incl. FAK no-match) Fill(rejected=True), zero booked
Timeout, 5xx, exception after send raises OrderUncertain: the order MAY exist. Call reconcile() before trading that market again
Unparseable matched amounts zero booked (fail closed)

reconcile(condition_id) cancels anything resting, verifies nothing stayed open, and returns (shares, usd, fees) from get_trades: the exchange truth.

At startup pmq introspects the installed py-clob-client-v2 against the API surface it was verified on, and refuses to trade on drift instead of sending orders through changed semantics. The whole table is pinned by an executable test per row plus a hypothesis fuzz suite (hundreds of generated adversarial responses per run, including NaN/Infinity and negative amounts, which book zero).

The signature_type decoder table

signature_type Wallet When it is yours
0 the EOA itself you trade from a bare private key
1 POLY_PROXY email/Magic accounts (legacy)
2 POLY_GNOSIS_SAFE browser-wallet proxy
3 POLY_1271 deposit wallet the Polymarket app's default wallet

If collateral() returns 0 while the funds are visible on-chain on your funder address, your signature_type is wrong. Debug trick: eth_call owner() (0x8da5cb5b) on the funder; if it returns your EOA and the wallet bytecode is an ERC-1167 proxy, you want signature_type=3.

Alternatives

NautilusTrader if you want a full backtesting and trading framework; pmxt if you accept routing writes through a hosted backend; raw py-clob-client-v2 if you want no opinion layered on the official client. The dated feature-by-feature table (written by an interested party, every row checkable) lives in docs/comparison.md.

Builder code disclosure

pmq ships with the maintainer's public Polymarket builder code as default attribution inside signed orders (pmq.executor.DEFAULT_BUILDER_CODE). Its commission is set to 0/0: it never adds any fee to your orders. Attribution feeds Polymarket's builder program and funds this project at zero cost to you.

Agents: the MCP server

pip install "pmquant[mcp]" then run pmq-mcp (stdio). Listed in the official MCP registry as io.github.crp4222/pmq. Any MCP-speaking client works: Claude Desktop or Code, ChatGPT, LangChain, a bare SDK loop; the server neither knows nor cares which model drives it.

What an agent can do, exactly:

Tool Needs What it does
find_markets nothing discover active markets, any category, full-text search
event nothing all binary markets of a multi-outcome event (elections, tournaments)
market nothing slug to condition id, outcome names, token ids, close time, winner
book nothing real-time bid/ask with sizes, depth in a price range, exchange minimums
taker_fee nothing official fee formula per category, cost per share including fee
account_collateral keys CLOB-visible balance, with sig_type diagnostic
account_trades keys exchange-truth totals of our trades on one market
fak_buy keys + PMQ_MCP_LIVE=1 open a position: fill-and-kill buy, nothing ever rests
fak_sell keys + PMQ_MCP_LIVE=1 close a position: fill-and-kill sell, same contract
cancel_and_reconcile keys + PMQ_MCP_LIVE=1 cancel everything resting on a market, return exchange truth

With PMQ_MCP_PAPER=1 (the 30-second demo above) the same trading and account tools are registered keyless: fills are simulated at the real best ask, capped by the displayed size, refused under the exchange minimum, and the account tools report the paper balance. Responses keep the live shape, flagged paper: true, and no order ever reaches the exchange.

The rails, all operator-set (server environment, invisible to and untouchable by the model):

Variable Effect Default
PMQ_MCP_LIVE unset: the three trading tools are never REGISTERED; an agent cannot call a tool that does not exist read-only
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
PMQ_MCP_PAPER_USD paper starting balance 1000
PMQ_MCP_MAX_USD hard cap per single order, live and paper alike 10
PMQ_MCP_DAILY_USD cumulative BUY budget per UTC day; confirmed spend counts, an unknown outcome conservatively consumes the full requested amount until reconciled off
POLY_* keys omit them entirely for a data-only server absent

Structural rails on top: only FAK orders exist (nothing rests unattended on the book), every uncertain outcome routes the agent to reconciliation before it may trade that market again, and fills are booked only from exchange confirmations, never from optimism.

{
  "mcpServers": {
    "pmq": {
      "command": "pmq-mcp",
      "env": { "POLY_PRIVATE_KEY": "...", "POLY_FUNDER": "0x...", "POLY_SIG_TYPE": "3" }
    }
  }
}

Leave the POLY_* variables out entirely for a read-only market-data server.

Bot template

bot-template/ is a complete bot minus the strategy, for ANY market (politics, sports, crypto, culture): paper mode against real books with real fees, per-market budgets with fee headroom, poisoned-market reconciliation, consecutive-failure halt, disk-persisted daily loss halt, a systemd unit with RestartPreventExitStatus=42 so halts stay halted, and a lightweight phone dashboard. You implement watchlist() and decide(); the shipped demo strategy is an API illustration meant to be replaced.

Security posture

  • Keys are read from the environment, used to instantiate the signer, and never logged. No custody, no backend, no telemetry, zero network calls besides Polymarket endpoints.
  • A documented wave of fake "polymarket bot" repositories steals private keys; pmq is deliberately small so the entire execution path stays readable in minutes by anyone who wants to look.
  • Fund the trading wallet with what you can afford to lose. Nothing here is financial advice; prediction-market access is restricted in some jurisdictions and compliance is on you.

If you feel like checking any of it

None of the claims above require taking my word; each one comes with a handle you can pull, whenever you care to:

  • Egress. PMQ_CANARY=1 pytest tests/test_canary_live.py -k egress -s records every DNS resolution during a full session (market data, auth derivation, one signed order) and fails on any host outside polymarket.com. Last observed list: clob.polymarket.com, gamma-api.polymarket.com, nothing else. The weekly canary prints that list in public CI logs. One designed exception: pmq-doctor's optional on-chain checks use the public Polygon RPCs named in its source.
  • Provenance. Releases carry a signed PEP 740 attestation (Sigstore, via PyPI trusted publishing): click "provenance" next to any file on the PyPI files page, or fetch it raw from PyPI's integrity API. The signing identity is this repository's publish.yml workflow.
  • Dependencies. Dependabot files weekly bump PRs (Python and SHA-pinned GitHub Actions), and the weekly canary runs pip-audit; a hit opens an issue by itself.
  • The source. Five small modules; the whole execution path reads in minutes. The grep targets that answer the important questions fastest are listed in SECURITY.md.

Stability and maintenance

Pre-1.0 SemVer with a written deprecation window and a stated bar for 1.0; one maintainer, trading his own money through this exact code daily. The operational rule worth knowing: if the canary badge goes red and stays red, treat the project as unmaintained and pin your last known-good version. Full policy and the precisely scoped help-wanted: docs/stability.md.

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pmquant-0.4.9.tar.gz (115.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pmquant-0.4.9-py3-none-any.whl (32.7 kB view details)

Uploaded Python 3

File details

Details for the file pmquant-0.4.9.tar.gz.

File metadata

  • Download URL: pmquant-0.4.9.tar.gz
  • Upload date:
  • Size: 115.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for pmquant-0.4.9.tar.gz
Algorithm Hash digest
SHA256 2581e7f0e261d933f5e5a05d678e6a8de1d35b8cdfcbcfd7a17fe28dff216119
MD5 61ce35930c99b8c3876bf9863091142c
BLAKE2b-256 958def5dcc5b2f03d1aed31d522621fa21304fdac8c5d1ae5eabeb48aa6f79d3

See more details on using hashes here.

Provenance

The following attestation bundles were made for pmquant-0.4.9.tar.gz:

Publisher: publish.yml on crp4222/pmq

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pmquant-0.4.9-py3-none-any.whl.

File metadata

  • Download URL: pmquant-0.4.9-py3-none-any.whl
  • Upload date:
  • Size: 32.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for pmquant-0.4.9-py3-none-any.whl
Algorithm Hash digest
SHA256 fd7eb81f843b6b0bf33641a15d23732227df9f367d2871ef9a88183c5d06a7f0
MD5 5e0576b24a25f2071702f0bb383511d8
BLAKE2b-256 401983aa775d6309ea0be5a05e87044ea1942cb4a5139b696ee1c770888163f9

See more details on using hashes here.

Provenance

The following attestation bundles were made for pmquant-0.4.9-py3-none-any.whl:

Publisher: publish.yml on crp4222/pmq

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page