Wallet Evidence Model
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 everysourceHealth 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 aValueState, 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_zeroonly when every relevant leg was checked; otherwise it isunavailable/partial. Unavailable token value never collapses into a$0total. - 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
sampledlatest-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).
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_REGISTRYentry with itsaddressand correctdecimals(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.
classifyStablecoinHolding tries the contract first (exact, case-insensitive), then
falls back to symbol (exact, uppercased, whitespace-trimmed) — never a substring.
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.
Related
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.
