4.2 OracleWatcher
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
A bot is done when all four scores are. What does done mean?
1. Bot Identity
| Layer | Intelligence Intelligence |
|---|---|
| Bot class | Signal Service |
| Authority | Read-only |
| Status | LIVE |
| Readiness | General live |
| 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 — 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
| Input | Source | Required? | Use |
|---|---|---|---|
| Market condition IDs and associated assertion IDs | Gamma API / internal market registry | Yes | Map 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) | Yes | Detect assertion lifecycle state changes in real time. |
| Oracle WebSocket market stream for assertion ID mapping | ws_market | No | Supplement on-chain event logs with low-latency assertion state updates. |
5. Required Internal Inputs
| Input | Source | Required? | Use |
|---|---|---|---|
| Watched condition ID list | config / StrategyRegistry | Yes | Filter UMA events to only condition IDs with open positions or pending strategies. |
| KillSwitch active flag | KillSwitch | Yes | Continue watching on-chain but suppress ObservationReport emissions when KillSwitch is active. |
6. Parameter Guide
| Parameter | Default | Warning | Hard | What it controls |
|---|---|---|---|---|
| poll_interval_s | 12 | 30 | 60 | Seconds between on-chain RPC polls for new UMA oracle events. Lower = fresher but higher RPC cost. |
| challenge_window_alert_s | 3600 | 1800 | 600 | Seconds before the 2-hour UMA challenge window closes at which a WARN alert is emitted to oracleriskmonitor. |
| alert_on_state_change | True | None | None | If 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
| Condition | Action |
|---|---|
| interval ≤ 12 s | Normal — one poll per block |
| 12–30 s | WARN — may miss events in high-throughput windows |
| > 60 s | Hard 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
| Condition | Action |
|---|---|
| alert_s ≥ 3600 | Normal — 1-hour pre-alert |
| 1800–3600 s | WARN — shorter pre-alert; tighter margin |
| < 600 s | Reject — 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
- 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 → 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–48 h; for DVM_VOTE, estimated 48 h.
- If oracle_state is PROPOSED and time_to_deadline_s ≤ 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.
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
| Code | Severity | Meaning | Action | User-facing message |
|---|---|---|---|---|
ORACLEWATCHER_CHALLENGE_WINDOW_CLOSING | WARN | Less 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_FILED | WARN | A 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_OPEN | WARN | DVM 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_SETTLED | INFO | UMA 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_ACTIVE | HARD_REJECT | KillSwitch active; ObservationReport emissions suppressed. | Continue watching on-chain but suppress emissions. | Oracle status updates are paused while trading is suspended system-wide. |
STALE_DATA | WARN | RPC provider unresponsive for > 2× poll_interval_s; oracle state may be stale. | Halt ObservationReport emissions until RPC recovers; alert on-call. | |
MARKET_CLOSED | EXPLAIN | UMA event detected for a condition_id that is already closed in the internal registry. | Skip emission; log for audit trail only. | |
ORACLEWATCHER_UNKNOWN_ASSERTION | WARN | Assertion 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
| Metric | Type | Unit | Labels | Meaning |
|---|---|---|---|---|
polytraders_intel_oraclewatcher_events_detected_total | counter | count | oracle_state | UMA oracle events detected on-chain per state type. |
polytraders_intel_oraclewatcher_observations_emitted_total | counter | count | oracle_state | ObservationReports emitted, broken down by oracle_state. |
polytraders_intel_oraclewatcher_active_disputes_gauge | gauge | count | Number of condition_ids currently in CHALLENGED or DVM_VOTE state. | |
polytraders_intel_oraclewatcher_time_to_challenge_deadline_s | gauge | seconds | condition_id | Seconds remaining before challenge window closes for each PROPOSED assertion. |
polytraders_intel_oraclewatcher_rpc_block_lag_s | gauge | seconds | Age of the most recently processed Polygon block. | |
polytraders_intel_oraclewatcher_unknown_assertions_total | counter | count | UMA events received for assertion IDs that could not be mapped to a condition_id. |
Alerts
| Alert | Condition | Severity | Runbook |
|---|---|---|---|
OracleWatcherRPCDown | polytraders_intel_oraclewatcher_rpc_block_lag_s > 60 | page | #runbook-oraclewatcher-rpc-down |
OracleWatcherChallengeWindowImminent | min(polytraders_intel_oraclewatcher_time_to_challenge_deadline_s) < 1800 | page | #runbook-oraclewatcher-challenge-imminent |
OracleWatcherDisputeSpike | polytraders_intel_oraclewatcher_active_disputes_gauge > 5 | warn | #runbook-oraclewatcher-dispute-spike |
OracleWatcherHighUnknownAssertions | rate(polytraders_intel_oraclewatcher_unknown_assertions_total[10m]) > 0.1 | warn | #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
| Situation | User-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 alert | The window to contest the proposed resolution for this market closes soon. Risk systems are reviewing exposure automatically. |
| Market resolution finalised | The UMA oracle has settled this market. Final outcome is now on-chain and settlement will proceed. |
18. Failure-Mode Block
| main_failure_mode | RPC 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_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× 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 → condition_id mapping, KillSwitch active flag readable |
19. Failure-Injection Recipes
| Scenario | How to inject | Expected behaviour | Recovery |
|---|---|---|---|
RPC_OUTAGE | Block TCP to primary Polygon RPC for 30 s | Automatic failover to secondary RPC; missed blocks re-fetched; emissions resume | |
DISPUTE_FILED_NEAR_DEADLINE | Inject mock DisputePrice event with time_to_deadline_s=1700 | Alert clears when market transitions to DVM_DEBATE or SETTLED | |
UNKNOWN_ASSERTION | Emit a ProposePrice event with an assertion_id not in Gamma API or internal registry | Once Gamma API returns the mapping, event is resolved and emitted | |
KILL_SWITCH_ON | Set killswitch.active=true; trigger a ProposePrice event | Emissions resume on first event after KillSwitch reset | |
DISPUTE_SPIKE | Inject 6 concurrent dispute events across 6 condition_ids | Alert 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
| Aspect | Specification |
|---|---|
| Execution model | single-threaded event loop |
| Max in-flight | 1 |
| Idempotency key | assertion_id + oracle_state |
| Per-call timeout (ms) | 5000 |
| Backpressure strategy | wal-then-retry — events buffered to Postgres WAL if internal bus is unavailable |
| Locking / mutual exclusion | row-level lock on oracle_states per condition_id during state transition |
22. Dependencies
Depends on (must run first)
| Bot | Why | Contract |
|---|---|---|
| risk.kill_switch | KillSwitch gate suppresses ObservationReport emissions when active. |
Emits to (downstream consumers)
| Bot | Why | Contract |
|---|---|---|
| risk.oracle_risk_monitor |
External services
| Service | Endpoint | SLA assumed | On failure |
|---|---|---|---|
| Polygon RPC (Alchemy / Infura) | 99.9% / 200 ms p99 | ||
| UMA Optimistic Oracle (on-chain) | Polygon mainnet contract | Blockchain-level finality | |
| Gamma API | https://gamma-api.polymarket.com | 99.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
| Aspect | Value |
|---|---|
| CLOB version | v2 |
| Collateral asset | pUSD |
| EIP-712 Exchange domain version | 2 |
| Aware of builderCode field | no |
| Aware of negative-risk markets | no |
| Multi-chain ready | no |
| 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–48 h debate + ~48 h vote). Bond amounts in all payloads are denominated in pUSD. |
API surfaces declared
Networks supported
25. Versioning & Migration
| Field | Value |
|---|---|
| spec | 2.0.0 |
| implementation | 2.1.0 |
| schema | 2 |
| released | 2026-04-28 |
Migration history
| Date | From | To | Reason | Action taken |
|---|---|---|---|---|
| 2026-04-28 | v1 | v2 | CLOB V2 cutover — collateral change to pUSD, bond denomination updated | 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. |
26. Acceptance Tests
Unit Tests
| Test | Setup | Expected result |
|---|---|---|
| PROPOSAL event emits ObservationReport with oracle_state=PROPOSED | Mock ProposePrice event for watched condition_id | ObservationReport emitted with oracle_state=PROPOSED, bond_pusd=750, time_to_deadline_s≈7200 |
| DISPUTE event emits ObservationReport with oracle_state=CHALLENGED | Mock DisputePrice event following ProposePrice | ObservationReport emitted with oracle_state=CHALLENGED, disputer populated |
| Challenge window alert fires at correct threshold | time_to_deadline_s = 3500, challenge_window_alert_s=3600 | WARN ORACLEWATCHER_CHALLENGE_WINDOW_CLOSING included in warnings |
| KillSwitch suppresses emissions but watching continues | killswitch.active=true; PROPOSAL event arrives | Event detected and logged; no ObservationReport emitted |
| RPC outage emits STALE_DATA and halts emissions | RPC 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=SETTLED | Mock Settle event with final price | ObservationReport with oracle_state=SETTLED, proposed_price=final resolved value |
Integration Tests
| Test | Expected result |
|---|---|
| Full lifecycle: PROPOSED → CHALLENGED → DVM_DEBATE → SETTLED generates 4 ObservationReports | oracleriskmonitor receives all four state transitions with correct condition_id and timings |
| Gamma API outage causes assertion_id resolution to fail gracefully | Event buffered; ObservationReport emitted with condition_id=null and STALE_DATA warning once API recovers |
| Non-Polymarket UMA event filtered out by ancillary data prefix check | No ObservationReport emitted for foreign assertion_id |
Property Tests
| Property | Required behaviour |
|---|---|
| OracleWatcher never submits, signs, or modifies any order or on-chain transaction | Always true |
| No ObservationReport emitted when KillSwitch is active | Always 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
| Alert | First step | Diagnosis | Mitigation | Escalate 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
| Gate | How measured | Threshold |
|---|---|---|
| Unit tests pass for PROPOSE, DISPUTE, and SETTLE state transitions | CI test run | 100% pass |
| RPC integration test: block log subscription fires on mock ProposePrice event | Integration test against Polygon testnet | Pass |
Promote to Limited live
| Gate | How measured | Threshold |
|---|---|---|
| rpc_block_lag_s p99 < 15 s over 48 h | polytraders_intel_oraclewatcher_rpc_block_lag_s gauge | p99 < 15 s |
| All test assertion_ids resolve correctly against Gamma API | Integration test with known Polymarket markets | 100% resolution |
Promote to General live
| Gate | How measured | Threshold |
|---|---|---|
| Zero missed PROPOSED events over 14 days (verified by reconciling on-chain logs vs emitted ObservationReports) | Post-hoc on-chain reconciliation script | 0 missed events |
| KillSwitch suppression: zero ObservationReports emitted when KillSwitch active | Integration test | Pass |
| Postgres 1-year retention verified: oracle_states rows survive 365-day TTL check | DB retention policy test | Pass |
29. Developer Checklist
Ready-to-ship score: 27/27 sections complete · 100%
| Requirement | Status |
|---|---|
| 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 |