Signal Engine
Location:src/data/signals/
The signal engine is the fact layer between raw on-chain data and AI narratives. It has zero external dependencies.
WalletSignal.evidence field and its sources provenance chain.
Files
| File | Exports |
|---|---|
src/data/signals/engine.js | detectSignals(), individual detectors, buildBaselineStats(), DEFAULT_OPTIONS |
src/data/signals/calculations.js | Pure math helpers: usualDailyVolumeUSD, baselineDeviation, deriveConfidence, etc. |
src/data/signals/known-labels.js | CEX/bridge label patterns: isCexLabel, isBridgeLabel, normaliseCexName |
src/data/signals/index.js | Re-exports all three modules |
API
WalletSignal shape from src/data/models/signals.js.
Signal taxonomy
| Signal type | Trigger condition | Requires baseline? |
|---|---|---|
accumulation | Net token inflows ≥ 60% of total baseline volume | Yes |
distribution | Net token outflows ≥ 60% of total baseline volume | Yes |
bridge | Live eventType === 'bridge' OR bridge-labelled counterparty OR baseline bridge counterparty | No (low confidence without) |
cex_deposit | CEX-labelled counterparty + net outflow direction | No (low confidence without) |
cex_withdrawal | CEX-labelled counterparty + net inflow direction | No (low confidence without) |
unusual_activity | Period volume ≥ 3× expected OR period tx count ≥ 3× expected | Yes |
new_counterparty | Event counterparty not in baseline top list, value ≥ $50k | Yes |
protocol_rotation | New protocol handles ≥ 20% of live event volume | Yes |
large_move_vs_baseline | Single event ≥ 5× usual daily volume | Yes |
Default thresholds
Partial<SignalEngineOptions> as the second argument to detectSignals().
Confidence rules
Confidence is derived from data completeness, not signal magnitude. A large move detected from a partial dataset is medium, not high.| Condition | Resulting confidence |
|---|---|
baseline === null | 'low' |
baseline.dataQuality.confidence === 'low' | 'low' |
baseline.dataQuality.isPartial === true | max 'medium' |
baseline.dataQuality.isEstimated === true | max 'medium' |
baseline.totalVolumeEstimated === true | max 'medium' |
Any event.valueUSD === null | max 'medium' |
baseline.dataQuality.confidence === 'medium' | 'medium' |
| All data complete and exact | 'high' |
deriveConfidence(baseline, events) from calculations.js directly when building custom detectors.
Deterministic calculations
All math is insrc/data/signals/calculations.js:
| Function | Description |
|---|---|
baselineWindowDays(baseline) | Days in the Dune query window (min 1) |
usualDailyVolumeUSD(baseline) | totalVolumeUSD / days — average daily volume |
usualDailyTxCount(baseline) | txCount / days — average daily tx count |
baselineDeviation(valueUSD, baseline) | valueUSD / usualDailyVolumeUSD — how many daily-avgs this event represents |
txCountDeviation(count, baseline) | Tx count ratio vs baseline daily average |
sumEventValues(events) | Sum of non-null valueUSD; null if all unknown |
deriveConfidence(baseline, events) | See confidence rules above |
strengthFromDeviation(deviation, medThresh, highThresh) | Maps a ratio to 'low'/'medium'/'high' |
CEX and bridge detection
CEX and bridge classification uses label matching oncounterpartyLabel, not address lookup. This keeps the engine decoupled from the API layer’s PROTOCOL_MAP and avoids hardcoding addresses that change after upgrades.
CEX_LABEL_PATTERNS and BRIDGE_LABEL_PATTERNS. Add new entries there when a new exchange or bridge needs to be covered — no engine logic changes required.
Missing-data behaviour
The engine never throws on missing or partial inputs. It degrades gracefully:baseline === null— detectors that require baseline returnnulland are filtered out. Only label-based detectors (bridge, cex) can still fire, atconfidence: 'low'.- Partial baseline (
isPartial: true) — all emitted signals cap atconfidence: 'medium'and carry a caveat noting the incomplete dataset. event.valueUSD === null— the event is included in tx-count comparisons but excluded from volume sums. If this causes a relevant deviation, the signal is emitted at max'medium'confidence.- Empty event array — only baseline-derived signals are considered.
- Empty event array AND
baseline === null—detectSignals()returns[].
Source provenance
Every signal includes:sources: SourceMetadata[]— the baseline source, each contributing event source, and anengineSourceentry (sourceType: 'computed').dataQuality.sources— same list, inside the DataQuality object.
engineSource entry has sourceId: 'signal-engine-v1' so consumers can filter engine-derived fields from raw data fields.
How the narrative engine uses signals
- Pass
signals[]as the primary context toNarrativeInput.signals— not the raw baseline or events. - Every narrative claim must trace to a signal’s
evidenceobject. If a number can’t be backed by evidence, it must not appear in the narrative. - Respect
signal.confidence. Narratives forconfidence: 'low'signals must use hedged language (“limited data suggests…”) and explicitly surface the relevant caveats. - Never override evidence fields. The AI may phrase and contextualise the evidence but must not change the numbers.
- Attach
signal.sourcesto theNarrativeCard.sourcesarray so the UI can show data-freshness badges for every claim.
Related: behavioral exposure signals
TheadversarialSignals namespace (src/lib/adversarial-heuristics.js) provides a separate set of behavioral exposure heuristics consumed by QuantumExposureCard. These are not part of the detectSignals() pipeline and are not WalletSignal objects. See Quantum Intelligence for the full specification.

