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?
ObservationReportSnapshot 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?
RiskVoteEvery 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)?
DecisionReportEach 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?
ExecutionReportExecution 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?
SettlementReportPost-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?
OperationsReportGovernance 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>.
- 14:32:01.001Pre-trade intelmarketscannerObservationReportinformationalTop-of-book snapshot taken, depth ±5% = $42k, no toxic flow detected.
- 14:32:01.012Pre-trade inteloraclewatcherObservationReportinformationalUMA queue clean, no proposals or disputes within 24h.
- 14:32:01.045Strategy decisionsum-to-one-arbDecisionReportinformationalEdge = 14.2 bps on outcome A. Proposed size $1,500 pUSD.
- 14:32:01.058Risk & complianceliquidityguardRiskVotepassDepth headroom 3.2x — comfortably within limit.
- 14:32:01.062Risk & complianceoracleriskmonitorRiskVotepassNo active proposal or dispute on this market.
- 14:32:01.068Risk & complianceportfolioguardRiskVotereshapeConcentration cap 8% would breach at $1,500 → reshape to $820.
- 14:32:01.071Risk & compliancecontractaddressguardRiskVotepassTarget contract on V2 allow-list, EIP-712 domain v2 confirmed.
- 14:32:01.078Strategy decisionsum-to-one-arbDecisionReportinformationalAll votes collected, intent reshaped to $820, emitted to exec.
- 14:32:01.116ExecutionsmartrouterExecutionReportpassPosted as maker @ 0.624, filled 740/740 in 38ms, builder code attached.
- 14:32:01.880Post-tradepnl-reporterSettlementReportinformationalFill settled. Gross +$12.40, fee -$1.80, rebate accrued $0.45.
- 14:32:02.030Governance & auditbuilderattributionOperationsReportinformationalBuilder fee $0.74 attributed to source bot sum-to-one-arb.
4. Reporting principles
- 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.
- 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 withsupersedesreferencing the priorreport_id. - 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. - Reports cite their evidence. — Every report includes an
evidencearray — URLs, snapshot hashes, on-chain tx refs. A regulator should be able to replay the decision from the report alone. - Reports separate verdict from reason. — A RiskVote of
reshapewith reason codePORTFOLIO_GUARD_CONCENTRATION_LIMITis human-readable; the verdict and the reason are independent fields. See reason-codes. - Reports declare their dependencies. —
depends_on_reportschains 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. - User-visible reports are explicitly flagged. —
policy.user_visible: truemeans 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).
| Kind | Producers | Consumers | Kafka topic | Partition key | Retention | QoS |
|---|---|---|---|---|---|---|
| ObservationReport | intel + disc bots | strat, risk, post-mortem dashboards | polytraders.reports.observation | market_id | 30 d full → 1 y rolled-up hourly | at-least-once, sample 1/N for high-volume polls |
| RiskVote | risk + sec bots | strat (synchronous), on-call, compliance | polytraders.reports.risk | trace_id | 2 y | exactly-once, emit-every |
| DecisionReport | strat bots | exec, governance audit, backtester replay | polytraders.reports.decision | trace_id | 2 y | exactly-once, emit-every |
| ExecutionReport | exec bots | post-trade, fill-quality, user trade history | polytraders.reports.execution | trace_id | 7 y | exactly-once, emit-every |
| SettlementReport | post-trade gov bots | treasury, accounting, attribution, user trade history | polytraders.reports.settlement | fill_id | 7 y (regulatory) | exactly-once, emit-every |
| OperationsReport | gov bots | on-call, finance, product owners | polytraders.reports.operations | bot_slug + epoch | 1 y | at-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.
| Kind | Emission p99 | Completeness | Freshness | Alert rule |
|---|---|---|---|---|
| ObservationReport | ≤ 50 ms after observation | ≥ 99.0% of polls | snapshot age ≤ 500 ms | page if ObservationReport completeness < 95% over 5m |
| RiskVote | ≤ 20 ms after intent received | 100% of intents (no exception) | n/a (synchronous) | page if any intent reaches exec without all RiskVotes |
| DecisionReport | ≤ 30 ms after votes complete | 100% of intents considered | n/a | page if DecisionReport missing for any emitted intent |
| ExecutionReport | ≤ 200 ms after final fill or cancel | 100% of OrderIntents emitted | fill freshness ≤ 100 ms | page if > 1% of intents have no ExecutionReport in 60s |
| SettlementReport | ≤ 5 s after on-chain settlement | 100% of fills observed on-chain | tx confirmation depth ≥ 6 | page if SettlementReport missing for any confirmed fill > 60s |
| OperationsReport | ≤ 1 min after period close | 100% of bots, 100% of cron jobs | n/a | page 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.
| Pattern | Action |
|---|---|
| 3 consecutive HARD_REJECT verdicts on KillSwitch in 60s | page on-call: kill-switch storm — likely operator action or upstream incident |
| Same RiskVote.reason_code RESHAPE fires > 50× / 5m on one bot | warn product: parameter drift, threshold may be too tight |
| DecisionReport.intent_emitted=false rate > 80% / 10m for one strat | warn strategy owner: bot is starving — check upstream signal freshness |
| ExecutionReport.slippage_bps_vs_mid worsens by > 2σ / hour | page exec on-call: route quality regression |
| SettlementReport missing for confirmed fill > 60s | page treasury: book-keeping gap |
| OperationsReport.bots_red > 0 for > 5 m | page SRE: bot down |
| Builder-fee accrual differs from on-chain by > 1¢ / day | page finance: attribution drift |
8. Cross-report join keys
The join surface that lets you reconstruct any trade or any time window.
| Key | Purpose | Applies to |
|---|---|---|
trace_id | Every report for one OrderIntent shares this. Primary key for trade reconstruction. | All kinds (mandatory) |
intent_id | Identifies the OrderIntent itself. May be null for OperationsReport. | All kinds except OperationsReport |
fill_id | Identifies a specific fill (one intent can produce multiple fills). | ExecutionReport, SettlementReport |
market_id | 32-byte hex Polymarket V2 market id. Fast filtering by market. | All kinds |
epoch | Hourly bucket. Enables "all reports between 14:00–15:00" queries. | All kinds |
bot.slug | Producer bot. Lets you ask "all reports from oracleriskmonitor today". | All kinds |
report_kind | ObservationReport | 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.
| Kind | User-visible by default? | Fields user sees | Fields redacted | Reason |
|---|---|---|---|---|
| ObservationReport | No | — | all (high cardinality, internal) | Internal context only — not useful to user. |
| RiskVote | Summary only | verdict, plain-English reason | internal thresholds, peer-bot vote details | Users see "your size was reduced for portfolio safety" — not the cap % |
| DecisionReport | Summary only | intent_emitted (yes/no), final size | edge_bps, internal model confidence | Strategy IP stays internal. |
| ExecutionReport | Yes | route, fills, fee, builder code, latency | internal slippage attribution | User trade history. |
| SettlementReport | Yes | P&L, fee, rebate, settled_at, tx | — | User books. |
| OperationsReport | No | — | all | Internal 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.
| Mode | Behaviour |
|---|---|
live | Real money, real fills. Default mode. |
shadow | Bot runs against live data but emits reports only — no orders signed. Used for dry-running new strategies. |
paper | Bot runs against live data, emits reports + simulated fills against the order book snapshot. P&L is hypothetical. |
replay | Bot 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.
| Kind | On bus down | Why |
|---|---|---|
| ObservationReport | Drop after 1s buffer overflow | High-volume; loss is acceptable, regenerable from next poll. |
| RiskVote | Block trade pipeline (fail-closed) | Without RiskVote durability, trade audit is broken — better to halt. |
| DecisionReport | Block trade pipeline (fail-closed) | Strategy must be auditable; never emit an intent we can't explain. |
| ExecutionReport | Buffer to local WAL, retry indefinitely, page on-call | Must not lose fills. |
| SettlementReport | Buffer to local WAL, retry indefinitely, page treasury | Must not lose books — regulatory. |
| OperationsReport | Drop after 60s buffer overflow | Re-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.
| Kind | Sampling rule | Exceptions |
|---|---|---|
| ObservationReport | Sample 1/N for high-frequency polls (e.g. 1/10 of WS depth ticks). | Always emit on threshold crossings (toxic-flow flip, depth halving). |
| RiskVote | Emit-every. Never sample. | — |
| DecisionReport | Emit-every for emitted intents. Emit 1/100 for considered-but-skipped intents (with a counter for the rest). | Always emit on policy changes. |
| ExecutionReport | Emit-every. Never sample. | — |
| SettlementReport | Emit-every. Never sample. | — |
| OperationsReport | Batch 1/min for system metrics; emit immediately on incident. | — |
14. Schema evolution rules
- Additive only — never remove or rename fields. Deprecated fields stay until a major version bump.
schema_versionuses semver. MAJOR = breaking, MINOR = additive, PATCH = doc-only.- Producers stamp the schema_version they emit. Consumers tolerate any MINOR/PATCH bump within their declared MAJOR.
- Unknown fields MUST be preserved on round-trip. Consumers ignore but never strip.
- Deprecation window: a field marked deprecated lives ≥ 90 days before removal in the next MAJOR.
- 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.
| Scope | All reports with policy.compliance_export = true: RiskVote, DecisionReport, ExecutionReport, SettlementReport. Limited to user-attributed intents. |
|---|---|
| Format | Newline-delimited JSON (NDJSON), gzip-compressed, one file per report kind per day, signed manifest sha256. |
| Cadence | Daily incremental + quarterly full archive. |
| Retention | 7 years on-prem; 7 years cold-storage replica in second region. |
| Redaction | PII fields stripped per policy.redacted_fields. Bot internal IP (edge_bps, model confidence) stripped on export unless explicitly opted in. |
| Delivery | S3 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/<trace_id>.
Panels
- Header — trace_id, intent_id, market title, total elapsed time, final outcome (filled / blocked / cancelled)
- Timeline — Vertical bar chart, 1 row per report, sorted by emitted_at, coloured by stage
- Report inspector — Click any bar → JSON envelope panel with collapsible payload + evidence
- Dependency graph — Force-directed graph of depends_on_reports edges for this trace_id
- Diff vs. shadow — If a paired shadow run exists, side-by-side diff of verdicts and fills
- 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_idj / k— jump to next / previous report in timelinev— toggle verdict-only filtere— open evidence panel
Cross-report dependency graph
Which kinds inform which. The DecisionReport reads ObservationReports + RiskVotes. The SettlementReport reads ExecutionReports. Operations rolls up everything.
| Producer | Consumer | Why | |
|---|---|---|---|
| ObservationReport | → | DecisionReport | strat bots read market snapshots before sizing |
| ObservationReport | → | RiskVote | risk bots read snapshots for liquidity / oracle checks |
| RiskVote | → | DecisionReport | strat collects all votes before final shape |
| DecisionReport | → | ExecutionReport | exec routes the shaped intent |
| ExecutionReport | → | SettlementReport | post-trade reconciles fills into books |
| ExecutionReport | → | OperationsReport | attribution + fill-quality aggregations |
| SettlementReport | → | OperationsReport | treasury and rebate roll-ups |
| RiskVote | → | OperationsReport | audit aggregates + reason-code dashboards |