1. Bot Identity
| Layer | Intelligence Intelligence |
|---|
| Bot class | Signal Service |
|---|
| Authority | Read-only |
|---|
| Status | BETA |
|---|
| Readiness | Limited live |
|---|
| Runs before | Strategy layer, LiquidityGuard, OrderFlowAnalyzer |
|---|
| Runs after | Polygon RPC subscription established; watched_wallets config loaded |
|---|
| Applies to | All Polymarket markets where watched wallets hold or have recently changed positions |
|---|
| Default mode | limited_live |
|---|
| User-visible | Advanced details only |
|---|
| Developer owner | Polytraders core — Intelligence pod |
|---|
2. Purpose
OnChainWatcher monitors Polygon on-chain events for a configured list of high-signal wallet addresses, detecting CTFExchangeV2 order fills, position transfers, and pUSD balance changes that exceed min_position_change_usd. It emits an ObservationReport for each qualifying wallet event after a publish_delay_s hold (to prevent front-running its own signals). Output feeds liquidity-aware strategies and the LiquidityGuard risk bot with smart-money flow intelligence. OnChainWatcher is strictly read-only — it never submits or signs orders.
3. Why This Bot Matters
High-signal wallet entry not detected
Strategy misses a leading indicator; enters after the smart-money move has already re-priced the market, capturing reduced or negative edge.
publish_delay_s misconfigured to zero
OnChainWatcher signals are published immediately, allowing the trading infrastructure itself to front-run other participants using its own on-chain data feed.
Watched wallet list stale after wallet rotation
Previously high-signal addresses are no longer the active trading wallets; signals become irrelevant and may mislead strategies.
RPC outage causes missed position changes during high-activity period
Smart-money entries during the blackout are not detected; strategies enter a market after the signal window has closed.
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.
6. Parameter Guide
| Parameter | Default | Warning | Hard | What it controls |
|---|
| min_position_change_usd | 500 | 200 | 50 | Minimum pUSD value of a single wallet position change to qualify for an ObservationReport emission. |
| publish_delay_s | 15 | 5 | 0 | Seconds to hold a detected wallet event before emitting ObservationReport. Prevents the infrastructure from acting on its own signal faster than public latency. |
| poll_interval_s | 12 | 30 | 60 | Seconds between Polygon RPC polls for new on-chain events. Aligns with Polygon block time. |
7. Detailed Parameter Instructions
min_position_change_usd
What it means
Minimum pUSD value of a single wallet position change to qualify for an ObservationReport emission.
Default
{ "min_position_change_usd": 500 }
Why this default matters
$500 filters out dust transactions and small test fills, ensuring only meaningful position changes generate signals.
Threshold logic
| Condition | Action |
|---|
| change ≥ 500 pUSD | Normal — emit ObservationReport |
| 200–500 pUSD | WARN — small position; emit with ONCHAINWATCHER_SMALL_POSITION warning |
| < 50 pUSD | Reject — dust threshold; do not emit |
Developer check
if (p.min_position_change_usd < p.hard) return; // silently skip dust
User-facing English
Only meaningful wallet movements above a minimum size generate intelligence signals.
publish_delay_s
What it means
Seconds to hold a detected wallet event before emitting ObservationReport. Prevents the infrastructure from acting on its own signal faster than public latency.
Default
{ "publish_delay_s": 15 }
Why this default matters
15 s gives on-chain event latency time to propagate broadly, ensuring the signal is not used for front-running relative to public market participants.
Threshold logic
| Condition | Action |
|---|
| delay ≥ 15 s | Normal |
| 5–15 s | WARN — reduced front-run protection window |
| < 5 s | Reject — PARAMETER_CHANGE_REQUIRES_APPROVAL; insufficient delay |
Developer check
if (p.publish_delay_s < 5) throw ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL');
User-facing English
Wallet signals are held briefly before being published to ensure fairness in market access.
poll_interval_s
What it means
Seconds between Polygon RPC polls for new on-chain events. Aligns with Polygon block time.
Default
{ "poll_interval_s": 12 }
Why this default matters
12 s matches the Polygon block interval, ensuring every 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 | Reject — PARAMETER_CHANGE_REQUIRES_APPROVAL |
Developer check
if (p.poll_interval_s > p.hard) throw ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL');
User-facing English
On-chain events are checked every Polygon block to ensure no wallet movement is missed.
8. Default Configuration
{
"bot_id": "intel.onchainwatcher",
"version": "2.1.0",
"mode": "limited_live",
"defaults": {
"min_position_change_usd": 500,
"publish_delay_s": 15,
"poll_interval_s": 12
},
"locked": {
"min_position_change_usd": {
"min": 50
},
"publish_delay_s": {
"min": 5
},
"poll_interval_s": {
"max": 60
}
}
}
9. Implementation Flow
- On startup, subscribe to Polygon RPC event log for CTFExchangeV2 OrderFilled events and pUSD Transfer events, filtered to watched_wallets addresses.
- On each block (poll_interval_s), fetch new events since last_processed_block.
- For each event, check if wallet address is in watched_wallets. If not, skip.
- Resolve CTFExchangeV2 token_id to condition_id via data_api. If resolution fails, buffer event and retry on next block.
- Compute position_change_pusd from event size fields (denominated in pUSD).
- If position_change_pusd < min_position_change_usd hard floor (50 pUSD), discard as dust.
- If position_change_pusd < min_position_change_usd default (500 pUSD), flag ONCHAINWATCHER_SMALL_POSITION.
- Add event to publish_queue with emit_at = now_ms() + publish_delay_s * 1000.
- Check KillSwitch. If active, continue monitoring and queuing but suppress emissions.
- On each tick, drain publish_queue of events whose emit_at <= now_ms().
- For each drained event (if KillSwitch not active), emit ObservationReport with: report_id, trace_id, wallet_address, condition_id, side (BUY/SELL), position_change_pusd, price, block_number, tx_hash, warnings.
- Log per-block summary: events_detected, events_emitted, events_queued, dust_skipped, killswitch_active.
10. Reference Implementation
Subscribes to Polygon RPC event log for CTFExchangeV2 OrderFilled events filtered to watched_wallets, resolves token IDs to condition_ids, applies dust filter and size threshold, holds events for publish_delay_s, then emits ObservationReports.
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 events since last_processed_block ---
events = rpc.getLogs(
address = CTFEXCHANGEV2_ADDR,
topics = [ORDER_FILLED_SIG, PUSD_TRANSFER_SIG],
fromBlock = last_processed_block + 1,
toBlock = block_number
)
// --- 2. KillSwitch gate ---
ks = FETCH internal.killswitch.status
FOR event IN events:
// --- 3. Wallet filter ---
wallet = event.makerAddress OR event.takerAddress
IF wallet NOT IN params.watched_wallets:
CONTINUE
// --- 4. Resolve token_id -> condition_id ---
condition_id = data_api.GET('/token/' + event.tokenId + '/condition')
IF condition_id IS NULL:
buffer_for_retry(event)
CONTINUE
// --- 5. Dust filter ---
position_change_pusd = toPusdUnits(event.fillAmount)
IF position_change_pusd < params.min_position_change_usd.hard:
dust_skipped_counter += 1
CONTINUE
// --- 6. Warning threshold ---
warnings = []
IF position_change_pusd < params.min_position_change_usd.default:
warnings.append('ONCHAINWATCHER_SMALL_POSITION')
side = 'BUY' IF event.makerSide == 'SELL' ELSE 'SELL'
// --- 7. Queue with publish delay ---
publish_queue.add({
wallet_address: wallet,
condition_id: condition_id,
side: side,
position_change_pusd: position_change_pusd,
price: event.price,
block_number: block_number,
tx_hash: event.transactionHash,
emit_at_ms: now_ms() + params.publish_delay_s * 1000,
warnings: warnings
})
// --- 8. Drain publish queue ---
FOR item IN publish_queue.due_items(now_ms()):
// KillSwitch suppress
IF ks.active:
LOG INFO 'KILL_SWITCH_ACTIVE — suppressing ObservationReport'
CONTINUE
EMIT ObservationReport {
report_id: 'rep_ocw_' + item.condition_id[:6] + '_' + now_ms(),
trace_id: new_trace_id(),
bot_id: 'intel.onchainwatcher',
kind: 'ObservationReport',
wallet_address: item.wallet_address,
condition_id: item.condition_id,
side: item.side,
position_change_pusd: item.position_change_pusd,
price: item.price,
block_number: item.block_number,
tx_hash: item.tx_hash,
publish_delay_applied_s: params.publish_delay_s,
warnings: item.warnings,
emitted_at_ms: now_ms()
}
last_processed_block = block_number
SDK calls used
rpc.getLogs(address=CTFEXCHANGEV2_ADDR, topics=[ORDER_FILLED_SIG], fromBlock, toBlock)data_api.GET('/token/<tokenId>/condition')toPusdUnits(fillAmount)
Complexity: O(E) per block where E = OrderFilled events for watched wallets; typically O(1) in steady state
11. Wire Examples
Input — what arrives on the wire
{
"label": "Polygon RPC CTFExchangeV2 OrderFilled event for watched wallet",
"source": "onchain",
"payload": {
"event": "OrderFilled",
"makerAddress": "0xF1A2B3C4D5E6F7A8B9C0D1E2F3A4B5C6D7E8F9A0",
"takerAddress": "0x000000000000000000000000000000000000dead",
"tokenId": "87654321",
"fillAmount": "12500000000",
"price": "0.68",
"transactionHash": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab",
"block_number": 72346001,
"timestamp_ms": 1746703000000
}
}
Output — what the bot emits
{
"label": "ObservationReport — high-signal wallet BUY detected",
"payload": {
"report_id": "rep_ocw_0xf1a2_1746703000000",
"trace_id": "trc_0xbeef0102030405060708",
"bot_id": "intel.onchainwatcher",
"kind": "ObservationReport",
"wallet_address": "0xF1A2B3C4D5E6F7A8B9C0D1E2F3A4B5C6D7E8F9A0",
"condition_id": "0xf1a2b30000000000000000000000000000000000000000000000000000000000",
"side": "BUY",
"position_change_pusd": 12500,
"price": 0.68,
"block_number": 72346001,
"tx_hash": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab",
"publish_delay_applied_s": 15,
"warnings": [],
"emitted_at_ms": 1746703015000
}
}
12. Decision Logic
APPROVE
Not applicable — OnChainWatcher is read-only; it never approves or submits orders.
RESHAPE_REQUIRED
Not applicable.
REJECT
Events below the hard dust floor (50 pUSD) are silently discarded. Events are suppressed (not emitted) only when KillSwitch is active (KILL_SWITCH_ACTIVE).
WARNING_ONLY
ONCHAINWATCHER_SMALL_POSITION is included as a warning when position_change_pusd is between the hard floor and the default threshold.
13. Standard Decision Output
This bot returns a ObservationReport object. See ObservationReport schema.
{
"report_id": "rep_ocw_0xf1a2_1746703000000",
"trace_id": "trc_0xbeef0102030405060708",
"bot_id": "intel.onchainwatcher",
"kind": "ObservationReport",
"wallet_address": "0xF1A2B3C4D5E6F7A8B9C0D1E2F3A4B5C6D7E8F9A0",
"condition_id": "0xf1a2b30000000000000000000000000000000000000000000000000000000000",
"side": "BUY",
"position_change_pusd": 12500,
"price": 0.68,
"block_number": 72346001,
"tx_hash": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab",
"publish_delay_applied_s": 15,
"warnings": [],
"emitted_at_ms": 1746703015000
}
14. Reason Codes
| Code | Severity | Meaning | Action | User-facing message |
|---|
ONCHAINWATCHER_LARGE_WALLET_ENTRY | WARN | Watched wallet made a position change >= min_position_change_usd on a Polymarket market. | Emit ObservationReport after publish_delay_s; downstream strategies consume as smart-money signal. | A significant wallet movement was detected on this market. |
ONCHAINWATCHER_SMALL_POSITION | WARN | Wallet position change is between the hard floor (50 pUSD) and default threshold (500 pUSD). | Emit ObservationReport with ONCHAINWATCHER_SMALL_POSITION warning; strategies apply lower weight to this signal. | |
ONCHAINWATCHER_RESOLUTION_RETRY | WARN | token_id could not be resolved to a condition_id via data_api; event buffered for retry. | Buffer event; retry on next block; emit ObservationReport once resolved. | |
STALE_DATA | WARN | RPC provider unresponsive for > 2× poll_interval_s; on-chain state may be stale. | Halt ObservationReport emissions until RPC recovers; alert on-call. | |
KILL_SWITCH_ACTIVE | HARD_REJECT | KillSwitch active; ObservationReport emissions suppressed. | Continue monitoring and queuing events but suppress all emissions. | Wallet monitoring is paused while trading is suspended system-wide. |
MARKET_CLOSED | EXPLAIN | OrderFilled event received for a condition_id that is already closed or resolved. | Skip emission; log for audit trail only. | |
PARAMETER_CHANGE_REQUIRES_APPROVAL | HARD_REJECT | A parameter change violates a locked bound (e.g. publish_delay_s < 5 or poll_interval_s > 60). | Reject config change; do not apply. | |
ONCHAINWATCHER_EMPTY_WALLET_LIST | WARN | watched_wallets config is empty; no on-chain events will be captured. | Log WARN at startup; emit no ObservationReports until wallets are configured. | |
15. Metrics & Logs
Metrics emitted
| Metric | Type | Unit | Labels | Meaning |
|---|
polytraders_intel_onchainwatcher_events_detected_total | counter | count | wallet_address | On-chain events detected for watched wallets per block cycle. |
polytraders_intel_onchainwatcher_observations_emitted_total | counter | count | side | ObservationReports emitted after publish_delay_s, broken down by side (BUY/SELL). |
polytraders_intel_onchainwatcher_dust_skipped_total | counter | count | | Events discarded as dust (below min_position_change_usd hard floor). |
polytraders_intel_onchainwatcher_rpc_block_lag_s | gauge | seconds | | Age of the most recently processed Polygon block. |
polytraders_intel_onchainwatcher_publish_queue_depth | gauge | count | | Number of detected events currently sitting in the publish queue awaiting publish_delay_s. |
polytraders_intel_onchainwatcher_resolution_retries_total | counter | count | | Total token_id -> condition_id resolution retries due to data_api failures. |
Alerts
| Alert | Condition | Severity | Runbook |
|---|
OnChainWatcherRPCDown | polytraders_intel_onchainwatcher_rpc_block_lag_s > 60 | page | #runbook-onchainwatcher-rpc-down |
OnChainWatcherQueueBuildup | polytraders_intel_onchainwatcher_publish_queue_depth > 50 | warn | #runbook-onchainwatcher-queue-buildup |
OnChainWatcherStaleData | rate(polytraders_intel_onchainwatcher_events_detected_total[10m]) == 0 AND polytraders_intel_onchainwatcher_rpc_block_lag_s < 60 | warn | #runbook-onchainwatcher-stale-data |
OnChainWatcherHighResolutionRetries | rate(polytraders_intel_onchainwatcher_resolution_retries_total[10m]) > 5 | warn | #runbook-onchainwatcher-resolution-retries |
Dashboards
- Grafana — Intelligence / OnChainWatcher wallet event rate and queue depth
- Grafana — Intelligence / per-wallet position change size distribution
16. Developer Reporting
{
"bot_id": "intel.onchainwatcher",
"block_number": 72346001,
"events_detected": 8,
"events_emitted": 3,
"events_queued": 2,
"dust_skipped": 3,
"condition_id_resolutions_failed": 0,
"killswitch_active": false
}
17. Plain-English Reporting
| Situation | User-facing explanation |
|---|
| Strategy increased size on a market after wallet signal | A tracked high-activity wallet made a significant move on this market. The system detected this as a potential informed signal and adjusted positioning accordingly. |
| Signal delay before strategy acts | Wallet signals are intentionally held briefly before being published to ensure no participant has an unfair advantage based on on-chain latency. |
| Strategy reduced size on a market after wallet exit signal | A tracked wallet reduced its position. This was treated as a potential exit signal, and exposure was trimmed as a precaution. |
18. Failure-Mode Block
| main_failure_mode | RPC provider outage during a high-activity period causes OnChainWatcher to miss wallet fills, leading strategies to miss smart-money entry signals and potentially enter markets after the signal window has closed. |
|---|
| false_positive_risk | A wash-trade or self-transfer between two watched wallets is counted as a large position change, generating a spurious buy signal on a market where no genuine informed entry occurred. |
|---|
| false_negative_risk | A watched wallet operating through a multi-step proxy contract has its CTFExchangeV2 fills attributed to an intermediate address not in watched_wallets, causing the signal to be missed entirely. |
|---|
| 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 from buffered events based on data older than 3 blocks. |
|---|
| required_dependencies | Polygon RPC (event log access), Polymarket Data API for token_id → condition_id resolution, KillSwitch active flag readable, watched_wallets config non-empty |
|---|
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; queued events emitted after publish_delay_s |
DATA_API_DOWN | Return 503 from data_api for 60 s | | Once data_api recovers, buffered events resolved and emitted |
KILL_SWITCH_ON | Set killswitch.active=true; inject large wallet BUY event | | Queued events emitted on first drain tick after KillSwitch reset |
DUST_FLOOD | Inject 100 OrderFilled events of 10 pUSD each for watched wallets | | Automatic; no action required |
QUEUE_BUILDUP | Inject 60 large wallet events while blocking emit drain for 30 s | | Once drain resumes, remaining queued events emitted normally |
20. State & Persistence
Cold-start recovery
On cold start, last_processed_block loaded from Redis. Missed blocks since last checkpoint re-fetched from RPC. publish_queue drained normally.
21. Concurrency & Idempotency
| Aspect | Specification |
|---|
| Execution model | single-threaded event loop |
| Max in-flight | 1 |
| Idempotency key | tx_hash + wallet_address |
| Per-call timeout (ms) | 5000 |
| Backpressure strategy | drop-after-buffer — if publish_queue > 200 entries, oldest entries dropped with STALE_DATA warning |
| Locking / mutual exclusion | Redis SETNX on tx_hash to prevent duplicate event processing across restarts |
22. Dependencies
Depends on (must run first)
Emits to (downstream consumers)
Used by (auto-aggregated)
4.14
External services
| Service | Endpoint | SLA assumed | On failure |
|---|
| Polygon RPC (Alchemy / Infura) | Polygon mainnet | 99.9% / 200 ms p99 | |
| Polymarket Data API (token_id resolution) | https://data-api.polymarket.com | 99.9% / 500 ms p99 | |
23. Security Surfaces
Abuse vectors considered
- Adversary discovers watched_wallets list and wash-trades to inject false buy/sell signals
- RPC provider returns crafted event logs to fake large wallet fills for a specific condition_id
Mitigations
- watched_wallets list is operator-confidential config; not exposed in ObservationReport payloads
- publish_delay_s minimum (5 s) prevents use as a sub-second front-run vector
- All ObservationReports are informational only — LiquidityGuard and strategies independently validate market state before acting on signals
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 | OnChainWatcher subscribes to CTFExchangeV2 OrderFilled events on Polygon; all position_change_pusd values are denominated in pUSD. The V1 CTFExchange contract is no longer monitored. |
API surfaces declared
onchaindata_apiinternal
Networks supported
polygon
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 — CTFExchangeV2 contract address, pUSD denomination, and updated OrderFilled event ABI | Updated on-chain subscription to CTFExchangeV2 (V1 CTFExchange removed). Position change amounts updated from USDC.e to pUSD denomination. OrderFilled ABI updated for V2 field names (removed nonce/feeRateBps from event). No signed-order plumbing in this bot. |
26. Acceptance Tests
Unit Tests
| Test | Setup | Expected result |
|---|
| Large wallet BUY above threshold emits ObservationReport after publish_delay_s | OrderFilled event for watched wallet, position_change_pusd=12500, publish_delay_s=15 | ObservationReport emitted 15 s after event detection with side=BUY, position_change_pusd=12500 |
| Dust event below hard floor discarded silently | OrderFilled event, position_change_pusd=30 pUSD, hard floor=50 | No ObservationReport; dust_skipped counter incremented |
| Small position between hard floor and default emits with ONCHAINWATCHER_SMALL_POSITION warning | position_change_pusd=300, min_position_change_usd default=500, hard=50 | ObservationReport emitted with warnings=['ONCHAINWATCHER_SMALL_POSITION'] |
| KillSwitch suppresses emissions but queuing continues | killswitch.active=true; large wallet BUY detected | Event queued; no ObservationReport emitted; KILL_SWITCH_ACTIVE logged |
| RPC outage triggers STALE_DATA and halts emissions | RPC unavailable for 30 s (> 2× poll_interval_s=12) | STALE_DATA WARN logged; no ObservationReports emitted during outage |
| Non-watched wallet event discarded | OrderFilled event from address not in watched_wallets | Event silently discarded; no metrics incremented |
Integration Tests
| Test | Expected result |
|---|
| Full lifecycle: wallet BUY detected, delayed, emitted, consumed by LiquidityGuard | LiquidityGuard receives ObservationReport with correct condition_id, side=BUY, and position_change_pusd |
| RPC failover: primary RPC down, secondary takes over, missed blocks re-fetched | Buffered events emitted after RPC recovery with correct block_number |
| Data API outage delays condition_id resolution; event eventually resolved and emitted | Event buffered; ObservationReport emitted once data_api returns condition_id mapping |
Property Tests
| Property | Required behaviour |
|---|
| OnChainWatcher never submits, signs, or modifies any order | Always true |
| No ObservationReport emitted when KillSwitch is active | Always true |
| publish_delay_s >= 5 enforced; all emissions are delayed | Always true |
27. Operational Runbook
OnChainWatcher incidents are most commonly RPC outages or data_api failures preventing condition_id resolution. An RPC outage causes smart-money signals to be missed for the outage duration. Page immediately on RPC down.
On-call actions
| Alert | First step | Diagnosis | Mitigation | Escalate to |
|---|
OnChainWatcherRPCDown | | | | |
OnChainWatcherQueueBuildup | | | | |
OnChainWatcherStaleData | | | | |
OnChainWatcherHighResolutionRetries | | | | |
Manual overrides
Healthcheck
Endpoint: /internal/health/onchainwatcher | Green: rpc_block_lag_s < 24 AND Redis reachable AND publish_queue_depth < 50 AND watched_wallets non-empty | Red: rpc_block_lag_s > 60 OR Redis unreachable OR watched_wallets empty OR publish_queue_depth > 200
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 |