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

Trade Report

Every bot publishes a ReportEnvelope. Together they form an end-to-end audit trail for one OrderIntent — observe → vote → decide → execute → reconcile → audit. This page is the spec for that reporting layer.

1. Lifecycle groups

Reporting is sequential. Bots are grouped by lifecycle stage — not by system layer — because a single trade walks through these six stages in order. Each group emits one canonical report kind.

1. Pre-trade intelligence reports

What did the world look like just before we acted?

ObservationReport

Snapshot reports — every observable signal that fed into the decision. These are read-only; they explain context, not action.

Example reports
  • ObservationReport for market 0xabc… at t=14:32:01.234Z
  • Top-of-book snapshot: bid 0.62, ask 0.64, depth ±5%: $42k
  • Last 50 trades clustering: 78% taker-buys, no toxic prints

2. Risk & compliance reports

What did each guardrail vote, and why?

RiskVote

Every risk and security bot publishes a RiskVote per intent: allow / reshape / reject + reason code. These reports are the audit trail for refusal — every blocked trade has a paper trail.

Example reports
  • RiskVote PASS — LiquidityGuard, depth_ok, headroom 3.2x
  • RiskVote RESHAPE — PortfolioGuard, size capped 1500→820 pUSD
  • RiskVote REJECT — KillSwitch active since 14:30:11Z (manual)

3. Strategy decision reports

Why was this OrderIntent emitted (or not)?

DecisionReport

Each strategy bot publishes a DecisionReport containing the intent it considered, the votes it received, the final shaped intent, and the reasoning. The OrderIntent is the artifact; the DecisionReport is the story.

Example reports
  • DecisionReport — sum-to-one-arb saw $0.014 edge on AB, sized $740
  • DecisionReport — maker-tight skipped: spread 1.2¢ < min 1.5¢
  • DecisionReport — neg-risk-sum-arb fired convert-arb on set#42

4. Execution reports

What actually happened on the wire?

ExecutionReport

Execution bots publish an ExecutionReport per OrderIntent: which venue/route was chosen, what fills came back, latency, fee paid, builder code attached, slippage vs. expected.

Example reports
  • ExecutionReport — SmartRouter posted maker, filled 740/740, 38ms
  • ExecutionReport — AntiToxicFill cancelled after 2 toxic prints
  • ExecutionReport — fill bps vs. mid: +0.4 bps (fav.), fee 1.80%

5. Post-trade reconciliation reports

Did the books balance after settlement?

SettlementReport

Post-trade bots publish a SettlementReport per fill and a DailyReconciliation rolling all fills into position deltas, P&L, and rebate accruals.

Example reports
  • SettlementReport — fill 0xf00…ba1: +$12.40 P&L, -$1.80 fee
  • DailyReconciliation — 412 fills, $4,302 gross, $73.16 net
  • RebateReport — Apr 28: $182.40 maker rebate accrued (sports pool)

6. Governance & audit reports

Is the system healthy and behaving as designed?

OperationsReport

Governance bots publish meta-reports about the system itself: bot uptime, missed cron runs, experiment results, builder fee accruals, attribution to source bots, RPC failovers, key rotation reminders. These are the SRE / finance reports.

Example reports
  • OperationsReport — health-heartbeat: 96/97 bots green
  • AttributionReport — builder fees: $1,204 across 18 strategies
  • ExperimentReport — A/B 17: maker-tight beat baseline by 4.2¢

2. ReportEnvelope schema

Every report — regardless of kind — wraps a kind-specific payload in this envelope. Identity, timing, outcome, evidence, dependencies, and retention policy are uniform across all six kinds. This is what makes cross-kind queries possible.

{
  "schema_version": "1.0.0",
  "report_id":      "rpt_<bot_slug>_<ulid>",
  "report_kind":    "ObservationReport | RiskVote | DecisionReport |
                     ExecutionReport | SettlementReport | OperationsReport",
  "supersedes":     null,                     // report_id of prior version, if correction
  "bot": {
    "bot_id":       "1.3",
    "slug":         "oracleriskmonitor",
    "version":      "2.1.3",
    "spec_version": "2.0.0"
  },
  "subject": {
    "trace_id":     "tr_01HX9...",            // shared across every report for one trade
    "intent_id":    "oi_01HX9...",            // null on OperationsReport
    "fill_id":      null,                     // populated only on ExecutionReport / SettlementReport
    "market_id":    "0xabcdef...",            // 32-byte hex (V2)
    "epoch":        "2026-05-09T14:00Z/15:00Z" // hourly bucket for time-window queries
  },
  "timing": {
    "observed_at":  "2026-05-09T14:32:01.234Z",
    "emitted_at":   "2026-05-09T14:32:01.241Z",
    "latency_ms":   7
  },
  "outcome": {
    "verdict":      "pass | reshape | reject | informational",
    "reason_code":  "LIQUIDITY_OK | ORACLE_DISPUTE_ACTIVE | ...",
    "severity":     "INFO | WARN | RESHAPE | HARD_REJECT | EXPLAIN"
  },
  "payload":        { /* report-kind-specific body, see below */ },
  "evidence": [
    { "kind": "polymarket.gamma.market", "url": "...", "snapshot_hash": "0x..." },
    { "kind": "onchain.tx",              "tx":  "0xf00...", "block": 64512345 }
  ],
  "depends_on_reports": [
    "rpt_marketscanner_01HX...",            // upstream reports that informed this one
    "rpt_liquidityguard_01HX..."
  ],
  "policy": {
    "user_visible":      true,
    "redacted_fields":   [],
    "retention_days":    2555,                // 7 years for SettlementReport
    "compliance_export": true                 // included in regulator bundle
  },
  "mode": "live"                              // live | shadow | paper | replay
}

Per-kind payload sketches

The envelope is uniform; the payload is not. Each kind has its own shape:

ObservationReport payload sketch
{
  "snapshot": {
    "best_bid": 0.62, "best_ask": 0.64,
    "depth_pUSD": { "bid_5pct": 42000, "ask_5pct": 38500 },
    "last_50_trades_lean": "+0.13"
  },
  "freshness_ms": 240,
  "source_api": "clob_public"
}
RiskVote payload sketch
{
  "vote": "reshape",
  "original_size_pUSD": 1500,
  "new_size_pUSD": 820,
  "reasons": ["PORTFOLIO_GUARD_CONCENTRATION_LIMIT"],
  "explanation": "single-market exposure would breach 8% cap"
}
DecisionReport payload sketch
{
  "intent_emitted": true,
  "intent_id": "oi_01HX9...",
  "edge_bps": 14.2,
  "votes_received": [
    {"bot": "liquidityguard",  "vote": "pass"},
    {"bot": "portfolioguard",  "vote": "reshape", "size": 820}
  ],
  "shaped_intent_size_pUSD": 820
}
ExecutionReport payload sketch
{
  "route": "clob_v2",
  "maker_or_taker": "maker",
  "fills": [{"price": 0.624, "size": 740, "tx": "0xf00...ba1"}],
  "fee_pUSD": 8.32,
  "builder_code": "0x...32bytes",
  "builder_fee_pUSD": 0.74,
  "latency_ms": 38,
  "slippage_bps_vs_mid": -0.4
}
SettlementReport payload sketch
{
  "fill_id": "fill_01HX9...",
  "tx": "0xf00...ba1",
  "gross_pnl_pUSD": 12.40,
  "fee_pUSD": -1.80,
  "rebate_accrued_pUSD": 0.45,
  "position_delta": { "0xabc...": +740 },
  "settled_at": "2026-05-09T14:32:01.880Z"
}
OperationsReport payload sketch
{
  "scope": "system | bot | experiment | budget",
  "metrics": { "bots_green": 96, "bots_red": 1 },
  "incidents": [],
  "period": { "from": "2026-05-09T00:00Z", "to": "2026-05-09T23:59Z" }
}

3. End-to-end example trace

Eleven reports, one trade, one trace_id. This is what the timeline viewer shows when you load /internal/trades/<trace_id>.

trace_id tr_01HX9KZQ7E8VR5intent_id oi_01HX9KZQ7F2A1Bmarket 0xabcdef0123… · "Will X exceed Y by June 30?"
  1. 14:32:01.001Pre-trade intelmarketscannerObservationReportinformationalTop-of-book snapshot taken, depth ±5% = $42k, no toxic flow detected.
  2. 14:32:01.012Pre-trade inteloraclewatcherObservationReportinformationalUMA queue clean, no proposals or disputes within 24h.
  3. 14:32:01.045Strategy decisionsum-to-one-arbDecisionReportinformationalEdge = 14.2 bps on outcome A. Proposed size $1,500 pUSD.
  4. 14:32:01.058Risk & complianceliquidityguardRiskVotepassDepth headroom 3.2x — comfortably within limit.
  5. 14:32:01.062Risk & complianceoracleriskmonitorRiskVotepassNo active proposal or dispute on this market.
  6. 14:32:01.068Risk & complianceportfolioguardRiskVotereshapeConcentration cap 8% would breach at $1,500 → reshape to $820.
  7. 14:32:01.071Risk & compliancecontractaddressguardRiskVotepassTarget contract on V2 allow-list, EIP-712 domain v2 confirmed.
  8. 14:32:01.078Strategy decisionsum-to-one-arbDecisionReportinformationalAll votes collected, intent reshaped to $820, emitted to exec.
  9. 14:32:01.116ExecutionsmartrouterExecutionReportpassPosted as maker @ 0.624, filled 740/740 in 38ms, builder code attached.
  10. 14:32:01.880Post-tradepnl-reporterSettlementReportinformationalFill settled. Gross +$12.40, fee -$1.80, rebate accrued $0.45.
  11. 14:32:02.030Governance & auditbuilderattributionOperationsReportinformationalBuilder fee $0.74 attributed to source bot sum-to-one-arb.

4. Reporting principles

  1. Every bot reports — even read-only ones. An Intelligence bot that observes but never votes still emits an ObservationReport per polled signal. If it produces no report, it cannot be audited.
  2. Reports are immutable, append-only. Once emitted to the report bus (Kafka topic polytraders.reports.<kind>), a report is never updated. Corrections emit a new report with supersedes referencing the prior report_id.
  3. One trace_id per OrderIntent — across every stage. All reports for a single trade share a trace_id. This is how the post-trade walkthrough joins 11 separate reports back into one story.
  4. Reports cite their evidence. Every report includes an evidence array — URLs, snapshot hashes, on-chain tx refs. A regulator should be able to replay the decision from the report alone.
  5. Reports separate verdict from reason. A RiskVote of reshape with reason code PORTFOLIO_GUARD_CONCENTRATION_LIMIT is human-readable; the verdict and the reason are independent fields. See reason-codes.
  6. Reports declare their dependencies. depends_on_reports chains a report back to the upstream reports that informed it. The DecisionReport from a strategy bot lists the RiskVote IDs from each guardrail it consulted.
  7. User-visible reports are explicitly flagged. policy.user_visible: true means the report is shown in the user-facing trade history. Most reports are internal-only.

5. Routing matrix — producers × consumers × topic × retention

One Kafka topic per report kind. Partition keys are chosen so that all reports for one trade land on one partition (trace_id) — except observation polls (sharded by market) and operations rollups (sharded by bot+epoch).

KindProducersConsumersKafka topicPartition keyRetentionQoS
ObservationReportintel + disc botsstrat, risk, post-mortem dashboardspolytraders.reports.observationmarket_id30 d full → 1 y rolled-up hourlyat-least-once, sample 1/N for high-volume polls
RiskVoterisk + sec botsstrat (synchronous), on-call, compliancepolytraders.reports.risktrace_id2 yexactly-once, emit-every
DecisionReportstrat botsexec, governance audit, backtester replaypolytraders.reports.decisiontrace_id2 yexactly-once, emit-every
ExecutionReportexec botspost-trade, fill-quality, user trade historypolytraders.reports.executiontrace_id7 yexactly-once, emit-every
SettlementReportpost-trade gov botstreasury, accounting, attribution, user trade historypolytraders.reports.settlementfill_id7 y (regulatory)exactly-once, emit-every
OperationsReportgov botson-call, finance, product ownerspolytraders.reports.operationsbot_slug + epoch1 yat-least-once, batched 1/min

6. SLOs per report kind

Reporting itself is a service — and it has its own SLOs. Miss any of these and a page fires.

KindEmission p99CompletenessFreshnessAlert rule
ObservationReport≤ 50 ms after observation≥ 99.0% of pollssnapshot age ≤ 500 mspage if ObservationReport completeness < 95% over 5m
RiskVote≤ 20 ms after intent received100% of intents (no exception)n/a (synchronous)page if any intent reaches exec without all RiskVotes
DecisionReport≤ 30 ms after votes complete100% of intents consideredn/apage if DecisionReport missing for any emitted intent
ExecutionReport≤ 200 ms after final fill or cancel100% of OrderIntents emittedfill freshness ≤ 100 mspage if &gt; 1% of intents have no ExecutionReport in 60s
SettlementReport≤ 5 s after on-chain settlement100% of fills observed on-chaintx confirmation depth ≥ 6page if SettlementReport missing for any confirmed fill &gt; 60s
OperationsReport≤ 1 min after period close100% of bots, 100% of cron jobsn/apage if any bot misses 3 consecutive OperationsReports

7. Reasoned alerts — patterns over reports

Alerts derived from report streams, not from raw metrics. Each pattern explains itself.

PatternAction
3 consecutive HARD_REJECT verdicts on KillSwitch in 60spage on-call: kill-switch storm — likely operator action or upstream incident
Same RiskVote.reason_code RESHAPE fires > 50× / 5m on one botwarn product: parameter drift, threshold may be too tight
DecisionReport.intent_emitted=false rate > 80% / 10m for one stratwarn strategy owner: bot is starving — check upstream signal freshness
ExecutionReport.slippage_bps_vs_mid worsens by > 2σ / hourpage exec on-call: route quality regression
SettlementReport missing for confirmed fill > 60spage treasury: book-keeping gap
OperationsReport.bots_red > 0 for > 5 mpage SRE: bot down
Builder-fee accrual differs from on-chain by > 1¢ / daypage finance: attribution drift

8. Cross-report join keys

The join surface that lets you reconstruct any trade or any time window.

KeyPurposeApplies to
trace_idEvery report for one OrderIntent shares this. Primary key for trade reconstruction.All kinds (mandatory)
intent_idIdentifies the OrderIntent itself. May be null for OperationsReport.All kinds except OperationsReport
fill_idIdentifies a specific fill (one intent can produce multiple fills).ExecutionReport, SettlementReport
market_id32-byte hex Polymarket V2 market id. Fast filtering by market.All kinds
epochHourly bucket. Enables "all reports between 14:00–15:00" queries.All kinds
bot.slugProducer bot. Lets you ask "all reports from oracleriskmonitor today".All kinds
report_kindObservationReport | RiskVote | DecisionReport | ExecutionReport | SettlementReport | OperationsReport.All kinds

9. User-visible vs. internal

Most reports are internal-only. The user trade history is built from a strict subset.

KindUser-visible by default?Fields user seesFields redactedReason
ObservationReportNoall (high cardinality, internal)Internal context only — not useful to user.
RiskVoteSummary onlyverdict, plain-English reasoninternal thresholds, peer-bot vote detailsUsers see &quot;your size was reduced for portfolio safety&quot; — not the cap %
DecisionReportSummary onlyintent_emitted (yes/no), final sizeedge_bps, internal model confidenceStrategy IP stays internal.
ExecutionReportYesroute, fills, fee, builder code, latencyinternal slippage attributionUser trade history.
SettlementReportYesP&L, fee, rebate, settled_at, txUser books.
OperationsReportNoallInternal SRE only.

10. Replay modes

Every report carries a mode field. This is how shadow runs, paper trading, and historical replays stay separable from live books.

ModeBehaviour
liveReal money, real fills. Default mode.
shadowBot runs against live data but emits reports only — no orders signed. Used for dry-running new strategies.
paperBot runs against live data, emits reports + simulated fills against the order book snapshot. P&L is hypothetical.
replayBot runs against historical data from the report archive. Used by Backtester. Reports tagged with original trace_id + replay_run_id.

11. Bus failure mode

If the report bus is down, what each kind does is different. RiskVote and DecisionReport fail-closed (block the pipeline). Settlement buffers to disk. Observation drops.

KindOn bus downWhy
ObservationReportDrop after 1s buffer overflowHigh-volume; loss is acceptable, regenerable from next poll.
RiskVoteBlock trade pipeline (fail-closed)Without RiskVote durability, trade audit is broken — better to halt.
DecisionReportBlock trade pipeline (fail-closed)Strategy must be auditable; never emit an intent we can't explain.
ExecutionReportBuffer to local WAL, retry indefinitely, page on-callMust not lose fills.
SettlementReportBuffer to local WAL, retry indefinitely, page treasuryMust not lose books — regulatory.
OperationsReportDrop after 60s buffer overflowRe-emitted next period; loss is recoverable.

12. Per-bot reporting checklist

Every bot's reporting block must declare the following — promotion gate to BETA blocks until all eight are present.

  • Bot declares which report kind(s) it emits.
  • Bot declares the topic(s) it publishes to.
  • Bot declares emission cadence (every-event, every-N, every-period).
  • Bot declares its payload schema (one of the six payload sketches, or a documented extension).
  • Bot declares retention class (matches policy.retention_days for the kind).
  • Bot declares user-visibility default + redaction list.
  • Bot declares behaviour when the report bus is unavailable.
  • Bot declares which upstream report kinds it consumes (depends_on_reports populator).

13. Sampling rules

High-volume kinds may sample. Audit-critical kinds (RiskVote, DecisionReport, ExecutionReport, SettlementReport) never sample.

KindSampling ruleExceptions
ObservationReportSample 1/N for high-frequency polls (e.g. 1/10 of WS depth ticks).Always emit on threshold crossings (toxic-flow flip, depth halving).
RiskVoteEmit-every. Never sample.
DecisionReportEmit-every for emitted intents. Emit 1/100 for considered-but-skipped intents (with a counter for the rest).Always emit on policy changes.
ExecutionReportEmit-every. Never sample.
SettlementReportEmit-every. Never sample.
OperationsReportBatch 1/min for system metrics; emit immediately on incident.

14. Schema evolution rules

  1. Additive only — never remove or rename fields. Deprecated fields stay until a major version bump.
  2. schema_version uses semver. MAJOR = breaking, MINOR = additive, PATCH = doc-only.
  3. Producers stamp the schema_version they emit. Consumers tolerate any MINOR/PATCH bump within their declared MAJOR.
  4. Unknown fields MUST be preserved on round-trip. Consumers ignore but never strip.
  5. Deprecation window: a field marked deprecated lives ≥ 90 days before removal in the next MAJOR.
  6. Breaking changes go through the principles → V2-migration playbook (see v2-migration).

15. Compliance / regulator export

The bundle a regulator receives. Daily incrementals + quarterly full archive. PII stripped per envelope policy.

ScopeAll reports with policy.compliance_export = true: RiskVote, DecisionReport, ExecutionReport, SettlementReport. Limited to user-attributed intents.
FormatNewline-delimited JSON (NDJSON), gzip-compressed, one file per report kind per day, signed manifest sha256.
CadenceDaily incremental + quarterly full archive.
Retention7 years on-prem; 7 years cold-storage replica in second region.
RedactionPII fields stripped per policy.redacted_fields. Bot internal IP (edge_bps, model confidence) stripped on export unless explicitly opted in.
DeliveryS3 bucket polytraders-compliance-export, IAM-gated, dual-control delete only.

Trade timeline viewer (UX sketch)

The internal tool that joins all reports for one trace_id into a single screen. URL pattern: /internal/trades/&lt;trace_id&gt;.

Panels

  1. Header trace_id, intent_id, market title, total elapsed time, final outcome (filled / blocked / cancelled)
  2. Timeline Vertical bar chart, 1 row per report, sorted by emitted_at, coloured by stage
  3. Report inspector Click any bar → JSON envelope panel with collapsible payload + evidence
  4. Dependency graph Force-directed graph of depends_on_reports edges for this trace_id
  5. Diff vs. shadow If a paired shadow run exists, side-by-side diff of verdicts and fills
  6. Replay button Re-runs this trace against current code in replay mode, opens new viewer with replay_run_id

Keyboard shortcuts

  • / search by trace_id, intent_id, fill_id, market_id
  • j / k jump to next / previous report in timeline
  • v toggle verdict-only filter
  • e open evidence panel

Cross-report dependency graph

Which kinds inform which. The DecisionReport reads ObservationReports + RiskVotes. The SettlementReport reads ExecutionReports. Operations rolls up everything.

ProducerConsumerWhy
ObservationReportDecisionReportstrat bots read market snapshots before sizing
ObservationReportRiskVoterisk bots read snapshots for liquidity / oracle checks
RiskVoteDecisionReportstrat collects all votes before final shape
DecisionReportExecutionReportexec routes the shaped intent
ExecutionReportSettlementReportpost-trade reconciles fills into books
ExecutionReportOperationsReportattribution + fill-quality aggregations
SettlementReportOperationsReporttreasury and rebate roll-ups
RiskVoteOperationsReportaudit aggregates + reason-code dashboards