USD yield-token support batch, August 2026: plan
Approved by Fred 2026-08-03 (chat). Evidence base: docs/plans/usd-yield-token-coverage-survey-2026-08-verified.md (all venue sizes verified first-hand 2026-08-03). Ships as ONE PR against staging.
Approved scope (the "act now" list)
New asset profiles (2):
- AA_FalconXUSDC, the Pareto senior tranche lending USDC to FalconX (0xC26A6Fa2C37b38E549a4a1807543801Db684f99C), CONDITIONAL on the redemption-rights gate below. wFalconX (0x4614F7A56A3Eb83b2Ff9fA4B4b9575B28Fb68644) folds into the same profile as a second address of the same exposure.
- USD3 by 3Jane (0x056B269Eb1f75477a8666ae8C7fE01b64dD55eCc), ERC-4626 over USDC.
Portfolio token registry rows, book USD, class variable_rate (8 addresses): AA_FalconXUSDC*, wFalconX*, USD3, srUSDe (Strata), sUSDD (staked USDD), stUSDS (0x99CD4Ec3f88A45940936F469E4bB72A2A701EEB9), wsrUSD (0xd3fD63209FA2D55B07A0f6db36C2f43900be3094) and its underlying srUSD (0x09D4214C03D01F49544C0448DBE3A27f768F2b34). (*conditional on the FalconX gate; on failure these two become declared exclusions instead.)
Declared-exclusion rows (071/073 pattern, book NULL):
- apyUSD (0x38EEb52F0771140d10c4E9A9a72349A329Fe8a6A): yield is STRC preferred dividends; sister token apxUSD depegged 2026-06-04 and 2026-06-25.
- sUSDat (0xD166337499E176bbC38a1FBd113Ab144e5bd2Df7): same STRC exposure; on-chain rate 0.9195 (below par) on 2026-08-03.
Rate history series (refresher entries + one-time backfills), 7 series: AA_FalconXUSDC*, wFalconX* (derived from the same vault), USD3, srUSDe, sUSDD, stUSDS, wsrUSD/srUSD. Exclusion rows get NO series.
No Dune price-mirror additions (no liquid secondaries; conserves credits).
Deferred (tracked in a GitHub issue, NOT in this PR): mM1-USD, siUSD, strUSD, savUSD, v-wmtUSDC (watchlist); USTB, mGLOBAL, JAAA, USCC, VBILL (parked with the Aave-Horizon venue decision); mF-ONE (registry+series if a tracked wallet ever borrows against it); wJAAA promotion trigger at $5M; Dune mirror follow-up for any token that develops real DEX depth.
Gates and decisions already made
- FalconX redemption gate: if redemption at face value is whitelist-only while transfers are free (the apyUSD test), then NO profile and NO USD rows for the pair; declared exclusions instead; finding goes in the PR body. Rest ships.
- Icons: approved sources only (brand repo, web3icons). Missing icon = neutral placeholder + a "missing icons" list in the PR body. Never hand-drawn.
- USD3 profile approved despite young age (Fred's call, 2026-08-03).
Work breakdown (agent-executed)
D1 Verification + wiring spec (blocks everything): FalconX gate verdict with on-chain evidence; for each series token the exact contract + function for the rate read, verified at latest AND at a months-old block (archive RPC); wFalconX wrap target pinned. D2 Migration scripts/sql/074-usd-yield-batch-tokens.sql: portfolio_tokens rows + declared exclusions. Conventions: additive, idempotent, forward-only, lower-cased addresses, decimals read on-chain, GRANTs to the app role (migrations run as owner; the 035/041/052 lesson). Plus runtime updates if the registry loader needs them (src/lib/portfolio/registry.ts, buckets.ts). D3 Rate series: refresher entries following the existing token-yields pattern + per-token backfill scripts modeled on scripts/backfill-sgho.ts / backfill-susds.ts. Backfill depth: token genesis (or earliest readable block). Scripts are ADDED but NOT RUN against the DB in this PR; running them is a server step post-merge. D4 USD3 asset profile: src/data/asset-narratives.ts entry (tone reference: the PST entry; plain product language; NO em-dashes anywhere in user-facing copy; no CSS help cursor), assets-table/refresher row, icon registry entries, docs page updates. D5 FalconX profile: same touchpoints, only if D1 gate passes. Runs after D4 (same files). D6 Docs: this plan + the survey land in docs/plans/; update the relevant docs pages (portfolio.md, data-pipeline.md, database.md as applicable) in the SAME PR; npm run docs:build (vitepress) must pass as the dead-link check. D7 GitHub issue for the deferred set (full table from the survey). D8 Review + fix + verify: profiles reviewed by Fable agents; migration, adapters, backfills, docs reviewed by Opus agents (financial soundness + 1st/2nd/3rd-order effects). Fixes applied, then: typecheck/build, unit tests, vitepress build all green. Review findings get posted to the PR.
Server steps (post-merge, NOT part of this PR's execution)
- Staging deploy auto-applies 074 (additive; no prerendered page reads the new rows, so no prerender-migrate deadlock).
- Verify 074 actually landed in the intended shape, before anything else.
ON CONFLICT DO NOTHINGcannot promote a row that already exists, and the weeklyportfolio-tokens-diff --approveflow writes rows that are not in git, so a pre-existing row would silently keep its old shape:SELECT address, symbol, decimals, book, class, wallet_tracked, rate_source, source, status FROM onchain_credit.portfolio_tokens WHERE chain_id = 1 AND address IN (USD3, sUSDD, stUSDS, wsrUSD, srUSDe, AA_FalconXUSDC, wFalconX, sUSDat);and check all eight against the shapes pinned insrc/lib/portfolio/buckets.test.ts. - Run the R6 backfill repair for any wallet already holding one of the four re-booked tokens (USD3, sUSDD, stUSDS, wsrUSD). They move EXCLUDED → USD, classification is read-time off the latest registry row, and stored rows keep the values they were written under, so a pre-migration leg carries
book = 'EXCLUDED'andvalue_redemptionNULL and would otherwise appear to spring into existence at the migration date with no matching flow (R6). Find them byaccounting_assetinportfolio_position_snapshots, then run the repair per wallet, the same procedure the 071 and 073 headers point at, with the same caveat: it rewrites that wallet's stored history rather than patching it. If the query returns nothing, record that as the check having been made. - Run the new backfill scripts once (run-cron pattern, drained window):
backfill-usd3.ts,backfill-srusde.ts,backfill-susdd.ts,backfill-stusds.ts,backfill-wsrusd.ts.backfill-wsrusd.tsis ~1,850 archive windows with no per-window retry, so a transient RPC blip skips a window and exits non-zero; re-running is idempotent and fills only the gaps. - Run the two shared follow-ups, which close the column shape the per-token backfills leave open (they write 6 of the 8 columns the live refresher does):
backfill-token-supply.ts(fillstotal_supplyfrom theblock_at_snapshoteach row already carries; only touches NULLs) thenbackfill-apy-30d.ts(pure DB, recomputesapy_30dfor every token). - Verify each new series has rows and the refresher appends on the next cycle.
- Confirm /portfolio values a wallet holding each new token (live + historical). Note the four new rows carry a REDEMPTION mark only: the batch adds no Dune mirror rows, so
value_marketis NULL for their legs and the market-mode toggle omits them (074's header records why).
Verification bar (before the PR opens)
- Every rate read re-verified against the survey's on-chain values (e.g. USD3 convertToAssets 1.1679, stUSDS 1.0673, wsrUSD 1.0913 on 2026-08-03).
- Unit tests use magnitude-unique fixtures (no vacuous regex asserts).
- No em-dashes in any user-facing string added by this PR.
git statusreviewed before commit; no blindgit add -A.