{
  "schema_version": "1.0.0",
  "bot_id": "1.3",
  "bot_name": "OracleRiskMonitor",
  "slug": "oracleriskmonitor",
  "layer": "Risk",
  "layer_key": "risk",
  "bot_class": "Guardrail",
  "authority": [
    "Reject",
    "Reshape"
  ],
  "status": "live",
  "readiness": "General live",
  "flagship": true,
  "is_reference": true,
  "public_export": false,
  "identity": {
    "layer": "Risk",
    "bot_class": "Guardrail",
    "authority": "Reject, Reshape",
    "runs_before": "ExecutionPlan emit",
    "runs_after": "Strategy OrderIntent",
    "applies_to": "Every OrderIntent on markets that use the UMA Optimistic Oracle for resolution",
    "default_mode": "general_live",
    "user_visible": "Advanced details only",
    "developer_owner": "Polytraders core \u2014 Risk pod"
  },
  "purpose": "OracleRiskMonitor watches the UMA Optimistic Oracle queue for proposals and active disputes on markets where open positions exist. When a market enters a resolution proposal or a dispute window, the bot can block new orders on that market, require reduced size, or flag a position for review. It protects against the scenario where a trade is submitted into a market moments before a contested resolution flips the outcome. It never overrides the strategy intent or changes the direction of an order \u2014 it only controls whether and how much the order is permitted to proceed.",
  "why_it_matters": [
    {
      "failure": "Trading into an active oracle dispute",
      "consequence": "A position opened after a dispute was filed may be immediately underwater if the dispute resolves against the expected outcome; the resolution risk was not priced in."
    },
    {
      "failure": "Holding a full position through the proposal window",
      "consequence": "Markets entering their proposal phase have uncertain resolution timing. Holding full size through this window increases exposure to a binary outcome that cannot be hedged once the proposal is live."
    },
    {
      "failure": "Stale oracle status",
      "consequence": "If the oracle queue feed is unavailable, a strategy could open or increase a position without knowing a dispute is already active. The safe behaviour is to block, not to approve on missing data."
    },
    {
      "failure": "Neg-risk market definition shift during proposal",
      "consequence": "On neg-risk markets, the 'Other' outcome definition can shift if a proposal is disputed, potentially invalidating the expected payout structure."
    }
  ],
  "polymarket_inputs": [
    {
      "input": "Market resolution-source flag and dispute window metadata",
      "source": "Gamma API",
      "required": true,
      "use": "Identify whether the market uses the UMA oracle and retrieve the resolution window start and end times."
    },
    {
      "input": "UMA Optimistic Oracle on-chain proposal and dispute events",
      "source": "UMA Optimistic Oracle",
      "required": true,
      "use": "Detect when a proposal has been submitted or a dispute has been filed for a market relevant to open positions or pending orders."
    },
    {
      "input": "Open position list with market identifiers",
      "source": "Data API",
      "required": true,
      "use": "Cross-reference oracle events against markets where the account currently holds a position, to determine which events require action."
    },
    {
      "input": "Neg-risk flag per market",
      "source": "Gamma API",
      "required": false,
      "use": "Apply stricter reduce_at_proposal_pct on neg-risk markets, as definition shifts can affect multiple related markets simultaneously."
    }
  ],
  "internal_inputs": [
    {
      "input": "Current position size per market",
      "source": "PortfolioGuard",
      "required": true,
      "use": "Calculate the maximum allowed position size after applying reduce_at_proposal_pct during the proposal window."
    },
    {
      "input": "KillSwitch active flag",
      "source": "KillSwitch",
      "required": true,
      "use": "If KillSwitch is active, reject all orders without checking oracle status."
    }
  ],
  "raw_params": [
    "reduce_at_proposal_pct \u00b7 0\u2013100",
    "block_disputed \u00b7 bool",
    "max_dispute_window_h \u00b7 0\u2013168",
    "downgrade_size_by_confidence \u00b7 bool"
  ],
  "parameters": [
    {
      "name": "reduce_at_proposal_pct",
      "default": 50,
      "warning": 70,
      "hard": 100,
      "controls": "Maximum allowed position size, expressed as a percentage of the configured per-market limit, when a market has an active UMA proposal but no dispute yet.",
      "why_default_matters": "Cutting position size to 50% during a proposal window limits exposure to a binary outcome while still allowing strategies to maintain partial coverage.",
      "threshold_logic": [
        {
          "condition": "No proposal active",
          "action": "APPROVE at full size"
        },
        {
          "condition": "Proposal active, size \u2264 50% of limit",
          "action": "APPROVE"
        },
        {
          "condition": "Proposal active, size > 50% of limit",
          "action": "RESHAPE_REQUIRED \u2014 cap to 50% of per-market limit"
        },
        {
          "condition": "reduce_at_proposal_pct = 100 (locked)",
          "action": "REJECT all new orders on that market during proposal window"
        }
      ],
      "dev_check": "if (proposalActive && orderSize > limit * (p.default / 100)) return reshape({ max_size_usd: limit * (p.default / 100) });",
      "user_facing": "This market is currently in its resolution proposal window. We limited your order size to reduce exposure while the outcome is being confirmed."
    },
    {
      "name": "block_disputed",
      "default": true,
      "warning": null,
      "hard": null,
      "controls": "When true, reject all new orders on any market where a UMA dispute is currently active.",
      "why_default_matters": "An active dispute means the resolution outcome is genuinely uncertain and contested on-chain. Adding exposure during a live dispute is inconsistent with sound risk management.",
      "threshold_logic": [
        {
          "condition": "block_disputed=true AND dispute active",
          "action": "REJECT \u2014 ORACLE_DISPUTE_ACTIVE"
        },
        {
          "condition": "block_disputed=false AND dispute active",
          "action": "WARN only \u2014 allow with annotation"
        }
      ],
      "dev_check": "if (p.block_disputed && disputeActive) return reject('ORACLE_DISPUTE_ACTIVE');",
      "user_facing": "This market has an active resolution dispute. We blocked this order while the dispute is ongoing."
    },
    {
      "name": "max_dispute_window_h",
      "default": 48,
      "warning": 72,
      "hard": 168,
      "controls": "Maximum duration in hours that a dispute window is considered 'recent' for monitoring purposes. Disputes older than this limit are treated as stale and require a manual review flag.",
      "why_default_matters": "UMA disputes typically resolve within 48 hours. Tracking disputes beyond 168 hours (one week) adds noise without meaningful risk signal.",
      "threshold_logic": [
        {
          "condition": "Dispute age \u2264 48 h",
          "action": "Block (block_disputed applies)"
        },
        {
          "condition": "Dispute age 48\u2013168 h",
          "action": "WARN \u2014 flag for manual review"
        },
        {
          "condition": "Dispute age > 168 h",
          "action": "Treat as resolved; escalate to incident commander"
        }
      ],
      "dev_check": "const ageH = (Date.now() - disputeStartMs) / 3600000; if (ageH > p.hard) escalate('DISPUTE_OVERDUE');",
      "user_facing": "This market has had an unresolved dispute for an unusually long time. New orders are paused until the situation is reviewed."
    },
    {
      "name": "downgrade_size_by_confidence",
      "default": true,
      "warning": null,
      "hard": null,
      "controls": "When true, apply an additional proportional reduction to the allowed order size based on the elapsed fraction of the proposal window. As the window progresses, less new size is permitted.",
      "why_default_matters": "Risk increases as a market moves deeper into its resolution window. Tapering size over time reflects rising uncertainty without requiring a hard binary block.",
      "threshold_logic": [
        {
          "condition": "downgrade_size_by_confidence=true, proposal fraction < 0.5",
          "action": "Use reduce_at_proposal_pct as-is"
        },
        {
          "condition": "downgrade_size_by_confidence=true, proposal fraction \u2265 0.5",
          "action": "Halve the allowed size cap proportionally to elapsed window fraction"
        },
        {
          "condition": "downgrade_size_by_confidence=false",
          "action": "Use reduce_at_proposal_pct flat regardless of elapsed time"
        }
      ],
      "dev_check": "if (p.downgrade_size_by_confidence) cap = cap * (1 - proposalFraction * 0.5);",
      "user_facing": "The resolution window for this market is well underway. We reduced the maximum order size further to account for the increased uncertainty."
    }
  ],
  "default_config": {
    "bot_id": "risk.oracle_risk_monitor",
    "version": "1.0.0",
    "mode": "hard_guard",
    "defaults": {
      "reduce_at_proposal_pct": 50,
      "block_disputed": true,
      "max_dispute_window_h": 48,
      "downgrade_size_by_confidence": true
    },
    "locked": {
      "block_disputed": {
        "immutable": true
      },
      "max_dispute_window_h": {
        "max": 168
      }
    }
  },
  "implementation_flow": [
    "Receive OrderIntent from Strategy layer including market_id, side, and size_usd.",
    "Check KillSwitch active flag; if active, return REJECT with KILL_SWITCH_ACTIVE immediately.",
    "Fetch market resolution-source metadata from Gamma API for the target market_id to confirm UMA oracle is used.",
    "Subscribe to or poll UMA Optimistic Oracle on-chain events to detect any active proposal or dispute for this market.",
    "If a dispute is active and block_disputed=true, return REJECT with reason_code=ORACLE_DISPUTE_ACTIVE.",
    "If a proposal is active (but no dispute), retrieve the current position size for this market from PortfolioGuard.",
    "Apply reduce_at_proposal_pct to compute the maximum allowed size. If downgrade_size_by_confidence=true, further reduce the cap based on elapsed fraction of the proposal window.",
    "If order.size_usd > computed cap, return RESHAPE_REQUIRED with constraints.max_size_usd set to the computed cap.",
    "If the market is neg-risk, apply a 20% additional reduction to the cap due to potential definition-shift risk.",
    "Return APPROVE if all checks pass, with inputs_used and checked_at timestamp."
  ],
  "decision_logic": {
    "approve": "No active proposal and no active dispute for the target market, or proposal is active and order size is within the reduce_at_proposal_pct cap after downgrade adjustment.",
    "reshape_required": "Market is in proposal window and order size exceeds the reduce_at_proposal_pct cap \u2014 emit constraints.max_size_usd at the computed safe level.",
    "reject": "Active UMA dispute and block_disputed=true (ORACLE_DISPUTE_ACTIVE), oracle feed is unavailable or stale (STALE_MARKET_DATA), or KillSwitch is active (KILL_SWITCH_ACTIVE).",
    "warning_only": "Not used as primary path \u2014 OracleRiskMonitor has reject authority. Spread-style anomalies and overdue disputes emit log annotations but the hard guard path is always reject or reshape."
  },
  "decision_output_schema": "RiskVote",
  "decision_output_example": {
    "guard_id": "risk.oracle_risk_monitor",
    "decision": "REJECT",
    "severity": "HARD",
    "reason_code": "ORACLE_DISPUTE_ACTIVE",
    "message": "Market CLOB:0xabc123 has an active UMA resolution dispute filed at 2026-05-08T14:00:00Z. New orders are blocked until the dispute resolves.",
    "constraints": {},
    "inputs_used": [
      "gamma_api.market.resolution_source",
      "uma.oracle.dispute_events",
      "internal.killswitch.status"
    ],
    "checked_at": "2026-05-09T07:00:00Z"
  },
  "developer_log": {
    "bot_id": "risk.oracle_risk_monitor",
    "decision": "REJECT",
    "reason_code": "ORACLE_DISPUTE_ACTIVE",
    "inputs_used": [
      "gamma_api.market.resolution_source",
      "uma.oracle.dispute_events"
    ],
    "metrics": {
      "market_id": "CLOB:0xabc123",
      "dispute_filed_at": "2026-05-08T14:00:00Z",
      "dispute_age_h": 17.0,
      "block_disputed": true,
      "proposal_active": true,
      "proposal_fraction_elapsed": 0.63
    },
    "checked_at": "2026-05-09T07:00:00Z"
  },
  "user_explanations": [
    {
      "situation": "Order blocked \u2014 oracle dispute active",
      "message": "This market currently has a contested resolution. Someone has challenged the proposed outcome on-chain. We blocked your order while this dispute is active because the final result is genuinely uncertain."
    },
    {
      "situation": "Order downsized \u2014 market in proposal window",
      "message": "This market has entered its resolution phase. We reduced your order size to limit your exposure while the outcome is being confirmed."
    },
    {
      "situation": "Order further reduced \u2014 late in proposal window",
      "message": "This market is well into its resolution window. We reduced the maximum allowed order size further because the uncertainty increases as the resolution deadline approaches."
    },
    {
      "situation": "Order blocked \u2014 oracle feed unavailable",
      "message": "We could not verify the current oracle status for this market. Rather than proceed without this information, we blocked the order until a fresh oracle status is available."
    },
    {
      "situation": "Neg-risk market \u2014 order reduced for definition-shift risk",
      "message": "This is a neg-risk market currently in proposal. We applied an additional size reduction because the outcome definition for related markets can shift during the proposal period."
    }
  ],
  "failure_modes": {
    "main_failure_mode": "Approving a trade into a market with an active dispute, resulting in a position that is exposed to a binary resolution outcome the strategy did not price in.",
    "false_positive_risk": "Blocking or downsizing orders on a market where a proposal has been filed but is very likely to pass uncontested, causing unnecessary missed exposure.",
    "false_negative_risk": "Approving an order against stale oracle data that was up-to-date at fetch time but has since entered a dispute, if the polling interval is too wide.",
    "safe_fallback": "If the UMA oracle event feed is unavailable or the last event fetch is older than stale_top_seconds, reject all orders on affected markets with STALE_MARKET_DATA. Never approve on missing oracle status.",
    "required_dependencies": [
      "UMA Optimistic Oracle on-chain event feed",
      "Gamma API market metadata",
      "PortfolioGuard per-market position ledger",
      "KillSwitch active flag"
    ]
  },
  "acceptance_tests": {
    "unit": [
      {
        "test": "Approve when no proposal and no dispute",
        "setup": "proposalActive=false, disputeActive=false",
        "expected": "APPROVE with no constraints"
      },
      {
        "test": "Reshape when proposal active and size exceeds cap",
        "setup": "proposalActive=true, reduce_at_proposal_pct=50, per_market_limit=2000, size_usd=1200",
        "expected": "RESHAPE_REQUIRED with constraints.max_size_usd=1000"
      },
      {
        "test": "Reject when dispute active and block_disputed=true",
        "setup": "disputeActive=true, block_disputed=true",
        "expected": "REJECT with reason_code=ORACLE_DISPUTE_ACTIVE"
      },
      {
        "test": "Allow when dispute active and block_disputed=false",
        "setup": "disputeActive=true, block_disputed=false",
        "expected": "APPROVE with warning annotation ORACLE_DISPUTE_ACTIVE"
      },
      {
        "test": "Downgrade cap further when proposal fraction > 0.5",
        "setup": "proposalActive=true, proposal_fraction=0.8, reduce_at_proposal_pct=50, downgrade_size_by_confidence=true",
        "expected": "RESHAPE_REQUIRED with cap reduced below standard 50% level"
      },
      {
        "test": "Reject when oracle feed is stale",
        "setup": "oracle_feed_age_s=200, stale_top_seconds=60",
        "expected": "REJECT with reason_code=STALE_MARKET_DATA"
      },
      {
        "test": "Neg-risk market applies additional 20% reduction",
        "setup": "proposalActive=true, neg_risk=true, reduce_at_proposal_pct=50, per_market_limit=2000",
        "expected": "RESHAPE_REQUIRED with constraints.max_size_usd=800 (50% \u00d7 0.8)"
      }
    ],
    "integration": [
      {
        "test": "Live UMA oracle event triggers block on in-flight OrderIntent",
        "expected": "REJECT(ORACLE_DISPUTE_ACTIVE) emitted within one polling cycle after dispute filed on-chain"
      },
      {
        "test": "Position size from PortfolioGuard correctly caps reshape size end-to-end",
        "expected": "Reshape constraint is consistent with PortfolioGuard ledger values"
      },
      {
        "test": "Oracle feed outage causes reject-safe fallback across all affected markets",
        "expected": "All orders on UMA-resolved markets return REJECT(STALE_MARKET_DATA) when feed is down"
      }
    ],
    "property": [
      {
        "property": "Missing oracle feed data never results in APPROVE",
        "required": "Always true \u2014 null or stale oracle status produces REJECT(STALE_MARKET_DATA)"
      },
      {
        "property": "Reshape size is always \u2264 requested order size",
        "required": "Always true \u2014 downgrade adjustments only reduce the cap, never increase it"
      },
      {
        "property": "block_disputed=true always results in REJECT when disputeActive=true",
        "required": "Always true regardless of order size or market state"
      }
    ]
  },
  "checklist_overrides": {},
  "legacy_goal": "Watch the UMA oracle queue for resolution risk on positions we hold.",
  "legacy_pm_signals": [
    "Market metadata: resolution-source flag, neg-risk \"Other\" definition shifts",
    "Open positions in markets entering proposal / dispute"
  ],
  "legacy_external_feeds": [
    "UMA Optimistic Oracle on-chain events",
    "UMA dispute & vote queues"
  ],
  "network": [
    "polygon"
  ],
  "api_surface": [
    "clob_public",
    "gamma_api",
    "data_api"
  ],
  "reference_implementation": {
    "summary": "Polls the UMA Optimistic Oracle for active proposals and disputes on the target market, then applies proposal-window size caps or hard rejects, depending on dispute status and the elapsed fraction of the proposal window.",
    "language_note": "Pseudocode is language-agnostic. FETCH = read input. EMIT = produce output. Translate to TS/Python/Go/Rust.",
    "pseudocode": "FUNCTION evaluateOracleRisk(intent):\n  // --- 0. KillSwitch gate ---\n  ks = FETCH internal.killswitch.status\n  IF ks.active:\n    EMIT RiskVote(decision=HARD_REJECT, reason=KILL_SWITCH_ACTIVE)\n    RETURN\n\n  // --- 1. Fetch market resolution metadata ---\n  marketMeta = fetchClobPublic('/markets/' + intent.market_id)\n  IF marketMeta IS NULL OR isStale(marketMeta, params.stale_top_seconds):\n    EMIT RiskVote(decision=HARD_REJECT, reason=STALE_MARKET_DATA)\n    RETURN\n\n  IF marketMeta.resolution_source != 'UMA':\n    // Non-UMA markets pass through without oracle checks\n    EMIT RiskVote(decision=APPROVE)\n    RETURN\n\n  // --- 2. Fetch UMA oracle state ---\n  oracleState = FETCH onchain.uma.proposal_status(intent.market_id)\n  // oracleState: { proposalActive, disputeActive, proposalStartMs, challengeWindowMs }\n  // $750 pUSD proposer bond; 2h challenge window; disputed \u2192 DVM 24-48h\n\n  // --- 3. Hard reject on active dispute ---\n  IF oracleState.disputeActive AND params.block_disputed:\n    EMIT RiskVote(decision=HARD_REJECT, reason=ORACLE_DISPUTE_ACTIVE)\n    RETURN\n\n  // --- 4. Proposal window size cap ---\n  IF oracleState.proposalActive:\n    position = FETCH internal.portfolio_guard.market_position(intent.market_id)\n    proposalFraction = (now_ms() - oracleState.proposalStartMs)\n                       / oracleState.challengeWindowMs\n    cap = position.per_market_limit * (params.reduce_at_proposal_pct / 100)\n    IF params.downgrade_size_by_confidence AND proposalFraction >= 0.5:\n      cap = cap * (1 - proposalFraction * 0.5)\n    // NegRisk: apply extra 20% reduction\n    IF marketMeta.neg_risk:\n      cap = cap * 0.80\n    cap = toUsdcUnits(cap)\n    IF intent.size_usd > cap:\n      EMIT RiskVote(decision=RESHAPE_REQUIRED,\n                    reason=ORACLE_RESOLUTION_PENDING,\n                    constraints={ max_size_usd: cap })\n      RETURN\n\n  // --- 5. Bond check ---\n  IF oracleState.proposerBondPUsd < 750:\n    EMIT RiskVote(decision=HARD_REJECT, reason=ORACLE_PROPOSER_BOND_BELOW_MIN)\n    RETURN\n\n  // --- 6. Happy path ---\n  EMIT RiskVote(decision=APPROVE, checked_at=now_iso())\n",
    "helpers": [
      {
        "name": "fetchClobPublic",
        "signature": "fetchClobPublic(path: str) -> JSON",
        "purpose": "Unauthenticated GET against https://clob.polymarket.com; returns parsed JSON or null on error."
      },
      {
        "name": "isStale",
        "signature": "isStale(snapshot: any, maxAgeS: int) -> bool",
        "purpose": "Returns true if snapshot._fetched_at_ms is older than maxAgeS seconds."
      },
      {
        "name": "toUsdcUnits",
        "signature": "toUsdcUnits(rawUsd: float) -> int",
        "purpose": "Rounds a raw pUSD float to the integer unit used by CTFExchangeV2 (6 decimals)."
      },
      {
        "name": "platformFee",
        "signature": "platformFee(notional: float, prob: float, feeRate: float) -> float",
        "purpose": "Computes platform fee C * feeRate * p * (1-p); used for cost estimation during proposal window."
      }
    ],
    "sdk_calls": [
      "fetchClobPublic('/markets/0xabc123...')",
      "fetchClobPublic('/book?market=0xabc123...')",
      "onchain.uma.proposal_status('0xabc123...')",
      "internal.killswitch.status()",
      "internal.portfolio_guard.market_position('0xabc123...')"
    ],
    "complexity": "O(1) per intent \u2014 single market oracle state lookup"
  },
  "wire_examples": {
    "input": [
      {
        "label": "OrderIntent on UMA-resolved market in active dispute",
        "source": "internal",
        "payload": {
          "intent_id": "int_9c2e4d6f8a0b1c3e",
          "market_id": "0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b",
          "side": "BUY",
          "outcome": "YES",
          "size_usd": 600,
          "neg_risk": false,
          "generated_at": "2026-05-09T07:00:00Z"
        }
      },
      {
        "label": "UMA oracle state (on-chain)",
        "source": "clob_public",
        "payload": {
          "market_id": "0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b",
          "resolution_source": "UMA",
          "proposal_active": true,
          "dispute_active": true,
          "proposal_start_ms": 1746770400000,
          "challenge_window_ms": 7200000,
          "proposer_bond_pusd": 750,
          "dispute_filed_at": "2026-05-09T07:00:00Z",
          "neg_risk": false
        }
      }
    ],
    "output": [
      {
        "label": "RiskVote \u2014 HARD_REJECT (active dispute)",
        "payload": {
          "guard_id": "risk.oracle_risk_monitor",
          "decision": "HARD_REJECT",
          "severity": "HARD",
          "reason_code": "ORACLE_DISPUTE_ACTIVE",
          "message": "Market 0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b has an active UMA resolution dispute filed at 2026-05-09T07:00:00Z. New orders are blocked until the dispute resolves.",
          "constraints": {},
          "inputs_used": [
            "gamma_api.market.resolution_source",
            "onchain.uma.dispute_events",
            "internal.killswitch.status"
          ],
          "checked_at": "2026-05-09T07:02:00Z"
        }
      },
      {
        "label": "RiskVote \u2014 RESHAPE_REQUIRED (proposal window, no dispute)",
        "payload": {
          "guard_id": "risk.oracle_risk_monitor",
          "decision": "RESHAPE_REQUIRED",
          "severity": "WARN",
          "reason_code": "ORACLE_RESOLUTION_PENDING",
          "message": "Market is in UMA proposal window (fraction=0.40). Max size capped to 1000 pUSD.",
          "constraints": {
            "max_size_usd": 1000,
            "passive_only": false,
            "close_only": false
          },
          "inputs_used": [
            "gamma_api.market.resolution_source",
            "onchain.uma.proposal_events",
            "internal.portfolio_guard.position"
          ],
          "checked_at": "2026-05-09T08:00:00Z"
        }
      }
    ],
    "curl": "curl 'https://clob.polymarket.com/markets/0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b'"
  },
  "reason_codes": [
    {
      "code": "KILL_SWITCH_ACTIVE",
      "severity": "HARD_REJECT",
      "meaning": "Global kill switch is active.",
      "action": "Immediately return HARD_REJECT without oracle lookup.",
      "user_message": "Trading is currently paused. Please try again later."
    },
    {
      "code": "STALE_MARKET_DATA",
      "severity": "HARD_REJECT",
      "meaning": "Oracle feed or market metadata is older than the staleness threshold.",
      "action": "Return HARD_REJECT; retry on next fresh fetch.",
      "user_message": "Oracle status could not be verified. The order was blocked until a fresh status is available."
    },
    {
      "code": "ORACLE_DISPUTE_ACTIVE",
      "severity": "HARD_REJECT",
      "meaning": "An active UMA dispute is filed for this market; outcome is contested on-chain.",
      "action": "Return HARD_REJECT if block_disputed=true.",
      "user_message": "This market has an active resolution dispute. Orders are blocked while the dispute is ongoing."
    },
    {
      "code": "ORACLE_RESOLUTION_PENDING",
      "severity": "RESHAPE",
      "meaning": "Market is in the 2-hour UMA proposal window but no dispute has been filed yet.",
      "action": "Return RESHAPE_REQUIRED with cap = reduce_at_proposal_pct * per_market_limit.",
      "user_message": "This market is in its resolution window. Your order size was reduced to limit exposure while the outcome is being confirmed."
    },
    {
      "code": "ORACLE_PROPOSER_BOND_BELOW_MIN",
      "severity": "HARD_REJECT",
      "meaning": "The UMA proposer bond for this market is below the required 750 pUSD minimum, indicating a misconfigured or suspicious market.",
      "action": "Return HARD_REJECT; log market_id and observed bond amount.",
      "user_message": "This market could not be verified for safe trading. The order was blocked."
    },
    {
      "code": "ORACLE_RESOLUTION_CONFIDENCE_DOWNGRADE",
      "severity": "RESHAPE",
      "meaning": "Market is more than 50% through its proposal window; size cap is further reduced proportionally.",
      "action": "Apply downgrade formula: cap = cap * (1 - proposal_fraction * 0.5).",
      "user_message": "This market is well into its resolution window. The maximum order size was reduced further due to the increased uncertainty."
    },
    {
      "code": "ORACLE_NEGRISK_PROPOSAL_REDUCTION",
      "severity": "RESHAPE",
      "meaning": "NegRisk market in proposal window; extra 20% size reduction applied due to definition-shift risk.",
      "action": "Apply 0.8 multiplier to cap before emitting RESHAPE_REQUIRED.",
      "user_message": "This is a neg-risk market in its resolution phase. An additional size reduction was applied."
    },
    {
      "code": "ORACLE_DISPUTE_OVERDUE",
      "severity": "WARN",
      "meaning": "Dispute age exceeds max_dispute_window_h; escalation required.",
      "action": "Emit WARN annotation and escalate to incident commander; continue blocking per block_disputed.",
      "user_message": ""
    }
  ],
  "metrics": {
    "emitted": [
      {
        "name": "polytraders_risk_oracleriskmonitor_decisions_total",
        "type": "counter",
        "unit": "count",
        "labels": [
          "decision",
          "reason_code",
          "market_id"
        ],
        "meaning": "Total RiskVote decisions broken down by decision type and reason."
      },
      {
        "name": "polytraders_risk_oracleriskmonitor_dispute_age_hours",
        "type": "gauge",
        "unit": "seconds",
        "labels": [
          "market_id"
        ],
        "meaning": "Age of the active dispute for markets currently in dispute; triggers alert when approaching max_dispute_window_h."
      },
      {
        "name": "polytraders_risk_oracleriskmonitor_proposal_fraction",
        "type": "gauge",
        "unit": "ratio",
        "labels": [
          "market_id"
        ],
        "meaning": "Elapsed fraction of the UMA 2-hour proposal window; drives confidence-downgrade logic."
      },
      {
        "name": "polytraders_risk_oracleriskmonitor_oracle_fetch_latency_ms",
        "type": "histogram",
        "unit": "seconds",
        "labels": [],
        "meaning": "Latency of UMA on-chain oracle state fetch."
      },
      {
        "name": "polytraders_risk_oracleriskmonitor_markets_in_proposal",
        "type": "gauge",
        "unit": "count",
        "labels": [],
        "meaning": "Number of markets currently in an active UMA proposal window."
      },
      {
        "name": "polytraders_risk_oracleriskmonitor_markets_in_dispute",
        "type": "gauge",
        "unit": "count",
        "labels": [],
        "meaning": "Number of markets currently in an active UMA dispute."
      }
    ],
    "alerts": [
      {
        "name": "OracleRiskMonitorDisputeActive",
        "condition": "polytraders_risk_oracleriskmonitor_markets_in_dispute > 0",
        "severity": "P1",
        "runbook": "#runbook-oracle-dispute-active"
      },
      {
        "name": "OracleRiskMonitorDisputeOverdue",
        "condition": "polytraders_risk_oracleriskmonitor_dispute_age_hours > 48",
        "severity": "P0",
        "runbook": "#runbook-oracle-dispute-overdue"
      },
      {
        "name": "OracleRiskMonitorStaleFeed",
        "condition": "rate(polytraders_risk_oracleriskmonitor_decisions_total{reason_code='STALE_MARKET_DATA'}[5m]) > 0.1",
        "severity": "P1",
        "runbook": "#runbook-oracle-stale-feed"
      },
      {
        "name": "OracleRiskMonitorHighRejectRate",
        "condition": "rate(polytraders_risk_oracleriskmonitor_decisions_total{decision='HARD_REJECT'}[5m]) / rate(polytraders_risk_oracleriskmonitor_decisions_total[5m]) > 0.3",
        "severity": "P2",
        "runbook": "#runbook-oracle-reject-rate"
      }
    ],
    "dashboards": [
      "Grafana \u2014 Risk overview / OracleRiskMonitor",
      "Grafana \u2014 UMA oracle status / proposal and dispute tracker"
    ],
    "log_levels": {
      "DEBUG": "proposalFraction value, cap computation, and bond amount on every evaluation.",
      "INFO": "RiskVote decision emitted (decision, reason_code, market_id).",
      "WARN": "Dispute age approaching max_dispute_window_h; oracle feed latency elevated.",
      "ERROR": "UMA on-chain feed unreachable; market metadata fetch returned null."
    }
  },
  "state": {
    "summary": "Maintains a short-lived in-memory cache of oracle state per market_id to avoid redundant on-chain calls within the same evaluation window.",
    "stores": [
      {
        "name": "oracle_state_cache",
        "kind": "in-memory",
        "key": "market_id",
        "value": "{ proposalActive: bool, disputeActive: bool, proposalStartMs: int, challengeWindowMs: int, proposerBondPUsd: float }",
        "ttl": "30s",
        "durability": "best-effort"
      }
    ],
    "recovery": "On cold start, the cache is empty. The first evaluation for each market triggers a fresh on-chain fetch.",
    "on_restart": "Oracle state is re-fetched on first evaluation; no durable state is loaded."
  },
  "concurrency": {
    "execution_model": "single-threaded event loop",
    "max_in_flight": 100,
    "idempotency_key": "intent_id",
    "replay_safe": true,
    "deduplication": "by intent_id within a 24h window",
    "ordering_guarantees": "FIFO per market_id",
    "timeout_ms": 250,
    "backpressure": "drop newest",
    "locking": "per-market_id mutex"
  },
  "dependencies": {
    "depends_on": [
      {
        "bot_id": "risk.kill_switch",
        "why": "Global brake \u2014 checked first before any oracle fetch.",
        "contract": "RiskVote.HARD_REJECT(KILL_SWITCH_ACTIVE) short-circuits."
      },
      {
        "bot_id": "risk.portfolio_guard",
        "why": "Per-market position limit is needed to compute the proposal-window size cap.",
        "contract": "cap = portfolio_guard.per_market_limit * reduce_at_proposal_pct."
      }
    ],
    "emits_to": [
      {
        "bot_id": "exec.smart_router",
        "why": "Approved or reshaped RiskVote passes to SmartRouter.",
        "contract": "HARD_REJECT on ORACLE_DISPUTE_ACTIVE causes SmartRouter to discard the intent."
      }
    ],
    "sibling": [],
    "external": [
      {
        "service": "UMA Optimistic Oracle (on-chain)",
        "endpoint": "Polygon RPC / UMA oracle contract",
        "sla": "best-effort / chain-dependent",
        "failure_mode": "HARD_REJECT(STALE_MARKET_DATA) until oracle state is readable."
      },
      {
        "service": "Gamma API",
        "endpoint": "https://gamma-api.polymarket.com",
        "sla": "99.9% / 500ms p99",
        "failure_mode": "HARD_REJECT(STALE_MARKET_DATA) if market metadata is unavailable."
      },
      {
        "service": "CLOB API (read)",
        "endpoint": "https://clob.polymarket.com",
        "sla": "99.95% / 200ms p99",
        "failure_mode": "Falls back to Gamma API for market metadata."
      }
    ]
  },
  "security_surfaces": {
    "summary": "OracleRiskMonitor is read-only. It never signs orders or holds private keys.",
    "signing": "This bot does NOT sign anything.",
    "secrets": [],
    "contract_calls": [],
    "abuse_vectors": [
      "Feeding a stale or forged oracle state to bypass dispute detection",
      "Manipulating proposalFraction to reduce the size cap during a proposal window"
    ],
    "mitigations": [
      "Oracle state has a 30s TTL; stale data triggers HARD_REJECT rather than pass-through",
      "proposalFraction is computed from on-chain timestamps, not from any mutable local state"
    ]
  },
  "polymarket_v2_compat": {
    "clob_version": "v2",
    "collateral": "pUSD",
    "eip712_domain_version": "2",
    "builder_code_aware": false,
    "negrisk_aware": true,
    "multichain_ready": false,
    "sdk_used": "@polymarket/clob-client-v2 ^2.x",
    "settlement_contract": "CTFExchangeV2 on Polygon",
    "notes": "UMA proposer bond is 750 pUSD (replaces 750 USDC.e from v1). Dispute challenge window is 2 hours; escalated disputes go to DVM voting (24-48h debate + ~48h voting). NegRisk markets carry extra size reduction during proposal window due to potential definition shifts."
  },
  "version": {
    "spec": "2.0.0",
    "implementation": "2.1.3",
    "schema": "2",
    "released": "2026-04-28"
  },
  "migration_history": [
    {
      "date": "2026-04-28",
      "from": "v1 (USDC.e + HMAC builder)",
      "to": "v2 (pUSD + builderCode field)",
      "reason": "Polymarket V2 cutover",
      "action_taken": "Migrated SDK. Proposer bond check now validates 750 pUSD instead of 750 USDC.e. No structural change to oracle dispute logic; removed feeRateBps references."
    }
  ],
  "failure_injection": [
    {
      "scenario": "DISPUTE_ACTIVE",
      "how_to_inject": "Set mock oracle state disputeActive=true for a target market",
      "expected_behavior": "All evaluations on that market return HARD_REJECT(ORACLE_DISPUTE_ACTIVE)",
      "recovery": "Returns to APPROVE when disputeActive=false in oracle state."
    },
    {
      "scenario": "STALE_ORACLE_FEED",
      "how_to_inject": "Freeze oracle state cache for 90s (TTL=30s)",
      "expected_behavior": "HARD_REJECT(STALE_MARKET_DATA) on every evaluation",
      "recovery": "Returns to normal within one evaluation after fresh oracle fetch."
    },
    {
      "scenario": "LOW_PROPOSER_BOND",
      "how_to_inject": "Set mock proposerBondPUsd=500 (below 750 floor)",
      "expected_behavior": "HARD_REJECT(ORACLE_PROPOSER_BOND_BELOW_MIN)",
      "recovery": "Returns to APPROVE once bond is at or above 750 pUSD."
    },
    {
      "scenario": "PROPOSAL_WINDOW_LATE",
      "how_to_inject": "Set proposalFraction=0.85 and downgrade_size_by_confidence=true",
      "expected_behavior": "RESHAPE_REQUIRED with cap significantly below reduce_at_proposal_pct baseline",
      "recovery": "Cap returns to baseline once proposal resolves."
    },
    {
      "scenario": "KILL_SWITCH_ON",
      "how_to_inject": "Set internal.killswitch.status.active=true",
      "expected_behavior": "HARD_REJECT(KILL_SWITCH_ACTIVE) on every intent without oracle fetch",
      "recovery": "Returns to normal pipeline on manual KillSwitch reset."
    }
  ],
  "runbook": {
    "summary": "OracleRiskMonitor incidents are driven by active UMA disputes or stale oracle feed. Disputes are expected events; on-call must validate the dispute is legitimate before considering any override.",
    "oncall_actions": [
      {
        "alert": "OracleRiskMonitorDisputeActive",
        "first_step": "Identify which market_id is in dispute from the Grafana panel.",
        "diagnosis": "Check UMA oracle on-chain for dispute details. Verify the dispute was filed legitimately (not a griefing attempt with insufficient bond).",
        "mitigation": "If legitimate, the guard is working correctly \u2014 do not override. If suspicious, escalate to Risk pod lead.",
        "escalation": "Risk pod lead immediately for any P1 dispute event."
      },
      {
        "alert": "OracleRiskMonitorDisputeOverdue",
        "first_step": "Check dispute age on the Grafana panel. Escalate immediately if > 48h.",
        "diagnosis": "DVM vote may be stalled. Check UMA DVM governance interface for vote status.",
        "mitigation": "Maintain HARD_REJECT on affected market until DVM resolves. Do not reduce block_disputed.",
        "escalation": "Incident commander + Risk pod lead within 15 minutes of alert."
      },
      {
        "alert": "OracleRiskMonitorStaleFeed",
        "first_step": "Check Polygon RPC connectivity and oracle contract read latency.",
        "diagnosis": "If RPC is degraded, oracle state fetch is timing out and falling back to reject-safe.",
        "mitigation": "Switch to backup RPC endpoint. The guard's reject-safe default protects positions.",
        "escalation": "Infra on-call if RPC is down > 5 minutes."
      },
      {
        "alert": "OracleRiskMonitorHighRejectRate",
        "first_step": "Check reason_code breakdown. If dominated by ORACLE_DISPUTE_ACTIVE, normal guard behaviour.",
        "diagnosis": "If STALE_MARKET_DATA is dominant, follow stale-feed runbook.",
        "mitigation": "Pause affected strategies if the rejection is blocking all orders unnecessarily.",
        "escalation": "Risk pod lead after 15 minutes of sustained high reject rate."
      }
    ],
    "manual_overrides": [
      {
        "command": "polytraders bot pause risk.oracle_risk_monitor",
        "effect": "Stops emitting RiskVotes. Use only during a known feed outage with explicit Risk pod lead sign-off."
      },
      {
        "command": "polytraders bot flush-cache risk.oracle_risk_monitor --market <market_id>",
        "effect": "Evicts the oracle state cache for a specific market, forcing a fresh on-chain fetch."
      }
    ],
    "healthcheck": "GET /health \u2192 200 if on-chain oracle state fetch latency < 500ms and no market has been in dispute for > max_dispute_window_h."
  },
  "promotion_gates": {
    "to_shadow": [
      {
        "gate": "Unit tests pass including all acceptance_tests.unit cases",
        "how_measured": "CI test run",
        "threshold": "100% pass"
      },
      {
        "gate": "Integration test: active dispute \u2192 HARD_REJECT verified end-to-end",
        "how_measured": "Integration test suite with mock oracle",
        "threshold": "Pass"
      }
    ],
    "to_limited_live": [
      {
        "gate": "Oracle feed latency p99 < 250ms over 48h",
        "how_measured": "polytraders_risk_oracleriskmonitor_oracle_fetch_latency_ms histogram",
        "threshold": "p99 < 250ms"
      },
      {
        "gate": "No false-positive HARD_REJECT during normal (no-dispute) operation",
        "how_measured": "Shadow vs live comparison",
        "threshold": "0 false positives"
      }
    ],
    "to_general_live": [
      {
        "gate": "Successfully blocked at least one synthetic dispute injection in staging",
        "how_measured": "Failure injection test log",
        "threshold": "Pass"
      },
      {
        "gate": "Proposal-window reshape correctly reduces size in E2E flow",
        "how_measured": "E2E test + fill log audit",
        "threshold": "100% compliance"
      }
    ]
  },
  "reporting_groups": [
    "risk_compliance"
  ],
  "capital_impact": "Direct",
  "mode_support": [
    "quarantine"
  ],
  "v3_status": {
    "phase": 4,
    "phase_name": "Core risk",
    "docs": {
      "done": 27,
      "total": 27,
      "state": "done"
    },
    "impl": {
      "done": 0,
      "total": 15,
      "state": "pending"
    },
    "runtime": {
      "done": 0,
      "total": 8,
      "state": "pending"
    },
    "overall": "pending"
  }
}