Polytraders Dev Guide
internal
v3 spine Phase 1 · Shared contracts 9 demo-wired · 0 shadow-ready · 0 production-live · 100 pending · 109 total 15/33 infra tasks the plan status board
HomeBy LayerIntelligence4.2 OracleWatcher

4.2 OracleWatcher

Intelligence Signal Service Read-only LIVE General live capital · Indirect P2 · Data normalisation pending stub

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 — it never submits or signs anything. Output is the primary feed for oracleriskmonitor, which uses it to gate positions during contested resolutions.

v3 readiness

Docs27/27
donehow scored
Impl0/15
pendinghow scored
Backtest0/4
pendinghow scored
Runtime0/8
pendinghow scored

A bot is done when all four scores are. What does done mean?

1. Bot Identity

LayerIntelligence  Intelligence
Bot classSignal Service
AuthorityRead-only
StatusLIVE
ReadinessGeneral live
Runs beforeoracleriskmonitor, risk layer
Runs afterPolymarket market creation / resolution-question registration
Applies toAll live Polymarket markets whose resolution source is UMA Optimistic Oracle
Default modegeneral_live
User-visibleAdvanced details only
Developer ownerPolytraders core — Intelligence pod

2. 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 — it never submits or signs anything. Output is the primary feed for oracleriskmonitor, which uses it to gate positions during contested resolutions.

3. Why This Bot Matters

  • Proposal missed before 2-hour challenge window closes

    Position held through unchallenged incorrect resolution; full collateral loss on the wrong outcome.

  • Dispute state not detected

    oracleriskmonitor is unaware that a market is in a 24–72-hour DVM debate; strategies continue trading as if resolution is certain.

  • DVM vote outcome not propagated

    Settlement logic based on outdated oracle state; payout mismatch or duplicate claim.

  • Stale UMA state cached during RPC outage

    Old proposal treated as current; oracleriskmonitor computes wrong time-to-resolution, potentially permitting over-leveraged entry.

No worked examples on this bot yet. Worked examples are optional but strongly recommended — they turn an abstract failure mode into something a developer can verify in a fixture.

4. Required Polymarket Inputs

InputSourceRequired?Use
Market condition IDs and associated assertion IDsGamma API / internal market registryYesMap UMA assertion IDs back to Polymarket condition IDs in ObservationReport payloads.
UMA Optimistic Oracle on-chain events (ProposePrice, DisputePrice, Settle)onchain (Polygon RPC + event log subscription)YesDetect assertion lifecycle state changes in real time.
Oracle WebSocket market stream for assertion ID mappingws_marketNoSupplement on-chain event logs with low-latency assertion state updates.

5. Required Internal Inputs

InputSourceRequired?Use
Watched condition ID listconfig / StrategyRegistryYesFilter UMA events to only condition IDs with open positions or pending strategies.
KillSwitch active flagKillSwitchYesContinue watching on-chain but suppress ObservationReport emissions when KillSwitch is active.

6. Parameter Guide

ParameterDefaultWarningHardWhat it controls
poll_interval_s123060Seconds between on-chain RPC polls for new UMA oracle events. Lower = fresher but higher RPC cost.
challenge_window_alert_s36001800600Seconds before the 2-hour UMA challenge window closes at which a WARN alert is emitted to oracleriskmonitor.
alert_on_state_changeTrueNoneNoneIf true, emit ObservationReport on every oracle state transition (proposal → dispute → DVM → settle). If false, only emit on PROPOSAL and SETTLED.

7. Detailed Parameter Instructions

poll_interval_s

What it means

Seconds between on-chain RPC polls for new UMA oracle events. Lower = fresher but higher RPC cost.

Default

{ "poll_interval_s": 12 }

Why this default matters

12 s aligns with Polygon block time, ensuring every new block is checked without redundant polls.

Threshold logic

ConditionAction
interval ≤ 12 sNormal — one poll per block
12–30 sWARN — may miss events in high-throughput windows
> 60 sHard cap — PARAMETER_CHANGE_REQUIRES_APPROVAL

Developer check

if (p.poll_interval_s > p.hard) throw ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL');

User-facing English

Oracle state is checked every Polygon block to ensure no resolution event is missed.

challenge_window_alert_s

What it means

Seconds before the 2-hour UMA challenge window closes at which a WARN alert is emitted to oracleriskmonitor.

Default

{ "challenge_window_alert_s": 3600 }

Why this default matters

1-hour pre-alert gives downstream risk bots time to reduce exposure before the challenge deadline.

Threshold logic

ConditionAction
alert_s ≥ 3600Normal — 1-hour pre-alert
1800–3600 sWARN — shorter pre-alert; tighter margin
< 600 sReject — insufficient time for risk response

Developer check

if (p.challenge_window_alert_s < p.hard) throw ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL');

User-facing English

You are alerted well before the window to challenge an oracle result closes.

alert_on_state_change

What it means

If true, emit ObservationReport on every oracle state transition (proposal → dispute → DVM → settle). If false, only emit on PROPOSAL and SETTLED.

Default

{ "alert_on_state_change": true }

Why this default matters

Full state-change tracking ensures oracleriskmonitor has the complete dispute lifecycle.

Threshold logic

— not yet authored —

Developer check

// not yet authored

User-facing English

Every step in the oracle resolution process generates a status update.

8. Default Configuration

{
  "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
    }
  }
}

9. Implementation Flow

  1. On startup, subscribe to Polygon RPC event log for UMA OptimisticOracle ProposePrice, DisputePrice, and Settle events filtered to Polymarket ancillary data prefixes.
  2. On each block (poll_interval_s), fetch new events since last processed block number.
  3. For each event: resolve assertion_id → condition_id via Gamma API / internal registry.
  4. Determine oracle_state: PROPOSED | CHALLENGED | DVM_DEBATE | DVM_VOTE | SETTLED.
  5. Compute time_to_deadline_s: for PROPOSED, time until 2-hour challenge window closes; for DVM_DEBATE, estimated 24–48 h; for DVM_VOTE, estimated 48 h.
  6. If oracle_state is PROPOSED and time_to_deadline_s ≤ challenge_window_alert_s, emit WARN ORACLEWATCHER_CHALLENGE_WINDOW_CLOSING.
  7. Check KillSwitch; if active, continue watching but suppress emissions.
  8. 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.
  9. Log per-event summary: condition_id, state transition, bond_pusd, time_to_deadline_s.

10. Reference Implementation

Subscribes to Polygon RPC event log for UMA oracle events, maps assertion_ids to condition_ids, tracks the full proposal→dispute→DVM→settle lifecycle, and emits an ObservationReport on every state change.

Pseudocode is language-agnostic. FETCH = read input. EMIT = produce output.

FUNCTION watchBlock(block_number):
  // --- 0. RPC health check ---
  IF rpc.last_block_age_s > 2 * params.poll_interval_s:
    EMIT WARN 'STALE_DATA — RPC unresponsive'
    RETURN

  // --- 1. Fetch new UMA events since last_processed_block ---
  events = rpc.getLogs(
    address   = UMA_OPTIMISTIC_ORACLE_ADDR,
    topics    = [PROPOSE_PRICE_SIG, DISPUTE_PRICE_SIG, SETTLE_SIG],
    fromBlock = last_processed_block + 1,
    toBlock   = block_number
  )

  // --- 2. KillSwitch gate ---
  ks = FETCH internal.killswitch.status

  FOR event IN events:
    // --- 3. Ancillary data prefix filter ---
    IF NOT event.ancillaryData.startsWith(POLYMARKET_PREFIX):
      CONTINUE

    // --- 4. Resolve assertion_id → condition_id ---
    condition_id = gamma_api.GET('/assertion/' + event.assertionId + '/condition')
    IF condition_id IS NULL:
      condition_id = internal.registry.lookup(event.assertionId)
    IF condition_id IS NULL OR condition_id NOT IN watched_condition_ids:
      CONTINUE

    // --- 5. Determine oracle state ---
    state = SWITCH event.topic:
      PROPOSE_PRICE_SIG  -> 'PROPOSED'
      DISPUTE_PRICE_SIG  -> 'CHALLENGED'
      SETTLE_SIG         -> 'SETTLED'

    // --- 6. Compute deadline ---
    IF state == 'PROPOSED':
      challenge_deadline_ms = event.timestamp_ms + 2*60*60*1000   // +2h
      time_to_deadline_s    = (challenge_deadline_ms - now_ms()) / 1000
    ELSE IF state == 'CHALLENGED':
      time_to_deadline_s    = 24*60*60  // DVM: 24–48h
    ELSE:
      time_to_deadline_s    = None

    // --- 7. Challenge window alert ---
    warnings = []
    IF state == 'PROPOSED' AND time_to_deadline_s <= params.challenge_window_alert_s:
      warnings.append('ORACLEWATCHER_CHALLENGE_WINDOW_CLOSING')

    // --- 8. KillSwitch suppress ---
    IF ks.active:
      LOG INFO 'KILL_SWITCH_ACTIVE — suppressing ObservationReport'
      CONTINUE

    // --- 9. Emit ---
    IF params.alert_on_state_change OR state IN ('PROPOSED', 'SETTLED'):
      report = ObservationReport(
        report_id         = 'rep_ow_' + condition_id[:6] + '_' + now_ms(),
        trace_id          = newTraceId(),
        bot_id            = 'intel.oraclewatcher',
        kind              = 'ObservationReport',
        condition_id      = condition_id,
        assertion_id      = event.assertionId,
        oracle_state      = state,
        bond_pusd         = 750,
        time_to_deadline_s= time_to_deadline_s,
        proposed_price    = event.price,
        disputer          = event.disputer IF state == 'CHALLENGED' ELSE null,
        dvm_question_id   = event.dvmQuestionId IF state IN ('CHALLENGED','DVM_VOTE') ELSE null,
        block_number      = block_number,
        warnings          = warnings,
        emitted_at_ms     = now_ms()
      )
      EMIT internal.bus.observations <- report

  last_processed_block = block_number

SDK calls used

  • 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

11. Wire Examples

Input — what arrives on the wire

{
  "label": "Polygon RPC ProposePrice event",
  "source": "onchain",
  "payload": {
    "event": "ProposePrice",
    "assertionId": "0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b",
    "ancillaryData": "0x706f6c796d61726b65743a313233",
    "price": 1,
    "timestamp_ms": 1746700900000,
    "block_number": 72345678
  }
}

Output — what the bot emits

{
  "label": "ObservationReport — 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
  }
}

12. Decision Logic

APPROVE

Not applicable — 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.

13. Standard Decision Output

This bot returns a ObservationReport object. See ObservationReport schema.

{
  "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
}

14. Reason Codes

CodeSeverityMeaningActionUser-facing message
ORACLEWATCHER_CHALLENGE_WINDOW_CLOSINGWARNLess than challenge_window_alert_s remain before the 2-hour UMA challenge deadline closes.Include in ObservationReport warnings; oracleriskmonitor triggers exposure review.The window to challenge the proposed market resolution is closing soon.
ORACLEWATCHER_DISPUTE_FILEDWARNA dispute has been filed on a Polymarket proposal; market enters DVM path (24–72 h).Emit ObservationReport with oracle_state=CHALLENGED; oracleriskmonitor restricts new entries.The proposed resolution for this market has been contested. The outcome will be determined by the UMA DVM over the next 24–72 hours.
ORACLEWATCHER_DVM_VOTE_OPENWARNDVM vote phase opened; resolution will be decided by UMA token holders (~48 h).Emit ObservationReport with oracle_state=DVM_VOTE; time_to_deadline_s≈172800.A community vote is underway to determine this market's outcome.
ORACLEWATCHER_SETTLEDINFOUMA oracle has finalised and settled the assertion; resolution is on-chain.Emit ObservationReport with oracle_state=SETTLED; downstream settlement proceeds.Market resolution is finalised and settlement is in progress.
KILL_SWITCH_ACTIVEHARD_REJECTKillSwitch active; ObservationReport emissions suppressed.Continue watching on-chain but suppress emissions.Oracle status updates are paused while trading is suspended system-wide.
STALE_DATAWARNRPC provider unresponsive for > 2× poll_interval_s; oracle state may be stale.Halt ObservationReport emissions until RPC recovers; alert on-call.
MARKET_CLOSEDEXPLAINUMA event detected for a condition_id that is already closed in the internal registry.Skip emission; log for audit trail only.
ORACLEWATCHER_UNKNOWN_ASSERTIONWARNAssertion ID from on-chain event could not be resolved to a Polymarket condition_id.Log with assertion_id; do not emit ObservationReport; retry on next block.

15. Metrics & Logs

Metrics emitted

MetricTypeUnitLabelsMeaning
polytraders_intel_oraclewatcher_events_detected_totalcountercountoracle_stateUMA oracle events detected on-chain per state type.
polytraders_intel_oraclewatcher_observations_emitted_totalcountercountoracle_stateObservationReports emitted, broken down by oracle_state.
polytraders_intel_oraclewatcher_active_disputes_gaugegaugecountNumber of condition_ids currently in CHALLENGED or DVM_VOTE state.
polytraders_intel_oraclewatcher_time_to_challenge_deadline_sgaugesecondscondition_idSeconds remaining before challenge window closes for each PROPOSED assertion.
polytraders_intel_oraclewatcher_rpc_block_lag_sgaugesecondsAge of the most recently processed Polygon block.
polytraders_intel_oraclewatcher_unknown_assertions_totalcountercountUMA events received for assertion IDs that could not be mapped to a condition_id.

Alerts

AlertConditionSeverityRunbook
OracleWatcherRPCDownpolytraders_intel_oraclewatcher_rpc_block_lag_s > 60page#runbook-oraclewatcher-rpc-down
OracleWatcherChallengeWindowImminentmin(polytraders_intel_oraclewatcher_time_to_challenge_deadline_s) < 1800page#runbook-oraclewatcher-challenge-imminent
OracleWatcherDisputeSpikepolytraders_intel_oraclewatcher_active_disputes_gauge > 5warn#runbook-oraclewatcher-dispute-spike
OracleWatcherHighUnknownAssertionsrate(polytraders_intel_oraclewatcher_unknown_assertions_total[10m]) > 0.1warn#runbook-oraclewatcher-unknown-assertions

Dashboards

  • Grafana — Intelligence / OracleWatcher dispute lifecycle
  • Grafana — Intelligence / challenge deadline countdown

16. Developer Reporting

{
  "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
}

17. Plain-English Reporting

SituationUser-facing explanation
Market shows 'Resolution contested'Someone has disputed the proposed outcome for this market. The UMA DVM will adjudicate over the next 24–72 hours. Your position is protected until the dispute is resolved.
Challenge window closing alertThe window to contest the proposed resolution for this market closes soon. Risk systems are reviewing exposure automatically.
Market resolution finalisedThe UMA oracle has settled this market. Final outcome is now on-chain and settlement will proceed.

18. Failure-Mode Block

main_failure_modeRPC provider outage causes OracleWatcher to miss a state transition (e.g., PROPOSED → CHALLENGED), leaving oracleriskmonitor with a stale oracle state and potentially allowing trades during an undetected dispute.
false_positive_riskA non-Polymarket UMA assertion sharing an ancillary data prefix causes a spurious ObservationReport for a condition_id that has no open positions.
false_negative_riskHigh 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_fallbackIf RPC is unavailable for > 2× 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_dependenciesPolygon RPC (event log access), Gamma API for assertion_id → condition_id mapping, KillSwitch active flag readable

19. Failure-Injection Recipes

ScenarioHow to injectExpected behaviourRecovery
RPC_OUTAGEBlock TCP to primary Polygon RPC for 30 sAutomatic failover to secondary RPC; missed blocks re-fetched; emissions resume
DISPUTE_FILED_NEAR_DEADLINEInject mock DisputePrice event with time_to_deadline_s=1700Alert clears when market transitions to DVM_DEBATE or SETTLED
UNKNOWN_ASSERTIONEmit a ProposePrice event with an assertion_id not in Gamma API or internal registryOnce Gamma API returns the mapping, event is resolved and emitted
KILL_SWITCH_ONSet killswitch.active=true; trigger a ProposePrice eventEmissions resume on first event after KillSwitch reset
DISPUTE_SPIKEInject 6 concurrent dispute events across 6 condition_idsAlert clears when disputes settle below threshold

20. State & Persistence

Cold-start 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.

21. Concurrency & Idempotency

AspectSpecification
Execution modelsingle-threaded event loop
Max in-flight1
Idempotency keyassertion_id + oracle_state
Per-call timeout (ms)5000
Backpressure strategywal-then-retry — events buffered to Postgres WAL if internal bus is unavailable
Locking / mutual exclusionrow-level lock on oracle_states per condition_id during state transition

22. Dependencies

Depends on (must run first)

BotWhyContract
risk.kill_switchKillSwitch gate suppresses ObservationReport emissions when active.

Emits to (downstream consumers)

External services

ServiceEndpointSLA assumedOn failure
Polygon RPC (Alchemy / Infura)99.9% / 200 ms p99
UMA Optimistic Oracle (on-chain)Polygon mainnet contractBlockchain-level finality
Gamma APIhttps://gamma-api.polymarket.com99.9% / 500 ms p99

23. Security Surfaces

Abuse vectors considered

  • 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 — settlement decisions require independent on-chain confirmation

24. Polymarket V2 Compatibility

AspectValue
CLOB versionv2
Collateral assetpUSD
EIP-712 Exchange domain version2
Aware of builderCode fieldno
Aware of negative-risk marketsno
Multi-chain readyno
SDK usedpy-clob-client-v2
Settlement contractCTFExchangeV2
NotesOracleWatcher tracks the UMA Optimistic Oracle with a $750 pUSD bond, 2-hour challenge window, and DVM dispute path (24–48 h debate + ~48 h vote). Bond amounts in all payloads are denominated in pUSD.

API surfaces declared

onchainws_marketinternal

Networks supported

polygon

25. Versioning & Migration

FieldValue
spec2.0.0
implementation2.1.0
schema2
released2026-04-28

Migration history

DateFromToReasonAction taken
2026-04-28v1v2CLOB V2 cutover — collateral change to pUSD, bond denomination updatedUMA 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.

26. Acceptance Tests

Unit Tests

TestSetupExpected result
PROPOSAL event emits ObservationReport with oracle_state=PROPOSEDMock ProposePrice event for watched condition_idObservationReport emitted with oracle_state=PROPOSED, bond_pusd=750, time_to_deadline_s≈7200
DISPUTE event emits ObservationReport with oracle_state=CHALLENGEDMock DisputePrice event following ProposePriceObservationReport emitted with oracle_state=CHALLENGED, disputer populated
Challenge window alert fires at correct thresholdtime_to_deadline_s = 3500, challenge_window_alert_s=3600WARN ORACLEWATCHER_CHALLENGE_WINDOW_CLOSING included in warnings
KillSwitch suppresses emissions but watching continueskillswitch.active=true; PROPOSAL event arrivesEvent detected and logged; no ObservationReport emitted
RPC outage emits STALE_DATA and halts emissionsRPC returns error for 30 s (> 2× poll_interval_s=12)STALE_DATA WARN logged; no ObservationReport emitted during outage
SETTLED event emits ObservationReport with oracle_state=SETTLEDMock Settle event with final priceObservationReport with oracle_state=SETTLED, proposed_price=final resolved value

Integration Tests

TestExpected result
Full lifecycle: PROPOSED → CHALLENGED → DVM_DEBATE → SETTLED generates 4 ObservationReportsoracleriskmonitor receives all four state transitions with correct condition_id and timings
Gamma API outage causes assertion_id resolution to fail gracefullyEvent buffered; ObservationReport emitted with condition_id=null and STALE_DATA warning once API recovers
Non-Polymarket UMA event filtered out by ancillary data prefix checkNo ObservationReport emitted for foreign assertion_id

Property Tests

PropertyRequired behaviour
OracleWatcher never submits, signs, or modifies any order or on-chain transactionAlways true
No ObservationReport emitted when KillSwitch is activeAlways true
No ObservationReport emitted when RPC state is stale (> 2 blocks unconfirmed)Always true

27. Operational Runbook

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.

On-call actions

AlertFirst stepDiagnosisMitigationEscalate to
OracleWatcherRPCDown
OracleWatcherChallengeWindowImminent
OracleWatcherDisputeSpike
OracleWatcherHighUnknownAssertions

Manual overrides

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.

28. Promotion Gates

A bot does not advance to the next readiness state until every gate below is green. Gates are observable from production data — no subjective sign-off.

Promote to Shadow

GateHow measuredThreshold
Unit tests pass for PROPOSE, DISPUTE, and SETTLE state transitionsCI test run100% pass
RPC integration test: block log subscription fires on mock ProposePrice eventIntegration test against Polygon testnetPass

Promote to Limited live

GateHow measuredThreshold
rpc_block_lag_s p99 < 15 s over 48 hpolytraders_intel_oraclewatcher_rpc_block_lag_s gaugep99 < 15 s
All test assertion_ids resolve correctly against Gamma APIIntegration test with known Polymarket markets100% resolution

Promote to General live

GateHow measuredThreshold
Zero missed PROPOSED events over 14 days (verified by reconciling on-chain logs vs emitted ObservationReports)Post-hoc on-chain reconciliation script0 missed events
KillSwitch suppression: zero ObservationReports emitted when KillSwitch activeIntegration testPass
Postgres 1-year retention verified: oracle_states rows survive 365-day TTL checkDB retention policy testPass

29. Developer Checklist

Ready-to-ship score: 27/27 sections complete · 100%

RequirementStatus
Purpose defined✓ done
Required inputs listed✓ done
Parameters defined✓ done
Defaults defined✓ done
Warning thresholds defined✓ done
Hard thresholds defined✓ done
Safe fallback defined✓ done
Structured output defined✓ done
Developer log defined✓ done
Plain-English explanation✓ done
Unit tests defined✓ done
Integration tests defined✓ done
Property tests defined✓ done
Failure-mode block complete✓ done
Reference implementation pseudocode✓ done
Wire examples (input + output)✓ done
Reason codes listed✓ done
Metrics & logs defined✓ done
State & persistence defined✓ done
Concurrency & idempotency defined✓ done
Dependencies declared✓ done
Security surfaces declared✓ done
Polymarket V2 compatibility declared✓ done
Version & migration history declared✓ done
Operational runbook defined✓ done
Promotion gates defined✓ done
Failure-injection recipes defined✓ done