{
  "schema_version": "1.0.0",
  "bot_id": "4.9",
  "bot_name": "SourceOfTruthVerifier",
  "slug": "sourceoftruthverifier",
  "layer": "Intelligence",
  "layer_key": "intel",
  "bot_class": "Signal Service",
  "authority": [
    "Read-only"
  ],
  "status": "planned",
  "readiness": "Spec started",
  "flagship": false,
  "is_reference": false,
  "public_export": false,
  "identity": {
    "layer": "Intelligence",
    "bot_class": "Signal Service",
    "authority": "Read-only",
    "runs_before": "",
    "runs_after": "",
    "applies_to": "",
    "default_mode": "shadow_only",
    "user_visible": "Advanced details only",
    "developer_owner": "Polytraders core"
  },
  "purpose": "Sanity-check that the source listed in a market's rule actually publishes what's needed for resolution.",
  "why_it_matters": [
    {
      "failure": "Cited source does not actually publish what's needed",
      "consequence": "A market that cites 'team-website.com' as its source-of-truth resolves on whatever that site publishes. If the site stops publishing, publishes only behind a paywall, or publishes too coarsely (winner only, no margin), the market cannot be resolved cleanly. Catching that at listing prevents the trade.",
      "worked_example": {
        "setup": "Market `Will [Driver] win the [Race] by more than 2.5 seconds?` cites the team's own website. The site historically publishes only winner + lap time of the winner, not gap-to-second.",
        "without_bot": "MarketScanner approves the market. A strategy enters on a 60/40 prior. At resolution, the cited source publishes only the winner. Polymarket falls back to a secondary source under dispute, and the market settles 9 days after the race.",
        "with_bot": "SourceOfTruthVerifier inspects the last 50 publications from the cited URL, fails to find a `gap_to_second` field, and emits `SOURCE_INSUFFICIENT_FOR_RULE`. Discovery excludes the market on listing."
      }
    },
    {
      "failure": "Rule change to a worse source goes undetected",
      "consequence": "A post-listing edit can swap the cited source for a less reliable one. Pairing this verifier with RuleChangeMonitor catches the moment the cited source becomes unsuitable."
    },
    {
      "failure": "Operators have no defensible exclusion reason",
      "consequence": "MarketScanner needs a structured reason to exclude a market with a broken source-of-truth. The verifier produces that reason in a form Discovery and Governance can both consume."
    }
  ],
  "polymarket_inputs": [
    {
      "input": "Market status and metadata from Gamma API",
      "source": "gamma",
      "required": true,
      "use": "First source in three-way verification."
    },
    {
      "input": "Market status and metadata from Data API",
      "source": "data",
      "required": true,
      "use": "Second source in three-way verification."
    },
    {
      "input": "On-chain condition state from CTFExchangeV2",
      "source": "onchain",
      "required": true,
      "use": "Third source \u2014 authoritative on-chain resolution status."
    }
  ],
  "internal_inputs": [
    {
      "input": "KillSwitch active flag",
      "source": "KillSwitch",
      "required": true,
      "use": "Suppress all emissions when KillSwitch is active."
    }
  ],
  "raw_params": [
    "min_uptime_pct \u00b7 0\u2013100",
    "required_publication_cadence_h \u00b7 int",
    "auto_flag_unverified \u00b7 bool",
    "publish_to \u00b7 list"
  ],
  "parameters": [
    {
      "name": "poll_interval_s",
      "default": 120,
      "warning": 600,
      "hard": 1800,
      "controls": "Seconds between three-source verification cycles per active market.",
      "why_default_matters": "120 s provides timely detection of source divergence without overloading Gamma, Data, and RPC endpoints.",
      "threshold_logic": [
        {
          "condition": "interval <= 120 s",
          "action": "Normal"
        },
        {
          "condition": "120\u2013600 s",
          "action": "WARN \u2014 reduced divergence detection speed"
        },
        {
          "condition": "> 1800 s",
          "action": "Reject \u2014 PARAMETER_CHANGE_REQUIRES_APPROVAL"
        }
      ],
      "dev_check": "if (p.poll_interval_s > p.hard) throw ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL');",
      "user_facing": "Market state is verified across multiple sources regularly to catch inconsistencies."
    },
    {
      "name": "staleness_threshold_s",
      "default": 300,
      "warning": 600,
      "hard": 3600,
      "controls": "Maximum age of any source response before verification is considered stale.",
      "why_default_matters": "300 s ensures all three sources are fresh before comparison.",
      "threshold_logic": [
        {
          "condition": "age <= 300 s",
          "action": "Normal"
        },
        {
          "condition": "300\u2013600 s",
          "action": "WARN \u2014 approaching stale threshold"
        },
        {
          "condition": "> 3600 s",
          "action": "Reject \u2014 emit STALE_DATA and halt verification"
        }
      ],
      "dev_check": "if (source_age_s > p.staleness_threshold_s.hard) emit('STALE_DATA');",
      "user_facing": "Source data must be fresh for verification to be reliable."
    }
  ],
  "default_config": {
    "bot_id": "intel.sourceoftruthverifier",
    "version": "0.1.0",
    "mode": "planned",
    "defaults": {
      "poll_interval_s": 120,
      "staleness_threshold_s": 300
    },
    "locked": {
      "poll_interval_s": {
        "max": 1800
      },
      "staleness_threshold_s": {
        "max": 3600
      }
    }
  },
  "implementation_flow": [],
  "decision_logic": {
    "approve": "",
    "reshape_required": "",
    "reject": "",
    "warning_only": ""
  },
  "decision_output_schema": "RiskVote",
  "decision_output_example": {
    "report_id": "rep_sotv_0xf1a2_1746703000000",
    "trace_id": "trc_0xbeef0102030405060710",
    "bot_id": "intel.sourceoftruthverifier",
    "kind": "ObservationReport",
    "condition_id": "0xf1a2b30000000000000000000000000000000000000000000000000000000000",
    "verified": true,
    "discrepancies": [],
    "emitted_at_ms": 1746703003000
  },
  "developer_log": {
    "bot_id": "intel.sourceoftruthverifier",
    "condition_id": "0xf1a2b30000000000000000000000000000000000000000000000000000000000",
    "verified": true,
    "discrepancies": [],
    "gamma_response_ms": 95,
    "data_response_ms": 110,
    "chain_response_ms": 180,
    "killswitch_active": false
  },
  "user_explanations": [
    {
      "situation": "Strategy paused entry pending source-of-truth verification",
      "message": "Before entering a position, the system checks that all data sources agree on the market's current state. This ensures positions are not opened on inconsistent data."
    },
    {
      "situation": "Mismatch detected between on-chain state and API",
      "message": "The on-chain market state differs from the API. This is usually a temporary propagation delay. The system paused new entries until sources agree."
    }
  ],
  "failure_modes": {
    "main_failure_mode": "On-chain RPC outage during a market resolution event causes the verifier to miss the chain-resolved status, potentially leaving strategies in positions on an already-resolved market.",
    "false_positive_risk": "Propagation lag between on-chain resolution and Gamma/Data API updates causes a transient CHAIN_API_MISMATCH that resolves within minutes.",
    "false_negative_risk": "All three sources share the same cached CDN layer during a large-scale outage, causing the verifier to report agreement when all three are returning stale data.",
    "safe_fallback": "If any source is unavailable for > staleness_threshold_s, emit STALE_DATA WARN and suppress new ObservationReports until all three sources return fresh data.",
    "required_dependencies": [
      "Polymarket Gamma API",
      "Polymarket Data API",
      "Polygon RPC",
      "KillSwitch active flag",
      "Postgres for verification audit log"
    ]
  },
  "acceptance_tests": {
    "unit": [
      {
        "test": "All three sources agree: verified=true ObservationReport emitted",
        "setup": "Gamma, Data, and chain all return consistent resolved=false for active market",
        "expected": "ObservationReport emitted with verified=true, discrepancies=[]"
      },
      {
        "test": "Chain/API mismatch detected and reported",
        "setup": "Chain resolved=true; Data API resolved=false",
        "expected": "ObservationReport emitted with verified=false, discrepancies=['chain_api_mismatch']"
      },
      {
        "test": "KillSwitch suppresses emission",
        "setup": "killswitch.active=true; all sources available",
        "expected": "No ObservationReport; KILL_SWITCH_ACTIVE logged"
      }
    ],
    "integration": [
      {
        "test": "Full lifecycle: verification triggers on strategy entry request; result consumed by strategy",
        "expected": "Strategy receives verified=true ObservationReport before entry"
      },
      {
        "test": "Gamma API down: STALE_DATA emitted; verification halted",
        "expected": "STALE_DATA WARN; no ObservationReports until Gamma recovers"
      }
    ],
    "property": [
      {
        "property": "SourceOfTruthVerifier never submits or signs orders",
        "required": "Always true"
      },
      {
        "property": "No ObservationReport emitted when KillSwitch is active",
        "required": "Always true"
      }
    ]
  },
  "checklist_overrides": {},
  "legacy_goal": "Sanity-check that the source listed in a market's rule actually publishes what's needed for resolution.",
  "legacy_pm_signals": [
    "ResolutionRuleParser (4.8) source-of-truth field",
    "Source-uptime and publication-cadence history",
    "Field-mapping coverage (does the source actually expose this number?)"
  ],
  "legacy_external_feeds": [
    "Approved source-of-truth registry"
  ],
  "reporting_groups": [
    "pretrade_intel"
  ],
  "network": [
    "polygon"
  ],
  "api_surface": [
    "gamma",
    "data",
    "onchain",
    "internal"
  ],
  "version": {
    "spec": "2.0.0",
    "implementation": "0.1.0",
    "schema": "2",
    "released": null,
    "planned_release": "Q3-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": false,
    "negrisk_aware": true,
    "multichain_ready": false,
    "sdk_used": "py-clob-client-v2",
    "settlement_contract": "CTFExchangeV2",
    "notes": "Cross-validates Gamma metadata against on-chain CTFExchangeV2 state and data API. All pUSD-denominated. No order signing."
  },
  "reference_implementation": {
    "pseudocode": "FUNCTION verifySourceOfTruth(condition_id):\n  // 0. KillSwitch check\n  IF FETCH internal.killswitch.status == ACTIVE:\n    RETURN\n\n  // 1. Fetch from all three sources\n  gamma_state = FETCH gamma.GET('/markets/' + condition_id)\n  data_state = FETCH data.GET('/markets/' + condition_id)\n  chain_state = FETCH onchain.CTFExchangeV2.getCondition(condition_id)\n\n  // 2. Compare resolution status\n  IF gamma_state.status != data_state.status:\n    EMIT WARN 'SOURCEOFTRUTH_GAMMA_DATA_MISMATCH'\n\n  // 3. Compare on-chain vs data API\n  IF chain_state.resolved != data_state.resolved:\n    EMIT WARN 'SOURCEOFTRUTH_CHAIN_API_MISMATCH'\n\n  // 4. Staleness check\n  IF now() - gamma_state.updated_at > staleness_threshold_s:\n    EMIT WARN 'STALE_DATA'\n\n  // 5. Emit ObservationReport\n  EMIT ObservationReport {\n    report_id: gen_id(),\n    kind: 'ObservationReport',\n    condition_id: condition_id,\n    verified: all_agree(gamma_state, data_state, chain_state),\n    discrepancies: collect_discrepancies(),\n    emitted_at_ms: now_ms()\n  }",
    "sdk_calls": [
      "gamma.GET('/markets/<condition_id>')",
      "data.GET('/markets/<condition_id>')",
      "onchain.CTFExchangeV2.getCondition(condition_id)"
    ],
    "complexity": "O(1) per market; 3 parallel API calls per verification"
  },
  "wire_examples": {
    "input": {
      "label": "Source-of-truth verification request for a condition",
      "source": "internal",
      "payload": {
        "condition_id": "0xf1a2b30000000000000000000000000000000000000000000000000000000000",
        "trigger": "pre-strategy-entry",
        "timestamp_ms": 1746703000000
      }
    },
    "output": {
      "label": "ObservationReport \u2014 sources agree",
      "payload": {
        "report_id": "rep_sotv_0xf1a2_1746703000000",
        "trace_id": "trc_0xbeef0102030405060710",
        "bot_id": "intel.sourceoftruthverifier",
        "kind": "ObservationReport",
        "condition_id": "0xf1a2b30000000000000000000000000000000000000000000000000000000000",
        "verified": true,
        "discrepancies": [],
        "emitted_at_ms": 1746703003000
      }
    }
  },
  "reason_codes": [
    {
      "code": "SOURCEOFTRUTH_GAMMA_DATA_MISMATCH",
      "severity": "WARN",
      "meaning": "Gamma API market status differs from Data API market status.",
      "action": "Emit ObservationReport with discrepancy; flag for strategy review.",
      "user_message": "Market state data sources are temporarily inconsistent."
    },
    {
      "code": "SOURCEOFTRUTH_CHAIN_API_MISMATCH",
      "severity": "WARN",
      "meaning": "On-chain CTFExchangeV2 resolution state differs from Data API.",
      "action": "Emit ObservationReport with chain_api_mismatch=true; alert on-call.",
      "user_message": "On-chain market state differs from API \u2014 resolution may be pending propagation."
    },
    {
      "code": "STALE_DATA",
      "severity": "WARN",
      "meaning": "One or more sources has not updated within staleness_threshold_s.",
      "action": "Suppress emission; retry on next poll; alert if persists.",
      "user_message": ""
    },
    {
      "code": "KILL_SWITCH_ACTIVE",
      "severity": "HARD_REJECT",
      "meaning": "KillSwitch active; all SourceOfTruthVerifier emissions suppressed.",
      "action": "Continue polling but suppress all ObservationReport emissions.",
      "user_message": "Verification signals paused while trading is suspended system-wide."
    }
  ],
  "metrics": {
    "emitted": [
      {
        "name": "polytraders_intel_sourceoftruthverifier_observations_emitted_total",
        "type": "counter",
        "unit": "count",
        "labels": [
          "verified"
        ],
        "meaning": "ObservationReports emitted, broken down by verified (true/false)."
      },
      {
        "name": "polytraders_intel_sourceoftruthverifier_discrepancies_total",
        "type": "counter",
        "unit": "count",
        "labels": [
          "discrepancy_type"
        ],
        "meaning": "Total discrepancy events detected by type."
      },
      {
        "name": "polytraders_intel_sourceoftruthverifier_verification_latency_ms",
        "type": "histogram",
        "unit": "ms",
        "labels": [],
        "meaning": "Time to complete a three-source verification per market."
      }
    ],
    "alerts": [
      {
        "name": "SourceOfTruthChainAPIMismatch",
        "condition": "rate(polytraders_intel_sourceoftruthverifier_discrepancies_total{discrepancy_type='chain_api'}[5m]) > 0",
        "severity": "page",
        "runbook": "#runbook-sourceoftruth-chain-api-mismatch"
      },
      {
        "name": "SourceOfTruthGammaDataMismatch",
        "condition": "rate(polytraders_intel_sourceoftruthverifier_discrepancies_total{discrepancy_type='gamma_data'}[10m]) > 2",
        "severity": "warn",
        "runbook": "#runbook-sourceoftruth-gamma-data-mismatch"
      }
    ],
    "dashboards": [
      "Grafana \u2014 Intelligence / SourceOfTruthVerifier discrepancy rate and verification latency"
    ],
    "log_level": "info"
  },
  "state": {
    "store": "postgres",
    "shape": "Per condition_id: last_verified_at_ms, last_discrepancy_type, last_chain_state, last_gamma_status.",
    "ttl": "Retained for compliance for 1 y; archived after market resolves",
    "recovery": "On cold start, reload from Postgres; re-verify all active markets on first cycle.",
    "size_estimate": "~2 KB per tracked market"
  },
  "concurrency": {
    "execution_model": "async per-market verification triggered by strategy entry or periodic poll",
    "max_in_flight": 15,
    "idempotency_key": "condition_id + verification_cycle_id",
    "timeout_ms": 15000,
    "backpressure": "drop-after-buffer \u2014 max 50 pending verifications queued",
    "locking": "Postgres advisory lock on condition_id during verification update"
  },
  "dependencies": {
    "depends_on": [
      {
        "bot_id": "risk.kill_switch",
        "why": "Suppress emissions when KillSwitch is active."
      },
      {
        "bot_id": "intel.resolutionruleparser",
        "why": "Consume parsed resolution rules for cross-validation."
      }
    ],
    "emits_to": [
      {
        "bot_id": "strat.all_strategies",
        "what": "ObservationReport with verified flag and discrepancies for pre-entry market state validation"
      }
    ],
    "sibling": [
      "intel.resolutionruleparser"
    ],
    "external": [
      {
        "service": "Polymarket Gamma API",
        "endpoint": "https://gamma-api.polymarket.com",
        "sla": "99.9% / 500 ms p99",
        "fallback": "Emit STALE_DATA if unavailable; skip verification cycle"
      },
      {
        "service": "Polygon RPC",
        "endpoint": "Polygon mainnet",
        "sla": "99.9% / 200 ms p99",
        "fallback": "Skip chain verification if RPC unavailable; emit SOURCEOFTRUTH_CHAIN_API_MISMATCH warning"
      }
    ]
  },
  "security_surfaces": {
    "signs_orders": false,
    "private_key_access": "none",
    "abuse_vectors": [
      "Attacker causes deliberate Gamma/chain mismatch to trigger strategy pauses via alert flooding",
      "Stale on-chain state during fast resolution causes spurious CHAIN_API_MISMATCH alerts"
    ],
    "mitigations": [
      "Alert thresholds require multiple mismatches before paging to reduce noise",
      "ObservationReports informational only \u2014 strategies verify independently before pausing"
    ]
  },
  "failure_injection": [
    {
      "scenario": "GAMMA_API_DOWN",
      "how_to_inject": "Block Gamma API for 10 min",
      "expected_behaviour": "STALE_DATA WARN; verifications skipped; no emissions",
      "recovery": "Automatic on Gamma recovery; re-verify all active markets"
    },
    {
      "scenario": "CHAIN_API_MISMATCH",
      "how_to_inject": "Inject resolved=true in mock chain state while Data API shows resolved=false",
      "expected_behaviour": "SOURCEOFTRUTH_CHAIN_API_MISMATCH WARN; ObservationReport emitted with discrepancy; SourceOfTruthChainAPIMismatch alert pages",
      "recovery": "Automatic once both sources agree"
    },
    {
      "scenario": "KILL_SWITCH_ON",
      "how_to_inject": "Set killswitch.active=true during active verification polling",
      "expected_behaviour": "All emissions suppressed; polling continues; KILL_SWITCH_ACTIVE logged",
      "recovery": "Emissions resume on KillSwitch reset"
    }
  ],
  "runbook": {
    "summary": "SourceOfTruthVerifier incidents are most critical when chain/API mismatch is detected \u2014 this may indicate a resolution propagation delay. Page immediately on chain mismatch.",
    "oncall_actions": [
      {
        "alert": "SourceOfTruthChainAPIMismatch",
        "first_step": "Check on-chain condition state via Polygon explorer. Compare with Data API response. If chain shows resolved, notify strategy team immediately.",
        "diagnosis": "",
        "mitigation": "",
        "escalation": "Intelligence pod lead immediately; escalate to market operations if > 5 min"
      },
      {
        "alert": "SourceOfTruthGammaDataMismatch",
        "first_step": "Check last_updated_at for both Gamma and Data API responses. If one is stale, wait for propagation.",
        "diagnosis": "",
        "mitigation": "",
        "escalation": "Intelligence pod lead if mismatch persists > 10 min"
      }
    ],
    "manual_overrides": [
      {
        "command": "force_reverify_all",
        "effect": "POST /internal/sourceoftruthverifier/reverify-all to force fresh three-source check on all active markets \u2014 After known Gamma or Data API outage recovery"
      }
    ],
    "healthcheck": "Endpoint: /internal/health/sourceoftruthverifier | Green: Last verification < 5 min ago AND Postgres reachable AND Gamma + RPC returning 200 | Red: No verification for > 15 min OR Postgres unreachable OR chain/API mismatch unresolved > 10 min"
  },
  "promotion_gates": {
    "to_shadow": [
      {
        "gate": "Unit tests pass for all three discrepancy types and KillSwitch suppression",
        "how_measured": "CI test run",
        "threshold": "100% pass"
      }
    ],
    "to_limited_live": [
      {
        "gate": "Verification latency p99 < 3 s over 24 h on staging",
        "how_measured": "polytraders_intel_sourceoftruthverifier_verification_latency_ms histogram",
        "threshold": "p99 < 3000 ms"
      }
    ],
    "to_general_live": [
      {
        "gate": "Zero missed mismatch detections over 7-day soak with synthetic injection",
        "how_measured": "Integration test log audit",
        "threshold": "100% detection of injected mismatches"
      }
    ]
  },
  "reporting": {
    "emits_kinds": [
      "ObservationReport"
    ],
    "topics": [
      "polytraders.reports.observation"
    ],
    "cadence": "every-event",
    "retention_class": "1y",
    "retention_notes": "Retained 1 y for compliance audit",
    "sampling_rule": "emit-every",
    "bus_failure_action": "drop-after-buffer",
    "user_visible": "summary-only",
    "consumes_kinds": []
  },
  "capital_impact": "Indirect",
  "v3_status": {
    "phase": 2,
    "phase_name": "Data normalisation",
    "docs": {
      "done": 27,
      "total": 27,
      "state": "done"
    },
    "impl": {
      "done": 0,
      "total": 15,
      "state": "pending"
    },
    "runtime": {
      "done": 0,
      "total": 8,
      "state": "pending"
    },
    "overall": "pending"
  }
}