1.18 StaleBookGuard
Rejects any OrderIntent priced against an order book older than the configured staleness threshold. The book may look healthy, but if its last update is too old, prices have almost certainly moved. StaleBookGuard fails closed: if it cannot prove the book is fresh, it rejects.
v3 readiness
A bot is done when all four scores are. What does done mean?
risk.stalebookguard
Maps to spec page stale_book_guard. SEARCH_SPACE declared. Fixture pack pending.
Source: @polytraders/bots · src/risk/stalebookguard.js · Impl 11/15 · Backtest 3/4
1. Bot Identity
| Layer | Risk Risk |
|---|---|
| Bot class | Guardrail |
| Authority | Reject |
| Status | PLANNED |
| Readiness | Spec ready |
| Runs before | exec.smart_router |
| Runs after | intel.orderflowanalyzer |
| Applies to | Per OrderIntent |
| Default mode | shadow |
| User-visible | Yes |
| Developer owner | Risk pod |
Operational profile
| Ownership | Risk pod · on-call risk-oncall · #polytraders-risk · escalates to Head of Risk · P2 |
|---|---|
| Latency budget | p50: 1ms · p99: 5ms |
| Modes supported | offshadowadvisoryenforcedquarantine |
| Data freshness | max_market_data_age_ms=2000 · max_orderbook_age_ms=2000 · on stale → REJECT — that is precisely what this bot does. |
| Human override | no · by — · logs — · time-bound: — · scope: — · single approver |
2. Purpose
Rejects any OrderIntent priced against an order book older than the configured staleness threshold. The book may look healthy, but if its last update is too old, prices have almost certainly moved. StaleBookGuard fails closed: if it cannot prove the book is fresh, it rejects.
3. Why This Bot Matters
Trading on stale prices
An OrderIntent priced against a 20-second-old book during a fast-moving event is essentially a market order; it will fill at whatever price the new book is.
Hidden feed lag
WebSocket latency can spike without disconnecting; a healthy-looking connection can still be delivering 30-second-old data.
No central freshness check
Without one canonical staleness rule, every strategy invents its own — some too lenient, some forgotten entirely.
4. Required Polymarket Inputs
| Input | Source | Required? | Use |
|---|---|---|---|
| OrderBookSnapshot.ts_ms per market_id | intel.orderflowanalyzer | Yes | The single source of truth for book freshness. |
5. Required Internal Inputs
| Input | Source | Required? | Use |
|---|---|---|---|
| OrderIntent.market_id | Strategy bot | Yes | Identifies which book to check. |
| Wall-clock now_ms | Runtime | Yes | Compute age of the book at decision time. |
6. Parameter Guide
| Parameter | Default | Warning | Hard | What it controls |
|---|---|---|---|---|
| max_book_age_ms | 2000 | 1000 | 2000 | Maximum allowed book age (ms) at which an OrderIntent may proceed. |
| warn_book_age_ms | 1000 | — | — | Soft warn threshold — logged but not blocking, used for observability. |
7. Detailed Parameter Instructions
max_book_age_ms
What it means
Maximum allowed book age (ms) at which an OrderIntent may proceed.
Default
{ "max_book_age_ms": 2000 }
Why this default matters
2 seconds is the longest the most active markets can drift before mid-price has demonstrably moved.
Threshold logic
| Condition | Action |
|---|---|
| ≤ 1000ms | PASS |
| 1000–2000ms | WARN |
| > 2000ms | REJECT |
Developer check
if (now - book.tsMs > p.max_book_age_ms) return reject('RISK_BOOK_STALE');
User-facing English
We did not place this order because the latest market data was too old to trust.
warn_book_age_ms
What it means
Soft warn threshold — logged but not blocking, used for observability.
Default
{ "warn_book_age_ms": 1000 }
Why this default matters
Lets ops see slowly degrading feed health before it becomes a hard reject.
Threshold logic
| Condition | Action |
|---|---|
| ≤ 1000ms | Silent |
| > 1000ms | WARN log only |
Developer check
if (now - book.tsMs > p.warn_book_age_ms) log.warn('BOOK_AGE_HIGH');
User-facing English
(Internal — not shown to users.)
8. Default Configuration
{
"max_book_age_ms": 2000,
"warn_book_age_ms": 1000
}9. Implementation Flow
— not yet authored —
10. Reference Implementation
Pseudocode is language-agnostic. FETCH = read input. EMIT = produce output. IF/THEN/ELSE = decision. Translate directly to TypeScript, Python, Go, or Rust.
snap = latest_snapshot(intent.market_id)
if snap is None: return reject('RISK_BOOK_STALE')
age = now_ms() - snap.ts_ms
if age > p.max_book_age_ms: return reject('RISK_BOOK_STALE', measured_age_ms=age)
if age > p.warn_book_age_ms: log_warn('BOOK_AGE_HIGH', age=age)
return pass_()11. Wire Examples
Input — what arrives on the wire
{
"intent_id": "intent_002",
"market_id": "0xabc"
}
Output — what the bot emits
{
"vote": "REJECT",
"reason_code": "RISK_BOOK_STALE",
"measured_age_ms": 3104
}12. Decision Logic
APPROVE
Look up most recent book snapshot for market_id. Compute age in ms. Emit measured age as metric for every decision.
RESHAPE_REQUIRED
This bot does not reshape orders.
REJECT
Reject if age > max_book_age_ms.
WARNING_ONLY
No warn-only path defined.
13. Standard Decision Output
This bot returns a RiskVote object. See RiskVote schema.
{
"vote": "REJECT",
"reason_code": "RISK_BOOK_STALE",
"measured_age_ms": 3104,
"explain": "Book age 3104ms > 2000ms threshold."
}14. Reason Codes
| Code | Severity | Meaning | Action | User-facing message |
|---|---|---|---|---|
RISK_BOOK_STALE | P1 | Risk Book Stale | See decision output and developer log for context. | We did not place this order because the latest market data was too old to trust. |
RISK_BOOK_STALE_WARN | P1 | Risk Book Stale Warn | See decision output and developer log for context. | We did not place this order because the latest market data was too old to trust. |
15. Metrics & Logs
Metrics emitted
| Metric | Type | Unit | Labels | Meaning |
|---|---|---|---|---|
book_age_ms_histogram | counter | event | bot_id | Book age ms histogram. |
rejects_with_book_stale | counter | event | market_id, reason_code | Rejects with book stale. |
warns_with_book_age_high | counter | event | bot_id | Warns with book age high. |
Dashboards
- 1.18 overview dashboard
16. Developer Reporting
"Per decision: intent_id, market_id, book.ts_ms, decision_ts_ms, measured_age_ms, vote, reason_code."17. Plain-English Reporting
| Situation | User-facing explanation |
|---|---|
| When this bot acts | We did not place this order because the latest market data was too old to trust. |
18. Failure-Mode Block
| main_failure_mode | Rejecting orders when the book is fine but the freshness clock has drifted. |
|---|---|
| false_positive_risk | Clock skew between feed publisher and bot host; mitigation: bots use NTP-synced clocks and the data-flow layer stamps an ingest_ts_ms used as a fallback. |
| false_negative_risk | A frozen book with a recent ts_ms (publisher bug) passes the check; mitigation: combine with MarketHaltDetector's TRADE_SILENCE rule. |
| safe_fallback | If no snapshot exists at all, REJECT — never assume freshness. |
| required_dependencies | — |
19. Failure-Injection Recipes
| Scenario | How to inject | Expected behaviour | Recovery |
|---|---|---|---|
Pause the synthetic feed for 4 seconds and assert all in-flight intents are reje | Pause the synthetic feed for 4 seconds and assert all in-flight intents are rejected. | Bot detects within its latency budget and emits the corresponding reason code. | Remove the injected fault; bot returns to healthy state within one debounce window. |
Inject a snapshot with future ts_ms and assert decision is still PASS (negative | Inject a snapshot with future ts_ms and assert decision is still PASS (negative age). | Bot detects within its latency budget and emits the corresponding reason code. | Remove the injected fault; bot returns to healthy state within one debounce window. |
20. State & Persistence
Stateless. Reads latest snapshot from the data-flow layer's in-memory cache.
State stores
| Name | Kind | Key | Value shape | TTL | Durability |
|---|---|---|---|---|---|
stale_book_guard_state | in-memory + fast KV mirror | bot_id | Stateless. Reads latest snapshot from the data-flow layer's in-memory cache. | 24h | crash-safe via KV mirror |
Cold-start recovery
Cold-start hydrates from fast KV; missing keys default to safe fallback.
On restart
All in-flight decisions are re-evaluated; no bot decision is trusted across restart without re-emit.
21. Concurrency & Idempotency
| Aspect | Specification |
|---|---|
| Execution model | Pure function of (snapshot, now_ms). Trivially safe under concurrent calls. |
| Max in-flight | 32 |
| Idempotency key | order_intent_id |
| Replay-safe | True |
| Deduplication | By idempotency_key within a 60s window. |
| Ordering guarantees | Per-market_id FIFO; cross-market unordered. |
| Per-call timeout (ms) | 250 |
| Backpressure strategy | Bounded queue; oldest-dropped with metric increment when full. |
| Locking / mutual exclusion | Per-market_id mutex; no global locks. |
22. Dependencies
Depends on (must run first)
| Bot | Why | Contract |
|---|---|---|
| intel.orderflowanalyzer |
Emits to (downstream consumers)
| Bot | Why | Contract |
|---|---|---|
| exec.smart_router |
Requires (graph.requires)
Required before (graph.required_before)
| Consumes | OrderIntent OrderBookSnapshot |
|---|---|
| Emits | RiskVote |
| Blocks orders | yes |
23. Security Surfaces
No external surface. Reads internal book cache only.
Signing surface
None — bot does not sign or submit.
Mitigations
- Rate-limit per source
- Audit-log every override
- Require role-based authz on admin paths
24. Polymarket V2 Compatibility
| Aspect | Value |
|---|---|
| CLOB version | V2 |
| Collateral asset | pUSD |
| EIP-712 Exchange domain version | 2 |
| Aware of builderCode field | yes |
| Aware of negative-risk markets | yes |
| Multi-chain ready | yes |
| SDK used | Polymarket CLOB V2 SDK |
| Settlement contract | CTFExchangeV2 |
| Notes | OrderBookSnapshot.ts_ms field is a CLOB V2 standard property. |
25. Versioning & Migration
| Field | Value |
|---|---|
| current | 0.1.0 |
| contract_version | 1.0.0 |
| last_breaking_change | none |
| deprecation_window_days | 30 |
26. Acceptance Tests
Unit Tests
| Test | Setup | Expected result |
|---|---|---|
| Pass when age = max_book_age_ms - 1. | Synthetic fixture per template. | Behaviour matches the rule described in the test name. |
| Reject when age = max_book_age_ms + 1. | Synthetic fixture per template. | Behaviour matches the rule described in the test name. |
| Reject when no snapshot is present. | Synthetic fixture per template. | Behaviour matches the rule described in the test name. |
Integration Tests
| Test | Expected result |
|---|---|
| End-to-end: synthetic CLOB stream paused for 4 seconds → all OrderIntents in the gap rejected. | End-to-end behaviour matches the spec without manual intervention. |
Property Tests
| Property | Required behaviour |
|---|---|
| For any (book.ts_ms, now_ms) pair, vote is determined purely by their difference and the threshold. | Always true across all generated inputs. |
27. Operational Runbook
If reject rate spikes: first check feed health (ingest latency p99). If feed is healthy and rejects persist, suspect publisher clock drift.
On-call actions
| Alert | First step | Diagnosis | Mitigation | Escalate to |
|---|---|---|---|---|
1.18_anomaly | Open the bot's reporting page and confirm the alert is real (not a metric hiccup). | Inspect developer log entries for the affected market_id over the last 30 minutes. | Force-clear via Admin UI if the rule is clearly stale; otherwise leave engaged and notify owner. | Risk pod |
Manual overrides
polytraders bot pause 1.18— Disables the bot's enforcement layer; downstream consumers fall back to safe defaults.
Healthcheck
GET /healthz/stale_book_guard → 200 if last successful evaluation < 60s ago.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 |
|---|---|---|
| Stub | 100% deterministic on fixtures. | Documented threshold met for the full window. |
Promote to Limited live
| Gate | How measured | Threshold |
|---|---|---|
| Shadow | 7 days; rejection rate < 0.5% on healthy feeds. | Documented threshold met for the full window. |
| Enforced | Risk Lead sign-off. | Documented threshold met for the full window. |
Promote to General live
| Gate | How measured | Threshold |
|---|---|---|
| Owner sign-off | Bot owner reviews 14 days of advisory data. | No P1 incidents attributable to this bot. |
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 |