Skip to main content

Model Layer

Location: src/data/models/ The model layer is a lightweight, dependency-free schema layer that gives every feature a shared vocabulary for data shapes, source provenance, and quality metadata. It has zero external dependencies. Before this layer, data shapes were embedded inline in individual API handlers. Features that build on the same on-chain data (Dune baselines, live wallet events, AI narratives) now share types without re-inventing them.

Model inventory

FileModelsUsed by
source-metadata.jsSourceMetadata, DataQualityAll models
historical-baseline.jsHistoricalWalletBaseline, HistoricalTokenFlow, HistoricalCounterparty, HistoricalProtocolUsageWhale Watcher, signal engine, narratives
live-events.jsLiveWalletEventWhale Watcher, signal engine
signals.jsWalletSignalWhale Watcher, signal engine, narratives
narrative.jsNarrativeInput, NarrativeCardNarrative engine
holder-wall.jsHolderWallTileHolder Wall treemap surfaces
market-radar.jsMarketRadarResultapi/market-radar.js
quantum-exposure.jsQuantumExposureScoreQuantum Intelligence

SourceMetadata — the provenance backbone

Every major model carries at least one SourceMetadata entry. This lets the UI display data-freshness badges and lets consumers decide how much to trust a value.
import { makeSourceMetadata } from './src/data/models/source-metadata.js';

const src = makeSourceMetadata({
  sourceId:   'dune-whale-activity',
  sourceType: 'dune_cached',
  queryId:    'q_whale_activity_30d',
  fetchedAt:  '2026-05-01T06:00:00.000Z',
  isCached:   true,
  cacheAgeSeconds: 3600,
});
Valid sourceType values: dune_scheduled · dune_cached · alchemy · etherscan · coingecko · the_graph · bigquery · ai_narrative · computed · mock Rules:
  • Dune data is scheduled/cached, never live. Use dune_scheduled for results from a Vercel Cron or Dune-scheduled run; use dune_cached when served from Redis.
  • bigquery is a reserved future source type. The MVP avoids a GCP dependency.

DataQuality

Structured quality annotation attached to every model. Use mergeDataQuality() when combining data from multiple sources.
import { makeDataQuality, mergeDataQuality } from './src/data/models/source-metadata.js';

const dq = makeDataQuality({
  isEstimated: true,
  confidence: 'medium',
  warnings: ['Price data estimated from nearest CoinGecko snapshot.'],
  sources: [duneSource],
});

// Merge two quality objects — confidence resolves to the lower of the two:
const merged = mergeDataQuality([duneQuality, alchemyQuality]);

WalletSignal

A WalletSignal is a deterministic, sourced fact about wallet behaviour. It is the atomic unit consumed by the narrative engine and the Whale Watcher workspace.
import { makeWalletSignal, makeSignalId } from './src/data/models/signals.js';

const signal = makeWalletSignal({
  walletAddress: '0x4838b106fce9647bdf1e7877bf73ce8b0bad5f97',
  chain:         'ethereum',
  signalType:    'accumulation',
  strength:      'high',
  confidence:    'high',
  windowStart:   '2026-04-01T00:00:00.000Z',
  windowEnd:     '2026-05-01T00:00:00.000Z',
  evidence:      { netInflowUSD: 40_200_000, primaryToken: 'ETH' },
  caveats:       ['Signal derived from Dune historical data (30-day window, cached).'],
  sources:       [duneCachedSource],
});
makeSignalId(walletAddress, signalType, windowStart) produces a stable 8-hex-char ID using djb2 — browser-safe, no crypto dependency.

NarrativeInput / NarrativeCard

NarrativeInput is what you pass to api/analyze.js. NarrativeCard is the structured output you store and render. makeNarrativeCard enforces: headline ≤ 120 chars, keyPoints ≤ 5 entries, caveats non-empty.

HolderWallTile

HolderWallTile represents one holder in the treemap. It deliberately separates:
  • historicalSource — always Dune scheduled/cached data
  • liveSource — optional live delta from supported providers; null when unavailable
This separation lets the UI show “balance as of [Dune run]” and ”+$14M (provider delta)” independently. HolderWallTile does not contain market-level statistics. Use MarketRadarResult for token/pool market lookup.

MarketRadarResult

Stable Seer data only. No holder analytics. No walletAddress field. The following fields are intentionally absent: holderCount, topHolders, holderType.

QuantumExposureScore

A 0–100 composite score. Key constraints enforced by the factory:
  • score is clamped to [0, 100]
  • riskBand is derived from score via scoreToRiskBand()
  • caveats defaults to REQUIRED_CAVEATS (4 entries) — never empty
  • disclaimer defaults to STANDARD_DISCLAIMER

Fixtures

src/data/models/fixtures/ contains six ready-to-use objects for tests, Storybook stories, and API fallbacks:
FileScenario
whale-accumulation.jsHigh-confidence whale accumulation — baseline + signal + narrative card
normal-activity.jsRegular DeFi user — medium confidence with estimated prices
low-confidence.jsTruncated Dune result — low confidence, multiple warnings
holder-wall-tile.jsETH whale tile — dual Dune + Alchemy source
market-radar-result.jsTrending PEPE — rising volume, no holder analytics
quantum-exposure-score.jsMedium-exposure wallet — full breakdown + caveats

Provenance UI

Use src/components/DataSourceBadge.jsx for compact source footnotes on cards, metric rows, and preview metadata. It accepts either model-style source / dataQuality objects or explicit props:
<DataSourceBadge
  provider="dune_cached"
  sourceType="dune_cached"
  confidence={signal.confidence}
  freshness="Scheduled/cache, not live"
  method="Deterministic wallet signal from historical Dune rows."
  warnings={signal.caveats}
/>
Query IDs are hidden by default. Pass a safe queryName for user-facing context and only set showQueryId when the ID is intentionally public.

Hard constraints

These are enforced by the test suite:
  1. Dune sources must have sourceType of dune_scheduled or dune_cached — never a live type.
  2. WalletSignal.caveats must be non-empty.
  3. NarrativeCard.caveats must be non-empty.
  4. QuantumExposureScore.caveats must be non-empty; disclaimer must be non-empty.
  5. MarketRadarResult must not contain holder analytics fields.
  6. HolderWallTile.historicalSource must be a Dune source type.
  7. QuantumExposureScore.score is clamped to [0, 100].
  8. No external dependencies — zero node_modules imports.

Running tests

npm test          # runs all *.test.mjs files including models-smoke.test.mjs
npm run check     # lint + test + build + security audit