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

# Data Quality and Observability

> API observability metadata, trust model, provider selection, and guardrails for the Wallet Graph, Holder Wall, and Stable Seer features.

export const resolveEvidenceState = input => {
  const STATUS_RANK = {
    fresh: 0,
    not_used: 0,
    partial: 1,
    degraded: 2,
    throttled: 3,
    unavailable: 4,
    not_configured: 5
  };
  const CONFIDENCE_LADDER = ['unknown', 'degraded', 'partial', 'complete_for_priced_scope'];
  const RENDERING = {
    priced: '$X',
    confirmed_zero: '$0',
    partial: 'Partial pricing',
    sampled: 'Loaded sample',
    unavailable: 'Value unavailable · provider limited'
  };
  const worstStatus = statuses => statuses.reduce((worst, next) => STATUS_RANK[next] > STATUS_RANK[worst] ? next : worst, 'fresh');
  const lowerConfidence = (confidence, steps) => {
    const index = CONFIDENCE_LADDER.indexOf(confidence);
    const safeIndex = index < 0 ? 0 : index;
    return CONFIDENCE_LADDER[Math.max(0, safeIndex - steps)];
  };
  const legStatuses = (p, h, b) => {
    const historyStatus = h === 'unavailable' ? 'unavailable' : h === 'sampled' ? 'partial' : 'fresh';
    const balanceStatus = b === 'unavailable' ? 'unavailable' : 'fresh';
    return [p, historyStatus, balanceStatus];
  };
  const provider = input?.provider ?? 'fresh';
  const history = input?.history ?? 'complete';
  const balance = input?.balance ?? 'positive';
  const enrichment = input?.enrichment ?? 'fresh';
  const fallback = input?.fallback ?? 'none';
  const claimStatus = worstStatus(legStatuses(provider, history, balance));
  const providerAnswered = provider === 'fresh' || provider === 'partial';
  let valueState;
  if (!providerAnswered || balance === 'unavailable') {
    valueState = 'unavailable';
  } else if (history === 'sampled') {
    valueState = 'sampled';
  } else if (provider === 'partial' || history === 'unavailable') {
    valueState = 'partial';
  } else if (balance === 'observed_zero') {
    valueState = 'confirmed_zero';
  } else {
    valueState = 'priced';
  }
  let confidence;
  if (claimStatus === 'fresh') confidence = 'complete_for_priced_scope'; else if (claimStatus === 'partial') confidence = 'partial'; else if (claimStatus === 'not_configured') confidence = 'unknown'; else confidence = 'degraded';
  if (enrichment === 'stale') confidence = lowerConfidence(confidence, 1);
  if (fallback === 'eligible') confidence = lowerConfidence(confidence, 1);
  if (fallback === 'prohibited') confidence = lowerConfidence(confidence, 1);
  let freshness;
  if (enrichment === 'not_configured') freshness = 'No scheduled enrichment configured'; else if (enrichment === 'stale') freshness = 'Stale — past this query’s cadence threshold'; else freshness = 'Within cadence — scheduled and cached, never live';
  const exclusions = ['NFTs (not priced)', 'non-Ethereum chain balances'];
  if (provider === 'not_configured') exclusions.push('Provider leg (no credential configured)');
  if (provider === 'unavailable') exclusions.push('Provider leg (failed for this request)');
  if (provider === 'throttled') exclusions.push('Provider leg (rate limited for this request)');
  if (history === 'sampled') exclusions.push('Transactions outside the latest-N sample window');
  if (history === 'unavailable') exclusions.push('Transaction history (did not load)');
  if (balance === 'unavailable') exclusions.push('Balance observation (did not load)');
  if (enrichment === 'not_configured') exclusions.push('Scheduled enrichment (alias not configured)');
  const warnings = [];
  if (provider === 'throttled') {
    warnings.push('Rate limited — a temporary gap, not a fact about the wallet.');
  }
  if (provider === 'unavailable' || provider === 'not_configured') {
    warnings.push('This is a gap, not a fact about the wallet. Missing evidence is not low risk.');
  }
  if (history === 'sampled') {
    warnings.push('Sampled window — totals may be partial and are labelled as a sample, never as a lifetime total.');
  }
  if (history === 'empty_observed' && providerAnswered) {
    warnings.push('History was visible and genuinely empty — an observation, not a gap.');
  }
  if (balance === 'observed_zero' && valueState !== 'confirmed_zero') {
    warnings.push('A zero was reported, but a degraded leg means it cannot be confirmed — shown as unavailable, not $0.');
  }
  if (enrichment === 'stale') {
    warnings.push('Scheduled evidence is stale — labelled as stale, not discarded. Stale is not the same as invalid.');
  }
  if (fallback === 'eligible') {
    warnings.push('An eligible fallback source was used and is labelled as a fallback — it is never presented as canonical evidence.');
  }
  if (fallback === 'prohibited') {
    warnings.push('A prohibited fallback was refused. The gap it would have filled remains open — no substituted value is shown.');
  }
  const sources = [];
  if (provider !== 'not_configured') sources.push('provider (primary)');
  if (enrichment !== 'not_configured') sources.push('scheduled enrichment (cached)');
  if (fallback === 'eligible') sources.push('fallback (labelled)');
  const zeroIsValid = valueState === 'confirmed_zero';
  let explanation;
  if (claimStatus === 'not_configured') {
    explanation = 'No credential is configured for this source, so nothing was even attempted. The claim reports not_configured and the value is unavailable — it is not a finding about the wallet.';
  } else if (valueState === 'unavailable') {
    explanation = 'At least one leg did not answer, so we do not know the value. It renders as unavailable rather than as $0, because an unanswered provider is a gap in our data, not a confirmed absence of holdings.';
  } else if (valueState === 'confirmed_zero') {
    explanation = 'Every leg answered and the balance was confirmed empty. This is the one case where $0 is honest: the zero is observed, not assumed.';
  } else if (valueState === 'sampled') {
    explanation = 'The history came from a truncated latest-N window, so any total derived from it describes the sample only. It is labelled as a sample and never as a lifetime figure.';
  } else if (valueState === 'partial') {
    explanation = 'Some categories loaded and some did not. The claim is labelled partial so the reader knows the figure understates coverage rather than describing the wallet completely.';
  } else {
    explanation = 'Every leg answered and the value is fully priced within the stated scope. Scope still excludes the categories listed below — a priced value is not a claim of total net worth.';
  }
  return Object.freeze({
    claimStatus,
    confidence,
    valueState,
    renderedAs: RENDERING[valueState],
    freshness,
    zeroIsValid,
    exclusions: Object.freeze(exclusions),
    warnings: Object.freeze(warnings),
    sources: Object.freeze(sources),
    explanation
  });
};

export const EvidenceStateExplorer = ({idPrefix = 'ese'}) => {
  const STYLES = `
.ww-ese{--ww-ese-bg:#FAFAF0;--ww-ese-panel:#FFF7E8;--ww-ese-ink:#2B2118;--ww-ese-muted:#59646A;--ww-ese-line:#B87333;--ww-ese-line-soft:#D9C7B3;--ww-ese-accent:#B84923;--ww-ese-ok:#526246;--ww-ese-warn:#8A5A1F;--ww-ese-risk:#8F2F1D;}
@media (prefers-color-scheme:dark){.ww-ese{--ww-ese-bg:#1B1512;--ww-ese-panel:#241C17;--ww-ese-ink:#F2E9DF;--ww-ese-muted:#A7B0B5;--ww-ese-line:#8A5A1F;--ww-ese-line-soft:#4A3B30;--ww-ese-accent:#D9714B;--ww-ese-ok:#9AAB89;--ww-ese-warn:#D6A85A;--ww-ese-risk:#E0836B;}}
.light .ww-ese,:root.light .ww-ese{--ww-ese-bg:#FAFAF0;--ww-ese-panel:#FFF7E8;--ww-ese-ink:#2B2118;--ww-ese-muted:#59646A;--ww-ese-line:#B87333;--ww-ese-line-soft:#D9C7B3;--ww-ese-accent:#B84923;--ww-ese-ok:#526246;--ww-ese-warn:#8A5A1F;--ww-ese-risk:#8F2F1D;}
.dark .ww-ese,:root.dark .ww-ese{--ww-ese-bg:#1B1512;--ww-ese-panel:#241C17;--ww-ese-ink:#F2E9DF;--ww-ese-muted:#A7B0B5;--ww-ese-line:#8A5A1F;--ww-ese-line-soft:#4A3B30;--ww-ese-accent:#D9714B;--ww-ese-ok:#9AAB89;--ww-ese-warn:#D6A85A;--ww-ese-risk:#E0836B;}
.ww-ese{background:var(--ww-ese-bg);color:var(--ww-ese-ink);border:2px solid var(--ww-ese-line);border-radius:4px;padding:1rem;font-size:0.875rem;line-height:1.5;}
.ww-ese *{box-sizing:border-box;}
.ww-ese-h{font-weight:600;font-size:0.75rem;letter-spacing:0.08em;text-transform:uppercase;color:var(--ww-ese-muted);margin:0 0 0.5rem;}
.ww-ese-grid{display:grid;grid-template-columns:1fr;gap:0.75rem;}
@media (min-width:40rem){.ww-ese-grid{grid-template-columns:1fr 1fr;}}
.ww-ese-field{display:flex;flex-direction:column;gap:0.25rem;}
.ww-ese-field label{font-weight:600;color:var(--ww-ese-ink);}
.ww-ese-field select{width:100%;padding:0.4rem 0.5rem;border:1px solid var(--ww-ese-line-soft);border-radius:3px;background:var(--ww-ese-panel);color:var(--ww-ese-ink);font:inherit;}
.ww-ese-field select:focus-visible{outline:2px solid var(--ww-ese-accent);outline-offset:2px;}
.ww-ese-out{margin-top:1rem;border:1px solid var(--ww-ese-line-soft);border-left:4px solid var(--ww-ese-accent);border-radius:3px;background:var(--ww-ese-panel);padding:0.75rem;}
.ww-ese-rows{display:grid;grid-template-columns:1fr;gap:0.35rem 1rem;margin:0;}
@media (min-width:40rem){.ww-ese-rows{grid-template-columns:auto 1fr;}}
.ww-ese-rows dt{font-weight:600;color:var(--ww-ese-muted);}
.ww-ese-rows dd{margin:0;}
.ww-ese-code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:0.8125rem;background:var(--ww-ese-bg);border:1px solid var(--ww-ese-line-soft);border-radius:2px;padding:0.05rem 0.3rem;}
.ww-ese-flag{font-weight:600;}
.ww-ese-flag[data-valid="yes"]{color:var(--ww-ese-ok);}
.ww-ese-flag[data-valid="no"]{color:var(--ww-ese-risk);}
.ww-ese-list{margin:0.25rem 0 0;padding-left:1.1rem;}
.ww-ese-list li{margin:0.15rem 0;}
.ww-ese-warn{color:var(--ww-ese-warn);}
.ww-ese-note{margin-top:0.75rem;padding-top:0.6rem;border-top:1px dashed var(--ww-ese-line-soft);color:var(--ww-ese-muted);font-size:0.8125rem;}
.ww-ese-hint{color:var(--ww-ese-muted);font-size:0.8125rem;}
.ww-ese-reset{margin-top:0.75rem;padding:0.35rem 0.7rem;border:1px solid var(--ww-ese-line);border-radius:3px;background:transparent;color:var(--ww-ese-ink);font:inherit;font-size:0.8125rem;cursor:pointer;}
.ww-ese-reset:focus-visible{outline:2px solid var(--ww-ese-accent);outline-offset:2px;}
`;
  const PROVIDER_OPTIONS = [{
    value: 'fresh',
    label: 'Fresh — answered normally'
  }, {
    value: 'partial',
    label: 'Partial — some of the data loaded'
  }, {
    value: 'throttled',
    label: 'Throttled — rate limited this request'
  }, {
    value: 'unavailable',
    label: 'Unavailable — failed this request'
  }, {
    value: 'not_configured',
    label: 'Not configured — no credential'
  }];
  const HISTORY_OPTIONS = [{
    value: 'complete',
    label: 'Complete — full history observed'
  }, {
    value: 'sampled',
    label: 'Sampled — latest-N window only'
  }, {
    value: 'empty_observed',
    label: 'Empty (observed) — history visible, no rows'
  }, {
    value: 'unavailable',
    label: 'Unavailable — history did not load'
  }];
  const BALANCE_OPTIONS = [{
    value: 'positive',
    label: 'Positive — a real balance was read'
  }, {
    value: 'observed_zero',
    label: 'Observed zero — confirmed nothing held'
  }, {
    value: 'unavailable',
    label: 'Unavailable — balance did not load'
  }];
  const ENRICHMENT_OPTIONS = [{
    value: 'fresh',
    label: 'Fresh — within its cadence window'
  }, {
    value: 'stale',
    label: 'Stale — older than its cadence threshold'
  }, {
    value: 'not_configured',
    label: 'Not configured — alias unset'
  }];
  const FALLBACK_OPTIONS = [{
    value: 'none',
    label: 'None — no fallback path taken'
  }, {
    value: 'eligible',
    label: 'Eligible fallback — used and labelled'
  }, {
    value: 'prohibited',
    label: 'Prohibited fallback — offered and refused'
  }];
  const [provider, setProvider] = useState('fresh');
  const [history, setHistory] = useState('complete');
  const [balance, setBalance] = useState('positive');
  const [enrichment, setEnrichment] = useState('fresh');
  const [fallback, setFallback] = useState('none');
  const result = useMemo(() => resolveEvidenceState({
    provider,
    history,
    balance,
    enrichment,
    fallback
  }), [provider, history, balance, enrichment, fallback]);
  const reset = () => {
    setProvider('fresh');
    setHistory('complete');
    setBalance('positive');
    setEnrichment('fresh');
    setFallback('none');
  };
  const fields = [{
    name: 'provider',
    label: 'Provider result',
    hint: 'How the primary balance/holdings provider answered.',
    value: provider,
    onChange: setProvider,
    options: PROVIDER_OPTIONS
  }, {
    name: 'history',
    label: 'Transaction history',
    hint: 'Whether history was complete, a sample, observed-empty, or missing.',
    value: history,
    onChange: setHistory,
    options: HISTORY_OPTIONS
  }, {
    name: 'balance',
    label: 'Balance observation',
    hint: 'A confirmed zero is an observation; a failed read is not.',
    value: balance,
    onChange: setBalance,
    options: BALANCE_OPTIONS
  }, {
    name: 'enrichment',
    label: 'Scheduled enrichment',
    hint: 'Scheduled and cached data, never live streaming.',
    value: enrichment,
    onChange: setEnrichment,
    options: ENRICHMENT_OPTIONS
  }, {
    name: 'fallback',
    label: 'Fallback use',
    hint: 'A fallback can only ever lower confidence, never raise it.',
    value: fallback,
    onChange: setFallback,
    options: FALLBACK_OPTIONS
  }];
  return <div className="ww-ese not-prose">
      <style>{STYLES}</style>

      <p className="ww-ese-h">Evidence state explorer — educational, no wallet data</p>

      <div className="ww-ese-grid">
        {fields.map(field => <div className="ww-ese-field" key={field.name}>
            <label htmlFor={`${idPrefix}-${field.name}`}>{field.label}</label>
            <select id={`${idPrefix}-${field.name}`} value={field.value} onChange={e => field.onChange(e.target.value)} aria-describedby={`${idPrefix}-${field.name}-hint`}>
              {field.options.map(option => <option key={option.value} value={option.value}>
                  {option.label}
                </option>)}
            </select>
            <span id={`${idPrefix}-${field.name}-hint`} className="ww-ese-hint">
              {field.hint}
            </span>
          </div>)}
      </div>

      <div className="ww-ese-out" aria-live="polite">
        <dl className="ww-ese-rows">
          <dt>Claim status</dt>
          <dd>
            <code className="ww-ese-code">{result.claimStatus}</code>
          </dd>

          <dt>Confidence</dt>
          <dd>
            <code className="ww-ese-code">{result.confidence}</code>
          </dd>

          <dt>Value state</dt>
          <dd>
            <code className="ww-ese-code">{result.valueState}</code> — renders as {result.renderedAs}
          </dd>

          <dt>Freshness</dt>
          <dd>{result.freshness}</dd>

          <dt>Is a $0 reading valid?</dt>
          <dd>
            <span className="ww-ese-flag" data-valid={result.zeroIsValid ? 'yes' : 'no'}>
              {result.zeroIsValid ? 'Yes — zero was observed and confirmed' : 'No — an unconfirmed zero must render as unavailable'}
            </span>
          </dd>

          <dt>Sources credited</dt>
          <dd>{result.sources.length > 0 ? result.sources.join(', ') : 'None'}</dd>

          <dt>Exclusions</dt>
          <dd>
            <ul className="ww-ese-list">
              {result.exclusions.map(item => <li key={item}>{item}</li>)}
            </ul>
          </dd>

          <dt>Warnings</dt>
          <dd>
            {result.warnings.length === 0 ? 'None' : <ul className="ww-ese-list ww-ese-warn">
                {result.warnings.map(item => <li key={item}>{item}</li>)}
              </ul>}
          </dd>

          <dt>What this means</dt>
          <dd>{result.explanation}</dd>
        </dl>
      </div>

      <p className="ww-ese-note">
        Illustrative scenario only. No address is accepted, no provider is called, and no wallet is
        connected. The status, confidence, and value-state names above are the same vocabulary the
        tables on this page define.
      </p>

      <button type="button" className="ww-ese-reset" onClick={reset}>
        Reset to a fully-fresh scenario
      </button>
    </div>;
};

# Data Quality and Observability

API observability metadata is surfaced in the wallet UI via an observability badge and a data-quality strip to provide users with clear insights into data provenance, staleness, partial data, and fallback states.

This is the trust model reference for the Wallet Graph (Holder Wall) and Stable Seer features.

## 1. Wallet Graph / Holder Wall

### Endpoint

```
GET /api/wallet?address=<0x-address-or-ENS>
```

All responses pass through a shared normalization layer before the route sends JSON.

### WalletGraph v1 response contract

Every successful response includes `contractVersion: "walletGraph.v1"`. This field is the authoritative version tag — never remove or rename it.

Key top-level fields:

| Field               | Type               | Notes                                                                |
| ------------------- | ------------------ | -------------------------------------------------------------------- |
| `contractVersion`   | `"walletGraph.v1"` | Stable version tag                                                   |
| `address`           | string             | Resolved 0x address                                                  |
| `chain`             | `"ethereum"`       | Currently Ethereum only                                              |
| `provider`          | string             | Normalized to: `mock`, `etherscan`, `alchemy`, `mixed`, or `unknown` |
| `source`            | string             | Normalized to: `mock`, `fallback`, or `live`                         |
| `sources`           | object             | Map of provider identities, e.g. `{ walletActivity, prices }`        |
| `dataQuality`       | object             | Quality flags                                                        |
| `isFallback`        | bool               | Alias for `dataQuality.isFallback`                                   |
| `transactionSample` | object             | Sample window metadata                                               |
| `apiErrors`         | array              | User-safe provider warnings (sanitized)                              |
| `nodes`             | array              | Graph nodes                                                          |
| `edges`             | array              | Graph edges                                                          |
| `observability`     | object             | Structured observability metadata                                    |

### Provider selection

<Tabs>
  <Tab title="No API keys">
    Condition: no `ETHERSCAN_API_KEY` and no `ALCHEMY_API_KEY`.

    * **Provider:** Mock provider
    * **`observability.source`:** `mock`
    * **`observability.provider`:** `mock`
  </Tab>

  <Tab title="ETHERSCAN_API_KEY set">
    Condition: `ETHERSCAN_API_KEY` set.

    * **Provider:** Live provider
    * **`observability.source`:** `live`
    * **`observability.provider`:** `etherscan`
  </Tab>

  <Tab title="ALCHEMY_API_KEY only">
    Condition: `ALCHEMY_API_KEY` only.

    * **Provider:** Live provider
    * **`observability.source`:** `live`
    * **`observability.provider`:** `alchemy`
  </Tab>

  <Tab title="Production, no keys">
    Condition: production + no keys + `ENABLE_MOCK_MODE` unset.

    * **Provider:** 503 error
    * **`observability.source`:** —
    * **`observability.provider`:** —
  </Tab>
</Tabs>

Mock data is deterministic and address-seeded via `ethers.id`. It never calls external APIs.

### Per-wallet provider roles (post-Dune pivot)

Per-wallet features are **provider-native**, not Dune-backed: parameterized per-wallet
Dune queries were incompatible with the read-only cache model, so wallet-detail data now
composes from sources the route already reaches directly:

| Role                            | Source                     | Notes                                                                                                                                                                                                                                                                             |
| ------------------------------- | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Transactions + ERC-20 transfers | Etherscan                  | Also derives protocol affinity, movements, and counterparties — no extra calls. Sub-requests retry once on transient failure.                                                                                                                                                     |
| Native ETH balance              | Alchemy `eth_getBalance`   | Alchemy is the sole source. A failed read reports an explicit unavailable state; it is never reported as a zero balance.                                                                                                                                                          |
| ERC-20 token balances           | Alchemy `getTokenBalances` | Walked under an explicit **5-page ceiling**. Reaching the ceiling is disclosed as partial coverage, never a silent truncation. If the call is throttled, the degradation is surfaced (`walletEvidence` status `throttled`, `valueScope: eth_only`) — never a silent empty result. |
| Contract detection              | Alchemy `eth_getCode`      | An EIP-7702-delegated EOA carries code and is reported as a contract. When the call fails the answer is *unknown*, never "externally owned account".                                                                                                                              |
| Gas snapshot                    | Alchemy                    |                                                                                                                                                                                                                                                                                   |
| Token USD pricing               | DeFiLlama (keyless)        | Priced **by contract address**, current and transfer-time historical. Spam-safe: a token with no DeFiLlama contract price is excluded from valuation instead of inheriting a real asset's symbol price.                                                                           |
| ETH USD price                   | CoinGecko                  | Fallback pricing sets `observability.fallback`.                                                                                                                                                                                                                                   |

**Portfolio value scope** is explicit: `valueScope: priced` means ETH + priced token
holdings; `valueScope: eth_only` means token balances were unavailable for this request
and the UI labels the total accordingly. The wallet route makes three Alchemy calls
(native balance, token balances, contract code) plus a gas snapshot — an earlier unused
NFT call was removed.

### What is sent to a data provider, and what each one supplies

<Info>
  Looking up a wallet sends that **wallet address to Alchemy from WalletWall's
  servers** — never from your browser — so the current chain state for it can be read.
  When you search an ENS name, the name is resolved to an address and that address is
  sent the same way. No other information about you is included in the request.
</Info>

Provider responsibilities are deliberately separated and are not interchangeable:

| Provider                  | Supplies                                                                             | Does not supply                                    |
| ------------------------- | ------------------------------------------------------------------------------------ | -------------------------------------------------- |
| **Alchemy**               | Live, per-wallet chain observations: native balance, token balances, contract status | Historical aggregates, cohort statistics, rankings |
| **Dune**                  | Scheduled aggregate and historical analytics over WalletWall-authored SQL            | Anything about an individual wallet on demand      |
| **Etherscan**             | Transaction and ERC-20 transfer history                                              | Balances                                           |
| **DeFiLlama / CoinGecko** | Prices                                                                               | Chain state                                        |

Two things follow from that separation, and both are enforced rather than promised:

* **A cached result is never labelled live.** Every served value carries its own
  source state and cache time. A value read from cache — including a stale value served
  because a provider was unavailable — is reported as cached, with the instant it was
  actually written. It is not re-stamped with the time you asked for it.
* **Naming a provider is attribution, not endorsement.** WalletWall does not claim
  any partnership with, or approval by, the providers it names, and makes no
  real-time-accuracy guarantee for any figure. Chain state can change between the
  observation and the moment you read it, which is why the observation time is shown
  rather than assumed.

### Observability and dataQuality fields

Every normalized response carries two overlapping representations of the same
underlying quality signals: `observability` (structured, always present) and
`dataQuality` (legacy booleans and flags, preserved for compatibility).

<CodeGroup>
  ```json observability theme={null}
  {
    "provider": "etherscan",
    "source": "live",
    "partial": false,
    "fallback": false,
    "providerErrors": false,
    "freshness": "2026-05-14",
    "durationMs": 312,
    "timing": { "totalMs": 312, "providerMs": 180, "coingeckoMs": 80 }
  }
  ```

  ```json dataQuality theme={null}
  {
    "isFallback": false,
    "isDemo": false,
    "isPartial": true,
    "warnings": ["Loaded latest 200 normal transactions; totals may be partial."],
    "flags": {
      "partial": true,
      "sampled": false,
      "fallback": false,
      "demo": false,
      "providerErrors": true
    },
    "providerErrors": []
  }
  ```
</CodeGroup>

**`observability` fields:**

* `provider`: normalized provider name.
* `source`: data class — `live` (real provider data), `mock` (demo/no keys), or `fallback` (price fallback active).
* `partial`: true when any provider sub-request failed or transaction data is sampled.
* `fallback`: true when fallback pricing or demo data is being served.
* `providerErrors`: true when `apiErrors` contains entries with `severity === 'partial'` or `'error'`.
* `freshness`: wallet's `lastActive` ISO date string, or `null`.
* `durationMs`: total route wall-clock time in milliseconds.
* `timing`: sub-phase breakdowns. Allowed keys: `totalMs`, `providerMs`, `coingeckoMs`, `graphMs`, `duneMs`.

**`dataQuality` flags:**

* `partial`: set when data is sampled or any provider sub-call degraded.
* `sampled`: set when `transactionSample.isSampled` is true.
* `fallback`: set when fallback pricing is active.
* `demo`: set only for explicit mock/demo data.
* `providerErrors`: set when `apiErrors` contains `partial` or `error` entries.

### apiErrors sanitization

`apiErrors` entries are user-safe. Before serialization, the error-message sanitizer:

1. Strips URL query strings (which may contain provider API keys or tokens) — replaced with `[params redacted]`.
2. Caps message length at 200 characters.

**Never add raw provider error messages, API keys, full URLs, or stack traces to `apiErrors`.**

This invariant applies to **every** API route, not just the wallet route: on a
cache-miss provider or store failure, routes return a generic warning (e.g.
"market data temporarily unavailable") — internal error text such as host names,
DNS errors, or store URLs is logged server-side only, never interpolated into the
client response.

### UI status badge behavior

<Warning>
  Neither the observability badge nor the data-quality strip ever exposes raw URLs, API keys, stack traces, or provider error details.
</Warning>

<AccordionGroup>
  <Accordion title="Observability badge">
    Compact badge rendered in the wallet header:

    | `observability.source` | Badge text              | Badge tone |
    | ---------------------- | ----------------------- | ---------- |
    | `mock`                 | `Demo`                  | muted      |
    | `fallback`             | `Fallback`              | warn       |
    | `live`                 | `Live · <ProviderName>` | safe       |
  </Accordion>

  <Accordion title="Data-quality strip">
    Fixed bottom bar:

    * Reads from `observability` when present; falls back to legacy `dataQuality` fields.
    * Invariant copy: "Read-only public wallet data · No wallet connection required"
    * Conditional: partial note, "Some provider data unavailable", "Fallback data source", "Demo mode", provider name, freshness, durationMs.
  </Accordion>
</AccordionGroup>

#### Try it — how these flags become a claim

The `partial`, `fallback`, and `throttled` flags above are inputs, not conclusions. This
explorer shows what the [Wallet Evidence Model](/architecture/wallet-evidence-model) does
with them — including why a throttled provider yields an explicit gap rather than a `$0`,
and why a labelled fallback can lower confidence but never raise it.

<EvidenceStateExplorer idPrefix="data-quality" />

## 2. Stable Seer

### Endpoint

```
GET /api/stable-seer?q=<search-query>
```

Stable Seer performs DEX token and pool market lookup using DEX Screener. It does not provide holder analytics. The `holderAnalyticsSupported: false` flag is present on every response.

### Cache behavior

| Layer           | When active                                               | TTL            |
| --------------- | --------------------------------------------------------- | -------------- |
| Redis (Upstash) | `UPSTASH_REDIS_REST_URL` + `UPSTASH_REDIS_REST_TOKEN` set | 900 s (15 min) |
| In-memory `Map` | Redis not configured (fallback)                           | 900 s (15 min) |

Cache key is the lowercased query string. Provider error and 4xx/5xx responses are never cached.

### confidence values

| State                       | `confidence`    | HTTP status |
| --------------------------- | --------------- | ----------- |
| Success (live or cache hit) | `"low"`         | 200         |
| Provider unreachable        | `"unavailable"` | 502         |

`confidence: "low"` on success is intentional — market data is DEX pool data, not audited fundamentals.

`confidence: "unavailable"` is a provider failure state. UI should suppress user-blame wording when this value is present.

## 3. Source confidence map

Positions each data source by how live it is and whether it is wallet-specific or
context-only. This is not a ranking of importance — a context-only source can still be
essential for pricing or peg context.

```mermaid theme={null}
%%{init: {'theme': 'base', 'themeVariables': {'quadrant1Fill': '#FAFAF0', 'quadrant2Fill': '#D6A85A', 'quadrant3Fill': '#A7B0B5', 'quadrant4Fill': '#9AAB89', 'quadrant1TextFill': '#2B2118', 'quadrant2TextFill': '#2B2118', 'quadrant3TextFill': '#172014', 'quadrant4TextFill': '#172014', 'quadrantPointFill': '#B84923', 'quadrantPointTextFill': '#2B2118', 'quadrantXAxisTextFill': '#2B2118', 'quadrantYAxisTextFill': '#2B2118', 'quadrantTitleFill': '#2B2118'}}}%%
quadrantChart
    title Source confidence map
    x-axis Scheduled / cached --> Live / fresh
    y-axis Context-only --> Wallet-specific
    quadrant-1 Wallet-specific & live
    quadrant-2 Context-only & live
    quadrant-3 Context-only & scheduled
    quadrant-4 Wallet-specific & scheduled
    Etherscan: [0.75, 0.9]
    Alchemy: [0.75, 0.85]
    DeFiLlama: [0.6, 0.35]
    CoinGecko: [0.7, 0.3]
    Dune cache: [0.2, 0.55]
```

*Etherscan and Alchemy are live, wallet-specific sources — transactions, transfers, and
token balances for the address being viewed. DeFiLlama and CoinGecko are live but
context-only — they price tokens; they do not enumerate a wallet's holdings. Dune is
always scheduled/cached, never live, and covers both wallet-specific and aggregate
queries depending on the surface. See the
[Wallet Evidence Model](/architecture/wallet-evidence-model) for how these sources roll
up into per-claim status.*

## 4. Agent guardrails

Rules enforced by the test suite:

* **Do not conflate Holder Wall and Stable Seer.** These are separate features with separate data sources and API routes.
* **Do not invent holder data.** `holderAnalyticsSupported: false` on all Stable Seer responses is a hard invariant.
* **Do not expose secrets.** The error-message sanitizer strips query strings from error messages. Tests verify no provider key strings appear in normalized output.
* **Keep partial provider degradation non-fatal and non-scary.** A failed sub-provider call sets `partial: true` and contributes an `apiErrors` entry. It does not fail the route. UI copy should say "Provider data may be partial" — not "Error" or "Failed".
* **Preserve existing response contracts.** `contractVersion: "walletGraph.v1"` must be present on every `/api/wallet` response. The `observability` field must be present on every normalized response.

## Implementation layers

Behind these contracts, a shared normalization layer handles response normalization, observability metadata, and error-message sanitization. The `/api/wallet` route handles provider selection, rate limiting, and cache headers, drawing on a provider layer that supplies the mock and live wallet providers (Etherscan, Alchemy, CoinGecko, Dune, and graph sources). The `/api/stable-seer` route handles DEX Screener fetches backed by a Redis-plus-in-memory cache (900 s / 15 min TTL). In the UI, the observability badge renders a compact provenance indicator in the wallet header and the data-quality strip renders a fixed-bottom provenance bar.

## Related

<Columns cols={2}>
  <Card title="Wallet Graph Contract" icon="diagram-project" href="/architecture/wallet-graph-contract">
    The full `/api/wallet` response contract, including the `dataQuality` and `transactionSample` field reference.
  </Card>

  <Card title="Wallet Evidence Model" icon="chart-line" href="/architecture/wallet-evidence-model">
    How these provider sources roll up into per-claim confidence and status.
  </Card>
</Columns>
