Task B1 on autotrade-phase2-firstmover: two fixes in sources.rs that make signal_id byte-identical regardless of which producer (cron or future on-chain watcher) mints it.

Problem

The engine deduplicates trade signals via ON CONFLICT (dedup_key) in the DB. dedup_key is derived from signal_id, which is minted by cohort_signal_id. For cron and watcher to deduplicate the same logical entry, they must produce the same signal_id from the same underlying position data.

Two bugs broke that parity:

1. No lowercasing before orders::dedup_key

cohort_signal_id passed origin_ref (wallet address), condition_id, and instance verbatim to orders::dedup_key. On-chain addresses are always lowercase hex. The cron side can carry checksummed (mixed-case) wallet addresses from the scored cohort. The same wallet produces two different strings: 0xAbCd… vs 0xabcd…, yielding different signal_ids for the same trade intent.

2. instance used event_slug (unstable Option<String>)

enter_signal passed p.event_slug.unwrap_or(condition_id) as the instance argument. event_slug is an Option<String>:

  • If the cron populates it, the instance includes the slug.
  • If a future watcher path omits it (no slug available at detection time), the instance falls back to condition_id.

These two paths produce different instance values → different signal_ids for the same position.

Fixes

Fix 1 — lowercase canonicalization in cohort_signal_id

All three inputs are lowercased before being passed to orders::dedup_key:

orders::dedup_key(&origin_ref.to_lowercase(), action, &condition_id.to_lowercase(), &instance.to_lowercase())

On-chain addresses (always lowercase hex) and cron checksummed addresses now collapse to the same key.

Fix 2 — instance = condition_id in enter_signal

// before
instance: p.event_slug.clone().unwrap_or_else(|| p.condition_id.clone())
 
// after
instance: p.condition_id.clone()

condition_id is always present and matches the on-chain identifier. It is stable across cron and watcher paths. The event_slug was a volatile fallback that broke cross-producer parity and introduced an Option into a field that should be deterministic.

One breaking test update required

enter_signal_freezes_reference_price_and_fills_fields previously passed "evt" as the expected instance. Updated to "0xcond" to match the new instance = condition_id behavior.

Tests

TestWhat it verifies
cohort_signal_id_is_canonical_lowercase_instance_conditionMixed-case origin_ref, condition_id, instance inputs → fully lowercase signal_id output
watcher_and_cron_signal_ids_byte_match_for_same_entryenter_signal (cron path) and direct cohort_signal_id with lowercase on-chain inputs produce byte-identical strings

Gate: all tests passing, gate=0.

For Agents

The byte-identity of signal_id across producers is the ONLY mechanism that prevents duplicate live orders when both cron and watcher detect the same cohort entry. The DB ON CONFLICT (dedup_key) DO NOTHING is the backstop, but correct dedup requires identical keys. Any future producer must: (1) lowercase origin_ref (wallet), condition_id, and instance before calling cohort_signal_id, and (2) use condition_id — not event_slug or any other Optional field — as the instance discriminator.

Key Design Decision

event_slug was removed from the instance computation because:

  1. It is Option<String> — absent on the watcher detection path (no CLOB context at detection time).
  2. Even when present, it can vary (e.g., event slugs can be updated or aliased).
  3. condition_id is the authoritative on-chain market identifier and is always present on both cron and watcher paths.

Branch / File

  • Branch: autotrade-phase2-firstmover
  • File: crates/polymarket-fetch/src/pmv2/autotrade/sources.rs