{
  "schema_version": "1.0.0",
  "bot_id": "6.12",
  "bot_name": "PostTradeExplainer",
  "slug": "posttradeexplainer",
  "layer": "Governance",
  "layer_key": "gov",
  "bot_class": "Governance Service",
  "authority": [
    "Explain"
  ],
  "status": "planned",
  "readiness": "Spec started",
  "flagship": true,
  "is_reference": false,
  "public_export": false,
  "identity": {
    "layer": "Governance",
    "bot_class": "Governance Service",
    "authority": "Explain",
    "runs_before": "Nothing \u2014 runs post-fill on every ExecutionReport event",
    "runs_after": "Order fill confirmation from CTFExchangeV2",
    "applies_to": "Every filled or partially filled order",
    "default_mode": "shadow_only",
    "user_visible": "summary-only",
    "developer_owner": "Polytraders core"
  },
  "purpose": "PostTradeExplainer turns every fill into a plain-English SettlementReport: which strategy, which signal, which guard checks passed, why this size, why this price. Retained 7 years for regulatory compliance.",
  "why_it_matters": [
    {
      "failure": "No post-trade explanation",
      "consequence": "Users and regulators cannot understand why a trade was made; compliance audit fails."
    },
    {
      "failure": "Explanation not linked to fill",
      "consequence": "Fill records and strategy intent cannot be reconciled; audit trail is broken."
    }
  ],
  "polymarket_inputs": [
    {
      "input": "Fill confirmation from CTFExchangeV2",
      "source": "clob_auth",
      "required": true,
      "use": "Consume fill metadata (size, price, side, builder_fee_pusd) for the explanation."
    },
    {
      "input": "ExecutionReport envelope",
      "source": "internal.report_bus",
      "required": true,
      "use": "Source of the full order lifecycle context for the explanation."
    }
  ],
  "internal_inputs": [
    {
      "input": "DecisionReport for the originating intent",
      "source": "internal.report_archive",
      "required": true,
      "use": "Fetch strategy name, signal inputs, and parameter values at decision time."
    },
    {
      "input": "RiskVote record for the originating intent",
      "source": "internal.report_archive",
      "required": true,
      "use": "Include guardrail votes in the explanation."
    }
  ],
  "raw_params": [
    "retain_explanation_days \u00b7 int",
    "min_detail_level \u00b7 enum",
    "publish_to_user \u00b7 bool",
    "require_for_advanced \u00b7 bool"
  ],
  "parameters": [
    {
      "name": "retain_explanation_days",
      "default": 2555,
      "warning": 1825,
      "hard": 2555,
      "controls": "Retention period in days for SettlementReport records (default 7 years = 2555 days).",
      "why_default_matters": "7-year retention is required by financial regulations.",
      "threshold_logic": [
        {
          "condition": "retain_explanation_days < 2555",
          "action": "WARN \u2014 below regulatory minimum"
        }
      ],
      "dev_check": "if p.retain_explanation_days < 2555: emit('RETENTION_BELOW_REGULATORY_MINIMUM')",
      "user_facing": "Your trade explanations are kept on record for 7 years as required by regulation."
    },
    {
      "name": "min_detail_level",
      "default": "standard",
      "warning": null,
      "hard": null,
      "controls": "Minimum level of detail in the explanation (standard / advanced).",
      "why_default_matters": "Standard includes strategy name, signal summary, and guard votes. Advanced adds raw parameter values.",
      "threshold_logic": [
        {
          "condition": "min_detail_level=standard",
          "action": "Include: strategy, signal summary, guard votes, size rationale, fill price"
        }
      ],
      "dev_check": "if p.min_detail_level == 'advanced': explanation.include(raw_params=True)",
      "user_facing": "Each trade record includes the reason it was made."
    }
  ],
  "default_config": {
    "bot_id": "gov.posttradeexplainer",
    "version": "0.1.0",
    "mode": "shadow_only",
    "defaults": {
      "retain_explanation_days": 2555,
      "min_detail_level": "standard",
      "publish_to_user": true,
      "require_for_advanced": false
    }
  },
  "implementation_flow": [
    "Subscribe to ExecutionReport events on the internal report bus.",
    "For each ExecutionReport, fetch the linked DecisionReport and RiskVote from the report archive.",
    "Compose the explanation: strategy name, signal summary, guard vote outcomes, size rationale, fill price, builder_fee_pusd.",
    "Generate a plain-English summary sentence for user display.",
    "Emit SettlementReport(event_type=TRADE_EXPLAINED) with full explanation and plain-English summary.",
    "Store SettlementReport in the post-trade archive with retain_explanation_days retention."
  ],
  "decision_logic": {
    "approve": "Not applicable \u2014 PostTradeExplainer does not approve or reject trades.",
    "reshape_required": "Not applicable.",
    "reject": "Not applicable.",
    "warning_only": "Emits RETENTION_BELOW_REGULATORY_MINIMUM if retain_explanation_days < 2555."
  },
  "decision_output_schema": "SettlementReport",
  "decision_output_example": {
    "report_id": "stl_posttradeexplainer_01HX9Z",
    "bot_id": "gov.posttradeexplainer",
    "event_type": "TRADE_EXPLAINED",
    "fill_id": "fill_00a1b2c3d4e5f6a7",
    "strategy": "sports-model",
    "signal_summary": "Model edge 4.2 bps on YES leg of market 0x9b0c",
    "guard_votes": [
      {
        "bot": "liquidityguard",
        "vote": "pass"
      },
      {
        "bot": "portfolioguard",
        "vote": "pass"
      }
    ],
    "size_rationale": "Position sized at 80% of max_size_pusd due to LiquidityGuard spread signal",
    "fill_price": 0.621,
    "fill_size_pusd": 430.0,
    "builder_fee_pusd": 1.075,
    "plain_english": "Bought 430 pUSD of YES on market 0x9b0c at 0.621 \u2014 sports model detected 4.2 bps edge; all risk checks passed.",
    "report_kind": "SettlementReport",
    "topic": "polytraders.reports.settlement",
    "retained_until": "2033-05-09"
  },
  "developer_log": {
    "bot_id": "gov.posttradeexplainer",
    "event_type": "EXPLANATION_COMPOSED",
    "fill_id": "fill_00a1b2c3d4e5f6a7",
    "archive_fetch_ms": 12,
    "explanation_length": 280
  },
  "user_explanations": [
    {
      "situation": "Trade explanation available",
      "message": "Your trade was made because the strategy detected a pricing edge. All risk checks passed. Full details are in the trade record."
    },
    {
      "situation": "Explanation archive unavailable",
      "message": "The detailed trade explanation is temporarily unavailable. The trade itself was executed correctly."
    }
  ],
  "failure_modes": {
    "main_failure_mode": "Report archive is unavailable; DecisionReport or RiskVote cannot be fetched, resulting in an incomplete explanation.",
    "false_positive_risk": "A stale DecisionReport is linked to the wrong fill, producing a misleading explanation.",
    "false_negative_risk": "Missing guard vote record causes the explanation to omit a guardrail outcome.",
    "safe_fallback": "If archive is unavailable, emit a partial explanation with fill metadata only and set explanation_complete=false.",
    "required_dependencies": [
      "internal.report_archive (DecisionReport, RiskVote)",
      "internal.report_bus (ExecutionReport)",
      "clob_auth (fill confirmation)"
    ]
  },
  "acceptance_tests": {
    "unit": [
      {
        "test": "Explanation composed with all required fields",
        "setup": "Full DecisionReport and RiskVote available",
        "expected": "SettlementReport with non-null plain_english and all guard_votes"
      },
      {
        "test": "Partial explanation emitted when archive unavailable",
        "setup": "report_archive returns 503",
        "expected": "SettlementReport with explanation_complete=false; fill metadata present"
      }
    ],
    "integration": [
      {
        "test": "End-to-end: fill confirmed \u2192 ExecutionReport \u2192 SettlementReport with explanation emitted",
        "expected": "SettlementReport on polytraders.reports.settlement; retained_until = now+7y"
      }
    ],
    "property": [
      {
        "property": "Every SettlementReport includes a non-empty plain_english field",
        "required": "Always true \u2014 even partial explanations include a fallback message"
      }
    ]
  },
  "checklist_overrides": {},
  "legacy_goal": "Turn every fill into a plain-English record: which strategy, which signal, which guard checks passed, why this size, why this price.",
  "legacy_pm_signals": [
    "OrderLifecycleManager (2.6) state trail",
    "Strategy intent, parameters, and signal inputs at decision time",
    "Guardrail vote record for the intent",
    "Builder-code, fees, and gas envelope on the realised fill"
  ],
  "legacy_external_feeds": [],
  "reporting_groups": [
    "governance_audit"
  ],
  "network": [
    "polygon"
  ],
  "api_surface": [
    "internal",
    "clob_auth"
  ],
  "version": {
    "spec": "2.0.0",
    "implementation": "0.1.0",
    "schema": "2",
    "released": null,
    "planned_release": "Q4-2026"
  },
  "migration_history": [
    {
      "date": "2026-04-28",
      "from": "n/a",
      "to": "v2-spec",
      "reason": "Spec drafted post-CLOB-V2 cutover; bot not yet implemented",
      "action_taken": "Designed against V2 schema (pUSD, builder codes, V2 EIP-712 domain)"
    }
  ],
  "polymarket_v2_compat": {
    "clob_version": "v2",
    "collateral": "pUSD",
    "eip712_domain_version": "2",
    "builder_code_aware": true,
    "negrisk_aware": false,
    "multichain_ready": false,
    "sdk_used": "py-clob-client-v2",
    "settlement_contract": "CTFExchangeV2",
    "notes": "PostTradeExplainer consumes ExecutionReport records from V2 fills; all amounts in pUSD. Reads builder_fee_pusd from fill metadata."
  },
  "reference_implementation": {
    "pseudocode": "// ---- SUBSCRIBE TO EXECUTION REPORTS ----\nFUNCTION onExecutionReport(execReport):\n  // Fetch supporting records from archive\n  decisionReport = FETCH internal.reportArchive.GET({\n    trace_id: execReport.trace_id, kind: 'DecisionReport'\n  })\n  riskVotes = FETCH internal.reportArchive.GET({\n    trace_id: execReport.trace_id, kind: 'RiskVote'\n  })\n\n  complete = decisionReport IS NOT NULL AND riskVotes IS NOT NULL\n\n  // Compose explanation\n  explanation = {\n    fill_id:         execReport.fill_id,\n    strategy:        decisionReport?.strategy_slug ?? 'unknown',\n    signal_summary:  decisionReport?.signal_summary ?? '',\n    guard_votes:     riskVotes ?? [],\n    size_rationale:  buildSizeRationale(decisionReport, riskVotes),\n    fill_price:      execReport.fill_price,\n    fill_size_pusd:  execReport.fill_size_pusd,\n    builder_fee_pusd: execReport.builder_fee_pusd,\n    plain_english:   composePlainEnglish(explanation),\n    explanation_complete: complete\n  }\n\n  // Emit SettlementReport\n  EMIT SettlementReport(event_type='TRADE_EXPLAINED',\n    ...explanation,\n    report_kind='SettlementReport',\n    topic='polytraders.reports.settlement',\n    retained_until=now() + days(config.retain_explanation_days))\n\nFUNCTION composePlainEnglish(exp):\n  side = 'Bought' IF exp.fill_side == 'BUY' ELSE 'Sold'\n  return f\"{side} {exp.fill_size_pusd} pUSD at {exp.fill_price} \u2014 {exp.signal_summary}; all risk checks passed.\"",
    "sdk_calls": [
      "internal.reportArchive.GET({trace_id, kind})",
      "alerting.emit('EXPLANATION_ARCHIVE_UNAVAILABLE', metadata)"
    ],
    "complexity": "O(1) per fill; O(R) for archive fetch where R = linked report count"
  },
  "wire_examples": {
    "input": {
      "label": "ExecutionReport from fill",
      "source": "internal.report_bus",
      "payload": {
        "fill_id": "fill_00a1b2c3d4e5f6a7",
        "trace_id": "trc_01HX9Z",
        "fill_price": 0.621,
        "fill_size_pusd": 430.0,
        "builder_fee_pusd": 1.075,
        "fill_confirmed_at_ms": 1746792060000
      }
    },
    "output": {
      "label": "SettlementReport \u2014 TRADE_EXPLAINED",
      "payload": {
        "report_id": "stl_01HX9Z",
        "event_type": "TRADE_EXPLAINED",
        "fill_id": "fill_00a1b2c3d4e5f6a7",
        "plain_english": "Bought 430 pUSD of YES at 0.621 \u2014 sports model edge 4.2 bps; all risk checks passed.",
        "report_kind": "SettlementReport",
        "topic": "polytraders.reports.settlement",
        "retained_until": "2033-05-09"
      }
    }
  },
  "reason_codes": [
    {
      "code": "TRADE_EXPLAINED",
      "severity": "INFO",
      "meaning": "A fill was successfully explained and a SettlementReport emitted.",
      "action": "Log and store.",
      "user_message": "Your trade record is available."
    },
    {
      "code": "EXPLANATION_ARCHIVE_UNAVAILABLE",
      "severity": "WARN",
      "meaning": "DecisionReport or RiskVote could not be fetched; partial explanation emitted.",
      "action": "Emit partial SettlementReport with explanation_complete=false.",
      "user_message": "Detailed trade explanation is temporarily unavailable."
    },
    {
      "code": "RETENTION_BELOW_REGULATORY_MINIMUM",
      "severity": "WARN",
      "meaning": "retain_explanation_days is below 2555 (7 years).",
      "action": "Emit WARN; do not reduce retention.",
      "user_message": ""
    },
    {
      "code": "EXPLANATION_MISSING_GUARD_VOTES",
      "severity": "WARN",
      "meaning": "RiskVote records not found for the fill; explanation is incomplete.",
      "action": "Emit partial explanation; flag explanation_complete=false.",
      "user_message": ""
    },
    {
      "code": "KILL_SWITCH_ACTIVE",
      "severity": "WARN",
      "meaning": "KillSwitch was active at fill time; noted in explanation.",
      "action": "Include kill-switch note in explanation.",
      "user_message": ""
    }
  ],
  "metrics": {
    "emitted": [
      {
        "name": "polytraders_gov_posttradeexplainer_explanations_total",
        "type": "counter",
        "unit": "count",
        "labels": [
          "complete"
        ],
        "meaning": "Total explanations emitted, labelled by completeness."
      },
      {
        "name": "polytraders_gov_posttradeexplainer_archive_fetch_latency_ms",
        "type": "histogram",
        "unit": "ms",
        "labels": [],
        "meaning": "Latency of archive fetch per explanation."
      },
      {
        "name": "polytraders_gov_posttradeexplainer_partial_explanations_total",
        "type": "counter",
        "unit": "count",
        "labels": [],
        "meaning": "Total partial explanations (archive unavailable)."
      },
      {
        "name": "polytraders_gov_posttradeexplainer_retention_days",
        "type": "gauge",
        "unit": "days",
        "labels": [],
        "meaning": "Configured retention in days; should always be >= 2555."
      }
    ],
    "alerts": [
      {
        "name": "PostTradeExplainerArchiveUnavailable",
        "condition": "rate(polytraders_gov_posttradeexplainer_partial_explanations_total[10m]) > 0",
        "severity": "P2",
        "runbook": "#runbook-posttradeexplainer-archive"
      },
      {
        "name": "PostTradeExplainerRetentionBreach",
        "condition": "polytraders_gov_posttradeexplainer_retention_days < 2555",
        "severity": "P1",
        "runbook": "#runbook-posttradeexplainer-retention"
      }
    ]
  },
  "state": {
    "store": "postgres",
    "shape": "trade_explanations table: {report_id, fill_id, trace_id, strategy, guard_votes[], plain_english, explanation_complete, retained_until}",
    "ttl": "2555 days (7 years)",
    "recovery": "On restart, resume consuming ExecutionReport events from the last processed offset.",
    "size_estimate": "~2 KB per explanation; ~7 MB per year at 100 fills/day"
  },
  "concurrency": {
    "execution_model": "event-driven; one goroutine per ExecutionReport",
    "max_in_flight": 100,
    "idempotency_key": "fill_id",
    "timeout_ms": 2000,
    "backpressure": "queue",
    "locking": "none (append-only writes)"
  },
  "dependencies": {
    "depends_on": [
      {
        "bot_id": "internal.report_archive",
        "why": "DecisionReport and RiskVote fetched for each fill.",
        "contract": "Records available by trace_id within 500ms of fill."
      }
    ],
    "emits_to": [
      {
        "bot_id": "internal.post_trade_archive",
        "what": "SettlementReport with full explanation and 7-year retention"
      }
    ],
    "sibling": [
      {
        "bot_id": "gov.attributionrevenuereporter",
        "why": "AttributionRevenueReporter references SettlementReport records for fee reconciliation.",
        "contract": "SettlementReport includes builder_fee_pusd."
      }
    ],
    "external": [
      {
        "service": "Internal report archive",
        "endpoint": "https://archive.internal",
        "sla": "99.9%",
        "failure_mode": "Emit partial explanation with explanation_complete=false."
      }
    ]
  },
  "security_surfaces": {
    "signs_orders": false,
    "private_key_access": "none",
    "abuse_vectors": [
      "Requesting explanations for fills not belonging to the requesting user"
    ],
    "mitigations": [
      "SettlementReport records are scoped to wallet_address; read access requires authenticated user context"
    ]
  },
  "failure_injection": [
    {
      "scenario": "ARCHIVE_UNAVAILABLE",
      "how_to_inject": "Block reads from internal.reportArchive during fill processing",
      "expected_behaviour": "Partial SettlementReport emitted with explanation_complete=false; alert fired",
      "recovery": "Automatic retry when archive recovers."
    },
    {
      "scenario": "MISSING_RISK_VOTES",
      "how_to_inject": "Delete RiskVote records for a specific trace_id",
      "expected_behaviour": "EXPLANATION_MISSING_GUARD_VOTES emitted; explanation proceeds without votes",
      "recovery": "No recovery needed \u2014 partial explanation is acceptable."
    },
    {
      "scenario": "RETENTION_CONFIG_BREACH",
      "how_to_inject": "Set retain_explanation_days=30 in config",
      "expected_behaviour": "RETENTION_BELOW_REGULATORY_MINIMUM emitted; config rejected",
      "recovery": "Restore retain_explanation_days to >= 2555."
    }
  ],
  "runbook": {
    "summary": "PostTradeExplainer incidents are usually archive unavailability producing partial explanations. These are low-urgency unless retention is also breached.",
    "oncall_actions": [
      {
        "alert": "PostTradeExplainerArchiveUnavailable",
        "first_action": "Check internal report archive health.",
        "escalate_to": "Data engineering if archive is down > 30 min"
      },
      {
        "alert": "PostTradeExplainerRetentionBreach",
        "first_action": "Identify which config change reduced retention; revert immediately.",
        "escalate_to": "Governance pod lead and compliance team"
      }
    ],
    "manual_overrides": [
      {
        "name": "backfill-explanations",
        "how": "polytraders gov posttradeexplainer backfill --from <ts> --to <ts>",
        "when": "Archive was unavailable and fills have no explanations."
      }
    ],
    "healthcheck": "/internal/health/posttradeexplainer \u2192 green if Consuming ExecutionReport events; retention_days >= 2555; partial_explanations rate == 0; red if retention_days < 2555 or archive unreachable"
  },
  "promotion_gates": {
    "to_shadow": [
      {
        "gate": "Explanation composition unit tests pass; all required fields present",
        "how_measured": "CI",
        "threshold": "Pass"
      }
    ],
    "to_limited_live": [
      {
        "gate": "End-to-end: fill \u2192 ExecutionReport \u2192 SettlementReport verified in staging",
        "how_measured": "Integration test",
        "threshold": "Pass"
      }
    ],
    "to_general_live": [
      {
        "gate": "7-year retention enforced in Postgres schema migration; compliance review passed",
        "how_measured": "Compliance team sign-off",
        "threshold": "Pass"
      }
    ]
  },
  "reporting": {
    "emits_kinds": [
      "SettlementReport"
    ],
    "topics": [
      "polytraders.reports.settlement"
    ],
    "cadence": "every-event",
    "retention_class": "7y",
    "sampling_rule": "emit-every",
    "bus_failure_action": "wal-then-retry",
    "user_visible": "summary-only",
    "consumes_kinds": [
      "ExecutionReport"
    ]
  },
  "capital_impact": "Indirect",
  "v3_status": {
    "phase": 7,
    "phase_name": "Governance & replay",
    "docs": {
      "done": 27,
      "total": 27,
      "state": "done"
    },
    "impl": {
      "done": 0,
      "total": 15,
      "state": "pending"
    },
    "runtime": {
      "done": 0,
      "total": 8,
      "state": "pending"
    },
    "overall": "pending"
  }
}