{
  "schema_version": "1.0.0",
  "bot_id": "4.10",
  "bot_name": "ContradictionDetector",
  "slug": "contradictiondetector",
  "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": "Surface markets whose resolution rule contradicts itself, the title, or a parent neg-risk constraint.",
  "why_it_matters": [
    {
      "failure": "Self-contradicting market traded as if resolvable",
      "consequence": "A market whose resolution rule contradicts its title or a parent neg-risk constraint may never resolve cleanly. Strategies that trade it can be left holding positions through ambiguous or court-overturned outcomes.",
      "worked_example": {
        "setup": "A neg-risk parent has 5 child markets that must sum to exactly one YES. Child #4 lists at 11:42 UTC with a rule that allows two outcomes (e.g. 'X wins OR ties') to both resolve YES.",
        "without_bot": "MarketScanner approves the market. A strategy bot enters. At resolution, both #2 and #4 settle YES, the parent invariant breaks, and the position settles in a way the strategy never modelled.",
        "with_bot": "ContradictionDetector flags the rule against the parent constraint at listing time. Discovery excludes the market with reason `RULE_CONTRADICTS_PARENT`, and Governance opens a review ticket before any trade is placed."
      }
    },
    {
      "failure": "Neg-risk parent constraint silently violated",
      "consequence": "Polymarket's neg-risk markets share a constraint that exactly one outcome must resolve YES across the set. A child market whose rule allows two simultaneous YES resolutions breaks that invariant; without detection, sizing and hedging logic on those markets is wrong."
    },
    {
      "failure": "Operators surprised at resolution time",
      "consequence": "Discovery of a contradiction at resolution is the worst possible time. Catching it at listing time lets Discovery exclude the market or Governance flag it for human review days before any trade is placed."
    },
    {
      "failure": "No structured reason for excluding a market",
      "consequence": "Without this signal, MarketScanner has no defensible reason to filter out a malformed market beyond an operator's gut call. The contradiction record provides the audit trail."
    }
  ],
  "polymarket_inputs": [
    {
      "input": "YES prices for all markets in a mutually-exclusive group",
      "source": "data",
      "required": true,
      "use": "Sum YES prices to detect contradiction (sum > 1.0 in mutually-exclusive group)."
    },
    {
      "input": "Market group definitions and related condition_ids",
      "source": "gamma",
      "required": true,
      "use": "Identify which markets form mutually-exclusive groups."
    }
  ],
  "internal_inputs": [
    {
      "input": "KillSwitch active flag",
      "source": "KillSwitch",
      "required": true,
      "use": "Suppress all emissions when KillSwitch is active."
    }
  ],
  "raw_params": [
    "severity_threshold \u00b7 enum",
    "publish_to \u00b7 list",
    "auto_pause_strategies \u00b7 list",
    "require_human_review \u00b7 bool"
  ],
  "parameters": [
    {
      "name": "contradiction_threshold",
      "default": 1.05,
      "warning": 1.02,
      "hard": 1.0,
      "controls": "Minimum sum of YES prices in a mutually-exclusive group to trigger a contradiction signal.",
      "why_default_matters": "1.05 absorbs normal bid-ask spread noise while still detecting genuine mis-pricing opportunities.",
      "threshold_logic": [
        {
          "condition": "sum >= 1.05",
          "action": "Normal \u2014 emit contradiction ObservationReport"
        },
        {
          "condition": "1.02\u20131.05",
          "action": "WARN \u2014 borderline contradiction; emit with low_confidence flag"
        },
        {
          "condition": "< 1.0",
          "action": "No contradiction \u2014 skip emission"
        }
      ],
      "dev_check": "if (total_yes < p.contradiction_threshold.hard) return;",
      "user_facing": "Contradictions are only reported when market prices are materially inconsistent."
    },
    {
      "name": "poll_interval_s",
      "default": 30,
      "warning": 120,
      "hard": 300,
      "controls": "Seconds between price checks per market group.",
      "why_default_matters": "30 s provides near-real-time contradiction detection across related markets.",
      "threshold_logic": [
        {
          "condition": "interval <= 30 s",
          "action": "Normal"
        },
        {
          "condition": "30\u2013120 s",
          "action": "WARN \u2014 reduced detection speed"
        },
        {
          "condition": "> 300 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": "Prices are checked frequently to catch contradictions as soon as they appear."
    }
  ],
  "default_config": {
    "bot_id": "intel.contradictiondetector",
    "version": "0.1.0",
    "mode": "planned",
    "defaults": {
      "contradiction_threshold": 1.05,
      "poll_interval_s": 30
    },
    "locked": {
      "contradiction_threshold": {
        "min": 1.0
      },
      "poll_interval_s": {
        "max": 300
      }
    }
  },
  "implementation_flow": [],
  "decision_logic": {
    "approve": "",
    "reshape_required": "",
    "reject": "",
    "warning_only": ""
  },
  "decision_output_schema": "RiskVote",
  "decision_output_example": {
    "report_id": "rep_cd_grp_uselection_1746703000000",
    "trace_id": "trc_0xbeef0102030405060711",
    "bot_id": "intel.contradictiondetector",
    "kind": "ObservationReport",
    "market_group_id": "grp_uselection2026",
    "contradiction_detected": true,
    "total_yes_sum": 1.18,
    "warnings": [],
    "emitted_at_ms": 1746703005000
  },
  "developer_log": {
    "bot_id": "intel.contradictiondetector",
    "market_group_id": "grp_uselection2026",
    "total_yes_sum": 1.18,
    "contradiction_detected": true,
    "prices_fetched": 2,
    "prices_missing": 0,
    "killswitch_active": false
  },
  "user_explanations": [
    {
      "situation": "Strategy identified a pricing inconsistency across related markets",
      "message": "The prices across related markets don't add up to 100% \u2014 one or more outcomes appear over-priced. This is flagged as a potential analytical signal."
    },
    {
      "situation": "No contradiction signal despite related markets",
      "message": "All related markets are priced consistently \u2014 their YES prices sum to approximately 100%."
    }
  ],
  "failure_modes": {
    "main_failure_mode": "Data API outage prevents price fetching for a market group, causing ContradictionDetector to miss genuine contradiction windows during the outage.",
    "false_positive_risk": "Momentary wide spreads during low liquidity cause the YES-sum to exceed the threshold briefly, generating a contradiction signal with no tradeable opportunity.",
    "false_negative_risk": "A market group with an incomplete set of condition_ids (one market delisted) causes the YES-sum check to be based on a partial group, missing genuine contradictions.",
    "safe_fallback": "If Data API is unavailable for any group member, skip that group's contradiction check entirely rather than computing a partial sum. Emit STALE_DATA WARN.",
    "required_dependencies": [
      "Polymarket Data API for live prices",
      "Polymarket Gamma API for group definitions",
      "KillSwitch active flag"
    ]
  },
  "acceptance_tests": {
    "unit": [
      {
        "test": "YES-sum above threshold emits contradiction ObservationReport",
        "setup": "Group of 2 markets, YES prices = [0.62, 0.56], sum = 1.18",
        "expected": "ObservationReport emitted with contradiction_detected=true, total_yes_sum=1.18"
      },
      {
        "test": "YES-sum below threshold suppresses emission",
        "setup": "YES prices = [0.55, 0.43], sum = 0.98",
        "expected": "No ObservationReport; no alert"
      },
      {
        "test": "KillSwitch suppresses emission",
        "setup": "killswitch.active=true; contradiction present",
        "expected": "No ObservationReport; KILL_SWITCH_ACTIVE logged"
      }
    ],
    "integration": [
      {
        "test": "Full lifecycle: contradiction detected and consumed by arbitrage strategy",
        "expected": "Strategy receives ObservationReport with contradiction_detected=true and total_yes_sum"
      },
      {
        "test": "Data API down: STALE_DATA emitted; group check skipped",
        "expected": "STALE_DATA WARN; no ObservationReport for affected groups"
      }
    ],
    "property": [
      {
        "property": "ContradictionDetector never submits or signs orders",
        "required": "Always true"
      },
      {
        "property": "No ObservationReport emitted when KillSwitch is active",
        "required": "Always true"
      }
    ]
  },
  "checklist_overrides": {},
  "legacy_goal": "Surface markets whose resolution rule contradicts itself, the title, or a parent neg-risk constraint.",
  "legacy_pm_signals": [
    "Title vs. rule consistency check",
    "Neg-risk sum-to-one consistency across siblings",
    "Cross-market resolution dependencies that can't both be true"
  ],
  "legacy_external_feeds": [],
  "reporting_groups": [
    "pretrade_intel"
  ],
  "network": [
    "polygon"
  ],
  "api_surface": [
    "gamma",
    "data",
    "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": "Detects mutually contradictory market prices using negRisk and related market linkage. Read-only. No order signing."
  },
  "reference_implementation": {
    "pseudocode": "FUNCTION detectContradictions(market_group):\n  // 0. KillSwitch check\n  IF FETCH internal.killswitch.status == ACTIVE:\n    RETURN\n\n  // 1. Fetch prices for all markets in group\n  prices = {}\n  FOR cid IN market_group.condition_ids:\n    p = FETCH data.GET('/prices/' + cid)\n    IF p IS NULL:\n      EMIT WARN 'STALE_DATA'\n      CONTINUE\n    prices[cid] = p\n\n  // 2. Check mutual-exclusivity constraint\n  total_yes = sum(prices[c].yes for c in prices)\n  IF total_yes > contradiction_threshold:\n    EMIT WARN 'CONTRADICTIONDETECTOR_PRICE_CONTRADICTION'\n    contradiction_detected = True\n  ELSE:\n    contradiction_detected = False\n\n  // 3. Emit ObservationReport\n  EMIT ObservationReport {\n    report_id: gen_id(),\n    kind: 'ObservationReport',\n    market_group_id: market_group.id,\n    contradiction_detected: contradiction_detected,\n    total_yes_sum: total_yes,\n    emitted_at_ms: now_ms()\n  }",
    "sdk_calls": [
      "data.GET('/prices/<condition_id>')",
      "gamma.GET('/market-groups/<group_id>')",
      "internal.killswitch.status"
    ],
    "complexity": "O(M) per market group where M = markets in group"
  },
  "wire_examples": {
    "input": {
      "label": "Contradiction check for a mutually-exclusive market group",
      "source": "internal",
      "payload": {
        "market_group_id": "grp_uselection2026",
        "condition_ids": [
          "0xabc1000000000000000000000000000000000000000000000000000000000000",
          "0xabc2000000000000000000000000000000000000000000000000000000000000"
        ],
        "timestamp_ms": 1746703000000
      }
    },
    "output": {
      "label": "ObservationReport \u2014 contradiction detected",
      "payload": {
        "report_id": "rep_cd_grp_uselection_1746703000000",
        "trace_id": "trc_0xbeef0102030405060711",
        "bot_id": "intel.contradictiondetector",
        "kind": "ObservationReport",
        "market_group_id": "grp_uselection2026",
        "contradiction_detected": true,
        "total_yes_sum": 1.18,
        "emitted_at_ms": 1746703005000
      }
    }
  },
  "reason_codes": [
    {
      "code": "CONTRADICTIONDETECTOR_PRICE_CONTRADICTION",
      "severity": "WARN",
      "meaning": "Sum of YES prices in a mutually-exclusive market group exceeds the contradiction_threshold.",
      "action": "Emit ObservationReport with contradiction_detected=true; downstream strategies may exploit arbitrage.",
      "user_message": "Inconsistent pricing detected across related markets."
    },
    {
      "code": "STALE_DATA",
      "severity": "WARN",
      "meaning": "One or more market prices in the group could not be fetched.",
      "action": "Skip contradiction check for this cycle; retry on next poll.",
      "user_message": ""
    },
    {
      "code": "KILL_SWITCH_ACTIVE",
      "severity": "HARD_REJECT",
      "meaning": "KillSwitch active; all ContradictionDetector emissions suppressed.",
      "action": "Continue polling but suppress all ObservationReport emissions.",
      "user_message": "Contradiction signals paused while trading is suspended system-wide."
    },
    {
      "code": "CONTRADICTIONDETECTOR_GROUP_INCOMPLETE",
      "severity": "WARN",
      "meaning": "Market group has fewer active condition_ids than expected; contradiction check may be incomplete.",
      "action": "Emit with incomplete_group=true warning; strategies apply lower confidence weight.",
      "user_message": ""
    }
  ],
  "metrics": {
    "emitted": [
      {
        "name": "polytraders_intel_contradictiondetector_observations_emitted_total",
        "type": "counter",
        "unit": "count",
        "labels": [
          "contradiction_detected"
        ],
        "meaning": "ObservationReports emitted, broken down by contradiction_detected (true/false)."
      },
      {
        "name": "polytraders_intel_contradictiondetector_contradictions_total",
        "type": "counter",
        "unit": "count",
        "labels": [
          "market_group_id"
        ],
        "meaning": "Total contradiction events detected per market group."
      },
      {
        "name": "polytraders_intel_contradictiondetector_yes_sum_gauge",
        "type": "gauge",
        "unit": "score",
        "labels": [
          "market_group_id"
        ],
        "meaning": "Latest sum of YES prices for each market group."
      }
    ],
    "alerts": [
      {
        "name": "ContradictionDetectorContradictionActive",
        "condition": "polytraders_intel_contradictiondetector_yes_sum_gauge > 1.05",
        "severity": "warn",
        "runbook": "#runbook-contradictiondetector-active"
      },
      {
        "name": "ContradictionDetectorStaleData",
        "condition": "rate(polytraders_intel_contradictiondetector_observations_emitted_total[10m]) == 0",
        "severity": "warn",
        "runbook": "#runbook-contradictiondetector-stale"
      }
    ],
    "dashboards": [
      "Grafana \u2014 Intelligence / ContradictionDetector YES-sum by market group"
    ],
    "log_level": "info"
  },
  "state": {
    "store": "redis",
    "shape": "Per market_group_id: last_yes_sum, last_contradiction_at_ms, last_prices snapshot.",
    "ttl": "Per-group state expires after 1 h of inactivity",
    "recovery": "On cold start, re-fetch all active group prices on first poll cycle.",
    "size_estimate": "~2 KB per tracked market group"
  },
  "concurrency": {
    "execution_model": "async per-group poll loop",
    "max_in_flight": 20,
    "idempotency_key": "market_group_id + poll_cycle_ms",
    "timeout_ms": 8000,
    "backpressure": "drop-after-buffer \u2014 max 50 pending group checks",
    "locking": "Redis SETNX on market_group_id + cycle to prevent duplicate checks"
  },
  "dependencies": {
    "depends_on": [
      {
        "bot_id": "risk.kill_switch",
        "why": "Suppress emissions when KillSwitch is active."
      }
    ],
    "emits_to": [
      {
        "bot_id": "strat.arbitrage_strategies",
        "what": "ObservationReport with contradiction_detected and total_yes_sum for cross-market arbitrage signals"
      }
    ],
    "sibling": [
      "intel.marketontologybuilder"
    ],
    "external": [
      {
        "service": "Polymarket Data API",
        "endpoint": "https://data-api.polymarket.com",
        "sla": "99.9% / 300 ms p99",
        "fallback": "Skip affected group; emit STALE_DATA on retry failure"
      }
    ]
  },
  "security_surfaces": {
    "signs_orders": false,
    "private_key_access": "none",
    "abuse_vectors": [
      "Adversary places thin orders to artificially inflate YES-sum and trigger false contradiction signals",
      "Data API returns stale prices during low-liquidity periods, causing spurious contradictions"
    ],
    "mitigations": [
      "contradiction_threshold parameter tuned above normal spread noise to reduce false positives",
      "ObservationReports are informational only \u2014 strategies independently validate before acting"
    ]
  },
  "failure_injection": [
    {
      "scenario": "DATA_API_DOWN",
      "how_to_inject": "Block Data API for 10 min",
      "expected_behaviour": "STALE_DATA WARN; all group checks skipped; ContradictionDetectorStaleData alert fires after 10 min",
      "recovery": "Automatic on API recovery; next poll cycle resumes normally"
    },
    {
      "scenario": "SYNTHETIC_CONTRADICTION",
      "how_to_inject": "Set mock prices to sum YES > 1.2 for a test group",
      "expected_behaviour": "CONTRADICTIONDETECTOR_PRICE_CONTRADICTION WARN; ObservationReport emitted with contradiction_detected=true",
      "recovery": "Automatic when prices normalise"
    },
    {
      "scenario": "KILL_SWITCH_ON",
      "how_to_inject": "Set killswitch.active=true during active polling",
      "expected_behaviour": "All emissions suppressed; KILL_SWITCH_ACTIVE logged; polling continues",
      "recovery": "Automatic on KillSwitch reset"
    }
  ],
  "runbook": {
    "summary": "ContradictionDetector incidents are either Data API outages or genuine price contradictions. Contradictions are signals for strategies, not errors.",
    "oncall_actions": [
      {
        "alert": "ContradictionDetectorContradictionActive",
        "first_step": "Verify prices on Polymarket.com for the affected market group. If genuine, downstream strategies will consume the signal. No manual action required unless prices are clearly erroneous.",
        "diagnosis": "",
        "mitigation": "",
        "escalation": "Intelligence pod lead if contradiction persists > 30 min"
      },
      {
        "alert": "ContradictionDetectorStaleData",
        "first_step": "Check Data API health. Verify network connectivity from bot host.",
        "diagnosis": "",
        "mitigation": "",
        "escalation": "Intelligence pod lead if API down > 10 min"
      }
    ],
    "manual_overrides": [
      {
        "command": "adjust_contradiction_threshold",
        "effect": "Update config.contradiction_threshold; requires approval gate if threshold < 1.02 \u2014 When market spreads widen due to low liquidity causing false positives"
      }
    ],
    "healthcheck": "Endpoint: /internal/health/contradictiondetector | Green: Last check < 2x poll_interval_s AND Redis reachable AND Data API returning 200 | Red: No check for > 10 min OR Redis unreachable"
  },
  "promotion_gates": {
    "to_shadow": [
      {
        "gate": "Unit tests pass for contradiction detection, incomplete group handling, and KillSwitch suppression",
        "how_measured": "CI test run",
        "threshold": "100% pass"
      }
    ],
    "to_limited_live": [
      {
        "gate": "No false positives on 10 synthetic non-contradictory group checks",
        "how_measured": "Integration test with injected prices",
        "threshold": "0 false positives"
      }
    ],
    "to_general_live": [
      {
        "gate": "100% detection of synthetic contradiction injection over 7-day soak",
        "how_measured": "Integration test log audit",
        "threshold": "100% detection"
      }
    ]
  },
  "reporting": {
    "emits_kinds": [
      "ObservationReport"
    ],
    "topics": [
      "polytraders.reports.observation"
    ],
    "cadence": "every-event",
    "retention_class": "30d",
    "retention_notes": "Full fidelity for 30 d; rolled-up summary retained for 1 y",
    "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"
  }
}