{
  "schema_version": "2.0.0",
  "bot_id": "4.2",
  "bot_name": "OracleWatcher",
  "slug": "oraclewatcher",
  "layer": "Intelligence",
  "layer_key": "intel",
  "bot_class": "Signal Service",
  "authority": [
    "Read-only"
  ],
  "status": "live",
  "readiness": "General live",
  "flagship": false,
  "is_reference": false,
  "public_export": false,
  "identity": {
    "layer": "Intelligence",
    "bot_class": "Signal Service",
    "authority": "Read-only",
    "runs_before": "oracleriskmonitor, risk layer",
    "runs_after": "Polymarket market creation / resolution-question registration",
    "applies_to": "All live Polymarket markets whose resolution source is UMA Optimistic Oracle",
    "default_mode": "general_live",
    "user_visible": "Advanced details only",
    "developer_owner": "Polytraders core \u2014 Intelligence pod"
  },
  "purpose": "OracleWatcher streams the UMA Optimistic Oracle on-chain, detecting assertion proposals, dispute filings, DVM debate escalations, and final vote outcomes for every Polymarket condition ID. It emits an ObservationReport on every state change. OracleWatcher is strictly read-only \u2014 it never submits or signs anything. Output is the primary feed for oracleriskmonitor, which uses it to gate positions during contested resolutions.",
  "why_it_matters": [
    {
      "failure": "Proposal missed before 2-hour challenge window closes",
      "consequence": "Position held through unchallenged incorrect resolution; full collateral loss on the wrong outcome."
    },
    {
      "failure": "Dispute state not detected",
      "consequence": "oracleriskmonitor is unaware that a market is in a 24\u201372-hour DVM debate; strategies continue trading as if resolution is certain."
    },
    {
      "failure": "DVM vote outcome not propagated",
      "consequence": "Settlement logic based on outdated oracle state; payout mismatch or duplicate claim."
    },
    {
      "failure": "Stale UMA state cached during RPC outage",
      "consequence": "Old proposal treated as current; oracleriskmonitor computes wrong time-to-resolution, potentially permitting over-leveraged entry."
    }
  ],
  "polymarket_inputs": [
    {
      "input": "Market condition IDs and associated assertion IDs",
      "source": "Gamma API / internal market registry",
      "required": true,
      "use": "Map UMA assertion IDs back to Polymarket condition IDs in ObservationReport payloads."
    },
    {
      "input": "UMA Optimistic Oracle on-chain events (ProposePrice, DisputePrice, Settle)",
      "source": "onchain (Polygon RPC + event log subscription)",
      "required": true,
      "use": "Detect assertion lifecycle state changes in real time."
    },
    {
      "input": "Oracle WebSocket market stream for assertion ID mapping",
      "source": "ws_market",
      "required": false,
      "use": "Supplement on-chain event logs with low-latency assertion state updates."
    }
  ],
  "internal_inputs": [
    {
      "input": "Watched condition ID list",
      "source": "config / StrategyRegistry",
      "required": true,
      "use": "Filter UMA events to only condition IDs with open positions or pending strategies."
    },
    {
      "input": "KillSwitch active flag",
      "source": "KillSwitch",
      "required": true,
      "use": "Continue watching on-chain but suppress ObservationReport emissions when KillSwitch is active."
    }
  ],
  "raw_params": [
    "watched_condition_ids \u00b7 list",
    "poll_interval_s \u00b7 int",
    "alert_on_state_change \u00b7 bool"
  ],
  "parameters": [
    {
      "name": "poll_interval_s",
      "default": 12,
      "warning": 30,
      "hard": 60,
      "controls": "Seconds between on-chain RPC polls for new UMA oracle events. Lower = fresher but higher RPC cost.",
      "why_default_matters": "12 s aligns with Polygon block time, ensuring every new block is checked without redundant polls.",
      "threshold_logic": [
        {
          "condition": "interval \u2264 12 s",
          "action": "Normal \u2014 one poll per block"
        },
        {
          "condition": "12\u201330 s",
          "action": "WARN \u2014 may miss events in high-throughput windows"
        },
        {
          "condition": "> 60 s",
          "action": "Hard cap \u2014 PARAMETER_CHANGE_REQUIRES_APPROVAL"
        }
      ],
      "dev_check": "if (p.poll_interval_s > p.hard) throw ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL');",
      "user_facing": "Oracle state is checked every Polygon block to ensure no resolution event is missed."
    },
    {
      "name": "challenge_window_alert_s",
      "default": 3600,
      "warning": 1800,
      "hard": 600,
      "controls": "Seconds before the 2-hour UMA challenge window closes at which a WARN alert is emitted to oracleriskmonitor.",
      "why_default_matters": "1-hour pre-alert gives downstream risk bots time to reduce exposure before the challenge deadline.",
      "threshold_logic": [
        {
          "condition": "alert_s \u2265 3600",
          "action": "Normal \u2014 1-hour pre-alert"
        },
        {
          "condition": "1800\u20133600 s",
          "action": "WARN \u2014 shorter pre-alert; tighter margin"
        },
        {
          "condition": "< 600 s",
          "action": "Reject \u2014 insufficient time for risk response"
        }
      ],
      "dev_check": "if (p.challenge_window_alert_s < p.hard) throw ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL');",
      "user_facing": "You are alerted well before the window to challenge an oracle result closes."
    },
    {
      "name": "alert_on_state_change",
      "default": true,
      "warning": null,
      "hard": null,
      "controls": "If true, emit ObservationReport on every oracle state transition (proposal \u2192 dispute \u2192 DVM \u2192 settle). If false, only emit on PROPOSAL and SETTLED.",
      "why_default_matters": "Full state-change tracking ensures oracleriskmonitor has the complete dispute lifecycle.",
      "threshold_logic": [],
      "dev_check": "",
      "user_facing": "Every step in the oracle resolution process generates a status update."
    }
  ],
  "default_config": {
    "bot_id": "intel.oraclewatcher",
    "version": "2.1.0",
    "mode": "general_live",
    "defaults": {
      "poll_interval_s": 12,
      "challenge_window_alert_s": 3600,
      "alert_on_state_change": true
    },
    "locked": {
      "poll_interval_s": {
        "max": 60
      },
      "challenge_window_alert_s": {
        "min": 600
      }
    }
  },
  "implementation_flow": [
    "On startup, subscribe to Polygon RPC event log for UMA OptimisticOracle ProposePrice, DisputePrice, and Settle events filtered to Polymarket ancillary data prefixes.",
    "On each block (poll_interval_s), fetch new events since last processed block number.",
    "For each event: resolve assertion_id \u2192 condition_id via Gamma API / internal registry.",
    "Determine oracle_state: PROPOSED | CHALLENGED | DVM_DEBATE | DVM_VOTE | SETTLED.",
    "Compute time_to_deadline_s: for PROPOSED, time until 2-hour challenge window closes; for DVM_DEBATE, estimated 24\u201348 h; for DVM_VOTE, estimated 48 h.",
    "If oracle_state is PROPOSED and time_to_deadline_s \u2264 challenge_window_alert_s, emit WARN ORACLEWATCHER_CHALLENGE_WINDOW_CLOSING.",
    "Check KillSwitch; if active, continue watching but suppress emissions.",
    "If alert_on_state_change is true (or state = PROPOSED | SETTLED), emit ObservationReport with: report_id, condition_id, assertion_id, oracle_state, bond_pusd, time_to_deadline_s, proposed_price, disputer (if any), dvm_question_id (if any), block_number.",
    "Log per-event summary: condition_id, state transition, bond_pusd, time_to_deadline_s."
  ],
  "decision_logic": {
    "approve": "Not applicable \u2014 OracleWatcher is read-only; it never approves or rejects trades.",
    "reshape_required": "Not applicable.",
    "reject": "Events are suppressed (not emitted) only when KillSwitch is active (KILL_SWITCH_ACTIVE). All oracle state changes for watched condition_ids are otherwise always emitted.",
    "warning_only": "ORACLEWATCHER_CHALLENGE_WINDOW_CLOSING is emitted as a WARN when < challenge_window_alert_s remain before the proposal deadline."
  },
  "decision_output_schema": "ObservationReport",
  "decision_output_example": {
    "report_id": "rep_ow_0xdef5_1746700900000",
    "trace_id": "trc_0xfeed000102030405",
    "bot_id": "intel.oraclewatcher",
    "kind": "ObservationReport",
    "condition_id": "0xdef5670000000000000000000000000000000000000000000000000000000000",
    "assertion_id": "0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b",
    "oracle_state": "CHALLENGED",
    "bond_pusd": 750,
    "time_to_deadline_s": null,
    "proposed_price": 1,
    "disputer": "0xABCDEF1234567890ABCDEF1234567890ABCDEF12",
    "dvm_question_id": "0x9f8e7d6c5b4a3f2e1d0c9b8a7f6e5d4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9f8e",
    "block_number": 72345678,
    "warnings": [
      "ORACLEWATCHER_CHALLENGE_WINDOW_CLOSING"
    ],
    "emitted_at_ms": 1746700900120
  },
  "developer_log": {
    "bot_id": "intel.oraclewatcher",
    "block_number": 72345678,
    "events_detected": 2,
    "events_emitted": 2,
    "killswitch_active": false,
    "condition_ids_watched": 14,
    "pending_proposals": 3,
    "active_disputes": 1,
    "dvm_votes_pending": 0
  },
  "user_explanations": [
    {
      "situation": "Market shows 'Resolution contested'",
      "message": "Someone has disputed the proposed outcome for this market. The UMA DVM will adjudicate over the next 24\u201372 hours. Your position is protected until the dispute is resolved."
    },
    {
      "situation": "Challenge window closing alert",
      "message": "The window to contest the proposed resolution for this market closes soon. Risk systems are reviewing exposure automatically."
    },
    {
      "situation": "Market resolution finalised",
      "message": "The UMA oracle has settled this market. Final outcome is now on-chain and settlement will proceed."
    }
  ],
  "failure_modes": {
    "main_failure_mode": "RPC provider outage causes OracleWatcher to miss a state transition (e.g., PROPOSED \u2192 CHALLENGED), leaving oracleriskmonitor with a stale oracle state and potentially allowing trades during an undetected dispute.",
    "false_positive_risk": "A non-Polymarket UMA assertion sharing an ancillary data prefix causes a spurious ObservationReport for a condition_id that has no open positions.",
    "false_negative_risk": "High Polygon block reorg causes an event to be processed twice or missed entirely; OracleWatcher does not re-check reorged blocks beyond the RPC provider's finality window.",
    "safe_fallback": "If RPC is unavailable for > 2\u00d7 poll_interval_s, emit STALE_DATA WARN and halt new ObservationReport emissions until RPC recovers. Do not emit reports based on cached state older than 2 blocks.",
    "required_dependencies": [
      "Polygon RPC (event log access)",
      "Gamma API for assertion_id \u2192 condition_id mapping",
      "KillSwitch active flag readable"
    ]
  },
  "acceptance_tests": {
    "unit": [
      {
        "test": "PROPOSAL event emits ObservationReport with oracle_state=PROPOSED",
        "setup": "Mock ProposePrice event for watched condition_id",
        "expected": "ObservationReport emitted with oracle_state=PROPOSED, bond_pusd=750, time_to_deadline_s\u22487200"
      },
      {
        "test": "DISPUTE event emits ObservationReport with oracle_state=CHALLENGED",
        "setup": "Mock DisputePrice event following ProposePrice",
        "expected": "ObservationReport emitted with oracle_state=CHALLENGED, disputer populated"
      },
      {
        "test": "Challenge window alert fires at correct threshold",
        "setup": "time_to_deadline_s = 3500, challenge_window_alert_s=3600",
        "expected": "WARN ORACLEWATCHER_CHALLENGE_WINDOW_CLOSING included in warnings"
      },
      {
        "test": "KillSwitch suppresses emissions but watching continues",
        "setup": "killswitch.active=true; PROPOSAL event arrives",
        "expected": "Event detected and logged; no ObservationReport emitted"
      },
      {
        "test": "RPC outage emits STALE_DATA and halts emissions",
        "setup": "RPC returns error for 30 s (> 2\u00d7 poll_interval_s=12)",
        "expected": "STALE_DATA WARN logged; no ObservationReport emitted during outage"
      },
      {
        "test": "SETTLED event emits ObservationReport with oracle_state=SETTLED",
        "setup": "Mock Settle event with final price",
        "expected": "ObservationReport with oracle_state=SETTLED, proposed_price=final resolved value"
      }
    ],
    "integration": [
      {
        "test": "Full lifecycle: PROPOSED \u2192 CHALLENGED \u2192 DVM_DEBATE \u2192 SETTLED generates 4 ObservationReports",
        "expected": "oracleriskmonitor receives all four state transitions with correct condition_id and timings"
      },
      {
        "test": "Gamma API outage causes assertion_id resolution to fail gracefully",
        "expected": "Event buffered; ObservationReport emitted with condition_id=null and STALE_DATA warning once API recovers"
      },
      {
        "test": "Non-Polymarket UMA event filtered out by ancillary data prefix check",
        "expected": "No ObservationReport emitted for foreign assertion_id"
      }
    ],
    "property": [
      {
        "property": "OracleWatcher never submits, signs, or modifies any order or on-chain transaction",
        "required": "Always true"
      },
      {
        "property": "No ObservationReport emitted when KillSwitch is active",
        "required": "Always true"
      },
      {
        "property": "No ObservationReport emitted when RPC state is stale (> 2 blocks unconfirmed)",
        "required": "Always true"
      }
    ]
  },
  "checklist_overrides": {},
  "legacy_goal": "Monitor UMA oracle for proposals, disputes, debates, votes.",
  "legacy_pm_signals": [
    "Per-market resolution-risk state & countdown"
  ],
  "legacy_external_feeds": [
    "UMA Optimistic Oracle on-chain events",
    "UMA dispute & vote queues"
  ],
  "reporting_groups": [
    "pretrade_intel"
  ],
  "network": [
    "polygon"
  ],
  "api_surface": [
    "onchain",
    "ws_market",
    "internal"
  ],
  "version": {
    "spec": "2.0.0",
    "implementation": "2.1.0",
    "schema": "2",
    "released": "2026-04-28"
  },
  "migration_history": [
    {
      "date": "2026-04-28",
      "from": "v1",
      "to": "v2",
      "reason": "CLOB V2 cutover \u2014 collateral change to pUSD, bond denomination updated",
      "action_taken": "UMA bond denomination updated from USDC.e to pUSD ($750 pUSD). ObservationReport payload bond_pusd field renamed from bond_usdc. No feeRateBps or signed-order plumbing in this bot."
    }
  ],
  "polymarket_v2_compat": {
    "clob_version": "v2",
    "collateral": "pUSD",
    "eip712_domain_version": "2",
    "builder_code_aware": false,
    "negrisk_aware": false,
    "multichain_ready": false,
    "sdk_used": "py-clob-client-v2",
    "settlement_contract": "CTFExchangeV2",
    "notes": "OracleWatcher tracks the UMA Optimistic Oracle with a $750 pUSD bond, 2-hour challenge window, and DVM dispute path (24\u201348 h debate + ~48 h vote). Bond amounts in all payloads are denominated in pUSD."
  },
  "reference_implementation": {
    "summary": "Subscribes to Polygon RPC event log for UMA oracle events, maps assertion_ids to condition_ids, tracks the full proposal\u2192dispute\u2192DVM\u2192settle lifecycle, and emits an ObservationReport on every state change.",
    "language_note": "Pseudocode is language-agnostic. FETCH = read input. EMIT = produce output.",
    "pseudocode": "FUNCTION watchBlock(block_number):\n  // --- 0. RPC health check ---\n  IF rpc.last_block_age_s > 2 * params.poll_interval_s:\n    EMIT WARN 'STALE_DATA \u2014 RPC unresponsive'\n    RETURN\n\n  // --- 1. Fetch new UMA events since last_processed_block ---\n  events = rpc.getLogs(\n    address   = UMA_OPTIMISTIC_ORACLE_ADDR,\n    topics    = [PROPOSE_PRICE_SIG, DISPUTE_PRICE_SIG, SETTLE_SIG],\n    fromBlock = last_processed_block + 1,\n    toBlock   = block_number\n  )\n\n  // --- 2. KillSwitch gate ---\n  ks = FETCH internal.killswitch.status\n\n  FOR event IN events:\n    // --- 3. Ancillary data prefix filter ---\n    IF NOT event.ancillaryData.startsWith(POLYMARKET_PREFIX):\n      CONTINUE\n\n    // --- 4. Resolve assertion_id \u2192 condition_id ---\n    condition_id = gamma_api.GET('/assertion/' + event.assertionId + '/condition')\n    IF condition_id IS NULL:\n      condition_id = internal.registry.lookup(event.assertionId)\n    IF condition_id IS NULL OR condition_id NOT IN watched_condition_ids:\n      CONTINUE\n\n    // --- 5. Determine oracle state ---\n    state = SWITCH event.topic:\n      PROPOSE_PRICE_SIG  -> 'PROPOSED'\n      DISPUTE_PRICE_SIG  -> 'CHALLENGED'\n      SETTLE_SIG         -> 'SETTLED'\n\n    // --- 6. Compute deadline ---\n    IF state == 'PROPOSED':\n      challenge_deadline_ms = event.timestamp_ms + 2*60*60*1000   // +2h\n      time_to_deadline_s    = (challenge_deadline_ms - now_ms()) / 1000\n    ELSE IF state == 'CHALLENGED':\n      time_to_deadline_s    = 24*60*60  // DVM: 24\u201348h\n    ELSE:\n      time_to_deadline_s    = None\n\n    // --- 7. Challenge window alert ---\n    warnings = []\n    IF state == 'PROPOSED' AND time_to_deadline_s <= params.challenge_window_alert_s:\n      warnings.append('ORACLEWATCHER_CHALLENGE_WINDOW_CLOSING')\n\n    // --- 8. KillSwitch suppress ---\n    IF ks.active:\n      LOG INFO 'KILL_SWITCH_ACTIVE \u2014 suppressing ObservationReport'\n      CONTINUE\n\n    // --- 9. Emit ---\n    IF params.alert_on_state_change OR state IN ('PROPOSED', 'SETTLED'):\n      report = ObservationReport(\n        report_id         = 'rep_ow_' + condition_id[:6] + '_' + now_ms(),\n        trace_id          = newTraceId(),\n        bot_id            = 'intel.oraclewatcher',\n        kind              = 'ObservationReport',\n        condition_id      = condition_id,\n        assertion_id      = event.assertionId,\n        oracle_state      = state,\n        bond_pusd         = 750,\n        time_to_deadline_s= time_to_deadline_s,\n        proposed_price    = event.price,\n        disputer          = event.disputer IF state == 'CHALLENGED' ELSE null,\n        dvm_question_id   = event.dvmQuestionId IF state IN ('CHALLENGED','DVM_VOTE') ELSE null,\n        block_number      = block_number,\n        warnings          = warnings,\n        emitted_at_ms     = now_ms()\n      )\n      EMIT internal.bus.observations <- report\n\n  last_processed_block = block_number\n",
    "sdk_calls": [
      "rpc.getLogs(address=UMA_OPTIMISTIC_ORACLE_ADDR, topics=[...], fromBlock, toBlock)",
      "gamma_api.GET('/assertion/<assertionId>/condition')",
      "internal.registry.lookup(assertionId)"
    ],
    "complexity": "O(E) per block where E = UMA oracle events in that block; typically O(1) in steady state"
  },
  "wire_examples": {
    "input": {
      "label": "Polygon RPC ProposePrice event",
      "source": "onchain",
      "payload": {
        "event": "ProposePrice",
        "assertionId": "0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b",
        "ancillaryData": "0x706f6c796d61726b65743a313233",
        "price": 1,
        "timestamp_ms": 1746700900000,
        "block_number": 72345678
      }
    },
    "output": {
      "label": "ObservationReport \u2014 PROPOSED state",
      "payload": {
        "report_id": "rep_ow_0xdef5_1746700900000",
        "trace_id": "trc_0xfeed000102030405060708",
        "bot_id": "intel.oraclewatcher",
        "kind": "ObservationReport",
        "condition_id": "0xdef5670000000000000000000000000000000000000000000000000000000000",
        "assertion_id": "0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b",
        "oracle_state": "PROPOSED",
        "bond_pusd": 750,
        "time_to_deadline_s": 7198,
        "proposed_price": 1,
        "disputer": null,
        "dvm_question_id": null,
        "block_number": 72345678,
        "warnings": [],
        "emitted_at_ms": 1746700900120
      }
    }
  },
  "reason_codes": [
    {
      "code": "ORACLEWATCHER_CHALLENGE_WINDOW_CLOSING",
      "severity": "WARN",
      "meaning": "Less than challenge_window_alert_s remain before the 2-hour UMA challenge deadline closes.",
      "action": "Include in ObservationReport warnings; oracleriskmonitor triggers exposure review.",
      "user_message": "The window to challenge the proposed market resolution is closing soon."
    },
    {
      "code": "ORACLEWATCHER_DISPUTE_FILED",
      "severity": "WARN",
      "meaning": "A dispute has been filed on a Polymarket proposal; market enters DVM path (24\u201372 h).",
      "action": "Emit ObservationReport with oracle_state=CHALLENGED; oracleriskmonitor restricts new entries.",
      "user_message": "The proposed resolution for this market has been contested. The outcome will be determined by the UMA DVM over the next 24\u201372 hours."
    },
    {
      "code": "ORACLEWATCHER_DVM_VOTE_OPEN",
      "severity": "WARN",
      "meaning": "DVM vote phase opened; resolution will be decided by UMA token holders (~48 h).",
      "action": "Emit ObservationReport with oracle_state=DVM_VOTE; time_to_deadline_s\u2248172800.",
      "user_message": "A community vote is underway to determine this market's outcome."
    },
    {
      "code": "ORACLEWATCHER_SETTLED",
      "severity": "INFO",
      "meaning": "UMA oracle has finalised and settled the assertion; resolution is on-chain.",
      "action": "Emit ObservationReport with oracle_state=SETTLED; downstream settlement proceeds.",
      "user_message": "Market resolution is finalised and settlement is in progress."
    },
    {
      "code": "KILL_SWITCH_ACTIVE",
      "severity": "HARD_REJECT",
      "meaning": "KillSwitch active; ObservationReport emissions suppressed.",
      "action": "Continue watching on-chain but suppress emissions.",
      "user_message": "Oracle status updates are paused while trading is suspended system-wide."
    },
    {
      "code": "STALE_DATA",
      "severity": "WARN",
      "meaning": "RPC provider unresponsive for > 2\u00d7 poll_interval_s; oracle state may be stale.",
      "action": "Halt ObservationReport emissions until RPC recovers; alert on-call.",
      "user_message": ""
    },
    {
      "code": "MARKET_CLOSED",
      "severity": "EXPLAIN",
      "meaning": "UMA event detected for a condition_id that is already closed in the internal registry.",
      "action": "Skip emission; log for audit trail only.",
      "user_message": ""
    },
    {
      "code": "ORACLEWATCHER_UNKNOWN_ASSERTION",
      "severity": "WARN",
      "meaning": "Assertion ID from on-chain event could not be resolved to a Polymarket condition_id.",
      "action": "Log with assertion_id; do not emit ObservationReport; retry on next block.",
      "user_message": ""
    }
  ],
  "metrics": {
    "emitted": [
      {
        "name": "polytraders_intel_oraclewatcher_events_detected_total",
        "type": "counter",
        "unit": "count",
        "labels": [
          "oracle_state"
        ],
        "meaning": "UMA oracle events detected on-chain per state type."
      },
      {
        "name": "polytraders_intel_oraclewatcher_observations_emitted_total",
        "type": "counter",
        "unit": "count",
        "labels": [
          "oracle_state"
        ],
        "meaning": "ObservationReports emitted, broken down by oracle_state."
      },
      {
        "name": "polytraders_intel_oraclewatcher_active_disputes_gauge",
        "type": "gauge",
        "unit": "count",
        "labels": [],
        "meaning": "Number of condition_ids currently in CHALLENGED or DVM_VOTE state."
      },
      {
        "name": "polytraders_intel_oraclewatcher_time_to_challenge_deadline_s",
        "type": "gauge",
        "unit": "seconds",
        "labels": [
          "condition_id"
        ],
        "meaning": "Seconds remaining before challenge window closes for each PROPOSED assertion."
      },
      {
        "name": "polytraders_intel_oraclewatcher_rpc_block_lag_s",
        "type": "gauge",
        "unit": "seconds",
        "labels": [],
        "meaning": "Age of the most recently processed Polygon block."
      },
      {
        "name": "polytraders_intel_oraclewatcher_unknown_assertions_total",
        "type": "counter",
        "unit": "count",
        "labels": [],
        "meaning": "UMA events received for assertion IDs that could not be mapped to a condition_id."
      }
    ],
    "alerts": [
      {
        "name": "OracleWatcherRPCDown",
        "condition": "polytraders_intel_oraclewatcher_rpc_block_lag_s > 60",
        "severity": "page",
        "runbook": "#runbook-oraclewatcher-rpc-down"
      },
      {
        "name": "OracleWatcherChallengeWindowImminent",
        "condition": "min(polytraders_intel_oraclewatcher_time_to_challenge_deadline_s) < 1800",
        "severity": "page",
        "runbook": "#runbook-oraclewatcher-challenge-imminent"
      },
      {
        "name": "OracleWatcherDisputeSpike",
        "condition": "polytraders_intel_oraclewatcher_active_disputes_gauge > 5",
        "severity": "warn",
        "runbook": "#runbook-oraclewatcher-dispute-spike"
      },
      {
        "name": "OracleWatcherHighUnknownAssertions",
        "condition": "rate(polytraders_intel_oraclewatcher_unknown_assertions_total[10m]) > 0.1",
        "severity": "warn",
        "runbook": "#runbook-oraclewatcher-unknown-assertions"
      }
    ],
    "dashboards": [
      "Grafana \u2014 Intelligence / OracleWatcher dispute lifecycle",
      "Grafana \u2014 Intelligence / challenge deadline countdown"
    ],
    "log_level": "info"
  },
  "state": {
    "store": "postgres",
    "shape": "oracle_states table: (condition_id, assertion_id, oracle_state, bond_pusd, proposed_price, disputer, time_to_deadline_s, last_block_number, updated_at). One row per watched condition_id.",
    "ttl": "1 year \u2014 UMA disputes have legal weight; retained beyond normal 30-day intel retention",
    "recovery": "On cold start, reload oracle_states from Postgres and resume watching from last_block_number. Any missed events since last_block_number are re-fetched from RPC.",
    "size_estimate": "~1 KB per condition_id row; ~14 KB for 14 watched markets"
  },
  "concurrency": {
    "execution_model": "single-threaded event loop",
    "max_in_flight": 1,
    "idempotency_key": "assertion_id + oracle_state",
    "timeout_ms": 5000,
    "backpressure": "wal-then-retry \u2014 events buffered to Postgres WAL if internal bus is unavailable",
    "locking": "row-level lock on oracle_states per condition_id during state transition"
  },
  "dependencies": {
    "depends_on": [
      {
        "bot_id": "risk.kill_switch",
        "why": "KillSwitch gate suppresses ObservationReport emissions when active."
      }
    ],
    "emits_to": [
      {
        "bot_id": "risk.oracle_risk_monitor",
        "what": "ObservationReport with oracle_state, time_to_deadline_s, and bond_pusd for every UMA state change"
      }
    ],
    "sibling": [],
    "external": [
      {
        "service": "Polygon RPC (Alchemy / Infura)",
        "sla": "99.9% / 200 ms p99",
        "fallback": "Fail over to secondary RPC provider; emit STALE_DATA if both unavailable"
      },
      {
        "service": "UMA Optimistic Oracle (on-chain)",
        "endpoint": "Polygon mainnet contract",
        "sla": "Blockchain-level finality",
        "fallback": "Continue watching; log missed blocks on recovery"
      },
      {
        "service": "Gamma API",
        "endpoint": "https://gamma-api.polymarket.com",
        "sla": "99.9% / 500 ms p99",
        "fallback": "Use internal assertion_id registry for resolution; retry Gamma API on next event"
      }
    ]
  },
  "security_surfaces": {
    "signs_orders": false,
    "private_key_access": "none",
    "abuse_vectors": [
      "Malicious ancillary data in a non-Polymarket UMA assertion crafted to match Polymarket prefix, injecting a spurious ObservationReport",
      "RPC provider substitution attack returning crafted event logs to fake a settlement"
    ],
    "mitigations": [
      "Ancillary data prefix validated against a hardcoded Polymarket prefix byte sequence",
      "Assertion IDs cross-checked against Gamma API and internal registry before emission",
      "All ObservationReports are informational only \u2014 settlement decisions require independent on-chain confirmation"
    ],
    "contract_calls": []
  },
  "failure_injection": [
    {
      "scenario": "RPC_OUTAGE",
      "how_to_inject": "Block TCP to primary Polygon RPC for 30 s",
      "expected_behaviour": "rpc_block_lag_s rises > 24 s; STALE_DATA WARN logged; OracleWatcherRPCDown alert fires; no ObservationReports emitted during outage",
      "recovery": "Automatic failover to secondary RPC; missed blocks re-fetched; emissions resume"
    },
    {
      "scenario": "DISPUTE_FILED_NEAR_DEADLINE",
      "how_to_inject": "Inject mock DisputePrice event with time_to_deadline_s=1700",
      "expected_behaviour": "ObservationReport emitted with oracle_state=CHALLENGED and ORACLEWATCHER_CHALLENGE_WINDOW_CLOSING warning; OracleWatcherChallengeWindowImminent alert fires",
      "recovery": "Alert clears when market transitions to DVM_DEBATE or SETTLED"
    },
    {
      "scenario": "UNKNOWN_ASSERTION",
      "how_to_inject": "Emit a ProposePrice event with an assertion_id not in Gamma API or internal registry",
      "expected_behaviour": "ORACLEWATCHER_UNKNOWN_ASSERTION logged; no ObservationReport emitted; retried on next block",
      "recovery": "Once Gamma API returns the mapping, event is resolved and emitted"
    },
    {
      "scenario": "KILL_SWITCH_ON",
      "how_to_inject": "Set killswitch.active=true; trigger a ProposePrice event",
      "expected_behaviour": "Event detected; oracle_state updated in Postgres; no ObservationReport emitted; KILL_SWITCH_ACTIVE logged",
      "recovery": "Emissions resume on first event after KillSwitch reset"
    },
    {
      "scenario": "DISPUTE_SPIKE",
      "how_to_inject": "Inject 6 concurrent dispute events across 6 condition_ids",
      "expected_behaviour": "active_disputes_gauge = 6; OracleWatcherDisputeSpike alert fires; all ObservationReports emitted correctly",
      "recovery": "Alert clears when disputes settle below threshold"
    }
  ],
  "runbook": {
    "summary": "OracleWatcher incidents are most commonly RPC outages or unexpected dispute spikes. Because UMA disputes can prevent settlement and have legal standing, page immediately on RPC outage or imminent challenge deadline.",
    "oncall_actions": [
      {
        "alert": "OracleWatcherRPCDown",
        "first_action": "Check rpc_block_lag_s. Verify primary and secondary Polygon RPC endpoints. Fail over manually if auto-failover has not triggered.",
        "escalate_to": "Infra on-call immediately; Intelligence pod lead within 5 minutes"
      },
      {
        "alert": "OracleWatcherChallengeWindowImminent",
        "first_action": "Identify the condition_id. Notify oracleriskmonitor on-call and trading pod \u2014 position reduction may be required before deadline.",
        "escalate_to": "Trading pod lead immediately"
      },
      {
        "alert": "OracleWatcherDisputeSpike",
        "first_action": "Review active dispute list. Verify each is a legitimate Polymarket dispute and not a data anomaly from the RPC.",
        "escalate_to": "Intelligence pod lead within 15 minutes"
      },
      {
        "alert": "OracleWatcherHighUnknownAssertions",
        "first_action": "Check unknown_assertions_total logs for assertion_ids. Verify against Gamma API manually. May indicate a new market not yet in registry.",
        "escalate_to": "Intelligence pod lead if unresolved > 30 min"
      }
    ],
    "manual_overrides": [
      {
        "name": "force_resync",
        "how": "Set config.resync_from_block=<block_number> to replay event logs from a specific block",
        "when": "After an extended RPC outage to recover missed state transitions"
      }
    ],
    "healthcheck": "GET /internal/health/oraclewatcher -> 200 if rpc_block_lag_s < 24 AND Postgres reachable AND last ObservationReport emitted within 10 min of last on-chain UMA event. RED if rpc_block_lag_s > 60 OR Postgres unreachable OR any PROPOSED assertion has time_to_deadline_s < 600 with no alert fired."
  },
  "promotion_gates": {
    "to_shadow": [
      {
        "gate": "Unit tests pass for PROPOSE, DISPUTE, and SETTLE state transitions",
        "how_measured": "CI test run",
        "threshold": "100% pass"
      },
      {
        "gate": "RPC integration test: block log subscription fires on mock ProposePrice event",
        "how_measured": "Integration test against Polygon testnet",
        "threshold": "Pass"
      }
    ],
    "to_limited_live": [
      {
        "gate": "rpc_block_lag_s p99 < 15 s over 48 h",
        "how_measured": "polytraders_intel_oraclewatcher_rpc_block_lag_s gauge",
        "threshold": "p99 < 15 s"
      },
      {
        "gate": "All test assertion_ids resolve correctly against Gamma API",
        "how_measured": "Integration test with known Polymarket markets",
        "threshold": "100% resolution"
      }
    ],
    "to_general_live": [
      {
        "gate": "Zero missed PROPOSED events over 14 days (verified by reconciling on-chain logs vs emitted ObservationReports)",
        "how_measured": "Post-hoc on-chain reconciliation script",
        "threshold": "0 missed events"
      },
      {
        "gate": "KillSwitch suppression: zero ObservationReports emitted when KillSwitch active",
        "how_measured": "Integration test",
        "threshold": "Pass"
      },
      {
        "gate": "Postgres 1-year retention verified: oracle_states rows survive 365-day TTL check",
        "how_measured": "DB retention policy test",
        "threshold": "Pass"
      }
    ]
  },
  "reporting": {
    "emits_kinds": [
      "ObservationReport"
    ],
    "topics": [
      "polytraders.reports.intel"
    ],
    "cadence": "every-event",
    "retention_class": "1y",
    "retention_notes": "1-year full retention \u2014 UMA disputes carry legal standing and audit requirements",
    "sampling_rule": "emit-every",
    "bus_failure_action": "wal-then-retry",
    "user_visible": "no",
    "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"
  }
}