pnl-truthteller
Audit your Polymarket bot's actual on-chain P&L vs what your bot thinks it earned.
pip install pnl-truthteller
pnl-truthteller --wallet 0xYourPolymarketProxy --output report.md
That's the entire workflow. Wallet address only. No API keys. Read-only. You get a markdown report showing how much your fill slippage is actually costing you.
What we found
We built this because our own crash-recovery bot looked profitable in its SQLite (+$34) but felt like it was bleeding capital. We were right:
| Source | Trades | DB-reported P&L | On-chain P&L | Hidden slippage |
|---|---|---|---|---|
| Our bot | 320 | $+34.31 | $-90.72 | $-125.03 |
Random stranger's wallet (0x1417...) |
65 | $+32.36 | $-30.29 | $-62.66 |
The gap generalises. We tested the tool on a random Polymarket trader's wallet pulled from the public CLOB feed — same pattern. Their DB-equivalent showed +$32 over 65 trades; the chain says -$30; that's $62 of hidden slippage they don't know about.
Both samples are in examples/.
Why this exists
Most Polymarket bots record P&L the moment an order is placed, not when it fills. The CLOB matching engine fills in stages (FOK rejects, partial fills, sweep retries, dust). If your bot writes profit=$3.20 to its DB the moment post_order returns OK, but the actual on-chain fills only retrieve $2.85, you're losing ~11% to slippage and your DB is lying to you about it.
This package finds that gap on your bot.
Install
pip install pnl-truthteller
Or from source:
git clone https://github.com/LuciferForge/pnl-truthteller
cd pnl-truthteller
pip install -e .
Usage — three input modes
1. Direct from CLOB API (recommended — zero setup)
If you trade through py-clob-client or py-clob-client-v2, your fills are queryable from Polymarket's CLOB API. Just give us your wallet address:
pnl-truthteller --wallet 0xYourProxyAddress --output report.md
This pulls every fill for the wallet, groups them by token+direction, and produces a slippage report.
2. From your bot's SQLite (if you've been logging fills locally)
If your bot stores raw CLOB responses in a SQLite file (column raw_response), point the tool at it:
pnl-truthteller --sqlite ~/bot/trades.db \
--positions ~/bot/positions.json \
--output report.md
Schema expected: a live_trades table with columns token_id, side, timestamp, raw_response where raw_response is the JSON string returned by client.post_order(). See docs/data-format.md.
3. From a JSONL file (custom integrations)
If you're on a non-Python stack or have custom logging, dump your trades + fills to JSONL and pass them in:
pnl-truthteller --trades trades.jsonl --fills fills.jsonl --output report.md
Format spec: docs/data-format.md.
What the report looks like
# Slippage Report — 2026-04-28T14:30:00+00:00
## TL;DR
- Closed trades total: 308 (live: 308, paper: 0)
- Lifetime theoretical P&L: +$33.49
- Lifetime actual P&L (on-chain fills): -$89.01
- Total slippage cost: -$122.50 (-365.8% of theoretical)
- Trades with stranded dust on-chain: 31 (total 47.3 shares dust)
## By exit reason
| Reason | n | Theoretical | Actual | Slippage |
|---|---|---|---|---|
| TIMEOUT | 142 | -$18.00 | -$84.50 | -$66.50 |
| TARGET | 71 | +$28.40 | +$22.10 | -$6.30 |
| RECOVERY_TRAILED | 50 | +$15.20 | +$12.40 | -$2.80 |
| STOP | 39 | +$7.89 | +$0.99 | -$6.90 |
## Worst 10 slippage events (per-trade)
[table of the 10 worst trades by slippage]
What this measures, precisely
For each closed trade in your data, the tool:
- Finds the actual BUY fills by matching token+timestamp window, deduplicated by
orderID. - Finds the actual SELL fills that closed the position (same matching, same dedup).
- Computes theoretical P&L =
(exit_price - entry_price) × shares(what the bot's DB says). - Computes actual P&L =
sum(sell_takingAmount) - sum(buy_makingAmount)(what the chain says). - Slippage = actual - theoretical. Negative = your fill ladder walked the book down.
The dedup-by-orderID step is critical. Sweep retries (where your bot tries 5%, 15%, 25% off ref price) often log the same orderID multiple times if your bot calls post_order from a retry loop without checking idempotency. Without dedup you double-count the proceeds and your slippage looks fine when it isn't.
What it does NOT do
- It does NOT execute trades. Read-only auditing.
- It does NOT need your private key. Wallet address only.
- It does NOT report tax-purpose P&L. Slippage-focused, not gain/loss accounting.
- It does NOT work for non-Polymarket markets (yet — see roadmap).
When to run it
| Frequency | Why |
|---|---|
| Once, now | If you've never done a fill-level audit, run it once to find out what your real P&L is. |
| Daily (cron) | If your bot is live, run the report nightly to catch slippage regressions early. |
| After every parameter change | Param changes (entry threshold, exit ladder) shift slippage. Run before/after to measure. |
Roadmap
- v0.2 — Per-market category breakdown (sports vs politics vs crypto have different liquidity profiles)
- v0.3 — Orderbook-depth-at-time-of-fill reconstruction (what was the actual book when you swept?)
- v0.4 — Adapter for Kalshi and other prediction-market venues
- v0.5 — Streamlit dashboard (real-time slippage monitoring)
About the author
Built by LuciferForge, a solo operator running a public-audited Polymarket crash-recovery bot (308 closed trades, 80.2% WR). I built this because my own bot's DB was lying to me — found -$122.50 of hidden slippage cost on 308 trades. Now that the math is honest, the parameters can be too. Yours can be too.
Other tools in the LuciferForge stack:
pip install polymarket-mcp-pro— Polymarket data as MCP tools for Claude / Cursor / Cline.pip install cross-signal-data— 308-trade labeled crash-recovery dataset (free, MIT, also on HuggingFace).pip install quant-rollout— staged-deployment toolkit (gates, kill switch, veto window).pip install sigil-ta— MCP-native TA runtime with the unique Polymarket Sentiment Divergence signal.- LuciferForge/polymarket-v2-migration — V1→V2 cookbook for the April 28, 2026 cutover.
License
MIT. Audit your bot, audit your friends' bots, audit anyone's bot. The chain is public.
Release files for pnl-truthteller 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pnl_truthteller-0.1.1.tar.gz | 19.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pnl_truthteller-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 37.7 kB
Release files / pnl_truthteller-0.1.1.tar.gz
| Download URL | pnl_truthteller-0.1.1.tar.gz |
|---|---|
| Size | 19.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
59d87074b7480b07a28b5df563fc0c847b1cffc5c3d451b92a2a3554fec60335
|
|
BLAKE2b-256 checksum How to use checksums |
bf396894e6a538c3fe3b50e6feda87a154d39c422656e81657e7dad40a15745f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.1.0 CPython/3.8.9
|
Release files / pnl_truthteller-0.1.1-py3-none-any.whl
| Download URL | pnl_truthteller-0.1.1-py3-none-any.whl |
|---|---|
| Size | 18.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
9e59fcc586af474c36e60d5e3aaed8e8ab95c7fd0c0bac67f8892947b0c0b73d
|
|
BLAKE2b-256 checksum How to use checksums |
8bce8d44155640e415ba83cadb92b129adf8d45fd5407d5cc05829f4953501ed
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.1.0 CPython/3.8.9
|