Skip to main content

Wallet Evidence Model

The Wallet Evidence Model adds no new provider, no Dune execution, no write action, and no wallet connection. It is a pure, synchronous transform over data the existing /api/wallet providers already compute — it fetches nothing on its own.
walletEvidence is a provenance-aware layer on the /api/wallet response. It normalizes Etherscan transaction/transfer/balance data, Alchemy token balances, DeFiLlama contract pricing, CoinGecko ETH pricing, and cache-only Dune DEX-trade enrichment into source-aware claims: what is claimed, which source(s) back it, what its status is, what scope it covers, what was excluded, what fallback was used, and how confident the UI should be. Its central job is distinguishing zero data from unavailable or degraded data — for example, a wallet that genuinely holds no ERC-20 tokens must never look identical to a wallet whose token balances simply failed to load. See Data Quality and Observability for the underlying observability / dataQuality fields this model builds on, and the Model Layer for the shared schema conventions it follows.

Wallet scan lifecycle

Every step below is read-only. There is no wallet connection, no signature request, and no transaction is ever executed on this path. The Wallet Evidence Model never calls a provider itself — it transforms data the route handler already fetched. If a provider is not configured or fails for this request, the affected claim reports a non-fresh status and an explicit caveat instead of silently reading as “no data.”

Evidence schema

WALLET_EVIDENCE carries exactly five CLAIMs today — pricedPortfolioValue, tokenHoldings, activity, protocolAffinity, and movements — plus one SOURCE_HEALTH entry per provider (Etherscan, Alchemy, DeFiLlama, CoinGecko, Dune). A CLAIM may name zero or more EXCLUSIONs (what it deliberately leaves out) and FALLBACKs (what degraded path it used); every excluded or fallback reason also rolls up into the top-level warnings list.

Status vocabulary

Every claim and every sourceHealth entry uses one shared status vocabulary, so “throttled” or “fresh” means the same thing everywhere in the object:

Claims

Not built for the mock/demo provider — synthetic data has no real provenance to report, so walletEvidence is simply absent there. Every consumer treats it as optional and falls back to the legacy fields it extends.

Canonical value model — WalletWallEvidence

The server walletEvidence claims above are provenance-rich but verbose. The canonical client model (buildWalletWallEvidence) normalizes them — plus the legacy payload fields — into one explicit, reusable object shared by Whale Watcher, Coinstellation, Quantum, Stable Seer, and future Vault readiness. It is a pure transform: no fetch, no Dune, no provider, no wallet connection.

Value-state semantics — unknown ≠ zero

Every value is a ValueState, a discriminated union with exactly five states. Only two may render as a dollar figure; the other three are gaps that must never render as “$0”: renderValueStateUsd(state, fmtUSD) is the single choke point: it returns a dollar string only for priced/confirmed_zero, so a component can never turn an unavailable value into $0. This is the structural fix for “Priced portfolio value: $0” when token/stablecoin data was merely unavailable.

Try it — evidence state explorer

The tables above are the contract. This explorer lets you vary a bounded, fictional scenario and watch which claim status, confidence, and value state fall out of it — in particular, why a failed provider never produces a confident $0. It accepts no address, calls no provider, and computes everything locally.

Separate concepts

Native ETH, stablecoins, other tokens, total, and transfer flow are separate evidence:
  • Native ETH value is priced on its own and, when tokens are unavailable, is surfaced as “ETH-only priced value” — never as an identity-wide portfolio.
  • Total priced value is priced/confirmed_zero only when every relevant leg was checked; otherwise it is unavailable/partial. Unavailable token value never collapses into a $0 total.
  • Stablecoin exposure is first-class (stablecoins / StablecoinExposureEvidence), classified against the curated stablecoin registry (contract-authoritative, symbol fallback). Unavailable stablecoin data is explicit; stablecoins are never spam by default.
  • Transfer flow distinguishes a sampled latest-N net flow from a complete lifetime flow, so a truncated sample never reads as “Net Flow”.

Stablecoin classification — one canonical registry

The curated stablecoin registry is the app-level source of truth for “is this token a stablecoin.” It exposes two complementary layers:
  • STABLECOIN_REGISTRY — the curated majors that carry a known Ethereum contract + on-chain decimals + peg (USDC, USDT, DAI, FDUSD, PYUSD, USDe, sUSDe, LUSD, FRAX, crvUSD). Contract matching is authoritative: a mislabeled symbol cannot defeat a known contract, and a scam token cannot borrow a real one.
  • STABLECOIN_SYMBOLS — the broader symbol allow-list used when a holding has no contract to match on (the display/evidence ecosystem, including non-USD fiat pegs like EURC/EURS that WalletWall already surfaces as stablecoins).
The display-layer token-classification module re-exports STABLECOIN_SYMBOLS / isStablecoinSymbol from the registry — a thin compatibility wrapper, so its long-standing display-layer callers share one source of truth instead of a second hand-maintained list. Adding a stablecoin safely
  • If the token has a known Ethereum contract, add a STABLECOIN_REGISTRY entry with its address and correct decimals (USDC/USDT/PYUSD are 6, most others 18). Its uppercase handle is automatically recognized.
  • If you only want symbol-level recognition (no contract), add the uppercase symbol to STABLECOIN_SYMBOLS.
  • Do not start a new stablecoin array elsewhere — a CI guard fails when a new hardcoded stablecoin list appears in the app source outside a documented exception.
Symbol vs. contract matching classifyStablecoinHolding tries the contract first (exact, case-insensitive), then falls back to symbol (exact, uppercased, whitespace-trimmed) — never a substring.
USD… scam tickers are not stablecoins. Because matching is exact membership, a phishing symbol like USDGift.cc, WorldCupAI.pro, or FiFaAI.cc contains “USD” but is never a member, so it never classifies as a stablecoin. Scam labels are independently redacted and spam-flagged upstream by the input-sanitization layer (URL/TLD detection) — classification and spam-flagging are separate layers, and neither one lets a scam token masquerade as a stablecoin.
Stablecoins are never spam by default. isLikelyMemeOrNoiseToken returns false for every STABLECOIN_SYMBOLS member, and Coinstellation’s isSpamNode reads the upstream spam flag, which is never set for real stablecoins — so a stablecoin node stays visible in the default graph and is never demoted as noise.
Scope-specific lists stay separate on purpose. The Stable Seer peg-deviation engine (yield-bearing and non-USD fiat pegs), the market-ticker gainers/losers exclusion list (includes USDS), and the global-search classifier (search routing — deliberately includes non-stables like FRXETH/RSR/PAXG) each carry intentionally different membership. Folding them into the classifier would broaden classification or change routing/peg behavior, so they remain documented exceptions listed in the consolidation guard’s allowlist — migrating them is tracked as follow-up work.

Identity ≠ sample

Identity comes from the shared wallet-identity resolver (one input → one canonical identity; the ENS label is preserved separately from the resolved address; the cache key is always a full lowercased address, never a shortened display). Fallback / sample / provider-limited states preserve the requested wallet identity (graph.requestedWalletPreserved) — no surface silently substitutes a sample wallet.
Dune remains gated (#1539). This model reads only what /api/wallet already computed. Live Dune result reads are a gated spend path — production Dune-backed surfaces degrade to fallback unless Vercel production sets WALLETWALL_ENABLE_LIVE_DUNE=true and WALLETWALL_ALLOW_DUNE_RESULT_READS=true (which can spend Dune credits). This PR does not change those env vars.

Data Quality and Observability

The underlying observability and data-quality fields this model builds on.

Model Layer

The shared schema layer for source provenance and quality metadata across features.

Wallet Graph Contract

The full /api/wallet response contract that walletEvidence extends.