1. Bot Identity
| Layer | Governance Governance |
|---|
| Bot class | Governance Service |
|---|
| Authority | Explain |
|---|
| Status | LIVE |
|---|
| Readiness | General live |
|---|
| Runs before | Nothing — runs post-trade on every fill event and on the configured report_window cadence |
|---|
| Runs after | Order fill confirmation from CTFExchangeV2 match event |
|---|
| Applies to | Every filled or partially filled order; daily and weekly P&L windows across all bots, markets, and builders |
|---|
| Default mode | general_live |
|---|
| User-visible | Summary and detail view |
|---|
| Developer owner | Polytraders core — Governance pod |
|---|
2. Purpose
PnLReporter reconciles all fill events into realised and unrealised P&L, denominated in pUSD. It reads fee from the fill's match event (not from the signed order — fees are operator-set at match time in V2). It accrues maker rebates (20–25% of fees, paid in pUSD, per market) and credits them against gross fees. It groups P&L by bot, market, and builder as configured. It emits a SettlementReport after every fill and on the configured window cadence for regulatory retention. PnLReporter is the authoritative post-trade ledger for all pUSD flows.
3. Why This Bot Matters
Fee read from signed order instead of match event
In V2, operator fees are set at match time — not on the signed order. Reading fees from the order produces systematically wrong P&L. Maker rebates accrued against wrong fee basis will cause discrepancies in regulatory reports.
Maker rebate not accrued
Gross fees overstate true cost. P&L appears worse than actual. Fee dispute evidence is incomplete.
P&L not denominated in pUSD
Mixed USDC.e and pUSD figures corrupt the ledger. Post-trade reports cannot be reconciled against on-chain settlement.
Realised P&L not separated from unrealised
Open positions inflate reported returns. Regulatory settlement reports misrepresent actual cash flows.
Negative-risk positions mis-valued
Multi-outcome market positions (NegRiskAdapter) have non-linear payoffs. Treating them as independent binary positions overstates or understates exposure.
No worked examples on this bot yet. Worked examples are optional but strongly recommended — they turn an abstract failure mode into something a developer can verify in a fixture.
6. Parameter Guide
| Parameter | Default | Warning | Hard | What it controls |
|---|
| report_window | daily | None | None | Cadence of the aggregate SettlementReport: daily or weekly. |
| group_by | bot | None | None | Dimension along which P&L is aggregated: bot, market, or builder. |
| include_paper | False | None | None | When true, include paper-trading fills in the P&L report alongside live fills. |
| reconcile_tolerance_pusd | 0.01 | 5.0 | 50.0 | Maximum pUSD difference between aggregate P&L total and on-chain balance before the reconciliation is flagged as failed. |
7. Detailed Parameter Instructions
report_window
What it means
Cadence of the aggregate SettlementReport: daily or weekly.
Default
{ "report_window": "daily" }
Why this default matters
Daily aligns with Polymarket's rolling attribution report and typical regulatory reconciliation cadence.
Threshold logic
| Condition | Action |
|---|
| report_window=daily | Emit aggregate SettlementReport every 24h |
| report_window=weekly | Emit aggregate SettlementReport every 7 days |
Developer check
assert p.report_window in ('daily', 'weekly')
User-facing English
Your P&L summary is updated and reported on a regular schedule.
group_by
What it means
Dimension along which P&L is aggregated: bot, market, or builder.
Default
{ "group_by": "bot" }
Why this default matters
Grouping by bot aligns with strategy-level performance attribution used for risk management.
Threshold logic
| Condition | Action |
|---|
| group_by in (bot, market, builder) | Apply grouping |
| invalid value | Reject config; emit ConfigError |
Developer check
assert p.group_by in ('bot', 'market', 'builder')
User-facing English
P&L is broken down by trading strategy.
include_paper
What it means
When true, include paper-trading fills in the P&L report alongside live fills.
Default
{ "include_paper": false }
Why this default matters
False by default — paper fills must not appear in regulatory SettlementReports. Enable only for development dashboards.
Threshold logic
| Condition | Action |
|---|
| include_paper=true AND report is regulatory | WARN — paper fills must not be in regulatory reports; flag report accordingly |
| include_paper=false | Only live fills included |
Developer check
if (p.include_paper && report.is_regulatory) log.warn('PNL_REPORTER_PAPER_IN_REGULATORY')
User-facing English
Live P&L reporting excludes test trades.
reconcile_tolerance_pusd
What it means
Maximum pUSD difference between aggregate P&L total and on-chain balance before the reconciliation is flagged as failed.
Default
{ "reconcile_tolerance_pusd": 0.01 }
Why this default matters
A 0.01 pUSD tolerance accounts for floating-point rounding only. Larger values risk masking genuine discrepancies.
Threshold logic
| Condition | Action |
|---|
| abs(pnl_total - onchain_balance) <= 0.01 | onchain_reconciled=true |
| 0.01–50 pUSD | WARN PNL_REPORTER_ONCHAIN_RECONCILE_FAIL |
| > 50 pUSD | Page alert; flag report as unreconciled; hold 7y retention record |
Developer check
if (drift > p.hard) alerting.emit("PNL_REPORTER_ONCHAIN_RECONCILE_FAIL", { drift })
User-facing English
— not yet authored —
8. Default Configuration
{
"bot_id": "gov.pnl_reporter",
"version": "2.0.0",
"mode": "general_live",
"defaults": {
"report_window": "daily",
"group_by": "bot",
"include_paper": false,
"reconcile_tolerance_pusd": 0.01
}
}
9. Implementation Flow
- On every fill event from CTFExchangeV2: extract fill_id, order_id, market_id, side, size_pusd, price, and fee_bps from the MATCH EVENT (not from the signed order).
- Compute gross_fee_pusd = size_pusd * fee_bps / 10_000. Distinguish maker vs taker side; apply maker rebate (20–25% of fee, per market, paid in pUSD).
- Compute net_fee_pusd = gross_fee_pusd - maker_rebate_pusd. Validate taker fee_bps <= 100, maker fee_bps <= 50.
- Determine position delta: BUY increases long position; SELL decreases or creates short position.
- Compute realised_pnl_pusd on position close using FIFO cost basis. Mark open positions at current mid-price for unrealised_pnl_pusd.
- For negative-risk markets (negRisk=true), use NegRiskAdapter payoff function instead of binary payoff.
- Append fill record to the P&L ledger (Postgres) with all fields including net_fee_pusd, maker_rebate_pusd, realised_pnl_pusd, unrealised_pnl_pusd.
- Emit SettlementReport per fill event (emit-every) to satisfy regulatory retention requirement.
- At report_window boundary, compute aggregate SettlementReport grouped by group_by dimension. Cross-check totals against on-chain pUSD balance.
- Retain all SettlementReport records for 7 years (regulatory requirement). Use WAL-backed store with retry on bus failure.
10. Reference Implementation
Consumes fill events from CTFExchangeV2, reads fee from match event, accrues maker rebates, computes realised and unrealised pUSD P&L with negRisk awareness, emits SettlementReport per fill and on window cadence.
Pseudocode is language-agnostic. FETCH = read input. EMIT = produce output. Translate to TS/Python/Go/Rust.
// ---- PER-FILL HANDLER ----
FUNCTION onFill(matchEvent):
// V2: fee is operator-set at match time — read from matchEvent, NOT from order
IF matchEvent.fee_bps IS NULL:
alerting.emit('PNL_REPORTER_FEE_MISSING', { fill_id: matchEvent.fill_id })
quarantine(matchEvent)
RETURN
side = matchEvent.side // 'maker' | 'taker'
IF side == 'taker' AND matchEvent.fee_bps > 100:
alerting.emit('PNL_REPORTER_FEE_CAP_EXCEEDED', { fill_id: matchEvent.fill_id, fee_bps: matchEvent.fee_bps })
quarantine(matchEvent)
RETURN
IF side == 'maker' AND matchEvent.fee_bps > 50:
alerting.emit('PNL_REPORTER_FEE_CAP_EXCEEDED', { fill_id: matchEvent.fill_id, fee_bps: matchEvent.fee_bps })
quarantine(matchEvent)
RETURN
size_pusd = toPusdUnits(matchEvent.size_usd)
gross_fee_pusd = size_pusd * matchEvent.fee_bps / 10_000
// Maker rebate: 20–25% of fee, paid in pUSD, per market
rebate_rate = FETCH clob_public.GET('/market-rebate-rate?market_id=' + matchEvent.market_id)
maker_rebate_pusd = 0
IF side == 'maker':
maker_rebate_pusd = gross_fee_pusd * rebate_rate // rebate_rate in [0.20, 0.25]
net_fee_pusd = gross_fee_pusd - maker_rebate_pusd
// Market metadata — check negRisk
market = FETCH clob_public.getMarketByConditionId(matchEvent.condition_id)
// Position delta and realised P&L (FIFO cost basis)
position = portfolio_sync.getPosition(matchEvent.market_id, matchEvent.token_id)
realised_pnl_pusd = computeRealisedPnL(position, matchEvent, costBasis='FIFO')
// Unrealised P&L: mark open positions at current mid-price
IF market.negRisk:
unrealised_pnl_pusd = negRiskPayoff(position, market)
ELSE:
mid_price = FETCH clob_public.GET('/midprice?token_id=' + matchEvent.token_id)
unrealised_pnl_pusd = (mid_price - position.avg_cost) * position.remaining_size
record = {
fill_id: matchEvent.fill_id,
market_id: matchEvent.market_id,
side: side,
size_pusd: size_pusd,
price: matchEvent.price,
fee_bps_match_event: matchEvent.fee_bps,
gross_fee_pusd: gross_fee_pusd,
maker_rebate_pusd: maker_rebate_pusd,
net_fee_pusd: net_fee_pusd,
realised_pnl_pusd: realised_pnl_pusd,
unrealised_pnl_pusd: unrealised_pnl_pusd,
negRisk: market.negRisk,
fill_confirmed_at_ms: matchEvent.timestamp
}
postgres.INSERT('pnl_ledger', record)
EMIT SettlementReport(event_type='FILL_PNL_COMPUTED', fill_id=matchEvent.fill_id, ...record)
// ---- WINDOW REPORT ----
FUNCTION emitWindowReport(windowStart, windowEnd):
rows = postgres.SELECT('pnl_ledger', WHERE fill_confirmed_at_ms BETWEEN windowStart AND windowEnd)
agg = aggregatePnL(rows, group_by=config.group_by)
onchain_balance = FETCH onchain.getBalance(wallet_address, token='pUSD')
onchain_reconciled = abs(agg.net_pnl_pusd - onchain_balance) < RECONCILE_TOLERANCE
EMIT SettlementReport(event_type='SETTLEMENT_REPORT', retention='7y', ...agg, onchain_reconciled=onchain_reconciled)
SDK calls used
clob_public.GET('/market-rebate-rate?market_id=...')clob_public.getMarketByConditionId(condition_id)clob_public.GET('/midprice?token_id=...')clob_auth.GET('/fills?order_id=...')onchain.getBalance(wallet_address, token='pUSD')toPusdUnits(raw_usd)postgres.INSERT('pnl_ledger', record)alerting.emit('PNL_REPORTER_FEE_MISSING', metadata)
Complexity: O(F) per window where F = fill count; O(1) per fill event
11. Wire Examples
Input — what arrives on the wire
{
"label": "CTFExchangeV2 match event (fill with operator-set fee)",
"source": "clob_auth",
"payload": {
"fill_id": "fill_00a1b2c3d4e5f6a7",
"order_id": "ord_00123",
"market_id": "0x9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c",
"condition_id": "0x3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f",
"side": "maker",
"size_usd": 500.0,
"price": 0.58,
"fee_bps": 25,
"negRisk": false,
"timestamp": 1746792060000
}
}
Output — what the bot emits
{
"label": "SettlementReport — FILL_PNL_COMPUTED",
"payload": {
"report_id": "settlement_fill_fill_00a1b2c3d4e5f6a7",
"bot_id": "gov.pnl_reporter",
"event_type": "FILL_PNL_COMPUTED",
"fill_id": "fill_00a1b2c3d4e5f6a7",
"market_id": "0x9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c",
"side": "maker",
"size_pusd": 500.0,
"price": 0.58,
"fee_bps_match_event": 25,
"gross_fee_pusd": 12.5,
"maker_rebate_pusd": 3.12,
"net_fee_pusd": 9.38,
"realised_pnl_pusd": 0,
"unrealised_pnl_pusd": 11.0,
"negRisk": false,
"fill_confirmed_at_ms": 1746792060000,
"report_kind": "SettlementReport",
"retained_until": "2033-05-09"
}
}
12. Decision Logic
APPROVE
Not applicable — PnLReporter does not approve or reject orders.
RESHAPE_REQUIRED
Not applicable.
REJECT
Not applicable as a trading decision. PnLReporter will flag and quarantine fills where fee_bps exceeds V2 caps (taker > 100 bps or maker > 50 bps).
WARNING_ONLY
include_paper=true on a regulatory report emits a warning. Unrealised P&L calculations that fail to fetch mark-to-market prices emit WARN STALE_DATA and use last known price.
13. Standard Decision Output
This bot returns a SettlementReport object. See SettlementReport schema.
{
"report_id": "settlement_pnl_20260509T000000Z",
"bot_id": "gov.pnl_reporter",
"event_type": "SETTLEMENT_REPORT",
"window_start": "2026-05-08T00:00:00Z",
"window_end": "2026-05-09T00:00:00Z",
"group_by": "bot",
"total_fills": 143,
"gross_volume_pusd": 87450.0,
"realised_pnl_pusd": 1240.5,
"unrealised_pnl_pusd": 312.0,
"gross_fees_pusd": 218.6,
"maker_rebates_pusd": 49.7,
"net_fees_pusd": 168.9,
"net_pnl_pusd": 1071.6,
"onchain_balance_pusd": 52341.0,
"onchain_reconciled": true,
"negRisk_positions": 2,
"report_kind": "SettlementReport",
"retained_until": "2033-05-09"
}
14. Reason Codes
| Code | Severity | Meaning | Action | User-facing message |
|---|
PNL_REPORTER_FILL_PNL_COMPUTED | INFO | Fill event successfully processed; realised and unrealised P&L computed; SettlementReport emitted. | No action — routine. | A trade was processed and your P&L was updated. |
PNL_REPORTER_FEE_MISSING | HARD_REJECT | Fill event arrived without fee_bps in the match event. Cannot compute net P&L without match-event fee. | Quarantine fill; emit alert; require manual review before inclusion in ledger. | |
PNL_REPORTER_FEE_CAP_EXCEEDED | WARN | Observed fee_bps in match event exceeds V2 cap (taker > 100 bps or maker > 50 bps). | Quarantine fill; emit alert for manual review. | |
PNL_REPORTER_PAPER_IN_REGULATORY | WARN | include_paper=true is set on a report that is flagged as regulatory. Paper fills must not appear in regulatory reports. | Flag report as mixed; emit WARN; do not include in 7-year retention tier. | |
PNL_REPORTER_ONCHAIN_RECONCILE_FAIL | WARN | Aggregate P&L total diverges from on-chain pUSD balance beyond the reconciliation tolerance. | Flag report as unreconciled; emit alert; retry reconciliation after RPC cooldown. | P&L reconciliation with on-chain balance is pending. |
STALE_DATA | WARN | Mark-to-market price for an open position is stale (last fetch > 60s ago). Unrealised P&L uses last known price. | Emit WARN; use stale price for unrealised calculation; retry fetch on next cycle. | Unrealised P&L estimate uses the most recently available price. |
PNL_REPORTER_NEGRISK_DETECTED | INFO | A negative-risk market position was detected; NegRiskAdapter payoff function applied. | No action — informational. | A multi-outcome market position is included in your P&L, valued using the correct payoff formula. |
MARKET_CLOSED | INFO | A market has resolved; open positions closed; realised P&L finalised at settlement price. | Compute final realised P&L; emit SettlementReport with event_type=MARKET_SETTLED. | A market you participated in has resolved. Final P&L has been recorded. |
15. Metrics & Logs
Metrics emitted
| Metric | Type | Unit | Labels | Meaning |
|---|
polytraders_gov_pnlreporter_fills_processed_total | counter | count | side, negRisk | Total fill events processed and included in the P&L ledger. |
polytraders_gov_pnlreporter_gross_volume_pusd_total | counter | usd | | Cumulative gross pUSD volume across all processed fills. |
polytraders_gov_pnlreporter_net_fees_pusd_total | counter | usd | side | Cumulative net fees (after maker rebates) in pUSD. |
polytraders_gov_pnlreporter_maker_rebates_pusd_total | counter | usd | | Cumulative maker rebates accrued in pUSD. |
polytraders_gov_pnlreporter_quarantine_count | gauge | count | | Number of fill records currently quarantined (missing fee, fee cap exceeded, etc.). |
polytraders_gov_pnlreporter_onchain_reconcile_drift_pusd | gauge | usd | | Absolute drift between aggregate P&L and on-chain pUSD balance at last reconciliation. |
Alerts
| Alert | Condition | Severity | Runbook |
|---|
PnLReporterFeeMissing | rate(polytraders_gov_pnlreporter_quarantine_count[5m]) > 0 | page | #runbook-pnlreporter-fee-missing |
PnLReporterOnchainReconcileDrift | polytraders_gov_pnlreporter_onchain_reconcile_drift_pusd > 10 | page | #runbook-pnlreporter-reconcile-drift |
PnLReporterNoFillsIn1h | rate(polytraders_gov_pnlreporter_fills_processed_total[1h]) == 0 | warn | #runbook-pnlreporter-no-fills |
PnLReporterFeeCap | rate(polytraders_gov_pnlreporter_quarantine_count[5m]) > 5 | page | #runbook-pnlreporter-fee-cap |
Dashboards
- Grafana — Governance / PnL Reporter daily P&L and fee breakdown
- Grafana — Fee accounting / maker rebates vs gross fees (pUSD)
16. Developer Reporting
{
"bot_id": "gov.pnl_reporter",
"event_type": "FILL_PNL_COMPUTED",
"fill_id": "fill_00a1b2c3d4e5f6a7",
"market_id": "0x9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c",
"side": "BUY",
"size_pusd": 500.0,
"price": 0.58,
"fee_bps_from_match_event": 25,
"gross_fee_pusd": 12.5,
"maker_rebate_pusd": 2.5,
"net_fee_pusd": 10.0,
"realised_pnl_pusd": 0,
"unrealised_pnl_pusd": 10.0,
"negRisk": false,
"fill_confirmed_at_ms": 1746792060000
}
17. Plain-English Reporting
| Situation | User-facing explanation |
|---|
| Daily P&L report generated | Today's trading summary: total volume, realised gains/losses, fees paid, and maker rebates received — all in pUSD. |
| Maker rebate credited | A portion of the trading fee was returned as a maker rebate, credited in pUSD to reduce the net cost of the trade. |
| P&L cross-check with on-chain balance passed | The reported P&L matches the actual on-chain pUSD balance. Records are reconciled. |
| Negative-risk position in report | Some positions are in multi-outcome markets. Their P&L is calculated using the correct multi-outcome payoff formula. |
18. Failure-Mode Block
| main_failure_mode | Fill event arrives without fee_bps in the match event (e.g., from a non-standard match path). PnLReporter cannot compute net P&L without the match-event fee. |
|---|
| false_positive_risk | On-chain balance check fails due to RPC latency, causing a spurious reconciliation mismatch. The report is flagged as unreconciled until the next check. |
|---|
| false_negative_risk | A negative-risk position is mis-identified as a standard binary position because the negRisk flag is not set on the market metadata, leading to incorrect unrealised P&L. |
|---|
| safe_fallback | If fee_bps is missing from the match event, quarantine the fill and emit PNL_REPORTER_FEE_MISSING. Do not estimate or default the fee — the fill must be manually reviewed. If mark-to-market prices are stale, use last known price and emit STALE_DATA warn. |
|---|
| required_dependencies | CTFExchangeV2 fill events with match-event fee_bps, CLOB public API (mark-to-market prices), On-chain pUSD balance RPC, gov.portfolio_sync position store, gov.builder_attribution fill log, Postgres P&L ledger |
|---|
19. Failure-Injection Recipes
| Scenario | How to inject | Expected behaviour | Recovery |
|---|
FEE_MISSING_FROM_MATCH_EVENT | Emit a synthetic fill event without the fee_bps field | PNL_REPORTER_FEE_MISSING raised; fill quarantined; not included in P&L totals | Manual review and re-ingestion of corrected fill event required. |
FEE_CAP_EXCEEDED | Inject fill with match_event.fee_bps=120 (taker side) | PNL_REPORTER_FEE_CAP_EXCEEDED raised; fill quarantined | Manual review; correct fee or reject fill. |
STALE_MIDPRICE | Block CLOB public API price endpoint for 90s | STALE_DATA warn emitted; unrealised P&L uses last known price | Once price endpoint recovers, next fill event re-fetches fresh price. |
ONCHAIN_RPC_FAILURE | Block Polygon RPC calls during window report generation | onchain_reconciled=false in SettlementReport; PNL_REPORTER_ONCHAIN_RECONCILE_FAIL alert | Retry reconciliation after RPC recovers; flag previous reports as pending reconciliation. |
NEGRISK_FLAG_MISSING | Submit fill for a negative-risk market but set negRisk=false in market metadata | Standard binary payoff used; WARN emitted if position size exceeds normal binary range | Update market metadata with correct negRisk flag; re-compute unrealised P&L. |
20. State & Persistence
Cold-start recovery
Postgres ledger is durable. On restart, in-flight fill events may be reprocessed — idempotency enforced by fill_id uniqueness constraint.
21. Concurrency & Idempotency
| Aspect | Specification |
|---|
| Execution model | single-threaded event loop (per-fill hook) + scheduled window report goroutine |
| Max in-flight | 500 |
| Idempotency key | fill_id |
| Per-call timeout (ms) | 1000 |
| Backpressure strategy | shed fill events beyond max_in_flight; emit WARN; process backlog on recovery |
| Locking / mutual exclusion | Postgres unique constraint on fill_id; serializable isolation for aggregate report generation |
22. Dependencies
Depends on (must run first)
Sibling bots (same OrderIntent)
External services
| Service | Endpoint | SLA assumed | On failure |
|---|
| Polymarket CLOB v2 (fill events, mid-prices, market metadata) | | 99.95% / 200ms p99 (Polymarket-published) | |
| Polygon on-chain RPC (pUSD balance) | | 99.9% / 500ms p99 | |
23. Security Surfaces
On-chain contract calls
| Contract | Method | Network | Effect |
|---|
CTFExchangeV2 | | polygon | |
Abuse vectors considered
- Injecting a fill event with artificially low fee_bps to inflate net P&L
- Setting include_paper=true to mix paper and live fills in regulatory reports
- Manipulating the mark-to-market price feed to inflate unrealised P&L
Mitigations
- Fee is always read from the match event — order-level fee manipulation is ignored
- Fee cap validation (taker <=100 bps, maker <=50 bps) rejects anomalous fills
- Regulatory reports are flagged if include_paper=true; paper fills are segregated
- On-chain balance reconciliation detects any P&L inflation that does not match actual pUSD flows
- Postgres unique constraint on fill_id prevents duplicate processing
24. Polymarket V2 Compatibility
| Aspect | Value |
|---|
| CLOB version | v2 |
| Collateral asset | pUSD |
| EIP-712 Exchange domain version | 2 |
| Aware of builderCode field | yes |
| Aware of negative-risk markets | yes |
| Multi-chain ready | no |
| SDK used | py-clob-client-v2 |
| Settlement contract | CTFExchangeV2 on Polygon |
| Notes | Fees are operator-set at match time in V2 — PnLReporter reads fee_bps exclusively from the CTFExchangeV2 match event, never from the signed order. Maker rebates are 20–25% of fees, paid in pUSD, per market. Builder fees: taker max 100 bps, maker max 50 bps, 1 bp granularity. All amounts denominated in pUSD. NegRiskAdapter payoff used for negative-risk market positions. |
API surfaces declared
clob_publicclob_authonchaininternal
Networks supported
polygon
25. Versioning & Migration
| Field | Value |
|---|
| spec | 2.0.0 |
| implementation | 2.1.0 |
| schema | 2 |
| released | 2026-04-28 |
Migration history
| Date | From | To | Reason | Action taken |
|---|
| 2026-04-28 | v1 (USDC.e, feeRateBps on order) | v2 (pUSD, fee from match event) | CLOB V2 cutover | Removed feeRateBps from signed-order P&L pipeline; now reads fee_bps exclusively from CTFExchangeV2 match event. Updated all P&L amounts from USDC.e to pUSD denomination. Added maker rebate accrual (20–25% per market). Added NegRiskAdapter payoff path for negative-risk positions. Updated regulatory retention to 7y. Switched SDK to py-clob-client-v2. |
26. Acceptance Tests
Unit Tests
| Test | Setup | Expected result |
|---|
| Fee read from match event, not from signed order | Fill event with match_event.fee_bps=30 and order.fee_bps=0 (V2 order has no fee field) | gross_fee_pusd computed from match_event.fee_bps=30 |
| Maker rebate accrued at 20–25% of fee | Maker fill; fee_bps=40; rebate_rate=0.25 | maker_rebate_pusd = size_pusd * 40/10000 * 0.25 |
| Taker fee_bps > 100 flagged | fill with fee_bps=110, side=taker | PNL_REPORTER_FEE_CAP_EXCEEDED quarantine; fill not included in P&L |
| Missing fee_bps in match event quarantines fill | Fill event with no fee_bps field | PNL_REPORTER_FEE_MISSING; fill quarantined for manual review |
| Negative-risk position uses correct payoff function | Market negRisk=true; position size=100; outcome price=0.4 | unrealised_pnl_pusd computed via NegRiskAdapter payoff, not binary |
| Realised P&L computed on position close (FIFO) | Open position: 100 shares at 0.50; close: 100 shares at 0.70 | realised_pnl_pusd = 20 pUSD (before fees) |
Integration Tests
| Test | Expected result |
|---|
| End-to-end fill → SettlementReport → Postgres ledger | SettlementReport emitted per fill; aggregate report at window boundary; 7y retention tag applied |
| On-chain reconciliation passes for matching balance | onchain_reconciled=true in aggregate SettlementReport |
Property Tests
| Property | Required behaviour |
|---|
| Every fill event produces exactly one SettlementReport entry | Always true — emit-every policy enforced |
| No fill with missing fee_bps is ever included in a P&L total | Always true — quarantine on missing fee enforced |
| All P&L amounts are denominated in pUSD (no USDC.e) | Always true |
27. Operational Runbook
PnLReporter incidents involve missing fees in match events (critical — halts ledger for affected fills), on-chain reconciliation drift (regulatory flag), or stale price feeds (unrealised P&L degraded). Missing fee is always P1.
On-call actions
| Alert | First step | Diagnosis | Mitigation | Escalate to |
|---|
PnLReporterFeeMissing | Identify which fills are quarantined; check match event schema for fee_bps field. May indicate a CLOB API format change. | | | Governance pod lead immediately |
PnLReporterOnchainReconcileDrift | Check Polygon RPC status. If RPC healthy, review pnl_ledger for quarantined fills that should be included. | | | Governance pod lead + finance team if drift persists > 1h |
PnLReporterNoFillsIn1h | Check CLOB feed and CTFExchangeV2 event stream. May be a market quiet period or a feed disconnection. | | | Exec pod lead if feed disconnection confirmed |
PnLReporterFeeCap | Review quarantined fills for fee_bps values. Check for operator mis-configuration of fee rates. | | | Governance pod lead |
Manual overrides
polytraders gov pnl reprocess --fill-id <id> --reviewed-by <operator> — After manually verifying a quarantined fill, re-ingest it with corrected fields.
Healthcheck
Endpoint: /internal/health/pnl-reporter | Green: Last fill processed < 300s ago (during active trading); Postgres pnl_ledger writable; quarantine_count not growing. | Red: No fills processed in > 1h during trading hours; Postgres write failure; quarantine_count growing at > 1/min.
29. Developer Checklist
Ready-to-ship score: 27/27 sections complete · 100%
| Requirement | Status |
|---|
| Purpose defined | ✓ done |
| Required inputs listed | ✓ done |
| Parameters defined | ✓ done |
| Defaults defined | ✓ done |
| Warning thresholds defined | ✓ done |
| Hard thresholds defined | ✓ done |
| Safe fallback defined | ✓ done |
| Structured output defined | ✓ done |
| Developer log defined | ✓ done |
| Plain-English explanation | ✓ done |
| Unit tests defined | ✓ done |
| Integration tests defined | ✓ done |
| Property tests defined | ✓ done |
| Failure-mode block complete | ✓ done |
| Reference implementation pseudocode | ✓ done |
| Wire examples (input + output) | ✓ done |
| Reason codes listed | ✓ done |
| Metrics & logs defined | ✓ done |
| State & persistence defined | ✓ done |
| Concurrency & idempotency defined | ✓ done |
| Dependencies declared | ✓ done |
| Security surfaces declared | ✓ done |
| Polymarket V2 compatibility declared | ✓ done |
| Version & migration history declared | ✓ done |
| Operational runbook defined | ✓ done |
| Promotion gates defined | ✓ done |
| Failure-injection recipes defined | ✓ done |