Commit bc38734 implemented 13 money-safety guardrails in emit.rs (the live order-placing binary). Covers critical data-model fixes, runtime safety gates, and data-model changes to types::BookTop and observe::Observation.
Blockers Fixed
condition_id Must Have 0x-Prefix
The Gamma API can return a null conditionId field. Previously the code silently fell back to the market slug when the field was absent, which is wrong — a slug is not a valid CLOB condition ID. The fix: require the 0x-prefix explicitly; a missing or non-0x conditionId is now a hard error that skips the window.
YES Token Selected by Outcome Label, Not Array Index
Previously the YES token was assumed to be clobTokenIds[0]. The Gamma API returns outcomes as a JSON string (not a parsed array) that is parallel to clobTokenIds. The fix parses the outcomes string, does a case-insensitive match for "up" or "yes", and uses the corresponding index into clobTokenIds. Additionally, token_id is now validated as a parseable u128 before use.
fired_windows and total_fires Hydrated from DB on Startup
Both state variables are now populated by running SELECT window_id FROM emitted_signals on startup. Before this fix, a process restart would silently allow re-firing windows that had already been emitted, and total_fires would reset to zero. The fix makes the emitter restart-safe.
High-Severity Fixes
Book Staleness Gate
A new guard skips emission if the Polymarket book observation is more than EMIT_BOOK_MAX_STALE_SECS (5 seconds) old. This required adding pm_book_as_of: Option<DateTime<Utc>> to observe::Observation, which is threaded from BookTop.as_of.
Zero / NaN Ask Guard
An explicit check !pm_ask_f64.is_finite() || pm_ask_f64 <= 0.0 now guards the ask price before any arithmetic. Previously a zero or NaN ask could silently corrupt max_price.
NATS Publish Error Now Skips Before Recording
The previous code path would call fired_windows.insert(window_id) (and write to the DB) regardless of whether the NATS publish succeeded. The fix uses continue on a publish error, so a window is only marked as fired after a confirmed successful publish.
Medium-Severity Fixes
valid_until_ts Freshness Check
The emitter now verifies that valid_until_ts >= now + 10s before firing. Windows with stale or already-expired valid_until_ts are skipped rather than emitting an immediately-invalid directive.
Gamma HTTP Client Timeout
The reqwest::Client used for Gamma API calls now has a 5-second timeout set via reqwest::Client::builder().timeout(Duration::from_secs(5)). Without this, a hung Gamma request could block the emit loop indefinitely.
Calibration p Clamped to [0, 1]
The calibration bin index calculation now clamps p to [0.0, 1.0] before computing the bin. An out-of-range p value previously could cause an out-of-bounds index panic.
Market Window-Match via open_ts
The market-to-observation match now verifies that market.open_ts.timestamp() equals obs.window_id. BookTop carries window_id: Option<String> (set by poll_book); this is threaded into Observation as pm_book_window_id: Option<String>.
Low-Severity Fixes
calibration.json Loaded via include_str!
calibration.json is now embedded at compile time via include_str!("../../calibration.json"), eliminating the CWD dependency. The CAL_PATH environment variable still overrides this when set.
emitted_signals INSERT Column Binding Bug
The INSERT was binding yes_token_id to both the token_id and yes_token_id columns (double-binding bug). token_id and size are now obsolete — they are dropped by migration 0005.
Binance Staleness Tracking
last_binance changed from Option<f64> to Option<(f64, DateTime<Utc>)> to carry the timestamp alongside the value. This enables a 30-second Binance staleness gate: if the last Binance tick is >30s old, the window is skipped.
Data Model Changes
Structural Changes — Read Before Touching BookTop or Observation
These fields were added in this commit. Code that constructs
BookToporObservationmanually (e.g., in tests) must supply them.
types::BookTop
New field: window_id: Option<String>
Set by poll_book to match the currently active window. Enables the market/observation window-match guard in the emitter.
observe::Observation
Three new fields added:
| Field | Type | Purpose |
|---|---|---|
pm_book_as_of | Option<DateTime<Utc>> | Timestamp of the book snapshot; used for the 5s book-staleness gate |
pm_book_window_id | Option<String> | Threaded from BookTop.window_id; used for the market window-match guard |
last_binance_ts | Option<DateTime<Utc>> | Timestamp of last Binance tick; used for the 30s Binance staleness gate |
Migration 0005
migrations/0005_emitted_signals_v3.sql drops the now-obsolete token_id and size columns from crypto_shortterm.emitted_signals. These were removed from the Directive struct in the schema alignment at commit c12c830 (see pmv2 Directive Schema Alignment (c12c830)) but not yet dropped from the table.
Files Changed
| File | Change |
|---|---|
crates/types/src/lib.rs | BookTop gains pub window_id: Option<String> |
crates/ingest/src/polymarket.rs | Outcome-label token selection, condition_id guard, HTTP timeout, poll_book stamps window_id onto BookTop, 3 new unit tests |
crates/observe/src/lib.rs | Observation gains pm_book_as_of, pm_book_window_id, last_binance_ts; EngineState::last_binance is Option<(f64, DateTime<Utc>)>; 7 new unit tests |
crates/observe/src/calibration.rs | from_json constructor, calibrate clamps p |
crates/observe/src/bin/emit.rs | Full rewrite with all guards |
migrations/0005_emitted_signals_v3.sql | Drops token_id and size columns |
Test Results
All passing after changes: 15 ingest tests (3 ignored/live-only), 14 fairvalue tests, 3 market_state tests, all observe tests including 7 new guard unit tests. Build, clippy -D warnings, and fmt clean.
BLOCKER-3 is the most dangerous restart trap
Before these changes, every process restart would re-fire the entire history of already-emitted windows. The DB hydration on startup is the fix — do not remove it.
BLOCKER-3 corollary — TRUNCATE alone won't clear the cap
The hydration is one-shot at boot. If you need to reset
total_fires(e.g. clearing a Pattern A → B cutover residue where pre-cutover rows are pinning the new emitter againstEMIT_MAX_FIRES), TRUNCATE-ingcrypto_shortterm.emitted_signalsis necessary but NOT sufficient — the in-memory counter keeps its boot value, and the emitter keeps loggingEMIT_MAX_FIRES reached, signal emission pausedat ~1Hz. You must bounce the container (sudo systemctl restart apps-shortterm-crypto-algo-emit.service) so the next boot re-hydrates against the now-empty table. The boot log lineINFO emit: guardrail hydrated from DB hydrated=0confirms the reset. Surfaced 2026-06-25 — see crypto-emit-max-fires-cap-clear-2026-06-25 for the full session and the corresponding [[gcp-terraform-ansible-gotchas|gotcha #38]].
DB insert vs NATS failure semantics
DB insert failure after successful NATS publish = safe (NATS
Msg-Iddedup prevents double-execution on retry). NATS publish failure = the window must NOT be marked fired —continueenforces this.
Guard Summary Table
| # | Severity | Guard | Skip Condition |
|---|---|---|---|
| 1 | Blocker | condition_id prefix | Missing or non-0x conditionId from Gamma |
| 2 | Blocker | YES token by label | No outcome matching “up”/“yes” in Gamma outcomes array |
| 3 | Blocker | fired_windows hydration | (startup — prevents re-fire after restart) |
| 4 | High | Book staleness | pm_book_as_of > EMIT_BOOK_MAX_STALE_SECS (5s) |
| 5 | High | Zero/NaN ask | pm_ask not finite or ⇐ 0 |
| 6 | High | NATS error skips before record | Publish failure → continue before fired_windows.insert |
| 7 | Medium | valid_until freshness | valid_until_ts < now + 10s |
| 8 | Medium | Gamma HTTP timeout | 5s timeout on Gamma API client |
| 9 | Medium | Calibration p clamp | p outside [0.0, 1.0] |
| 10 | Medium | Market window-match | market.open_ts != obs.window_id |
| 11 | Low | include_str! for calibration | (no CWD dependency; CAL_PATH still overrides) |
| 12 | Low | INSERT column binding | Double-bind of yes_token_id fixed; migration 0005 drops stale columns |
| 13 | Low | Binance staleness | Last Binance tick >30s old |
Related
- crypto-shortterm-phase1-emit — phase-1 emitter architecture this commit hardens
- crypto-shortterm-data-sources — Gamma API, CLOB, Binance feed details
- crypto-shortterm — project overview
- crypto-emit-max-fires-cap-clear-2026-06-25 — operator-side case study: Pattern A → B cutover residue + in-process hydration left the emitter spinning paused for ~22h; full diagnostic + fix procedure + NATS
/jszrecipe - gcp-terraform-ansible-gotchas — gotcha #38: in-process DB-hydrated counters only re-read on boot (the operational corollary to BLOCKER-3)