Metadata-Version: 2.4
Name: pmquant
Version: 0.4.8
Summary: 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-URL: Homepage, https://github.com/crp4222/pmq
Project-URL: Issues, https://github.com/crp4222/pmq/issues
Author: crp4222
License: MIT
License-File: LICENSE
Keywords: builder-code,clob,clob-v2,deposit-wallet,execution,fak,market-order,orderbook,poly-1271,polymarket,prediction-markets,quant,trading
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Financial and Insurance Industry
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Office/Business :: Financial :: Investment
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: py-clob-client-v2<2,>=1.0.2
Provides-Extra: dev
Requires-Dist: hypothesis>=6.100; extra == 'dev'
Requires-Dist: mcp>=1.2; extra == 'dev'
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: pytest-cov>=5; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Provides-Extra: mcp
Requires-Dist: mcp>=1.2; extra == 'mcp'
Description-Content-Type: text/markdown

# pmq

<!-- mcp-name: io.github.crp4222/pmq -->

[![PyPI](https://img.shields.io/pypi/v/pmquant)](https://pypi.org/project/pmquant/)
[![tests](https://github.com/crp4222/pmq/actions/workflows/test.yml/badge.svg)](https://github.com/crp4222/pmq/actions/workflows/test.yml)
[![canary](https://github.com/crp4222/pmq/actions/workflows/canary.yml/badge.svg)](https://github.com/crp4222/pmq/actions/workflows/canary.yml)
[![coverage gate](https://img.shields.io/badge/coverage-%E2%89%A585%25%20enforced%20in%20CI-blue)](.github/workflows/test.yml)
[![typed](https://img.shields.io/badge/types-mypy%20strict-blue)](pyproject.toml)
[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/crp4222/pmq/badge)](https://scorecard.dev/viewer/?uri=github.com/crp4222/pmq)
[![license](https://img.shields.io/badge/license-MIT-green)](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.

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

```json
{ "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:

```text
> 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](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](#agents-the-mcp-server).

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](docs/comparison.md) names the
alternatives and what each does instead; open an issue if it goes stale.

## Quickstart

Market data needs no keys:

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

```python
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](https://github.com/Polymarket/py-clob-client-v2/issues/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](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](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`](https://polygonscan.com/tx/0x387f5f09c031bb36a71c54adc978b1ed4d50c67f6dd3f0c2c8068391100d88a8)
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`](https://polygonscan.com/tx/0x1b60f19a6f089624f27babb58bf82538c49f044ee83778783195e26a33c35d09),
where the `maker_orders` slice accounting that release 0.4.6 encodes is
visible in the raw trade record. A weekly
[canary workflow](.github/workflows/canary.yml) 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

```bash
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](docs/assets/pmq-doctor.svg)

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](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](https://registry.modelcontextprotocol.io) 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](#try-it-in-30-seconds-no-keys)
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.

```json
{
  "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/](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](../../actions/workflows/canary.yml) 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](https://pypi.org/project/pmquant/#files), 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](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](docs/stability.md).

## License

MIT
