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 license

Fail-closed execution and market data for Polymarket CLOB V2, in Python. Local signing (your keys never leave your process), exchange-confirmed fills only, fee-correct math, and deposit-wallet (POLY_1271) support that actually works in production.

pip install pmquant        # distribution name pmquant, import name pmq

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

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: the comparison table below names the alternatives and what each does instead; open an issue if it goes stale.

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: the CLOB treats FAK/FOK buys as market orders. pmq routes them through the market-order builder with the correct rounding.
  • 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

The maintainer's own bot trades through this exact executor 24/7 with real money. Example receipt (2026-07-03): settlement transaction 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. Additionally, 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, not your hopes.

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.

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. The buy path has carried live volume; treat the sell path as following the same documented semantics with less battle time.

The signature_type table nobody gives you

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.

Comparison (2026-07-03, factual)

pmq py-clob-client-v2 (official) pmxt NautilusTrader caiovicentino MCP
CLOB V2 signing yes, local yes, local writes via its hosted backend yes, local V1 only (rejected in prod since 2026-04-28)
Confirmed-fill contract yes (core design) no (raw responses) n/a engine-level no
Deposit wallet / POLY_1271 yes, production-proven open issues (#70 and others) n/a untested claim no
Fee math official per-category formula fee at match, no helper via backend fee model fee-blind
Reconciliation helper yes no n/a engine-level no
Footprint one small lib one small lib multi-venue platform full trading framework MCP server

NautilusTrader is excellent if you want a full framework; pmq is the small library you embed in your own bot. pmxt is convenient if you accept routing writes through their backend; pmq exists for self-custody.

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.

Opt out or replace it, one line either way:

PolymarketExecutor(builder_code=None)            # no attribution
PolymarketExecutor(builder_code="0xYOURS...")    # your own code

or set the POLY_BUILDER_CODE environment variable. (Same model as JKorf/Polymarket.Net; the official client defaults to zero attribution.)

MCP server (agents)

pip install "pmquant[mcp]" then run pmq-mcp (stdio). Read tools (market, book, taker_fee, account_collateral, account_trades) always exist. Trading tools (fak_buy, fak_sell, cancel_and_reconcile) are only registered when the operator sets PMQ_MCP_LIVE=1 in the server environment: an agent cannot talk its way past a tool that was never created. Every order is capped per call by PMQ_MCP_MAX_USD (default 10).

{
  "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.
  • Beware of the documented wave of fake "polymarket bot" repositories that steal private keys. Read the source: pmq is small on purpose.
  • 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.

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.0.tar.gz (59.9 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.0-py3-none-any.whl (25.4 kB view details)

Uploaded Python 3

File details

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

File metadata

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

File hashes

Hashes for pmquant-0.4.0.tar.gz
Algorithm Hash digest
SHA256 a69f9f1e3edc5c929e3e213f3ff2b10e28af1f02a37212a3c8095b43b36f896c
MD5 f333772232dc281c2a46266664400af4
BLAKE2b-256 37f3095d019325775f705ec5815d1c4971e8d84c1355738691f68907977a210d

See more details on using hashes here.

Provenance

The following attestation bundles were made for pmquant-0.4.0.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.0-py3-none-any.whl.

File metadata

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

File hashes

Hashes for pmquant-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 0495a5f7c28f42d99dce4d0c989927e5222823105323dc014c47fa40feab7dad
MD5 668b5626791d4b5f01f403654dec7df6
BLAKE2b-256 82b52b1096cfb402776977c7404f4239c20d607b603ba36c95210b56e4850c39

See more details on using hashes here.

Provenance

The following attestation bundles were made for pmquant-0.4.0-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