> ## Documentation Index
> Fetch the complete documentation index at: https://docs.walletwall.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Signal Engine

> Deterministic fact layer between raw on-chain data and AI narratives. Produces WalletSignal[] from Dune baselines and live wallet events.

# Signal Engine

The signal engine is the fact layer between raw on-chain data and AI narratives. It has zero external dependencies.

```mermaid theme={null}
flowchart LR
    accTitle: Signal engine data flow
    accDescr: A Dune scheduled or cached baseline and live wallet events from Alchemy or Etherscan both feed detectSignals, which produces an array of WalletSignal objects consumed by the AI narrative layer.
    dune["Dune scheduled/cached baseline"]:::input
    live["Live wallet events<br/>(Alchemy/Etherscan)"]:::input
    detect["detectSignals()"]:::process
    signals["WalletSignal[]"]:::process
    narrative["AI narrative"]:::output
    dune --> detect
    live --> detect
    detect --> signals
    signals --> narrative
    classDef input fill:#FAFAF0,stroke:#B87333,color:#2B2118,stroke-width:1.5px;
    classDef process fill:#B84923,stroke:#6B2412,color:#FFF7E8,stroke-width:1.5px;
    classDef output fill:#9AAB89,stroke:#526246,color:#172014,stroke-width:1.5px;
```

*The Dune baseline and live wallet events feed `detectSignals()` independently — either can be absent (see [Missing-data behaviour](#missing-data-behaviour)) — and only its `WalletSignal[]` output, never the raw baseline or events, is passed on to the AI narrative layer.*

AI narratives must consume these deterministic signals — they must not invent facts. Every number, address, and time window in a narrative must trace back to a `WalletSignal.evidence` field and its `sources` provenance chain.

## Modules

The signal engine is organised into three conceptual modules, all available from its public entry point. Each is documented in full further down this page.

<AccordionGroup>
  <Accordion title="Detection engine">
    `detectSignals()`, individual detectors, `buildBaselineStats()`, `DEFAULT_OPTIONS`
  </Accordion>

  <Accordion title="Calculations">
    Pure math helpers: `usualDailyVolumeUSD`, `baselineDeviation`, `deriveConfidence`, etc.
  </Accordion>

  <Accordion title="Known labels">
    CEX/bridge label patterns: `isCexLabel`, `isBridgeLabel`, `normaliseCexName`
  </Accordion>
</AccordionGroup>

## API

```js theme={null}
// detectSignals is the signal engine's public entry point
const signals = detectSignals({
  walletAddress: '0x4838…5f97',
  chain:         'ethereum',
  baseline:      historicalWalletBaseline,   // from Dune — may be null
  events:        liveWalletEvents,            // from Alchemy/Etherscan
  windowStart:   '2026-04-01T00:00:00.000Z',
  windowEnd:     '2026-05-01T00:00:00.000Z',
}, options);
// returns WalletSignal[]
```

All outputs conform to the `WalletSignal` shape from the model layer.

## 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

```js theme={null}
export const DEFAULT_OPTIONS = {
  largeMovThresholdMultiplier:  5,     // event ≥ 5× usualDailyVolumeUSD
  unusualVolumeMultiplier:      3,     // period volume ≥ 3× expected
  unusualTxMultiplier:          3,     // period tx count ≥ 3× expected
  minSignalValueUSD:            10_000,
  newCounterpartyMinUSD:        50_000,
  accumulationNetInflowRatio:   0.6,
  distributionNetOutflowRatio:  0.6,
  protocolRotationVolumeShare:  0.2,
};
```

All thresholds can be overridden by passing a `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'`             |

Rules are evaluated in order; the first match wins. Use `deriveConfidence(baseline, events)` directly when building custom detectors.

## Deterministic calculations

All math lives in the signal engine's calculation helpers:

| 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 on `counterpartyLabel`, not address lookup. This keeps the engine decoupled from the API layer's `PROTOCOL_MAP` and avoids hardcoding addresses that change after upgrades.

```js theme={null}
isCexLabel('Coinbase Prime')  // → true
isBridgeLabel('Hop Protocol') // → true
normaliseCexName('Binance 14') // → 'Binance'
```

Known patterns live in `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 return `null` and are filtered out. Only label-based detectors (bridge, cex) can still fire, at `confidence: 'low'`.
* Partial baseline (`isPartial: true`) — all emitted signals cap at `confidence: '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 an `engineSource` entry (`sourceType: 'computed'`).
* `dataQuality.sources` — same list, inside the DataQuality object.

The `engineSource` entry has `sourceId: 'signal-engine-v1'` so consumers can filter engine-derived fields from raw data fields.

## How the narrative engine uses signals

1. Pass `signals[]` as the primary context to `NarrativeInput.signals` — not the raw baseline or events.
2. Every narrative claim must trace to a signal's `evidence` object. If a number can't be backed by evidence, it must not appear in the narrative.
3. Respect `signal.confidence`. Narratives for `confidence: 'low'` signals must use hedged language ("limited data suggests…") and explicitly surface the relevant caveats.
4. Never override evidence fields. The AI may phrase and contextualise the evidence but must not change the numbers.
5. Attach `signal.sources` to the `NarrativeCard.sources` array so the UI can show data-freshness badges for every claim.

## Related: behavioral exposure signals

A separate behavioral-signals engine provides a set of behavioral exposure heuristics consumed by the Vault Readiness Card. These are not part of the `detectSignals()` pipeline and are not `WalletSignal` objects. See [Quantum Intelligence](/features/quantum-intelligence) for the full specification.

## Running tests

```bash theme={null}
node --test    # runs all suites — 101 tests total, including 35 signal-engine tests
npm run check  # lint + test + build + audit
```

## Related pages

<Columns cols={2}>
  <Card title="Model Layer" icon="cubes" href="/architecture/model-layer">
    The `WalletSignal` shape and shared schema conventions the signal engine outputs conform to.
  </Card>

  <Card title="Quantum Intelligence" icon="atom" href="/features/quantum-intelligence">
    The separate behavioral-signals engine and Vault Readiness Card specification.
  </Card>

  <Card title="Whale Watcher" icon="binoculars" href="/features/whale-watcher">
    A consumer of `WalletSignal[]` alongside the signal engine and narrative layer.
  </Card>
</Columns>
