diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml
index 65a46c7..f281581 100644
--- a/.github/ISSUE_TEMPLATE/bug.yml
+++ b/.github/ISSUE_TEMPLATE/bug.yml
@@ -25,6 +25,6 @@ body:
options:
- live
- estimated
- - mocked
+ - last good (cached)
validations:
required: true
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 31d0e9a..cc7a5c0 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -5,8 +5,8 @@
## Checklist
- [ ] CI green
-- [ ] Every new metric renders a `DataSourceBadge` (live | estimated | mocked)
-- [ ] Mock values live only in `src/lib/mockData.ts`, with `source` + `asOf`
+- [ ] Every new metric renders a `DataSourceBadge` (live | estimated | cached)
+- [ ] No mocked values; fallbacks come from `src/data/last-good.json` with their original `asOf`
- [ ] Dune SQL changes mirrored in `/queries`
- [ ] Live numbers spot-checked against Etherscan (link txs):
- [ ] `STATUS.md` / `DEVLOG.md` updated if the phase changed
diff --git a/CLAUDE.md b/CLAUDE.md
index 09bbce3..9aeafc4 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -14,15 +14,15 @@ Portfolio project. Solo. Stack: Next.js 16, TypeScript, Tailwind, Recharts, viem
Run /project:log-session — writes DEVLOG entry + updates STATUS.md automatically.
## Non-Negotiables
-- Every metric needs a DataSourceBadge: live | estimated | mocked
-- Mock data lives only in /src/lib/mockData.ts with source + asOf date
+- Every metric needs a DataSourceBadge: live | estimated | cached (shown as "Last good" with its date)
+- No mocked or invented values. Fallbacks are real earlier reads in /src/data/last-good.json (written by scripts/snapshot-last-good.ts)
- No PDF generation — deferred to v2
- Design system: `DESIGN.md` (tokens in `src/app/globals.css`).
## Structure
- /queries — metric definitions in Dune SQL (the app computes the same from on-chain logs)
- /docs — RAG context, reference with @docs/filename.md
-- /src/lib — data layer (flowEvents.ts, types.ts, mockData.ts, addressRegistry.ts)
+- /src/lib — data layer (flowEvents.ts, types.ts, lastGood.ts, addressRegistry.ts)
- /src/components — UI (see subdirectory CLAUDE.md)
## Tooling
diff --git a/README.md b/README.md
index 8a87121..1988ec7 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@

-A Next.js dashboard that decodes Ondo's OUSG and USDY InstantManager events on Ethereum and charts weekly mint volume, redeem volume, net flow and unique wallets. The `/issuance` page puts the same measurement on six assets from five issuers: USDC (Circle), USDT (Tether), PYUSD (PayPal), BUIDL (BlackRock) and OUSG and USDY (Ondo). It also shows where each stablecoin's supply sits by chain and how net issuance moved around the Fed's rate cuts. Every number on the page carries a **live**, **estimated** or **mocked** badge.
+A Next.js dashboard that decodes Ondo's OUSG and USDY InstantManager events on Ethereum and charts weekly mint volume, redeem volume, net flow and unique wallets. The `/issuance` page puts the same measurement on six assets from five issuers: USDC (Circle), USDT (Tether), PYUSD (PayPal), BUIDL (BlackRock) and OUSG and USDY (Ondo). It also shows where each stablecoin's supply sits by chain and how net issuance moved around the Fed's rate cuts. Every number on the page carries a **live**, **estimated** or **last good** badge. A last good value is a real earlier read, shown with its date. The dashboard shows no mocked numbers.
## Results
@@ -24,14 +24,14 @@ From the dated snapshot in `src/data/issuance.json` (event logs read 2026-10-24,
| **Five issuers** | **$526.3B** | **$490.5B** | **$35.8B** |
- The snapshot holds 3,212,656 decoded event logs (the `audit` counts in `src/data/issuance.json`). USDC is 3.17M of them.
-- $192.1B of USDC mints and burns are Circle CCTP transfers between chains. The tracker excludes them because moving USDC between chains is not new money. It also excludes Paxos's mistaken 300 trillion PYUSD mint and its burn on 2025-10-15.
+- The five issuers total about $1T of mints and redemptions: $1.02T after removing $192.1B of Circle CCTP transfers, $1.21T if they were counted (`grossUsd` and `grossWithCctpUsd` in `src/lib/issuance.ts`). The tracker excludes CCTP because moving USDC between chains is not new money. It also excludes Paxos's mistaken 300 trillion PYUSD mint and its burn on 2025-10-15.
- In the 2024 easing episode (Sep to Dec 2024, 100 bp), USDC and USDT net issuance ran above their own norm (+0.5σ each), and BUIDL and OUSG ran below theirs (−0.2σ and −0.3σ). The 2025 episode (Sep to Dec 2025, 75 bp) only partly repeated it. With two episodes this is descriptive, not evidence of cause. `/summary` renders these figures from `src/lib/issuance.ts`.
- Ethereum log-derived net issuance matched DefiLlama's Ethereum supply change within 17% for USDC and 7% for PYUSD (`src/lib/issuance.check.ts`, run 2026-10-24).
## How it works
- **Ondo flows (`/`, `/flows`).** `src/lib/flowEvents.ts` pages the InstantManager logs from Blockscout's keyless v2 API, decodes them with viem and groups them by Monday-UTC week. It reads three contracts: the current OUSG manager, the legacy OUSG manager (Apr 2024 to Apr 2025) and the USDY manager. Log pages older than a fixed block cache for a week. Newer pages refresh hourly. `queries/mint_redeem_volume.sql` holds the same definition in Dune SQL as a written reference.
-- **Home page pillars.** `src/lib/chainSupply.ts` reads total supply on 13 chains and prices it with the Ethereum `OndoOracle`. `src/lib/topHolders.ts` reads Ethereum holders from Blockscout. `src/lib/benchmark.ts` reads BUIDL from DefiLlama's stablecoin series and the Ondo row from the on-chain total; Superstate and OpenEden TVL have no free live source and stay labeled snapshots. When a source fails, each getter returns a dated, labeled snapshot from `src/lib/mockData.ts`.
+- **Home page pillars.** `src/lib/chainSupply.ts` reads total supply on 13 chains and prices it with the Ethereum `OndoOracle`. `src/lib/topHolders.ts` reads Ethereum holders from Blockscout and Stellar USDY holders from stellar.expert. `src/lib/benchmark.ts` reads BUIDL from DefiLlama's stablecoin series and the Ondo row from the on-chain total; Superstate and OpenEden TVL have no free live source and stay labeled snapshots. When a source fails, each getter returns its last good rows from `src/data/last-good.json`, labeled **last good** with the date of the original read. A row with no stored value is left out. `scripts/snapshot-last-good.ts` refreshes that file from the live sources; `scripts/verify-supply.ts` compares the supply reads with independent explorers.
- **Stablecoin flows (`/issuance`, `/summary`).** `scripts/backfill-issuance.mjs` reads `eth_getLogs` from Tenderly's public mainnet gateway in 100,000-block windows. `scripts/backfill-context.mjs` adds DefiLlama supply by chain and the FRED `DFEDTARU` rate. Both scripts write `src/data/*.json`. The pages read these snapshots through `src/lib/issuance.ts`, not on each request.
- `/api/metrics` returns the same data as JSON.
@@ -44,10 +44,10 @@ npm ci
npm run lint
npx tsc --noEmit
npm test
-npm run dev # http://localhost:3000
+npm run dev # serves on port 3000; the deployed copy is ondo-nexus.vercel.app
```
-`npm test` runs the seven self-check scripts in `src/lib/*.check.ts` with `tsx` (pinned in `package.json`). They need no network. `issuance.check.ts` reconciles the committed snapshots. CI runs lint, the type check, `npm test` and `npm run build`.
+`npm test` runs the seven self-check scripts in `src/lib/*.check.ts` with `tsx` (pinned in `package.json`). They need no network. All seven passed on 2026-10-26. `issuance.check.ts` reconciles the committed snapshots. CI runs lint, the type check, `npm test` and `npm run build`.
To refresh the stablecoin snapshots (about 25 minutes on the free public RPC), then rebuild the study guide:
@@ -65,9 +65,9 @@ The USDC share card (`docs/img/usdc-share-card.svg` and `.png`, 1024px wide, nav
```
src/app/ Pages (/, /flows, /issuance, /summary) and /api/metrics
src/components/ Dashboard charts, issuance figures and UI atoms
-src/lib/ Data getters, types, mock fallbacks and *.check.ts self-checks
-src/data/ Dated snapshots for the stablecoin flows tracker
-scripts/ Backfill scripts that write src/data/*.json
+src/lib/ Data getters, types, last-good fallbacks and *.check.ts self-checks
+src/data/ Dated snapshots: the stablecoin flows tracker and the last good values
+scripts/ Backfill and snapshot scripts that write src/data/*.json, and the supply cross-check
queries/ Metric definitions in Dune SQL (reference; the app does not call Dune)
docs/ Methodology, contract research, decisions and the study guide
.claude/ Claude Code agents, commands and the live-pillar skill
@@ -87,10 +87,10 @@ docs/ Methodology, contract research, decisions and the study guide
- **Ondo flows.** InstantManager event logs from [Blockscout](https://eth.blockscout.com) (v2 API, keyless), read at render time and cached for one hour. Full history from Apr 2024 (OUSG) and Dec 2025 (USDY).
- **Chain supply and price.** public RPCs and chain APIs on 13 chains, priced with `OndoOracle` on Ethereum. The per-chain table is in [docs/METHODOLOGY.md](docs/METHODOLOGY.md).
-- **Holders.** Blockscout token holders API, Ethereum only.
+- **Holders.** Blockscout token holders API for Ethereum (OUSG, USDY) and the stellar.expert asset holders API for Stellar USDY.
- **Benchmark.** [DefiLlama](https://defillama.com) TVL per product, plus redemption terms quoted from each issuer's docs.
- **Stablecoin flows.** Ethereum event logs through Tenderly's public gateway, DefiLlama stablecoin supply and FRED `DFEDTARU`. The backfill scripts freeze these as dated JSON in `src/data/`, with `asOf` 2026-10-24.
-- Fallback snapshots in `src/lib/mockData.ts` carry their own source and date.
+- **Last good values.** `src/data/last-good.json` holds real reads from the sources above, written on 2026-10-26 (Superstate USTB and OpenEden TBILL rows date from 2026-09-24).
## Scope and limits
@@ -100,7 +100,7 @@ docs/ Methodology, contract research, decisions and the study guide
- Gross flows are Ethereum only. Tron holds about half of USDT supply, so Ethereum logs understate USDT and USDC issuance. Net issuance for the three stablecoins uses all-chain supply instead.
- The stablecoin data are a snapshot from 2026-10-24. It is stale until someone reruns the backfill scripts.
- The rate-cut comparison covers two easing episodes, so it is descriptive only.
-- On 2026-10-21, DefiLlama's `/tvl/
-
+ Unavailable: the event-log source did not respond and no last good series is stored. +
+ ) : ( +Circle, Tether, PayPal, BlackRock and Ondo, on Ethereum since January 2023. Where the money sits by chain, and what weekly issuance did around the Fed's rate cuts.
-
- Ethereum event logs. Circle CCTP transfers ({formatUsdCompact(s.cctpBridgedUsd)} of USDC mints and burns) are excluded: moving USDC between chains is not - issuance. USDC is {Math.round(s.usdcShareOfGross * 100)}% of the total and is mostly market-maker churn; its net is negative on Ethereum because holders + Ethereum event logs. The total is {formatTrillions(s.grossUsd)} because Circle CCTP transfers ({formatUsdCompact(s.cctpBridgedUsd)} of USDC mints and burns) are excluded: moving USDC between chains is not + issuance. Counting them gives {formatTrillions(s.grossWithCctpUsd)}. USDC is {Math.round(s.usdcShareOfGross * 100)}% of the total and is mostly market-maker churn; its net is negative on Ethereum because holders redeem there and mint natively elsewhere, while its circulating supply on all chains grew.
diff --git a/src/app/page.tsx b/src/app/page.tsx index 03edbbe..2e0f4a8 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -3,7 +3,7 @@ import { formatUsdCompact, formatNumberCompact, formatPercent } from '@/lib/form import { getWeeklyFlows } from '@/lib/weeklyFlows'; import { formatWeek, fourWeekStats, weeklySeries } from '@/lib/flowStats'; import { getChainTVL, toLiquidityCells } from '@/lib/chainSupply'; -import { combinedSource } from '@/lib/dataSource'; +import { cachedAsOf, combinedSource } from '@/lib/dataSource'; import { MetricCard } from '@/components/ui/MetricCard'; import { TopHoldersChart } from '@/components/dashboard/TopHoldersChart'; import { FlowCharts } from '@/components/flows/FlowCharts'; @@ -37,11 +37,11 @@ export default async function DashboardPage() { const stats = fourWeekStats(flows.rows); // formatPercent keeps one decimal of a percent: anything that rounds to 0.0% is flat. const trend = stats.volumeTrend === null ? null : Math.round(stats.volumeTrend * 1000) / 1000; - const prefix = flows.dataSource === 'mocked' ? '~' : ''; + const prefix = flows.dataSource === 'cached' ? '~' : ''; const totalTvlUsd = chainRows.reduce((sum, r) => sum + r.tvlUsd, 0); const tvlSource = combinedSource(chainRows); - // MetricCard adds '~' itself only when fully mocked; a mixed total needs it too. - const tvlPrefix = tvlSource === 'estimated' && chainRows.some((r) => r.dataSource === 'mocked') ? '~' : ''; + // MetricCard adds '~' itself only when fully cached; a mixed total needs it too. + const tvlPrefix = tvlSource === 'estimated' && chainRows.some((r) => r.dataSource === 'cached') ? '~' : ''; return (
- Five issuers, {formatUsdCompact(s.grossUsd)} of mints and redemptions on Ethereum. In the Fed's two easing episodes the two largest stablecoins
+ Five issuers, about $1T of mints and redemptions on Ethereum.{' '}
+ That is {formatTrillions(s.grossUsd)} after removing {formatUsdCompact(s.cctpBridgedUsd)} of Circle CCTP transfers, which only move USDC between chains; counting them gives {formatTrillions(s.grossWithCctpUsd)}. In the Fed's two easing episodes the two largest stablecoins
grew faster than their own norm in 2024 (USDC {sd(zDuring(r, 'USDC', 0))}, USDT {sd(zDuring(r, 'USDT', 0))}) while the Treasury-backed funds ran below theirs (BUIDL {sd(zDuring(r, 'BUIDL', 0))}, OUSG {sd(zDuring(r, 'OUSG', 0))}). 2025 only
half repeated it: BUIDL fell further ({sd(zDuring(r, 'BUIDL', 1))}, the weakest window in its history) and USDT stayed above average ({sd(zDuring(r, 'USDT', 1))}), but USDC fell back to average and OUSG recovered. With two episodes, this
is a pattern to watch, not evidence of cause.
diff --git a/src/components/dashboard/ChainBreakdown.tsx b/src/components/dashboard/ChainBreakdown.tsx
index a620728..7692a2d 100644
--- a/src/components/dashboard/ChainBreakdown.tsx
+++ b/src/components/dashboard/ChainBreakdown.tsx
@@ -1,7 +1,7 @@
'use client';
import type { Chain, ChainTVL } from '@/lib/types';
-import { combinedSource } from '@/lib/dataSource';
+import { cachedAsOf, combinedSource } from '@/lib/dataSource';
import { DataSourceBadge } from '@/components/ui/DataSourceBadge';
import { formatUsdCompact, formatPercent } from '@/lib/format';
@@ -26,16 +26,16 @@ const CHAIN_COLORS: Record
Chain breakdown
-
- Ethereum only. Names come from a manual address registry, then Blockscout public tags; unlabeled wallets show their address. + Ethereum (OUSG and USDY) and Stellar (USDY). Names come from a manual address registry, then Blockscout public tags; unlabeled wallets show their address.
+ Unavailable: the holder sources did not respond and no last good value is stored. +
+ )} + {GROUPS.map(({ chain, token, label }) => { + const rows = data.filter((d) => d.token === token && d.chain === chain).sort((a, b) => b.balance - a.balance); if (!rows.length) return null; const total = rows.reduce((sum, d) => sum + d.tvlUsd, 0); return ( -| - {token} ({formatUsdCompact(total)} in top {rows.length}) + {label} ({formatUsdCompact(total)} in top {rows.length}) | Balance | Value | @@ -43,12 +57,12 @@ export function TopHoldersChart({ data }: TopHoldersChartProps) {
|---|---|---|
| @@ -62,7 +76,7 @@ export function TopHoldersChart({ data }: TopHoldersChartProps) { {prefix}{formatUsdCompact(row.tvlUsd)} |
- |