Polytraders Dev Guide
internal
v3 spine Phase 1 · Shared contracts 9 demo-wired · 0 shadow-ready · 0 production-live · 100 pending · 109 total 15/33 infra tasks the plan status board
HomeBy LayerGovernance6.3 PnL Reporter

6.3 PnL Reporter

Governance Governance Service Explain LIVE General live capital · Indirect P3 · Reporting & event store pending stub

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.

v3 readiness

Docs27/27
donehow scored
Impl0/15
pendinghow scored
Backtest0/4
pendinghow scored
Runtime0/8
pendinghow scored

A bot is done when all four scores are. What does done mean?

1. Bot Identity

LayerGovernance  Governance
Bot classGovernance Service
AuthorityExplain
StatusLIVE
ReadinessGeneral live
Runs beforeNothing — runs post-trade on every fill event and on the configured report_window cadence
Runs afterOrder fill confirmation from CTFExchangeV2 match event
Applies toEvery filled or partially filled order; daily and weekly P&L windows across all bots, markets, and builders
Default modegeneral_live
User-visibleSummary and detail view
Developer ownerPolytraders 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.

4. Required Polymarket Inputs

InputSourceRequired?Use
Fill confirmation from CTFExchangeV2 match event (includes operator-set fee_bps at match time)clob_authYesPrimary fill record. Fee is extracted from match event field — not from the signed order.
Mark-to-market prices for open positionsclob_publicYesCompute unrealised P&L by marking open positions at current mid-price.
On-chain balance query (pUSD wallet balance)onchainYesCross-check realised P&L against actual on-chain pUSD balance for regulatory reconciliation.
Market metadata (negRisk flag, condition_id, market_type)clob_publicYesIdentify negative-risk markets for correct payoff valuation.

5. Required Internal Inputs

InputSourceRequired?Use
Position store snapshotgov.portfolio_syncYesCurrent open position sizes per token per market, used for unrealised P&L computation.
BuilderAttribution fill loggov.builder_attributionYesJoin fills with builder code and builder_fee_pusd for per-builder P&L breakdown.
KillSwitch active flagKillSwitchNoWhen KillSwitch is active, continue recording fills but flag the reporting window as impacted.

6. Parameter Guide

ParameterDefaultWarningHardWhat it controls
report_windowdailyNoneNoneCadence of the aggregate SettlementReport: daily or weekly.
group_bybotNoneNoneDimension along which P&L is aggregated: bot, market, or builder.
include_paperFalseNoneNoneWhen true, include paper-trading fills in the P&L report alongside live fills.
reconcile_tolerance_pusd0.015.050.0Maximum 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

ConditionAction
report_window=dailyEmit aggregate SettlementReport every 24h
report_window=weeklyEmit 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

ConditionAction
group_by in (bot, market, builder)Apply grouping
invalid valueReject 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

ConditionAction
include_paper=true AND report is regulatoryWARN — paper fills must not be in regulatory reports; flag report accordingly
include_paper=falseOnly 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

ConditionAction
abs(pnl_total - onchain_balance) <= 0.01onchain_reconciled=true
0.01–50 pUSDWARN PNL_REPORTER_ONCHAIN_RECONCILE_FAIL
> 50 pUSDPage 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

  1. 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).
  2. 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).
  3. Compute net_fee_pusd = gross_fee_pusd - maker_rebate_pusd. Validate taker fee_bps <= 100, maker fee_bps <= 50.
  4. Determine position delta: BUY increases long position; SELL decreases or creates short position.
  5. Compute realised_pnl_pusd on position close using FIFO cost basis. Mark open positions at current mid-price for unrealised_pnl_pusd.
  6. For negative-risk markets (negRisk=true), use NegRiskAdapter payoff function instead of binary payoff.
  7. 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.
  8. Emit SettlementReport per fill event (emit-every) to satisfy regulatory retention requirement.
  9. At report_window boundary, compute aggregate SettlementReport grouped by group_by dimension. Cross-check totals against on-chain pUSD balance.
  10. 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

CodeSeverityMeaningActionUser-facing message
PNL_REPORTER_FILL_PNL_COMPUTEDINFOFill 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_MISSINGHARD_REJECTFill 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_EXCEEDEDWARNObserved 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_REGULATORYWARNinclude_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_FAILWARNAggregate 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_DATAWARNMark-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_DETECTEDINFOA 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_CLOSEDINFOA 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

MetricTypeUnitLabelsMeaning
polytraders_gov_pnlreporter_fills_processed_totalcountercountside, negRiskTotal fill events processed and included in the P&L ledger.
polytraders_gov_pnlreporter_gross_volume_pusd_totalcounterusdCumulative gross pUSD volume across all processed fills.
polytraders_gov_pnlreporter_net_fees_pusd_totalcounterusdsideCumulative net fees (after maker rebates) in pUSD.
polytraders_gov_pnlreporter_maker_rebates_pusd_totalcounterusdCumulative maker rebates accrued in pUSD.
polytraders_gov_pnlreporter_quarantine_countgaugecountNumber of fill records currently quarantined (missing fee, fee cap exceeded, etc.).
polytraders_gov_pnlreporter_onchain_reconcile_drift_pusdgaugeusdAbsolute drift between aggregate P&L and on-chain pUSD balance at last reconciliation.

Alerts

AlertConditionSeverityRunbook
PnLReporterFeeMissingrate(polytraders_gov_pnlreporter_quarantine_count[5m]) > 0page#runbook-pnlreporter-fee-missing
PnLReporterOnchainReconcileDriftpolytraders_gov_pnlreporter_onchain_reconcile_drift_pusd > 10page#runbook-pnlreporter-reconcile-drift
PnLReporterNoFillsIn1hrate(polytraders_gov_pnlreporter_fills_processed_total[1h]) == 0warn#runbook-pnlreporter-no-fills
PnLReporterFeeCaprate(polytraders_gov_pnlreporter_quarantine_count[5m]) > 5page#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

SituationUser-facing explanation
Daily P&L report generatedToday's trading summary: total volume, realised gains/losses, fees paid, and maker rebates received — all in pUSD.
Maker rebate creditedA 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 passedThe reported P&L matches the actual on-chain pUSD balance. Records are reconciled.
Negative-risk position in reportSome positions are in multi-outcome markets. Their P&L is calculated using the correct multi-outcome payoff formula.

18. Failure-Mode Block

main_failure_modeFill 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_riskOn-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_riskA 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_fallbackIf 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_dependenciesCTFExchangeV2 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

ScenarioHow to injectExpected behaviourRecovery
FEE_MISSING_FROM_MATCH_EVENTEmit a synthetic fill event without the fee_bps fieldPNL_REPORTER_FEE_MISSING raised; fill quarantined; not included in P&L totalsManual review and re-ingestion of corrected fill event required.
FEE_CAP_EXCEEDEDInject fill with match_event.fee_bps=120 (taker side)PNL_REPORTER_FEE_CAP_EXCEEDED raised; fill quarantinedManual review; correct fee or reject fill.
STALE_MIDPRICEBlock CLOB public API price endpoint for 90sSTALE_DATA warn emitted; unrealised P&L uses last known priceOnce price endpoint recovers, next fill event re-fetches fresh price.
ONCHAIN_RPC_FAILUREBlock Polygon RPC calls during window report generationonchain_reconciled=false in SettlementReport; PNL_REPORTER_ONCHAIN_RECONCILE_FAIL alertRetry reconciliation after RPC recovers; flag previous reports as pending reconciliation.
NEGRISK_FLAG_MISSINGSubmit fill for a negative-risk market but set negRisk=false in market metadataStandard binary payoff used; WARN emitted if position size exceeds normal binary rangeUpdate 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

AspectSpecification
Execution modelsingle-threaded event loop (per-fill hook) + scheduled window report goroutine
Max in-flight500
Idempotency keyfill_id
Per-call timeout (ms)1000
Backpressure strategyshed fill events beyond max_in_flight; emit WARN; process backlog on recovery
Locking / mutual exclusionPostgres unique constraint on fill_id; serializable isolation for aggregate report generation

22. Dependencies

Depends on (must run first)

BotWhyContract
gov.portfolio_syncPosition store snapshot is required to compute unrealised P&L on open positions.
gov.builder_attributionFill log joined for per-builder P&L breakdown and builder_fee_pusd.

Sibling bots (same OrderIntent)

BotWhyContract
gov.builderattributionShares the fill event stream; PnLReporter focuses on P&L, BuilderAttribution focuses on attribution.

External services

ServiceEndpointSLA assumedOn 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

ContractMethodNetworkEffect
CTFExchangeV2polygon

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

AspectValue
CLOB versionv2
Collateral assetpUSD
EIP-712 Exchange domain version2
Aware of builderCode fieldyes
Aware of negative-risk marketsyes
Multi-chain readyno
SDK usedpy-clob-client-v2
Settlement contractCTFExchangeV2 on Polygon
NotesFees 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

FieldValue
spec2.0.0
implementation2.1.0
schema2
released2026-04-28

Migration history

DateFromToReasonAction taken
2026-04-28v1 (USDC.e, feeRateBps on order)v2 (pUSD, fee from match event)CLOB V2 cutoverRemoved 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

TestSetupExpected result
Fee read from match event, not from signed orderFill 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 feeMaker fill; fee_bps=40; rebate_rate=0.25maker_rebate_pusd = size_pusd * 40/10000 * 0.25
Taker fee_bps > 100 flaggedfill with fee_bps=110, side=takerPNL_REPORTER_FEE_CAP_EXCEEDED quarantine; fill not included in P&L
Missing fee_bps in match event quarantines fillFill event with no fee_bps fieldPNL_REPORTER_FEE_MISSING; fill quarantined for manual review
Negative-risk position uses correct payoff functionMarket negRisk=true; position size=100; outcome price=0.4unrealised_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.70realised_pnl_pusd = 20 pUSD (before fees)

Integration Tests

TestExpected result
End-to-end fill → SettlementReport → Postgres ledgerSettlementReport emitted per fill; aggregate report at window boundary; 7y retention tag applied
On-chain reconciliation passes for matching balanceonchain_reconciled=true in aggregate SettlementReport

Property Tests

PropertyRequired behaviour
Every fill event produces exactly one SettlementReport entryAlways true — emit-every policy enforced
No fill with missing fee_bps is ever included in a P&L totalAlways 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

AlertFirst stepDiagnosisMitigationEscalate to
PnLReporterFeeMissingIdentify which fills are quarantined; check match event schema for fee_bps field. May indicate a CLOB API format change.Governance pod lead immediately
PnLReporterOnchainReconcileDriftCheck 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
PnLReporterNoFillsIn1hCheck CLOB feed and CTFExchangeV2 event stream. May be a market quiet period or a feed disconnection.Exec pod lead if feed disconnection confirmed
PnLReporterFeeCapReview 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.

28. Promotion Gates

A bot does not advance to the next readiness state until every gate below is green. Gates are observable from production data — no subjective sign-off.

Promote to Shadow

GateHow measuredThreshold
Unit tests pass: fee from match event, maker rebate, fee cap, FIFO P&L, negRisk payoffCI test run100% pass
Postgres pnl_ledger schema migration verifiedIntegration testPass

Promote to Limited live

GateHow measuredThreshold
Fill processing latency p99 < 200ms per fillPerformance test on 1000 fills< 200ms
Missing fee quarantine fires correctlyFailure injection testPass

Promote to General live

GateHow measuredThreshold
End-to-end: fill → SettlementReport → Postgres → on-chain reconciliation passE2E test in staging with live Polygon RPCPass
7-year retention tag applied to all SettlementReport recordsPostgres retention policy auditPass

29. Developer Checklist

Ready-to-ship score: 27/27 sections complete · 100%

RequirementStatus
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