Root cause and fix for a silent bug in crates/ingest/src/polymarket.rs that caused discover_active_market to return Ok(None) on every single poll, keeping the emit market-cache empty and producing zero emitted events.
Symptoms
discover_active_marketreturnedOk(None)on every poll.- Emit market-cache remained empty.
- Zero emitted events — no signal fired, no NATS publishes.
- No panic or error log: the bug was a silent guard failure.
Root Cause
try_build_market validated Polymarket CLOB token IDs using token.parse::<u128>().
Polymarket token IDs are 256-bit (77-decimal-digit) values. The example live token ID at the time:
73086175040291113731614083457106176109233409549997382340708254660069213796683
That value overflows u128 (max ~3.4 × 10³⁸, vs a 77-digit decimal which can reach ~10⁷⁶). The parse fails on every real market. The validation guard therefore rejected every valid token, so try_build_market returned None for every market response, and discover_active_market propagated Ok(None) indefinitely.
Design trap
Polymarket CLOB token IDs look like large integers but are opaque 256-bit decimal strings. They must never be parsed into any fixed-width integer type (
u128,u64,i64, etc.). Always treat them as digit strings.
A secondary issue: the GammaMarket struct had two serde field-name mismatches that silently produced default/zero values for order sizing fields:
| Struct field | Wrong serde name | Correct serde name | Live value |
|---|---|---|---|
min_size | minSize | orderMinSize | 5 |
tick_size | tickSize | orderPriceMinTickSize | 0.01 |
Fix
Commit 7bfe3a0, branch main.
1. Token ID validation — u256-safe digit-string check
Replaced token.parse::<u128>() with:
!s.is_empty() && s.bytes().all(|b| b.is_ascii_digit())This validates that the token ID is a non-empty decimal string without imposing any width limit.
2. Serde field-name corrections on GammaMarket
// before
#[serde(rename = "minSize")]
pub min_size: f64,
#[serde(rename = "tickSize")]
pub tick_size: f64,
// after
#[serde(rename = "orderMinSize")]
pub min_size: f64,
#[serde(rename = "orderPriceMinTickSize")]
pub tick_size: f64,3. Regression tests added
Three tests cover the fix:
accepts_u256_token_id— passes the 77-digit live token ID; must returnSome(market).invalid_token_id_empty_returns_none— empty string; must returnNone.gamma_market_deserializes_order_field_names— deserializes a minimal Gamma JSON payload withorderMinSize/orderPriceMinTickSize; assertsmin_size == 5.0andtick_size == 0.01.
Lesson
For Agents
Polymarket CLOB token IDs are opaque 256-bit decimal strings — field names
clobTokenIdsin CLOB API, nested in Gammatokens[].token_id. Never parse into any fixed-width integer. The canonical safe check:!s.is_empty() && s.bytes().all(|b| b.is_ascii_digit()). Anyu128/u64/i64parse on a Polymarket token ID is wrong and will silently reject all real markets.
When deserializing GammaMarket (Gamma REST API /markets), the relevant field names are:
| Concept | Gamma JSON key |
|---|---|
| Minimum order size | orderMinSize |
| Minimum tick size | orderPriceMinTickSize |
| CLOB token IDs | clobTokenIds (array of strings) |
| Condition ID | conditionId |
These differ from the shorter camelCase names (minSize, tickSize) that appear in some Polymarket documentation and SDK examples.
Affected File
crates/ingest/src/polymarket.rs — try_build_market, GammaMarket struct.
How It Was Found
Root cause was found via a live Gamma probe (no VM logs needed): emit fired 0 times in ~2 days of dry-validate. The probe deserialized a real Gamma /markets response, which exposed both the u128 overflow on the 77-digit token id and the wrong serde field names. The condition_id-absent hypothesis was REFUTED — Gamma supplies a real 0x conditionId. The tickSize mistake was a 10× error: Gamma’s real tick is 0.01 (orderPriceMinTickSize) but the wrong tickSize rename defaulted to 0.001.
Separate Issue — observe went ~5h+ stale
Distinct from the emit bug: observe also went ~5h+ stale (latest window lagging), a separate feeds issue, likely the shared Alchemy creds drain (@polymarket #269). This MUST also clear — if observe has no live windows, emit has nothing to fire on even after the token fix. Pending @deploy.
Related
- crypto-shortterm-emit-prelive-pass2 — previous pre-live pass that added
token_down u128 validation(partial fix; this commit fully resolves the pattern) - crypto-shortterm-phase1-emit — Phase-1 emitter build;
polymarket.rsGamma API integration - crypto-shortterm-emit-guardrails — money-safety guardrails; token/condition_id validation guards (the original
u128validate this fix corrects) - crypto-shortterm-phase1-execution-decision — the staged $100 latency test this fix unblocks
- crypto-shortterm-repo-rename-pmv2-rearch —
7bfe3a0is noworigin/mainof the renamed repo - crypto-shortterm — project index