Risk-adjusted yield tracking and ranking for yield-bearing stablecoins and curated lending opportunities. Computes APY from deterministic on-chain reads, curated DeFiLlama pools, protocol-native yield APIs, price history, and benchmark-derived fallbacks; scores each coin via the Pharos Yield Score (PYS); and serves a dedicated /yield page plus a stablecoin-detail yield section. That detail section now renders for any asset with a live published ranking row, even when the coin itself is not statically marked yieldBearing (for example USDC/USDT lending opportunities or XAUT's curated Yo Protocol venue).
Methodology Versioning
- Current methodology version:
v8.19 - Public changelog page:
/methodology/yield-changelog/ - Canonical source:
shared/lib/yield-methodology-version.ts
Yield versions are bumped when APY source resolution, source arbitration, history semantics, PYS scoring logic, or score-affecting publication rules change.
Yield v8.19 adds Royco Dawn structured-tranche opportunities as protocol-native Yield Intelligence rows. Senior and junior rows publish under royco-dawn:<chainId>:<marketId>:senior / royco-dawn:<chainId>:<marketId>:junior, attach to the tracked underlying stablecoin when the deposit token resolves, and use an opportunity-level tranche Safety Score for PYS while leaving the underlying Report Card Safety Score unchanged.
Yield v8.18 corrects base-asset source ownership for GHO, DOLA, and USDN/SMARDEX routing. GHO and DOLA no longer publish wrapper APY through base rows, and usdn-smardex now uses its exact single-exposure DeFiLlama pool after the asset was corrected from NAV-token to rebase semantics.
Yield v8.17 tracks sdusd-dtrinity as the first-class dTRINITY dStake yield wrapper. The existing Ethereum/Fraxtal weighted sdUSD DeFiLlama source now publishes under sdusd-dtrinity, while dusd-dtrinity stops owning wrapper yield metadata and parent-side sdUSD history is suppressed by the ownership-handoff cleanup path.
Yield v8.16 fixes fpi-frax, silk-shade-protocol, and isc-international-stable-currency NAV-token metadata so they route through NAV-appreciation coverage, and adds rate-derived proxy coverage for cgusd-cygnus-finance and usdn-noble.
Yield v8.15 gives cetes-etherfuse a curated protocol-api:etherfuse-cetes-current-issuance APY source from Etherfuse's current Stablebond issuance rate. This replaces the selected USD price-derived NAV fallback when Etherfuse is reachable, so MXN/USD FX movement is no longer interpreted as CETES yield. The MXN benchmark still prefers Banxico SIE SF43936, but when BANXICO_TOKEN is missing or Banxico fails, the daily benchmark cron can use the Etherfuse CETES current issuance rate as an explicit degraded proxy fallback (isFallback: true, isProxy: true).
Yield v8.14 ships the public adapter manifest endpoint at /api/yield-adapter-manifest as the canonical machine-readable source list for every yield-bearing asset. Each entry carries stablecoinId, coinSymbol, family (onchain / protocol-api / defillama / defillama-auto / rate-derived / price-derived / intentional-gap), nullable sourceKey, optional sourceKeyPattern, label, optional chain/project hints, lifecycle (active / quarantined / intentional-gap / experimental), quarantineReason when lifecycle is quarantined, the current methodologyVersion label, and a methodology-snapshot updatedAt. sourceKey is populated only when the entry has an exact runtime key that can join to rankings, decision rows, or /api/yield-history?sourceKey=...; runtime-resolved DeFiLlama variant rows and disabled/quarantined readers use sourceKey: null plus sourceKeyPattern instead of publishing synthetic keys. Scoring, source resolution, history semantics, and publication rules are unchanged in v8.14; the bump tracks the new public contract only.
Yield v8.14 also exposes a bounded public decision ledger on every /api/yield-rankings row through decisionLedger, persists pys_at_publish / safety_at_publish / variance_at_publish snapshots on each yield_history row, and migrates retained alternates from the legacy alternatives_json blob to a sibling yield_source_decision_alternatives table. decisionLedger.selectedReasonCode is a stable enum (best-by-confidence-and-apy, deterministic-preferred, curated-over-discovered, tier-preference, tvl-floor, freshness-tiebreaker, fallback, no-alternatives) so UI surfaces render display text from a client-side template. Up to 2 alternates are retained per row, sorted by absolute APY30d delta; each carries a coded rejectionReasonCode (thinner, stale, lower-confidence, rewards-only, smaller, unspecified). When a source switch's previous source is present in the evaluated candidate set, decisionLedger.apy30dDeltaFromPrevious carries the selected source's APY30d delta versus that previous source. A new nullable retention_reason column on yield_source_decisions tags each row as trend (source switch, any evaluated-source anomaly, or rejected higher-confidence source) or audit; audit-only rows and old null rollout rows that cannot be inferred as trend are pruned at 30 days while trend rows persist for long-running analytics. The legacy alternatives_json blob is still written for one cycle of co-existence.
The /yield route-level Yield Sources board is presentation-only: it derives chosen sources and retained alternate source observations from the existing /api/yield-rankings payload and does not change APY source resolution, source arbitration, scoring, or methodology versioning.
Yield v8 activates nested source-risk penalties for PYS and same-confidence source arbitration while preserving neutral behavior for missing evidence and old payloads. The publisher derives the penalty from measured reward share, source depth, source age, source changes, bootstrap observation count, and sourced venue tier where those inputs exist. Public source-risk fields are nested under sourceRisk.*; calibration artifacts may normalize those fields internally, but public API examples must not present flattened row fields such as top-level sourceRiskPenalty.
Yield v8.13 expands the benchmark registry to GBP, JPY, MXN, BRL, AUD, and CAD, ending the universal USD T-Bill fallback for those non-USD-pegged stablecoins. The same release derives sourceRisk.sourceRiskScore from the resolved source-risk penalty when no upstream value is provided (ending the 100% null rate documented in the v8 production-sample calibration) and lands the first reviewed venue tier batch: Aave V3, Compound V3, and Spark move to low (currently a no-op penalty), and Morpho Blue moves to medium (+0.15 penalty contribution). Remaining tracked venue families stay unknown with rationale/evidence fields until the next monthly coverage audit. AED, IDR, TRY, ZAR, and SGD continue to fall back to USD until a stable public feed is wired for each.
Rankings provenance now carries source-native freshness for derived sources:
- active tracked yield variants now project eligible native/wrapper sources onto their active parent stablecoin as linked alternative candidates, so routes such as
yBOLDandsBOLDcan appear underBOLDwithout removing the variants' own rows or creating no-source coverage - curated auto-discovery now includes Felix and Sovryn exact lending venues plus a Loopscale tGBP exact pool pin when those rows pass the normal APY, TVL, and Safety Score gates
sourceObservedAt/sourceAgeSecondsreflect the actual latest observation backing the ranking, not just the cron run timecomparisonAnchorObservedAt/comparisonAnchorAgeSecondsare included when APY is derived from a prior anchor, such as price-derived and on-chain exchange-rate calculations- dTRINITY sdUSD now owns its curated TVL-weighted DeFiLlama pool group across the Ethereum and Fraxtal dStake vaults; dUSD no longer publishes wrapper yield as parent-owned metadata
sUSDe,sUSDS,sDAI,sfrxUSD,scrvUSD, andsavUSDnow own the wrapper APY rows that used to publish throughUSDe,USDS,DAI,frxUSD,crvUSD, andavUSD- Solayer
sUSDnow has rate-derived Treasury fallback coverage through the yield manifest, while newly added reward-bearing account or restricted strategy assets stay out of runtime yield until a reliable APY source is wired cgusd-cygnus-financeandusdn-noblenow have rate-derived proxy coverage through the yield manifest- Parent-side wrapper history for those five base assets is filtered immediately from
/api/yield-historyand purged on the hourly sync path, so the post-handoff discontinuity is explicit rather than silently grandfathered sUSDaiis now a first-class tracked yield-bearing NAV token, so baseUSDaino longer inherits the USD.AI savings venue throughYIELD_VARIANT_MAP- Risk-bearing wrappers with materially different holder exposure now own their yield rows directly when they are tracked as separate assets:
stcUSD,sAID,msY, and K3sBOLDno longer publish through their base stablecoin rows stUSDSuses a direct ERC-4626 exchange-rate reader, while Aave UmbrellastkGHOis inventoried as an intentional runtime-yield gap until reliable reward APY telemetry is availablescrvusd-curvenow owns the dedicated scrvUSD current-rate on-chain reader based on the Yearn V3 profit-unlock stream, while legacy parent-sidecrvusd-curverows are suppressed- PYS now keeps raw APY as the base yield term, then adds 25% of the row's benchmark spread before applying the safety and consistency penalties
- supplemental protocol families now keep asset-scoped source identity for same-chain markets (notably Aave V3), preventing cross-coin cache collapse and preserving per-asset alternative-source coverage
- protocol-native lending venue readers such as Aave V3 and Compound V3 stay in the curated Tier 2.5 lane rather than inheriting Tier 1 deterministic wrapper precedence, so a lower-yield supplemental market does not displace a stronger native wrapper purely by source family
- wrapper resolution can now pin the intended DeFiLlama project in addition to chain and address, which keeps shared wrapper tokens fail-closed even when the same wrapper appears across multiple single-exposure venues
- read-time
data-stalewarnings now follow source cadence: hourly families use the shared three-cycle publish threshold, supplemental families wait 6 hours so they do not false-positive inside their 4-hour refresh window, andprice-derivedrows wait 36 hours because they are backed by dailysupply_historysnapshots - published
lending-opportunitysuggestions now require observable venue TVL and a size floor ofmax(existing absolute floor, 0.1% of the tracked stablecoin supply), so tiny markets do not surface as the live recommendation for large base assets - published
lending-opportunitysuggestions now explicitly exclude Resolv /USR,stUSR, andwstUSR-linked venues across both supplemental protocol APIs and auto-discovered DeFiLlama lending pools, so impaired wrapper ecosystems do not surface as recommended base-asset yield routes - yield-bearing assets with no live runtime source now publish as explicit intentional manifest gaps rather than appearing as covered entries with zero strategies; this currently includes issuer/account-product assets such as
a7a5-old-vector,bfusd-binance,home-homecoin, andgusd-gate,bd-basedollar,pusd-polaris,trusd-tori, and the Tradable private-credit notes usg-tangentis not markedyieldBearing: USG is the borrowable stablecoin, while sUSG is the separate savings wrapper that accrues protocol revenue- Zephyr yield is attributed to
zys-zephyr-protocol, the tracked ZYS yield-share NAV wrapper. Basezsd-zephyr-protocolstays non-yield-bearing. - explicit and deterministic lending candidates are ignored unless the target asset is in the active stablecoin universe, so pre-launch metadata cannot surface on the live yield leaderboard before launch
- deterministic on-chain bootstrap seed rows still persist exchange-rate anchors, but they are excluded from rolling APY, excess-yield, stability, and PYS stats once real on-chain APY samples exist
Tracked Coins
Every stablecoin with flags.yieldBearing: true in shared/lib/stablecoins/registry.ts is inventoried by the yield manifest. Live cron resolution operates on the active subset (status !== "pre-launch"), while pre-launch or intentionally uncovered assets remain visible to operators through explicit manifest entries instead of silently disappearing from coverage accounting. Pre-launch status is declared in the asset's per-coin file under shared/data/stablecoins/coins/*.json, loaded through shared/data/stablecoins/coins.generated.json, and skipped by explicit lending override publication until the per-coin entry moves into the active lifecycle. The sync also supports deterministic custom sources for select non-yield-bearing coins, exact-pool curated overrides for select non-stablecoin assets, plus automatic lending pool discovery for tracked non-gold/silver stablecoins rated C- or above (safety score >= 50), including coins already flagged yieldBearing. yieldConfig is used when present to provide canonical source/type labels; auto-discovered lending rows synthesize protocol-derived labels when the source is defillama-auto. Tracked savings wrappers own their own runtime pool/on-chain readers and history. When a tracked wrapper has a live native/wrapper yield source, the publisher may also expose that source on the active parent stablecoin with a linked-variant:<variantId>:<sourceKey> key for comparison and coverage context; this does not mark the parent yieldBearing purely because the wrapper exists, and third-party lending-opportunity rows are not projected from variants to parents.
| Field | Type | Description |
|---|---|---|
yieldSource | string | Human-readable source name (e.g. "Ethena staking"). Optional for auto-discovered lending rows |
yieldType | YieldType | Mechanism classification (see below). Optional for auto-discovered lending rows |
Yield Types
| Type | Label | Description |
|---|---|---|
lending-vault | Native | Deposited into lending protocols or vault strategies |
rebase | Rebase | Token supply rebases to distribute yield |
fee-sharing | Fee Share | Protocol fees passed to holders |
lp-receipt | LP Receipt | LP position wrapped as stablecoin |
nav-appreciation | NAV | Token price appreciates as backing grows |
governance-set | Gov. Set | Yield rate set by governance vote |
lending-opportunity | Lending Opp. | Auto-discovered best lending market from the curated allowlist |
structured-tranche | Tranche | Senior/junior tranche opportunity over an underlying yield market |
Labels and styles are centralized in shared/lib/classification.ts (YIELD_TYPE_LABELS, YIELD_TYPE_STYLES), both typed as Record<YieldType, ...> so adding a new variant without updating the maps is a compile error.
Source-Aware APY Resolution
The sync cron resolves APY for each coin using a priority-ordered strategy. Deterministic and curated rows can coexist; the cron then applies confidence-weighted arbitration to choose the primary row while retaining alternatives.
Tier 1: On-Chain Reads
Reads protocol state directly via eth_call RPC. The main path reads vault exchange rates and computes APY from the 7-day rate delta; special-case estimators can also derive APR from raw protocol state.
Config: ON_CHAIN_RATE_CONFIGS in worker/src/cron/yield-config.ts
interface OnChainRateConfig {
stablecoinId: string;
chain: string;
contract: string; // vault contract address
selector: string; // 4-byte function selector
decimals: number;
inputAmount: string; // hex-encoded input (e.g. 1e18)
}
Currently configured for 11 generic vaults (all use selector 0x07a2d13a — convertToAssets(uint256)):
| Coin ID | Wrapper | Contract | Chain |
|---|---|---|---|
susde-ethena | sUSDe | 0x9D39...7497 | Ethereum |
iusd-infinifi | siUSD | 0xDBDC...bCB | Ethereum |
susds-sky | sUSDS | 0xa393...fbD | Ethereum |
stusds-sky | stUSDS | 0x99cd...eB9 | Ethereum |
sdai-sky | sDAI | 0x83F2...BEeA | Ethereum |
sfrxusd-frax | sfrxUSD | 0xcf62...5b6 | Ethereum |
bold-liquity | yBOLD | 0x9F43...a3d8 | Ethereum |
usdf-falcon | sUSDf | 0xc8cf...4b0 | Ethereum |
susn-noon | sUSN | 0xE24a...B91D | Ethereum |
ustb-superstate | USTB | ERC-4626 (6 decimals) | Ethereum |
thbill-theo | thBILL | ERC-4626 (6 decimals) | Ethereum |
scrvusd-curve is intentionally quarantined from this generic Tier 1 reader because its trailing 7-day convertToAssets(1e18) delta understated Curve's current scrvUSD savings APY. It uses the scrvUSD special-case estimator below instead. reusd-re-protocol is also quarantined from the generic reader for now because its current convertToAssets(1e18) probe does not return a usable value, so it continues to rely on non-deterministic source paths until a protocol-specific deterministic adapter is added.
APY formula:
apy = ((rate_now / rate_7d_ago) ^ (365.25 / 7) - 1) * 100
Even when Tier 1 succeeds, the cron still falls through to Tier 2 to collect additional wrapper/native DeFiLlama rows. If no previous exchange rate exists yet (first sync), Tier 1 emits a seed row with currentApy: 0, apyBase: null, and the current exchangeRate so the rate is persisted in yield_history. This breaks the bootstrapping deadlock: without the seed, the on-chain source would never resolve because it needs a 7-day-old rate, but the rate was never stored because the source never resolved. Subsequent syncs (7+ days later) will find the seed rate and compute a real APY. Once real on-chain APY samples exist, those bootstrap seeds are excluded from rolling apy7d, apy30d, excessYield, yield stability, and PYS calculations because they are anchor placeholders, not observed zero-yield periods.
Special-case Tier 1 estimator: Curve scrvUSD
scrvUSD uses a protocol-specific on-chain current-rate reader instead of the generic 7-day exchange-rate reader. The vault is a Yearn V3 vault that distributes newly reported crvUSD rewards through a profit-unlock stream, so the current APY shown by Curve and DeFiLlama is the daily-compounded value of the active unlock rate rather than the trailing 7-day pricePerShare delta.
Reads:
scrvUSD.totalAssets()scrvUSD.totalSupply()scrvUSD.profitUnlockingRate()scrvUSD.fullProfitUnlockDate()
Formula:
sharesPerSecond = profitUnlockingRate / 1e12 / 1e18
apr = sharesPerSecond * 31_536_000 / totalSupply
apy = ((1 + apr / 365) ^ 365 - 1) * 100
When fullProfitUnlockDate is no longer in the future, the current unlock rate is treated as 0. The row publishes under source key onchain:scrvusd-curve:scrvusd-current-rate, leaving the old parent-owned crvUSD history unmixed. The curated DeFiLlama pool 5fd328af-4203-471b-bd16-1705c726d926 remains an alternative/fallback source.
Special-case Tier 1 estimator: LUSD / B.Protocol Stability Pool
LUSD also has a deterministic on-chain estimator for the Liquity v1 Stability Pool via B.Protocol. This row is intentionally conservative and is labeled B.Protocol Stability Pool (LQTY only).
Reads:
stabilityPool.getTotalLUSDDeposits()on EthereumcommunityIssuance.totalLQTYIssued()on Ethereum- CoinGecko
liquityUSD price
Formula:
remainingLqtyRewards = max(0, 32_000_000 - totalLQTYIssued)
dailyIssuanceFactor = 1 - 0.5^(1 / 365)
apr = remainingLqtyRewards * dailyIssuanceFactor * lqtyPriceUsd / totalLUSDDeposits * 365 * 100
Caveat: This source captures only the projected LQTY incentive stream. It deliberately excludes ETH liquidation gains, so it is a lower-bound estimate of the full Stability Pool return.
Tier 2: DeFiLlama Yields API (Multi-Source)
Collects all matching DL pools per coin via matchAllDlPools (three layers). Each unique pool found becomes a separate row in yield_data. The row with the highest currentApy per coin is marked is_best = 1; others are is_best = 0.
Before matching, the worker now preserves any single-exposure pool that is either:
- a normal DeFiLlama stablecoin pool (
stablecoin === true), or - explicitly relevant via
YIELD_POOL_MAP, or - explicitly relevant via a configured wrapper symbol in
YIELD_VARIANT_MAP, or - explicitly relevant via
EXPLICIT_YIELD_SOURCE_POOL_MAP
This keeps wrapper pools like fxSAVE and msY eligible even when DeFiLlama marks them stablecoin: false, and it also preserves exact curated non-stablecoin venues such as XAUT's isolated Yo Protocol market.
Layer 1 — Static map: YIELD_POOL_MAP maps Pharos ID to a DL pool UUID. Filters for exposure === "single". Finds the native/primary yield source. If a mapped UUID is missing from the DL payload, the sync logs [yield-sync] Pool UUID ... not found in DL response, falling through and continues to Layer 2/3 fallback matching.
2026-05-22 source corrections: usdn-smardex now uses the exact SMARDEX USDN DeFiLlama single-exposure pool after its navToken flag was corrected to false, while a7a5-old-vector is represented as an intentional yield gap until a reliable RUB benchmark or issuer APY source is wired.
Layer 2 — Variant map: YIELD_VARIANT_MAP maps to a wrapper/savings pool symbol and can also pin the wrapper chain, address, and preferred DeFiLlama project. Resolution prefers (chain, address, project) when configured, then (chain, address), and only falls back to symbol on an unambiguous chain-scoped match. Filters for exposure === "single" only (stablecoin flag intentionally relaxed, since savings wrappers like fxSAVE are not flagged stablecoin = true in DeFiLlama).
Layer 3 — Base-symbol fallback: Used only when both static maps miss. Resolution first tries underlying-token address matches and only uses chain-scoped .includes() symbol fallback when the remaining candidate set is unambiguous. Symbols shorter than 4 characters are excluded from .includes() matching to prevent false positives (e.g., "USD" matching everything). Filters for exposure === "single" and stablecoin === true. Ambiguous fallback candidates are dropped instead of guessed.
Exact weighted pool groups: YIELD_WEIGHTED_POOL_GROUPS can collapse multiple exact DeFiLlama pool UUIDs into one TVL-weighted APY row when Pharos tracks one protocol asset but the yield wrapper is deployed as chain-isolated, non-fungible vaults. This is currently used for sdusd-dtrinity, where Ethereum and Fraxtal sdUSD dStake pools publish under one synthetic DeFiLlama source key.
Exact-pool overrides: EXPLICIT_YIELD_SOURCE_POOL_MAP can publish curated non-stablecoin venues when the pool UUID, project, chain, and symbol all match and the usual APY / TVL quality gates still pass. This is currently used for xaut-tether via Yo Protocol. These overrides stay outside generic gold/silver auto-discovery, which prevents basket venues such as Multipli's mixed-RWA pools from being treated as single-asset commodity yield sources. On 2026-05-13, XAUT gained a second curated venue on Lista Lending (BSC) alongside the existing Yo Protocol pin, and PAXG was added as a non-yield-bearing exact-pool entry on Hydration (Polkadot).
Variant mapping: YIELD_VARIANT_MAP entries supply labels and pool matching for wrapper/savings tokens:
| Base Coin | Wrapper | Purpose |
|---|---|---|
| BOLD (269) | yBOLD | Liquity Stability Pool wrapper |
| USBD (253) | sUSBD | BIMA savings wrapper |
| reUSD (339) | stUSR | Resolv staking wrapper |
| AZND (327) | loAZND | Mu Digital locked wrapper |
| Neutrl USD (346) | sNUSD | Neutrl staked USD |
| Avalon USDa (220) | sUSDa | Avalon staked USDa |
| infiniFi USD (298) | siUSD | infiniFi savings |
| Falcon USD (246) | sUSDf | Falcon Finance savings |
| Unitas (283) | sUSDu | Unitas savings |
| Yuzu USD (344) | syzUSD | Yuzu savings |
| fxUSD (168) | fxSAVE | Concentrator savings |
| Flying Tulip ftUSD | sftUSD | Flying Tulip staking |
| Hermetica USDh | sUSDh | Hermetica staking wrapper |
| Saturn USDat | sUSDat | Saturn staking vault |
YIELD_VARIANT_MAP is only used when the yield-bearing wrapper is not already modeled as its own tracked asset. As of May 13, 2026, sUSDe, sUSDS, sDAI, sfrxUSD, scrvUSD, sUSDai, stcUSD, sAID, msY, K3 sBOLD, and savUSD are tracked directly, so their base assets no longer resolve through those wrapper paths. Added 2026-05-13: gtUSDC (Gauntlet/Morpho), spUSDC and spUSDT (Spark Savings), sGHO (Aave SM), yBOLD, and yvUSDC (Yearn) now own their own native pool sources. Added 2026-05-22: base gho-aave no longer inherits the tracked sGHO source, and base dola-inverse-finance no longer publishes the untracked sDOLA wrapper source. AA_FalconXUSDC remains NAV/price-derived until a usable single-exposure nonzero APY source is available.
APY, base/reward split, pool TVL, and pool UUID are all taken directly from the DL response.
Tier 2.5: Protocol-Native Yield APIs
For coins whose native savings path is published by the protocol itself but is not exposed as a usable DeFiLlama pool, the sync can ingest a curated protocol-owned earn endpoint directly.
Protocol-specific lending-market readers that query protocol state directly also live in this tier. Even when the transport is an on-chain call, these rows are treated as curated protocol-native venues rather than Tier 1 deterministic native-wrapper sources, so arbitration still prefers a stronger native wrapper or savings source when one exists.
This tier can also carry explicit wrapper-over-wrapper native sources when the upstream venue is a distinct managed wrapper around a tracked native yield token. K3 sBOLD now owns this path directly as sbold-k3-capital, while base BOLD keeps its Yearn yBOLD native-wrapper source.
Royco Dawn markets also live in this tier. The supplemental source lane reads the Dawn market explorer API and emits separate structured-tranche rows for the senior and junior vaults in each verified market above the local Royco TVL floor. Rows use the stable source keys royco-dawn:<chainId>:<marketId>:senior and royco-dawn:<chainId>:<marketId>:junior; the tranche share tokens are not added to the stablecoin registry. Identity resolution maps the Royco deposit token back to a tracked underlying stablecoin by chain/address first, with configured wrapper-variant addresses such as Neutrl sNUSD attached to their tracked parent when the wrapper is not a first-class stablecoin row.
Published lending-opportunity suggestions also apply an explicit venue exclusion for Resolv / USR, stUSR, and wstUSR-linked markets. They now also require observable venue TVL and must clear a size floor equal to the higher of the existing absolute floor and 0.1% of the tracked stablecoin's current supply. These filters are scoped to the suggestion layer for base assets such as USDC or USDT; they do not remove native tracked yield assets from the broader methodology inventory.
Current tracked optional adapters:
| Coin ID | Source | Endpoint |
|---|---|---|
scrvusd-curve | Curve Savings crvUSD current-rate | on-chain scrvUSD Yearn V3 profit-unlock reader |
usbd-bima | BIMA savings (sUSBD) | https://bima.money/api/earn/pools?network=Ethereum&user=0x0000000000000000000000000000000000000000 |
cetes-etherfuse | Etherfuse CETES current issuance | Etherfuse first-party Next data at https://app.etherfuse.com/bonds/cetes |
lusd-liquity | B.Protocol LQTY-only source | deterministic on-chain LQTY-only source reader |
usyc-hashnote | Hashnote USYC | Hashnote protocol API |
usdy-ondo-finance | Ondo USDY oracle | on-chain Ondo oracle with historical anchor rows |
zys-zephyr-protocol | Zephyr Scanner ZYS returns | https://zephyrprotocol.com/api/v1/historicalreturns |
The BIMA adapter uses the protocol's published Ethereum earn feed, selects the USBD savings row, maps amountTVL to sourceTvlUsd, and uses the higher of unboostedAPR / boostedAPR as the current APY. Low-signal rows with negligible TVL or effectively zero APR are dropped instead of being published as meaningful yield. These rows are source-keyed as protocol-api:bima-susbd and participate in the same confidence-weighted arbitration as other curated sources.
The Royco Dawn adapter maps APY ratios to percent APY, tranche vault TVL to sourceTvlUsd, and market coverage/utilization/status/drawdown plus share-token addresses into nested sourceRisk fields. Royco rows carry venueRiskTier: "unknown" until a reviewed venue-risk audit assigns a sourced tier. They also carry investability flags for withdrawal constraints, verified listing status, and whether the row is senior protected or junior first-loss. KYC/access booleans are nullable; the current Dawn market payload does not expose explicit KYC or jurisdiction restriction fields, so those penalties apply only if future source evidence populates them.
The Etherfuse CETES adapter reads the current CETES Stablebond issuance from Etherfuse's first-party Next data and maps interestRateBps / 100 to APY. It publishes protocol-api:etherfuse-cetes-current-issuance with the current token amount as the exchange-rate observation when available. This source prevents MXN NAV appreciation plus USD/MXN FX movement from being annualized by the generic price-derived fallback.
The Zephyr adapter reads the protocol's historical-return API and publishes the one-day effective APY only for zys-zephyr-protocol. This keeps base ZSD non-yield-bearing while still showing the native ZYS yield-share return on /yield.
Opportunity-Level Tranche Safety
Royco Dawn rows use a row-level tranche Safety Score for PYS instead of blindly reusing the underlying stablecoin's Report Card Safety Score. The raw underlying score remains the ceiling for senior rows unless a future methodology explicitly allows first-loss protection to create an uplift. The current implementation does not uplift senior rows; it only subtracts tranche-specific penalties. Junior rows start with a large first-loss penalty, then add utilization, coverage, market-status, drawdown, TVL, withdrawal, access, and venue-posture penalties. At high utilization, junior rows should usually score materially below the underlying stablecoin.
Published Royco source-risk metadata includes trancheSide, underlyingSafetyScore, trancheSafetyScore, trancheSafetyPenalty, marketCoverageRatio, marketMinCoverageRatio, marketUtilizationRatio, marketUtilizationLimitRatio, marketDrawdownRatio, marketStatus, marketTvlUsd, trancheTvlUsd, withdrawalDelaySeconds, kycRequired, and accessRestricted when available. API hydration recomputes those scores from the live underlying Report Card snapshot before ranking, so a stale cached Royco score is not allowed to overwrite current stablecoin safety data.
Tier 3: Price-Derived APY
For navToken coins and explicit PRICE_DERIVED_FALLBACK_IDS. Derives APY from price appreciation in the existing supply_history table using the oldest available anchor between 7 and 45 days. These fallback-only paths are now explicit manifest strategies rather than implicit behavior hidden behind navToken.
apy = ((price_now / price_anchor) ^ (365.25 / lookbackDays) - 1) * 100
Zero new API calls — reuses cached price data. Falls through if no price history exists or if the coin has fewer than 7 days of priced history.
Tier 3 as additional source: For navToken and PRICE_DERIVED_FALLBACK_IDS coins, Tier 3 also runs when Tier 2 found sources but they all report 0% APY. This handles DL pools that exist but have stale/broken yield data (e.g., a tiny Aave lending market matched via Layer 3 symbol fallback). The price-derived source is added alongside the DL source, and is_best picks the higher-APY winner.
Known limitation: Price-derived cannot capture yield for tokens that distribute dividends as new tokens while maintaining a fixed $1.00 NAV (e.g., BUIDL, YLDS). These tokens use the rate-derived tier instead (see below).
Tier 4: Rate-Derived APY
For dividend-distributing tokens (maintain $1.00 NAV, pay yield as new token mints), rebase proxies, and T-bill-backed funds whose yield mechanically tracks short-term rates. Configured via RATE_DERIVED_CONFIGS in yield-config.ts.
apy = max(0, benchmarkRate - spreadBps / 100)
Uses the structured benchmark cache refreshed daily by fetch-tbill-rate. USD defaults to the 3-month Treasury yield (DGS3MO), but the resolver can switch to a peg-native benchmark when one exists. EUR rows use the ECB's official 3-month compounded €STR series. CHF rows use delayed public SAR3MC (3-month compounded SARON) from SIX. If a benchmark fetch fails, the cron retains the last known market benchmark when available and marks provenance as degraded instead of immediately snapping back to the hardcoded default. Because the public SIX compound-rate file is delayed, CHF benchmark recordDate can trail the fetch date by one business day even on a healthy run.
Configured tokens:
| Token | Spread (bps) | Rationale |
|---|---|---|
| BUIDL | 20 | BlackRock fund, 0.20% management fee |
| cgUSD | 35 | Cygnus Finance T-bill proxy, net of 0.35% protocol fee |
| YLDS | 50 | Figure Markets, T-bill rate - 50 bps formula |
| mTBILL | 0 | Midas, tracks T-bill rate directly |
| USDN | 0 | Noble M0 T-bill rebase proxy |
| OUSG | 50 | Ondo US Government Bond fund, 0.50% management fee |
| BENJI | 20 | Franklin Templeton FOBXX gov MMF, 0.20% mgmt fee |
| WTGXX | 25 | WisdomTree Government MMF Digital Fund, 0.25% mgmt fee |
| USTBL | 10 | Spiko US T-Bills MMF (UCITS), 0.10% TER |
| EUTBL | 15 | Spiko EU T-Bills MMF (UCITS), modeled net of 0.15%, EUR-denominated (€STR benchmark) |
| sUSD | 0 | Solayer sUSD, T-bill proxy |
| UKTBL | 15 | Spiko UK T-Bills MMF, modeled net of 0.15%, GBP-denominated |
| EURSAFO | 0 | Spiko Amundi Smart Cash overnight swap proxy, EUR-denominated |
| GBPSAFO | 0 | Spiko Amundi Smart Cash overnight swap proxy, GBP-denominated |
| EURSPKCC | 0 | Spiko cash-and-carry strategy proxy (EUR risk-free leg) |
Note: USTB and thBILL were previously rate-derived but have been promoted to Tier 1 ON_CHAIN_RATE_CONFIGS (ERC-4626 convertToAssets).
Rate-derived runs after Tier 3 in the resolution loop and participates in the is_best selection like any other source. For tokens that also have price-derived or DL sources, the highest-APY source wins.
Automatic Lending Pool Discovery (Wave 2)
For tracked non-gold/silver stablecoins rated C- or above (safety score >= 50), the sync cron can append the best lending pool from a curated protocol allowlist. This runs after the base four-tier resolution, so yield-bearing coins can also receive an additional defillama-auto source row when a distinct lending market passes filters. LUSD uses this to retain Aave as an alternative source alongside the deterministic B.Protocol estimate.
Allowlist (LENDING_PROTOCOL_ALLOWLIST in worker/src/cron/yield-config.ts):
| Tier | Protocols |
|---|---|
| Tier 1 | aave-v3, compound-v2, compound-v3, dolomite, sparklend, spark-savings, maple, yearn-finance |
| Tier 2 | fluid-lending, euler-v2, venus-core-pool, kamino-lend, morpho-v1, morpho-blue, pendle, curve-llamalend, exactly, flux-finance, gains-network, lazy-summer-protocol, moonwell-lending, silo-v2 |
| Tier 3 | justlend, openeden-usdo, multipli.fi, jupiter-lend, stables-labs-usdx, benqi-lending |
| Tier 4 | radiant-v2, fraxlend-v2, clearpool, centrifuge, sturdy-v2, goldfinch, truefi, lagoon, liqwid, lista-lending, loopscale, more-markets, navi-lending, overnight-finance, smardex-usdn, vesper, felix-cdp, sovryn-dex |
| Tier A (2026-03-25, >$50M TVL) | wildcat-protocol, tectonic, upshift, venus-flux, avantis, cap, resupply, zerobase-cedefi |
| Tier B (2026-03-25, $10M–$50M TVL) | convex-finance, yo-protocol, clearpool-lending, 3jane-lending, hyperlend-pooled, zest-v2, liquity-v2, echelon-market, termmax, beefy, gearbox |
| Tier C (2026-05-13, $10M+ TVL audit) | autofinance, neverland, metrom, mystic-finance-lending, bitway, frankencoin |
Discovery logic: Filters DL pools by exposure === "single", stablecoin === true, project in allowlist, and reserved-pool exclusion. Resolution prefers underlying-token address matches over symbol matches. Symbol-only matching is allowed only when the coin remains unambiguous after chain scoping; otherwise the candidate is dropped. Current quality gates require apy >= 0.1, an ecosystem-aware absolute TVL floor ($100K standard, $25K on the smaller-chain allowlist), and for tracked stablecoins a supply-relative floor of 0.1% of the asset's current circulating supply.
Observable size requirement: Published lending-opportunity suggestions require venue-level sourceTvlUsd. Protocol-native lending readers that cannot attach measurable venue TVL are omitted from published suggestion coverage until they can prove size.
Explicit venue exclusion: Even when a pool clears the generic quality gates above, published lending-opportunity suggestions exclude venues whose DeFiLlama poolMeta or supplemental source label identifies them as Resolv / USR, stUSR, or wstUSR linked.
Yield type: lending-opportunity — distinguishes these from native yield coins on the frontend. The direct Aave v3 on-chain supply-rate path uses the same classification so rankings/cache schema validation stays aligned across deterministic and auto-discovered lending rows.
Data source: defillama-auto — distinguishes from static-mapped defillama pools.
Eligibility evaluated dynamically: If a coin's safety score drops below 50, it stops receiving auto-discovered yield data. If it rises back to 50 or above, it starts automatically.
Explicit deterministic edge cases: AUTO_LENDING_POOL_MAP can also pin a small number of exact-symbol, single-asset lending markets for coins that would otherwise be blocked by ambiguous matching or by the generic safety gate. These rows still pass the same pool-shape and source-quality checks; the bypass is coin-specific and documented rather than global.
Current May 2026 deterministic pins include reusd-resupply, xusd-babelfish, and usda-anzens. xusd-babelfish has a coin-specific safety bypass because the tracked asset is Rootstock-only and Sovryn is the canonical Bitcoin-side venue; the pool still has to pass the normal DeFiLlama shape, APY, and TVL checks.
Commodity-specific exact venues: EXPLICIT_YIELD_SOURCE_POOL_MAP is a separate lane for curated non-stablecoin assets such as xaut-tether. Unlike AUTO_LENDING_POOL_MAP, these rows are not broadening the generic stablecoin discovery universe; they only publish the exact named pool after matching the expected venue metadata.
The monthly coverage audit now treats both AUTO_LENDING_POOL_MAP and EXPLICIT_YIELD_SOURCE_POOL_MAP as exact covered DeFiLlama surfaces. Its high-TVL gap report intentionally focuses on unsupported protocol families instead of re-flagging already-allowlisted markets that the runtime already supports dynamically.
Pharos Yield Score (PYS)
Risk-adjusted ranking (0–100) that balances yield magnitude against source risk, stablecoin safety, and consistency.
Formula (computePYS() in shared/lib/yield-scoring.ts):
benchmarkSpread = apy30d - benchmarkRate
effectiveYield = max(0, apy30d + benchmarkSpread * 0.25)
sourceRiskPenalty = clamp(sourceRisk.sourceRiskPenalty ?? 1, 1, 2.5)
rowUtility = effectiveYield / sourceRiskPenalty
riskPenalty = max(0.5, (101 - safetyScore) / 20)
yieldEfficiency = rowUtility / (riskPenalty ^ 1.75)
sustainabilityMult = max(0.3, 1.0 - apyVarianceScore)
PYS = clamp(round(yieldEfficiency * sustainabilityMult * scalingFactor), 0, 100)
Components:
| Component | Range | Meaning |
|---|---|---|
benchmarkRate | depends on row | Row-level benchmark selected from the benchmark registry |
benchmarkSpread | unbounded | apy30d - benchmarkRate; positive means the row clears its local benchmark |
effectiveYield | >= 0 | Raw APY plus 25% of benchmark spread, floored at zero before the safety divisor |
sourceRisk.sourceRiskPenalty | 1–2.5 after resolution | Nested source-risk multiplier derived from measured source evidence. Missing/invalid values are neutral (1); values below 1 clamp to 1 and above 2.5 clamp to 2.5 |
rowUtility | >= 0 | effectiveYield / sourceRiskPenalty, used before the safety curve |
safetyScore | 0–100 | Report card overall score. DEFAULT_SAFETY_SCORE (40) for unrated coins |
riskPenalty | 0.5–5.05 | Raw safety penalty before the power curve is applied |
riskPenalty^1.75 | ~0.30–17.01 | Effective divisor used by PYS after the steeper safety curve is applied |
apyVarianceScore | 0–1 or null | Coefficient of variation of 30-day APY samples, clamped to [0, 1]. Returns null if < 2 samples or mean ≈ 0 (` |
scalingFactor | 8 | Global constant (PYS_SCALING_FACTOR in constants.ts) tuned after the steeper safety curve |
Returns 0 when apy30d <= 0 or the benchmark-aware effectiveYield is non-positive.
The shared scorer exposes the intermediate values (benchmarkSpread, benchmarkAdjustment, effectiveYield, sourceRiskPenalty, rowUtility, riskPenalty, adjustedRiskPenalty, yieldEfficiency, sustainabilityMult). Frontend breakdown components should pass the nested sourceRisk.sourceRiskPenalty when they surface v8 source-risk details; the final PYS value is always served by the API.
Source-Risk, Rank Attribution, and Neutral Policy
Yield v8 exposes optional sourceRisk and rankChangeAttribution shapes in shared API schemas. PYS and source arbitration consume the nested source-risk penalty; source-board, source-sheet, detail-page, and DEWS consumers can also read populated source-risk and rank-attribution evidence. Missing evidence remains neutral:
sourceRiskmay be omitted,null, or partially populated on ranking, history, and alt-source rows.- Public API source-risk fields are nested under
sourceRisk.*(sourceRisk.sourceRiskPenalty,sourceRisk.rewardShare, and so on). Do not document or consume flattened public fields such as top-levelsourceRiskPenalty; calibration scripts that ingest saved payloads must normalize from the nested API contract before analysis. sourceRisk.sourceRiskPenaltyis the active v8 source-risk multiplier. It is derived from reliablerewardShare,sourceDepthRatio,sourceAgeSeconds,sourceSwitchCount30d,observationCount30d, and sourcedvenueRiskTierinputs where available. Missing,null, or invalid evidence is equivalent to a neutral multiplier of1; values below 1 clamp to 1 and values abovePYS_MAX_SOURCE_RISK_PENALTY(2.5) clamp to 2.5.- Frontend source-risk driver labels use the same scoring thresholds:
reward-heavywhenrewardShare > 0.5,thin source depthwhensourceDepthRatio < 0.001,stale sourcewhensourceAgeSeconds > 6h,limited historywhen0 < observationCount30d < 7, andsource changedwhen the selected source changed versus the prior published snapshot orsourceSwitchCount30d > 0. - The
/yielddepth lens is explanatory context, not guaranteed executable capacity. It classifies rows only when bothsourceRisk.sourceDepthRatioandsourceTvlUsdare present:deepis>= 1%of tracked stablecoin supply,moderateis0.1%to< 1%,thinis< 0.1%, and missing TVL or supply-relative depth isunknown. - DeFiLlama rows use the shared DeFiLlama input metadata timestamp/age when an individual resolved row does not carry
sourceObservedAt, so provenance and PYS source-age penalties are based on the same freshness evidence. sourceRisk.venueRiskTier: "unknown",null, or omitted means the venue tier has not been sourced. Unknown tier is neutral, not a hidden high-risk default.worker/src/cron/yield-sync/source-risk.tsowns the sparse typedyieldRiskConfighome for reviewed venue tiers. The first reviewed batch (v8.13) assignsaave-v3,compound-v3, andsparklendtolow(currently a no-op on penalty derivation because no negative bonus exists today) andmorpho-bluetomedium(+0.15 contribution on affected rows). Maple, Yearn, Pendle, and Beefy remainunknownwith rationale/evidence fields until approved methodology evidence assigns a non-unknown tier. Review is tied to the monthly yield coverage audit; guessed venue penalties are explicitly out of scope.sourceRisk.sourceRiskScoreis the 0–100 display normalization of the resolved source-risk penalty. As of v8.13, when no upstream score is provided, the publisher fills it viacomputeSourceRiskScoreFromPenalty(penalty = 1.0→0;penalty = PYS_MAX_SOURCE_RISK_PENALTY(2.5) →100). The score is informational and does not change PYS — PYS continues to consume thesourceRiskPenaltydirectly. Rollback compatibility: legacy v7.48 payloads still resolve to a neutral penalty when source-risk is absent, and an explicit upstreamsourceRiskScorevalue still wins over derivation.sourceRisk.sourceDepthRatio,sourceRisk.rewardShare,sourceRisk.sourceAgeSeconds,sourceRisk.observationCount30d,sourceRisk.sourceSwitchCount30d,sourceRisk.deploymentPlace,sourceRisk.venueProtocol,sourceRisk.venueChain, andsourceRisk.investabilityFlagsare populated only when supported by existing rows, provenance, publication-generation evidence, or sourced yield-risk config. Missing precision stays missing instead of being guessed from labels.- v8 rollout calibration evidence is split between
docs/process/archive/yield-pys-v8-calibration-2026-05-13.mdfor source-risk golden fixtures anddocs/process/archive/yield-pys-v8-production-sample-calibration-2026-05-13.mdfor the current production snapshot. The production snapshot was regenerated after publication generationyield-1778700012emitted populated public rankingsourceRisk.*fields, so it records live source-risk coverage, including the priorsourceRiskScorenull-rate, plus rank churn, capped rows, distribution, movers, and non-USD cohorts. The v8.13 delta — benchmark registry expansion,sourceRiskScorederivation rule, and the first venue tier batch — is documented indocs/process/archive/yield-pys-v8-13-calibration-2026-05-15.md. - External
lending-opportunityrows do not modify the base stablecoin's Safety Score, Dependency Risk, Resilience, or overall report-card grade. They may inform opportunity-level yield risk labels or DEWS yield anomaly inputs only through explicitly versioned consumer methodology; report-card modifiers still require a separate report-card methodology update. - Report-card consumers treat yield source-risk as no-op today.
shared/lib/report-card-yield-risk.tsnames the no-op reasons asexternal-lending-opportunity,external-structured-tranche,missing-yield-config,missing-source-risk, andsource-risk-unconsumed; the helper may normalize the source-risk payload, but it does not emit score modifiers, Resilience caps, or Dependency Risk caps. - Any future report-card score impact from yield source-risk requires a report-card methodology update and matching report-card timeline entry before runtime scoring can consume those fields.
- DEWS methodology v5.99 consumes a bounded subset of populated structured yield evidence inside the existing Yield Anomaly sub-signal: reward-heavy rows, thin or stale sources, source switches, high source-risk penalties, reviewed high-risk venue tiers, and rank-attribution drivers for source-risk or source-switch moves. Missing, malformed, or neutral source-risk fields and legacy warning-only rows remain explicit no-ops.
Rollback compatibility is part of the contract. Production-shaped v7.48 payloads without publication, publishedRank, liveRank, sourceRisk, or rankChangeAttribution remain valid. With no nested source-risk penalty, v8 resolves the same neutral penalty (1) and keeps the benchmark-aware v7 scoring path equivalent.
Supporting Metrics
| Metric | Formula | Description |
|---|---|---|
yieldStability | 1 - CV(30d samples) | 0–1, higher = more consistent. Null if < 2 samples or mean ≈ 0 (` |
yieldToRisk | apy30d / (101 - safetyScore) | Raw yield per unit of risk |
excessYield | apy30d - benchmarkRate | 30-day average APY above the row's selected benchmark |
effectiveYield | max(0, apy30d + 0.25 * excessYield) | Benchmark-aware yield term used by PYS before source-risk, safety, and consistency penalties |
rowUtility | effectiveYield / sourceRisk.sourceRiskPenalty after neutral/clamp resolution | Source-risk-adjusted utility term used before the safety penalty |
apy7d | Timestamp-filtered 7d average | 7-day trailing APY (uses recorded_at >= now - 7d, not proportional slicing) |
apy30d | Simple average of 30d samples | 30-day trailing APY |
variance30d | Standard deviation of 30d APY samples | APY volatility measure |
Benchmark Registry
Yield Intelligence now uses a small benchmark registry instead of a single global T-bill field.
Benchmarks currently supported:
| Key | Label | Primary source | Notes |
|---|---|---|---|
USD | USD 3M T-Bill | FRED DGS3MO, then Treasury.gov yield curve XML | Default benchmark and backward-compatible top-level riskFreeRate; Treasury.gov is used as a fallback when FRED is unavailable |
EUR | EUR 3M compounded €STR | ECB Data API (EST/B.EU000A2QQF32.CR) | Native benchmark for EUR pegs; retained-last-market fallback covers feed outages |
CHF | CHF 3M compounded SARON | SIX delayed SAR3MC download | Public feed is delayed by one business day; not labeled as a proxy |
GBP | GBP SONIA (overnight, proxy) | FRED IUDSOIA mirror | Used as a proxy for "3M compounded SONIA"; full compounding can be added later |
JPY | JPY overnight call (TONA proxy) | FRED IRSTCB01JPM156N (uncollateralized overnight call rate) | Used as a TONA-equivalent proxy |
MXN | MXN CETES 28d | Banxico SIE API (series SF43936), then Etherfuse CETES current issuance fallback | BANXICO_TOKEN enables the official Banxico feed; when missing/failing, Etherfuse is marked isFallback and isProxy. CETES (Etherfuse) is itself a 28d CETES tokenization, so the spread is ~0% — see "Self-reference caveat" below |
BRL | BRL SELIC over | BCB SGS API (series 11) | No auth required; daily |
AUD | AUD 3M interbank (RBA proxy) | FRED IR3TIB01AUM156N | 3-month interbank as a proxy for the RBA cash rate target |
CAD | CAD overnight repo (CORRA proxy) | Bank of Canada Valet API (series V122530) | Overnight repo; CORRA-equivalent |
SGD | SGD SORA (unavailable) | — | Reserved for a future MAS SORA feed; SGD pegs fall back to USD until a stable public source is wired |
Source URLs:
https://fred.stlouisfed.org/graph/fredgraph.csv?id=DGS3MO
https://home.treasury.gov/sites/default/files/interest-rates/yield.xml
https://data-api.ecb.europa.eu/service/data/EST/B.EU000A2QQF32.CR?lastNObservations=5&format=csvdata
https://indexdata.six-group.com/pro/oauth/token
https://indexdata.six-group.com/pro/api/report-download
https://indexdata.six-group.com/download/saron/h_sar3mc_delayed.csv
https://fred.stlouisfed.org/graph/fredgraph.csv?id=IUDSOIA
https://fred.stlouisfed.org/graph/fredgraph.csv?id=IRSTCB01JPM156N
https://fred.stlouisfed.org/graph/fredgraph.csv?id=IR3TIB01AUM156N
https://www.banxico.org.mx/SieAPIRest/service/v1/series/SF43936/datos/oportuno
https://app.etherfuse.com/bonds/cetes
https://api.bcb.gov.br/dados/serie/bcdata.sgs.11/dados/ultimos/1?formato=json
https://www.bankofcanada.ca/valet/observations/V122530/json?recent=1
Stored as: cache table, key "risk_free_rates", with the legacy USD-only key "risk_free_rate" still written for compatibility.
Fallback: RISK_FREE_RATE_FALLBACK = 3.75% applies to USD only. Other benchmarks prefer a retained last-known source-backed value when available; otherwise they remain unavailable and rows fall back to USD when selection requires it. MXN additionally tries Etherfuse CETES current issuance as a degraded proxy when Banxico is unavailable.
Self-reference caveat (CETES): Benchmarking the CETES (Etherfuse) yield against the MXN CETES rate produces a ~0% spread, which under-rewards the asset. The MXN benchmark is wired here and may use Etherfuse as a fallback proxy when Banxico is unavailable; a future tokenized-treasury rule can override per-source by selecting the next-tier-up safe rate in the same currency. The same pattern applies to EUTBL (vs €STR) and to any future UKTBL (vs SONIA).
Currencies still falling back to USD: AED, IDR, TRY, ZAR, SGD (and any other peg currency not listed above). These remain as benchmarkSelectionMode: "fallback-usd" until a stable public feed is wired for each.
Selection rules:
- USD is the default benchmark for the stack and remains the top-level
riskFreeRate/provenance.benchmark - Yield rows switch to a peg-native benchmark when the stablecoin's benchmark currency is supported
- Rate-derived configs can explicitly override the benchmark key when the asset's benchmark should differ from the peg currency
- When a native benchmark is unavailable, the row falls back to USD and records
benchmarkSelectionMode: "fallback-usd"
Usage: The hourly core yield sync resolves excessYield from 30-day average APY and rate-derived APY against each row's selected benchmark. Detail cards, hero chips, and history charts render that row-level label. The /yield scatter plot now always keeps a benchmark frame visible: homogeneous scopes use the shared visible benchmark, while mixed scopes use the default USD benchmark as an orientation frame and rely on row-level tags for the exact hurdle.
Warning Signals (Phase 2)
yield-helpers.ts::detectWarningSignals() runs in the sync cron and stores baseline results in the warning_signals column of yield_data. Rankings responses also add a read-time freshness signal. Frontend-visible warning keys are:
| Signal | Condition | Meaning |
|---|---|---|
yield-spike | currentApy > 2% AND currentApy / apy30d > 2.0 | Sudden 2× jump vs. 30d average (absolute floor: 2% APY) |
yield-divergence | currentApy > medianApy * 3 | 3× the market median |
negative-trend | apy30d > 1% AND currentApy < apy30d * 0.7 | 30% decline from average (absolute floor: 1% baseline) |
reward-heavy | apyReward / apy > 0.8 | 80%+ from incentives, not base yield |
tvl-outflow | TVL dropped > 20% from prev week | Capital leaving the protocol |
zero-yield | currentApy === 0 AND apy30d > 0.5% | Yield dropped to zero but had recent activity |
data-stale | Hourly source families are older than 3 sync-yield-data intervals (currently 180 min); supplemental Aave/Compound + protocol-API rows are older than 6 hours; price-derived rows are older than 36 hours from their latest supply_history snapshot | Yield data is older than the expected cadence for that source family |
All frontend surfaces (leaderboard, detail section, history chart) format warning signals via the shared formatYieldWarningSignal() function in src/lib/yield-constants.ts, which maps known signal keys to human-readable labels and falls back to hyphen-to-space conversion for unknown signals.
At rankings cache-build time, sync-yield-data decorates rows with the read-time-only data-stale signal when the resolved source observation exceeds its freshness window. Hourly publication families use the shared three-interval threshold (currently 180 minutes; STALE_THRESHOLD_MS), supplemental protocol-API and optional Aave/Compound rows use a 6 hour threshold (SUPPLEMENTAL_SOURCE_STALE_THRESHOLD_MS) so the hourly publisher does not flag them stale during a normal 4 hour supplemental cycle, and price-derived rows use a 36 hour threshold because their source observations come from daily supply_history snapshots (PRICE_DERIVED_STALE_THRESHOLD_MS). This signal is included in cached rankings responses but is not written back to yield_data.
The sync also performs a confidence-aware cross-source arbitration pass before is_best is chosen:
- deterministic sources (
onchain,rate-derived) outrank curated protocol-native and DeFiLlama rows - curated DeFiLlama rows outrank discovered lending opportunities and fallback-derived rows
- non-positive APY rows cannot outrank positive rows
- within the same confidence tier, arbitration compares source-risk-adjusted row utility (
effectiveYield / sourceRiskPenalty) after source-risk penalty resolution before falling back to raw current APY - materially divergent discovered or fallback rows can be rejected when a higher-confidence canonical source disagrees by more than 35%
- a
canonical-zero-vs-positiveanomaly is flagged when a high-confidence source reads 0% but a lower-confidence source reports > 1% APY
This selection behavior is surfaced in row-level provenance metadata on /api/yield-rankings.
Database Schema
Migrations: the current checked-in schema lives in worker/migrations/0000_baseline.sql. Historical pre-squash yield schema changes (initial table creation, warning signals, multi-source keys, and source-aware history rows) are tracked in worker/migrations/MANIFEST.md.
yield_data — Current Snapshot (one row per coin per source)
CREATE TABLE yield_data (
stablecoin_id TEXT NOT NULL,
source_key TEXT NOT NULL, -- DL pool UUID, "onchain:<stablecoin_id>", "protocol-api:<family>", "price-derived", or "rate-derived"
symbol TEXT NOT NULL,
current_apy REAL NOT NULL,
apy_base REAL,
apy_reward REAL,
apy_7d REAL NOT NULL,
apy_30d REAL NOT NULL,
yield_source TEXT NOT NULL,
yield_type TEXT NOT NULL,
source_pool TEXT, -- DL pool UUID
source_tvl_usd REAL,
data_source TEXT NOT NULL, -- "onchain" | "defillama" | "defillama-auto" | "protocol-api" | "price-derived" | "rate-derived"
safety_score REAL,
safety_grade TEXT,
pharos_yield_score REAL,
yield_to_risk REAL,
excess_yield REAL,
yield_stability REAL, -- 0-1
apy_variance_30d REAL,
apy_min_30d REAL,
apy_max_30d REAL,
exchange_rate REAL, -- current vault rate (Tier 1 only)
exchange_rate_prev REAL, -- 7d-ago vault rate
warning_signals TEXT, -- JSON array of active signal keys (migration 0033)
is_best INTEGER NOT NULL DEFAULT 1, -- 1 = highest-APY source per coin
updated_at INTEGER NOT NULL,
PRIMARY KEY (stablecoin_id, source_key)
);
Indices: idx_yield_pys (PYS DESC), idx_yield_apy (apy_30d DESC), idx_yield_best (stablecoin_id, is_best).
Multi-source behavior: Coins with both a native pool and a savings wrapper get multiple rows. The confidence-weighted arbitration pass marks the selected row with is_best = 1; non-selected rows remain available as alternatives with is_best = 0. This also covers mixed source types such as LUSD, where a deterministic on-chain B.Protocol row can coexist with an auto-discovered Aave lending row. Eligible tracked wrapper variants also project linked native/wrapper sources to the active parent with linked-variant:<variantId>:<sourceKey> source keys, while third-party lending opportunities stay attached only to the asset that owns the lending row. Rankings queries filter WHERE is_best = 1. Alt-source rows are read separately and attached as altSources[] in the cached API response. After each batch write, stale rows for coins refreshed in that run are purged so old primary sources cannot linger alongside the new winner.
Generation-aware publisher fields added by migration 0125_yield_publication_generations.sql are nullable so old rows and rollback payloads remain readable:
publication_generation_id TEXT,
publication_state TEXT -- "staged" | "published" | "failed"
Rows are replaced in the same D1 batch as the yield-rankings cache publication. The batch first performs a cache compare-and-swap, then guards the current/history row replacement, decision evidence write, and generation publish on yield-rankings.updated_at = startSec. Successful runs write rows with a publication_generation_id and publication_state = 'published'. If preflight validation, cache publication, or an older-run cache CAS fails, the generation is marked failed without replacing the previous published D1 snapshot.
yield_history — Historical Data Points
CREATE TABLE yield_history (
stablecoin_id TEXT NOT NULL,
source_key TEXT NOT NULL,
recorded_at INTEGER NOT NULL, -- Unix seconds
is_best INTEGER NOT NULL DEFAULT 0,
apy REAL NOT NULL,
apy_base REAL,
apy_reward REAL,
exchange_rate REAL,
source_tvl_usd REAL,
data_source TEXT NOT NULL,
warning_signals TEXT,
yield_source TEXT,
yield_type TEXT,
publication_generation_id TEXT,
publication_state TEXT,
PRIMARY KEY (stablecoin_id, source_key, recorded_at)
);
Indices: idx_yield_hist_coin (stablecoin_id, recorded_at DESC), idx_yield_hist_coin_source (stablecoin_id, source_key, recorded_at DESC), idx_yield_hist_best (stablecoin_id, is_best, recorded_at DESC).
Retention: 365 days. Older rows are pruned at the end of each sync run.
Legacy migration behavior: historical pre-migration rows are preserved as source_key = 'legacy-best'. Same-source trailing metrics only reuse these rows when the coin currently has a single resolved source and the legacy source family matches the current source family. If the current winner changed source family (for example price-derived → rate-derived), the new source starts a clean series instead of inheriting mixed legacy history.
Estimated volume: source-aware hourly snapshots. sync-yield-data runs once per hour and writes one history row per resolved source, so annual row volume depends on the active source set rather than a fixed one-row-per-coin estimate.
yield_publication_generations and yield_source_decisions
Migration 0125_yield_publication_generations.sql adds a compact generation ledger:
yield_publication_generationsrecordsgeneration_id,started_at, publicationstate, ranking/source row counts,published_atorfailed_at, and failure/debug metadata.yield_source_decisionsrecords the selected source per stablecoin for a generation, selected confidence/data-source details, selected 30d APY and score, previous best source, source-changed flag, rejected-candidate count, and a bounded alternatives JSON payload.
The generation ID format is yield-<startSec>. Public payloads expose only published generations. Legacy rows with no generation ID remain readable through the existing cutoff fallback, which is what keeps rollback to old Worker versions compatible with the additive schema.
Cron Jobs
sync-yield-data
Schedule: 20 * * * * (every hour on a dedicated post-DEX trigger)
Files: worker/src/cron/sync-yield-data.ts orchestration + helper modules under worker/src/cron/yield-sync/
Execution flow:
- Filter the active stablecoin universe where
flags.yieldBearing === truefor the base four-tier resolution, then evaluate explicit exact-pool overrides plus auto-discovery across eligible active non-gold/silver coins - Fetch DeFiLlama pools (
https://yields.llama.fi/pools) — circuit-breaker protected - Load the latest cached supplemental-source snapshot from
sync-yield-supplemental; stale or missing supplemental cache is treated as optional loss, not a publisher hard-stop - Fetch on-chain exchange rates via
eth_callforON_CHAIN_RATE_CONFIGSentries, unless the deterministic lane is in a cooldown window after consecutive masked all-fail runs; protocol-specific hourly on-chain readers such as scrvUSD's current-rate source run during per-coin resolution and fall back to curated rows if unavailable - Read the cached benchmark registry from D1, with USD as the default benchmark and EUR / CHF available when fetched successfully
- Compute safety scores via shared helper
computeSafetyScoresSnapshot(db, { includeNavTokens: true, outputMode: "map" }); this helper now reuses the same peg-analytics path as/api/report-cardsso live peg deviation inputs stay aligned across Safety Score and Yield Intelligence. Treat the helper's explicit degraded result as degraded input, and also classify coverage below the minimum ratio as degraded even when the helper itself succeeded - Resolve APY for each yield-bearing coin (Tier 1 → 2 → 3 → 4, potentially multiple sources per coin), reusing cached supplemental families instead of live-fetching them on the publisher path, then append auto-discovered lending rows for any remaining eligible tracked coins and linked parent rows for eligible tracked yield variants
- Batch preload source-aware
yield_historydatasets (previous best source, previous TVL by source, 30d APY history by source) and legacy best-history fallbacks, chunking stablecoin ID lists so each D1 statement stays under the 100-bind limit - Compute 7d/30d APY, variance, and PYS per resolved source using source-specific history instead of coin-level mixed history
- Run confidence-weighted arbitration to select
is_bestper coin and flag source changes vs. the prior best source - NaN/Infinity guard: clamp PYS, variance, and stability to finite values before DB write
- Stage a publication generation and preflight the rankings payload
- Publish the
yield-rankingscache payload withpublication.status = "published"plus row-levelpublishedRank, upsertyield_data(all sources), insertyield_historypoints, insert selected-source decision evidence, and mark the generationpublishedin one guarded D1 batch; if cache publication fails or CAS skips because a newer cache exists, mark only the generationfailedand leave the previous published D1 rows in place - Purge stale rows for refreshed coins only on non-degraded successful publication so obsolete primary/alt sources are removed together, then scan
yield_datafor orphan coin IDs and delete those in chunkedIN (...)batches instead of a single largeNOT IN (...) - Prune
yield_historyolder than 365 days - Query best-source rows, fetch alt-source rows, attach as
altSources[], add read-timedata-stalewarning decoration using source-cadence freshness windows (three hourly publish cycles for hourly families; 6 hours for supplemental protocol-API and optional Aave/Compound rows; 36 hours forprice-derivedrows), and include top-level + per-row provenance in the cached rankings payload whenever the payload passes schema validation and the new payload has not collapsed severely versus the previous cache. Safety-degraded runs still publish fresh rankings when the payload is valid but skipreport_card_cache.
Degraded semantics: If computeSafetyScoresSnapshot() returns a degraded result, safety coverage is below the minimum ratio (0.75), the default USD benchmark is on a true fallback path (isFallback === true), the fallbackMode contains "retained" (indicating a benchmark fetch failure with last-known-good retention), the retained last-known-good USD benchmark is older than 48 hours, DeFiLlama pool inputs are unavailable, the direct DeFiLlama fetch payload is invalid or yields zero relevant stablecoin pools, all configured deterministic on-chain sources fail in the same cycle without full alternative coverage, a deterministic cooldown suppresses on-chain reads but coverage gaps reappear, or rankings publication fails schema/severe-shrink guards, sync-yield-data returns status: "degraded". Repeated deterministic all-fail runs that are fully masked by non-onchain coverage now arm a cooldown instead of burning the full deterministic path every hour. Stale or missing supplemental cache does not by itself degrade the hourly publisher; it only reduces optional source coverage. Retained benchmark metadata still appears in rankings provenance via provenance.benchmark.fallbackMode, including the last market-derived rate/date/source preserved across fallback streaks. Row-level benchmark provenance also exposes the selected benchmark key, label, rate, fallback state, and selection mode. Schema-invalid or severe-shrink runs skip cache overwrite. Safety-degraded runs continue to publish a fresh yield-rankings cache when the rankings payload is valid, but they still skip report_card_cache writes so the degraded condition remains visible without taking the public API offline.
Implementation stages:
yield-sync/sources.ts+yield-sync/pool-filter.ts: DL pool loading, wrapper-relevant pool filtering, on-chain reads, benchmark cache loading, price-derived, scrvUSD current-rate, and B.Protocol helpersyield-sync/resolve.ts: per-coin source resolution and auto-discovery candidate shapingyield-sync/evaluation.ts: source-aware history normalization, trailing metric computation, confidence arbitration, and source-change trackingyield-sync/publication.ts+yield-sync/rankings.ts: persistence helpers, rankings shaping, provenance/warning parsing, TVL-weighted median helper, and cache writesyield-sync/history.ts: batched history preloads plus stale/orphan cleanupsync-yield-data.ts: safety snapshot handling, orchestration glue, and degraded-mode policy
sync-yield-supplemental
Schedule: 25 */4 * * * (every 4 hours on a dedicated supplemental trigger)
Files: worker/src/cron/sync-yield-supplemental.ts + helper modules under worker/src/cron/yield-sync/
This best-effort cron owns the heavier optional families that used to run inline on the publisher path:
MorphoPendleYearn/KongBeefyCompound V3Aave V3
It fetches those families, serializes the resolved candidate set into a cache snapshot, and lets the hourly publisher consume that snapshot. Empty snapshots are treated as degraded and do not overwrite the previous cache.
Shared safety scores: The report-cards API handler doesn't cache results, so both yield sync and daily digest call the same shared safety-score pipeline. That helper now shares peg analytics with /api/report-cards, preventing rated coins with live price/peg coverage from falling back to NR inside yield rankings. It still uses the two-phase dependency approach (independent first, then CeFi-dependent).
Batch query policy: No per-coin query loops are used for the three high-volume yield_history reads (previous exchange rate, previous TVL, 30d APY history); these are loaded in chunked batches and indexed by stablecoin ID in-memory. Cleanup deletes also chunk ID lists to stay under D1's 100-bound-parameter cap.
fetch-tbill-rate
Schedule: 0 8 * * * (daily lightweight snapshot/fetch lane)
File: worker/src/cron/fetch-tbill-rate.ts
Fetches the benchmark registry used by Yield Intelligence:
- USD 3M Treasury yield from FRED
DGS3MO, falling back to Treasury.gov yield XML when FRED is unavailable - EUR 3M compounded €STR from the ECB Data API (
EST/B.EU000A2QQF32.CR) - CHF 3M compounded SARON (
SAR3MC) from SIX's delayed public download, fetched through the guest OAuth + report-download flow used by their public site - GBP SONIA daily proxy from FRED
IUDSOIA(v8.13) - JPY call-rate proxy from FRED
IRSTCB01JPM156N(v8.13) - MXN CETES 28-day from Banxico SIE (
SF43936), falling back to Etherfuse CETES current issuance as a degraded proxy when Banxico is unavailable (v8.15) - BRL SELIC overnight from BCB SGS series 11 (no auth) (v8.13)
- AUD interbank proxy from FRED
IR3TIB01AUM156N(v8.13) - CAD CORRA proxy from Bank of Canada Valet
V122530(v8.13)
Validated rates must stay within [-10, 20] so EUR / CHF support can tolerate negative-rate regimes. The cron writes the structured "risk_free_rates" cache and also mirrors USD into the legacy "risk_free_rate" key for compatibility.
When a fetch fails but a prior benchmark exists, the cache preserves the last market-derived benchmark fields (lastMarketRate, lastMarketRecordDate, lastMarketFetchedAt, lastMarketSource) across retained fallback streaks. That lets downstream yield provenance distinguish "still carrying the last market rate" from a hardcoded or proxy default.
API Endpoints
GET /api/yield-rankings
Cache-backed rankings written by sync-yield-data, with safetyScore, safetyGrade, yieldToRisk, and pharosYieldScore hydrated from the current report-card snapshot at API read time. This keeps Yield Intelligence aligned with /api/report-cards even when underlying safety inputs move between yield cron runs. If a ranking row has no matching live report-card snapshot, the API now retains the row and falls back to DEFAULT_SAFETY_SCORE (40) and grade NR instead of dropping coverage.
Cache profile: Standard (s-maxage=300, max-age=60)
Response shape:
{
"rankings": [
{
"id": "usde-ethena",
"symbol": "USDe",
"name": "Ethena USDe",
"currentApy": 12.4,
"apy7d": 11.8,
"apy30d": 10.2,
"apyBase": 10.2,
"apyReward": null,
"yieldSource": "Ethena staking (sUSDe)",
"yieldSourceUrl": "https://ethena.fi/",
"yieldType": "lending-vault",
"dataSource": "defillama",
"sourceTvlUsd": 5200000000,
"pharosYieldScore": 28,
"safetyScore": 65,
"safetyGrade": "B",
"yieldToRisk": 0.33,
"excessYield": 5.95,
"benchmarkKey": "USD",
"benchmarkLabel": "USD 3M T-Bill",
"benchmarkCurrency": "USD",
"benchmarkRate": 4.25,
"benchmarkRecordDate": "2026-03-25",
"benchmarkIsFallback": false,
"benchmarkFallbackMode": null,
"benchmarkSelectionMode": "native",
"benchmarkIsProxy": false,
"yieldStability": 0.82,
"apyVariance30d": 2.1,
"apyMin30d": 7.5,
"apyMax30d": 15.3,
"warningSignals": [],
"altSources": [
{
"sourceKey": "ee0b7069-...",
"yieldSource": "Concentrator (fxSAVE)",
"yieldSourceUrl": "https://fx.aladdin.club",
"yieldType": "lending-vault",
"currentApy": 9.1,
"apy30d": 8.8,
"sourceTvlUsd": 31000000,
"dataSource": "defillama",
"sourceRisk": {
"sourceRiskPenalty": 1.12,
"sourceDepthRatio": 0.004,
"rewardShare": 0.18,
"venueRiskTier": "unknown"
}
}
],
"provenance": {
"confidenceTier": "curated",
"selectionReason": "curated canonical source selected by confidence-weighted arbitration",
"sourceSwitch": false
},
"publicationGenerationId": "yield-1772000000",
"publishedRank": 3,
"liveRank": 3,
"sourceRisk": {
"sourceRiskPenalty": 1,
"sourceDepthRatio": 0.02,
"rewardShare": 0,
"sourceAgeSeconds": 900,
"venueRiskTier": "unknown"
}
}
],
"riskFreeRate": 4.25,
"benchmarks": {
"USD": { "key": "USD", "label": "USD 3M T-Bill", "currency": "USD", "rate": 4.25, "recordDate": "2026-03-25", "source": "fred-dgs3mo", "isFallback": false, "fallbackMode": null, "isProxy": false },
"EUR": { "key": "EUR", "label": "EUR 3M compounded €STR", "currency": "EUR", "rate": 1.9358, "recordDate": "2026-03-26", "source": "ecb-estr-3m", "isFallback": false, "fallbackMode": null, "isProxy": false },
"CHF": { "key": "CHF", "label": "CHF 3M compounded SARON", "currency": "CHF", "rate": -0.0539, "recordDate": "2026-03-25", "source": "six-sar3mc", "isFallback": false, "fallbackMode": null, "isProxy": false }
},
"scalingFactor": 8,
"medianApy": 4.21,
"updatedAt": 1772000000,
"publication": {
"generationId": "yield-1772000000",
"updatedAt": 1772000000,
"cutoffAt": 1772000000,
"schemaVersion": 1,
"status": "published"
},
"methodology": {
"version": "8.19",
"currentVersion": "8.19",
"changelogPath": "/methodology/yield-changelog/"
},
"_meta": { "updatedAt": 1772000000, "ageSeconds": 42, "status": "fresh" }
}
Default sort: pharos_yield_score DESC. altSources is an empty array for coins with only one yield source. medianApy is the TVL-weighted median of best-source apy30d values and is used by peer-reference warning heuristics. Generation-aware payloads include top-level publication and optional row-level publicationGenerationId; legacy rows remain valid when those fields are omitted. Rankings payloads may also include the standard Yield Intelligence methodology envelope.
Each best-source row and alt-source row can also include yieldSourceUrl. The worker resolves this from the curated yield-source link registry (worker/src/lib/yield-source-links.ts) and falls back to coin metadata links when no source-specific override exists.
Public source-risk evidence is nested under sourceRisk on selected rows and retained alternates. Missing, null, or venueRiskTier: "unknown" evidence is neutral; public examples must not expose flattened top-level fields such as sourceRiskPenalty or rewardShare.
The response includes a _meta freshness object (see Response Body Freshness) indicating data age and staleness status. The frontend uses this to power the StaleDataBanner on the yield page.
GET /api/yield-history
Historical APY data points for a single coin. Reads from yield_history directly. If a stored warning_signals payload is malformed, the API treats it as an empty array rather than failing the whole request.
Cache profile: Slow (s-maxage=3600, max-age=300)
| Param | Type | Default | Bounds | Description |
|---|---|---|---|---|
stablecoin | string | required | — | Pharos stablecoin ID |
days | integer | 90 | 1–365 | Lookback window |
mode | string | best | best, source | best for historically selected best-source rows; source requires sourceKey |
sourceKey | string | — | — | When present, returns source-specific history for that source key |
Response: Envelope with:
current: latest row in the returned window, ornullhistory: array sorted bydateASCwarning: optional handler warning when the freshness cutoff lookup falls backmethodology: standard methodology envelope for Yield Intelligence
Each history row includes:
dateapyapyBaseapyRewardexchangeRatesourceTvlUsdwarningSignalssourceKeyyieldSourceyieldSourceUrlyieldTypedataSourceisBestsourceSwitch
warningSignals type: string[].
Frontend
Page: /yield
Files: src/app/yield/page.tsx (SSG wrapper), src/app/yield/client.tsx (interactive)
Layout (top to bottom):
QueryErrorNoticewhen the rankings query fails- Stale data banner (tracks the hourly core publish lane and warns when rankings are delayed or stale)
- Snapshot count text, optional
Data Provenance, and peg filter controls - Yield leaderboard table
- Yield vs Safety scatter card with the summary metrics integrated into the card header
- Yield Sources board for the active peg-filtered ranking scope
- Disclaimer
Page: /stablecoin/[id]/yield/
Files: src/app/stablecoin/[id]/yield/page.tsx (SSG wrapper), src/app/stablecoin/[id]/yield/client.tsx (interactive)
Generated for every non-pre-launch tracked coin (lending-opportunity rows can appear for any tracked stablecoin, so the deep link must be reachable beyond flags.yieldBearing). The route renders per-source APY history, warning-signal timeline, and source-switch history for the selected coin; coins with no live yield row render an empty-state card, and only IDs absent from the tracked registry return notFound(). All known-coin pages carry noindex (robots: { index: false, follow: true }) metadata.
YieldSourceBoard (src/app/yield/source-board.tsx)
Compact source-mix board rendered after the scatter on /yield. It consumes the pure buildYieldSourceBoardModel(rankings, options) output from src/app/yield/source-board-model.ts and stays scoped to the currently visible, peg-filtered rankings.
The board renders a minimal surface focused on which data families back the visible rows:
- header: kicker, "Source mix in the current view" heading, and a one-line helper noting the board counts every chosen source plus retained alternates
- chips: chosen-source source-changed count and chosen-source anomaly count from row provenance, each with a tooltip explaining the meaning
- source lanes grouped by
yieldTypeanddataSource, each row showing the yield-type badge, data-family label, total observation count (chosen + retained alternates), and up to three of the most-represented source labels with the long tail summarised as "+N more"
The shared buildYieldSourceBoardModel continues to expose additional fields (APY min/median/max, confidence tiers, depth lens, benchmark labels) for any future consumer, but the visible board no longer renders them — those signals already live in per-row source sheets and the collapsed Data Provenance details below the leaderboard.
Stablecoin Detail: YieldDetailSection (src/components/yield-detail-section.tsx)
Yield intelligence section for stablecoin detail pages. Shows stat cards (Current APY, 30d APY, PYS with breakdown, Stability) plus 30d Excess Yield, source info, source links, alt sources, warning callouts, embedded YieldHistoryChart, and contextual methodology hints / footer links for PYS and yield stability. Conditional: only renders for coins with yield data.
It reuses the cached /api/yield-rankings payload to find the coin's chosen source row, surfaces row-level provenance (selection reason, source age, benchmark state, source-changed state, previous source key when available), and passes the selected benchmark context, peer median, and source list into YieldHistoryChart.
Layout (top to bottom):
- Header row: "Yield Intelligence" + yield-type badge from
YIELD_TYPE_LABELS/YIELD_TYPE_STYLES - Warning treatment:
- 2+ active signals: amber callout block listing every warning label
- 1 active signal: compact inline alert row
- 30d Excess Yield callout plus four stat cards: Current APY, 30d APY, PYS (with click/focus disclosure for the score breakdown), Stability
- Source info row: clickable source name, normalized data-source badge, source TVL
- Retained alternates list when
altSources.length > 0 - Shared
YieldHistoryChart
The section returns null once rankings have loaded and the coin is neither yieldBearing nor present in the rankings cache. If a coin is marked yieldBearing in metadata but has no ranking row yet, it renders an inline empty/error state instead of silently disappearing.
YieldScatterPlot (src/components/yield-scatter-plot.tsx)
Recharts scatter chart. X = safety score, Y = APY (%). The chart plots one best-source point per stablecoin, auto-focuses the x-axis on the occupied safety-score band instead of always rendering the full 0-100 range, and keeps the safety threshold at 60 visible for quadrant context. Scatter markers render each stablecoin's logo (with an initial fallback if no logo exists), and yield type information lives in the tooltip instead of a separate legend. Rare high-APY outliers are pinned to a disclosed top rail so one extreme point does not flatten the rest of the plot.
Quadrants (divided at safety = 60 and APY = the visible benchmark frame rate):
| Quadrant | Position | Color |
|---|---|---|
| Sweet Spot | High safety, above benchmark | Green (5% opacity) |
| Danger Zone | Low safety, above benchmark | Red (5% opacity) |
| Play It Safe | High safety, below benchmark | Blue (5% opacity) |
| Why Bother? | Low safety, below benchmark | Gray (5% opacity) |
Dashed reference line at the benchmark frame rate. On benchmark-homogeneous scopes, that frame uses the shared visible benchmark. On mixed scopes, the chart keeps the overlay visible by using the default USD benchmark as a shared orientation frame while the table and row tags continue to show each stablecoin's local benchmark context. Scatter tooltips show each row's own benchmark label/rate and 30d excess-yield value. Click a dot to navigate to that coin's detail page.
YieldLeaderboard (src/components/yield-leaderboard.tsx)
Sortable, paginated leaderboard (25 rows/page). Default sort: PYS descending. From md upward it renders the dense table; below md it renders first-class mobile cards with the same sort and pagination state plus direct Detail, Provider, Source, and History actions. Table headers for PYS, Stability, and Signals use the shared methodology-hint trigger so users can read the local definition without leaving the leaderboard.
The filter row above the table is backed by YieldViewModel and URL query keys for q, peg, yieldType, warnings, minSafety, minTvl, depth, sourceChanged, sourceConfidence, benchmark, and opportunity. The trust rail promotes body-level API warnings from YieldRankingsResponse.warnings, including degraded live safety hydration, before the table. Search and filters feed rows into the shared sort/pagination pipeline, with page index reset whenever controls change the visible row set.
Columns: Rank, Coin (logo + symbol), APY (30d), Grade, PYS, Source, Type (badge), TVL, Stability (bar + %), 30d Range, Signals, and a trailing chevron for row expansion.
Stability display multiplies the raw 0–1 value by 100 for both the bar width and the percentage text.
PYS tooltip: Hovering a non-null PYS score opens a component breakdown tooltip with Effective Yield, benchmark adjustment, Source Risk Penalty, Yield Efficiency, the adjusted safety-penalty line, Safety (grade + score with 40 fallback), Consistency (max(0.3, yieldStability) shown as a percentage), and the active source-risk driver labels. Detail-page PYS breakdowns pass the nested sourceRisk.sourceRiskPenalty so the displayed decomposition matches the API score.
Signals column (desktop/tablet): Rows with no active warnings show an em dash. Rows with one warning show an amber outline alert icon. Rows with two or more warnings show a filled amber icon and an additional subtle amber left border on the row. Hovering the icon opens a tooltip with human-readable warning descriptions and an actionable next check (yield-spike, yield-divergence, negative-trend, reward-heavy, tvl-outflow, zero-yield, data-stale, low-source-tvl).
Source inspection: The table row delegates source inspection to the shared YieldSourceSheet, which opens from the retained source controls in the row/expanded state instead of the old inline +N popover.
Inline expansion: Clicking a leaderboard row toggles an inline YieldHistoryChart panel directly beneath that row. The expanded panel repeats the selected source as a clickable link above the chart, passes the selected row benchmark, medianApy, and available source list into compact mode, and only one row can remain expanded at a time.
YieldHistoryChart (src/components/yield-history-chart.tsx)
The chart now supports source-aware inspection. When retained alternates exist, users can switch between:
Best source: historically selected best-source rows (mode=best)- a specific retained source row (
sourceKey=<key>)
This lets the detail page and leaderboard inspect the actual history of a retained alternate instead of only showing its current snapshot.
Recharts line chart. Primary APY line with optional base/reward breakdown toggle. Two reference lines: the row's benchmark rate and peer median APY. Warning signal markers on data points. Time presets: 7d / 30d / 90d / 1y. The legend labels the primary line source and keys source overlays by sourceKey, adding a short source-key suffix when two retained sources share the same display name.
It reads /api/yield-history through useYieldHistory, and points carrying warningSignals get amber markers so spike/divergence/reward-heavy regimes are visible without expanding the tooltip. Source-change markers use readable tooltip copy that explains the marker as source provenance churn, not an asset-risk change.
The control row exposes four fixed lookback presets (7d, 30d, 90d, 1y) plus an optional breakdown toggle. Compact mode keeps the same data semantics for inline leaderboard expansion, but shortens the chart height to 200px and drops reference-line labels to protect legibility in tighter rows.
Hooks
| Hook | File | Endpoint | Stale Time |
|---|---|---|---|
useYieldRankings | src/hooks/api-hooks.ts | /api/yield-rankings | CRON_YIELD (1 hour) |
useYieldHistory | src/hooks/api-hooks.ts | /api/yield-history | CRON_YIELD (1 hour) |
Constants
File: worker/src/lib/constants.ts
| Constant | Value | Purpose |
|---|---|---|
RISK_FREE_RATE_FALLBACK | 3.75 | Fallback T-bill rate (%) |
FRED_TBILL_CSV_URL | https://fred.stlouisfed.org/graph/fredgraph.csv?id=DGS3MO | FRED daily 3-month Treasury yield series |
TREASURY_YIELD_XML_URL | https://home.treasury.gov/sites/default/files/interest-rates/yield.xml | Treasury.gov daily yield curve XML fallback for USD 3M |
ECB_ESTR_3M_CSV_URL | https://data-api.ecb.europa.eu/service/data/EST/B.EU000A2QQF32.CR?lastNObservations=5&format=csvdata | Official ECB 3M compounded €STR feed |
SIX_OAUTH_TOKEN_URL | https://indexdata.six-group.com/pro/oauth/token | Public SIX guest OAuth endpoint for delayed downloads |
SIX_REPORT_DOWNLOAD_URL | https://indexdata.six-group.com/pro/api/report-download | SIX download broker for delayed public report files |
SIX_SARON_3M_CSV_URL | https://indexdata.six-group.com/download/saron/h_sar3mc_delayed.csv | Delayed public CSV for SAR3MC |
SIX_BROWSER_USER_AGENT | Mozilla/5.0 | Browser-compatible UA required by SIX guest endpoints |
PYS_SCALING_FACTOR | 8 | PYS distribution tuning parameter after safety-curve steepening |
DEFAULT_SAFETY_SCORE | 40 | Safety score for unrated coins (most NAV tokens) |
CIRCUIT_SOURCE.DL_YIELDS | "defillama-yields" | Circuit breaker key for DL Yields API |
CIRCUIT_SOURCE.TREASURY_RATES | "treasury-rates" | Circuit breaker key for the benchmark-registry fetch lane |
Testing
Pure function tests: worker/src/cron/__tests__/yield-helpers.test.ts
computeApyFromRate— 7-day rate change, zero/negative inputs, decreasing ratescomputeApyFromPrice— delegates tocomputeApyFromRatecomputePYS— safe high-yield, safety penalty, variance penalty, 100 cap, negative APYcomputeYieldStability— stable vs. volatile yields, empty/single samples, near-zero mean → null guardcomputeApyVarianceScore— near-zero mean → null guard, insufficient samples → nulldetectWarningSignals— yield spike (with absolute floor), negative trend (with absolute floor), reward-heavy, TVL outflow, zero-yield, healthy baseline, boundary conditions, negative APY inputmatchAllDlPools— Layer 1/2/3 source matching, dedup, relaxed Layer 2 stablecoin filter, exact symbol match (no cross-contamination), highest-TVL selection, empty pools edge case, Layer 3 minimum symbol length guard (4-char cutoff)findBestLendingPool— allowlist filtering, symbol match, address fallback, quality gatescomputeTvlWeightedMedianApy— empty input, null/zero TVL, single row, TVL-weighted vs simple median, zero APY filtering
Integration tests: worker/src/cron/__tests__/sync-yield-data.test.ts
- Happy path, stale/orphan cleanup, D1 chunking, cached DL pools, deterministic auto-discovery override, B.Protocol LUSD, DL API failure, circuit breaker open, schema validation, price-derived fallback, source-specific history, legacy history carry-forward, rate-derived, degraded safety coverage, and mixed benchmark publication
- Deterministic/native coexistence: verifies on-chain rows can coexist with curated native rows without source-key collision
- Auto-discovery labeling: verifies
defillama-autorows on yield-bearing coins publish protocol-derived labels andlending-opportunity - Retained benchmark degradation: verifies retained benchmark fallbacks stay marked degraded in rankings provenance
Pool-filter tests: worker/src/cron/__tests__/pool-filter.test.ts
- Preserves stablecoin single-exposure pools
- Preserves explicitly relevant wrappers like
fxSAVEeven when upstreamstablecoinis false - Rejects unrelated non-stablecoin pools and non-single exposure pools
Resolve/arbitration tests: worker/src/cron/__tests__/yield-resolve.test.ts
- DL curated source selection, deterministic rate-derived preference, cross-source divergence rejection, benchmark-aware excess yield, negative excess yield, hardcoded fallback rate, rate-derived from benchmark minus spread, rate floor at zero, yield-spike warning, TVL-outflow warning, stable conditions, PYS computation, PYS=0 for zero APY
- Price-derived as explicit source (Tier 3 path through resolve for navToken coins)
- Auto-discovery path (non-yield-bearing coin matches a lending pool via
AUTO_LENDING_POOL_MAP)
Cache parsing tests: worker/src/cron/__tests__/yield-cache.test.ts
parseRiskFreeRateCache/parseRiskFreeRatesCache— valid JSON, malformed JSON, missing fields, and negative-rate support for non-USD benchmarksparseDlStablecoinPoolsCache— valid JSON, malformed JSON, legacy format, missing fields, cache age computation, stale cache rejection
Source link tests: worker/src/lib/__tests__/yield-source-links.test.ts
- Curated protocol link for discovered source, source-specific override, metadata app link fallback, website fallback
- No-match case: returns null when no curated link, no protocol match, and no metadata link
- New lending protocols: verifies newly added protocol (Radiant v2) resolves a URL
Edge Cases
- First sync (no history):
apy7dandapy30dequalcurrentApy. PYS still computed. - On-chain rate bootstrapping: When a Tier 1 vault has no previous exchange rate in history (first 7 days after config is added), the sync emits a seed row with
currentApy: 0and the currentexchangeRate. This persists the rate inyield_historyso future cycles can compute APY once a 7-day-old reference point exists. - Unrated coins (no safety grade): Safety score defaults to
DEFAULT_SAFETY_SCORE(40, D-equivalent). Most NAV tokens hit this path since the report card framework doesn't grade them yet. - Incomplete live safety hydration: rankings rows stay published with safety fallback
40 / NRinstead of being dropped. - All tiers fail: Coin is recorded with
yield: nulland skipped in the write phase. No PYS computed. Logged as warning. - Negative APY: Stored and displayed. PYS returns 0 for
apy30d <= 0. - DL Yields circuit-broken: Tier 2 skipped entirely. Coins with Tier 1 or Tier 3 coverage still get APY. Others get
yield: null. - Cron gaps (7d filter): The 7d trailing average uses timestamp-based filtering (
recorded_at >= now - 7d) rather than proportional slicing, so gaps don't shift the window. - DL pool returns 0% for navToken: When Tier 2 finds a DL pool but it reports 0% APY, Tier 3 price-derived is tried as an additional source. The
is_bestlogic picks the higher APY. This covers upstream DL data staleness and spurious Layer 3 symbol matches. - Dividend-distributing tokens (BUIDL, YLDS): These maintain a fixed $1.00 NAV and distribute yield as new tokens. Price-derived returns ~0% because the price doesn't change. Resolved via Tier 4 rate-derived, which computes APY from the selected benchmark rate minus the token's management fee spread.
- Malformed persisted warning payloads:
yield-historytreats them as[]so a single bad row cannot 500 the endpoint.
File Index
| File | Role |
|---|---|
worker/migrations/0000_baseline.sql | Baseline yield_data / yield_history schema, including the historical multi-source and warning-signal additions |
worker/src/cron/sync-yield-data.ts + worker/src/cron/yield-sync/* | Yield sync orchestration and stage modules: source loading, resolution, evaluation, publication, history maintenance, and rankings caching |
worker/src/cron/sync-yield-supplemental.ts | Slower optional-source cron that refreshes the cached supplemental candidate snapshot |
worker/src/cron/yield-config.ts | Static config: YIELD_POOL_MAP, YIELD_VARIANT_MAP, ON_CHAIN_RATE_CONFIGS, RATE_DERIVED_CONFIGS |
worker/src/cron/yield-helpers.ts | Pure functions: APY, PYS, stability, variance, warning signals, matchAllDlPools |
worker/src/cron/yield-sync/pool-filter.ts | Pre-filter for wrapper-relevant DeFiLlama pools before matching |
worker/src/lib/yield-source-links.ts | Curated yield-source link registry plus metadata fallback resolver for rankings/history payloads |
worker/src/cron/fetch-tbill-rate.ts | Daily benchmark-registry cron (USD T-bill, EUR 3M compounded €STR, CHF 3M compounded SARON, GBP SONIA proxy, JPY call-rate proxy, MXN CETES 28d with Etherfuse fallback, BRL SELIC, AUD interbank proxy, CAD CORRA proxy) |
worker/src/api/cache-handlers.ts | Cache-backed GET /api/yield-rankings handler with live Safety Score hydration (handleYieldRankings) |
worker/src/api/yield-history.ts | GET /api/yield-history handler |
shared/types/index.ts | YieldConfig, YieldType, YieldRanking (.altSources: AltYieldSource[]), AltYieldSource, YieldRankingsResponse, YieldHistoryPoint |
shared/lib/classification.ts | YIELD_TYPE_LABELS, YIELD_TYPE_STYLES |
src/hooks/api-hooks.ts | TanStack Query hook exports for useYieldRankings() and useYieldHistory() |
src/lib/yield-constants.ts | Warning-signal labels, formatYieldWarningSignal, getPysColor, computePysBreakdown — shared frontend yield utilities |
src/lib/__tests__/yield-constants.test.ts | Unit tests for shared frontend yield utilities |
src/app/yield/page.tsx | SSG page wrapper with metadata |
src/app/yield/client.tsx | Interactive page: stats, scatter, leaderboard |
src/app/yield/source-board-model.ts | Pure source-board model for chosen/retained-alternate source counts, source lanes, confidence counts, anomaly/source-change counts, depth counts, APY context, and benchmarks |
src/app/yield/source-board.tsx | Compact /yield source-provenance board rendered before the scatter |
src/app/stablecoin/[id]/yield/page.tsx | SSG per-coin yield-analysis wrapper for every non-pre-launch tracked stablecoin; the client renders an empty state when no live yield row exists |
src/app/stablecoin/[id]/yield/client.tsx | Interactive per-coin yield-analysis surface with APY history, warning timeline, and source-switch history |
src/components/yield-detail-section.tsx | Stablecoin detail-page yield section with warnings, source metadata, metric cards, and shared history chart |
src/components/yield-leaderboard.tsx | Sortable rankings table with +N alt-source pill badge |
src/components/yield-history-chart.tsx | Shared APY history chart with row-benchmark / peer-median reference lines, optional base-reward split, and warning markers |
src/components/yield-scatter-plot.tsx | Risk-adjusted scatter visualization |
worker/src/cron/__tests__/yield-helpers.test.ts | Unit tests for all pure yield functions |
worker/src/cron/__tests__/sync-yield-data.test.ts | Integration tests for sync-yield-data orchestration (on-chain, rate-derived, DL, supplemental-cache, cooldown, auto-discovery) |
worker/src/cron/__tests__/pool-filter.test.ts | Tests wrapper-preserving pre-filter behavior for cached/direct DeFiLlama pool ingestion |
worker/src/cron/__tests__/yield-resolve.test.ts | Resolve/arbitration tests (price-derived, auto-discovery, DL source selection, warnings) |
worker/src/cron/__tests__/yield-cache.test.ts | Cache parsing tests for DL pools and benchmark caches |
worker/src/lib/__tests__/yield-source-links.test.ts | Yield source link resolution tests (curated, protocol, metadata fallback) |
Presentation Contracts (v8.13.x)
These conventions govern how /yield/-adjacent surfaces render the data already
published by /api/yield-rankings. They are presentation rules, not methodology
changes; the underlying scores and warnings are unchanged.
Row visual budget
Every leaderboard row carries at most three fixed chip positions, in this order: (a) confidence tier pill, (b) freshness stamp, (c) severity glyph stack that combines warning-signal count with the source-risk penalty severity. New row-level affordances must reuse one of these three positions or be deferred to the expanded panel or detail page; the row itself stays scannable.
Sparkbar grammar
The page exposes exactly two sparkbar idioms. The source-risk score (0-100)
sparkbar lives only in the source cell of leaderboard rows and in the
chosen-source card of the source sheet. The PYS history sparkline (planned
Phase 4) lives only in the expanded-row panel. No other sparkbar grammars
are introduced on /yield/.
Embedded vs deep-link contract
The embedded yield section on the stablecoin detail page
(src/components/yield-detail-section.tsx) is at-a-glance: current state,
recent narrative, one chart, and the top alternates. The deep-link
/stablecoin/<id>/yield/ surface
(src/app/stablecoin/[id]/yield/client.tsx) is history-first: full
timelines, decision rationale, and (planned) peer rail. Embedded summarises;
deep-link reveals. Both surfaces must avoid duplicating panels.
Deep-link yield pages are noindex and omitted from sitemap.xml; the indexable
SEO surface remains /yield/ plus each stablecoin detail page because the
deep-link pages are runtime-data workbenches rather than static crawl targets.
PYS attribution convention
To express a PYS component as a marginal-point delta, neutralize the factor
under inspection (set it to its neutral baseline: source-risk penalty →
1.0, sustainability multiplier → 1.0, safety penalty → 1.0)
and recompute PYS via computePYS. The signed difference vs the actual PYS
is that factor's contribution in PYS points. Because attribution is
per-factor and not sequential, the attributed points may not sum exactly to
the total PYS; this caveat must accompany any rendered attribution so users
read it as decomposition, not a residual-free reconciliation.