Methodology
How Sharpe Finance derives risk, complexity, and exposure metrics from on-chain Morpho data. Every formula is open and auditable — if you disagree with a weight, you can replicate the calculation.
All metrics are computed from the public Morpho Blue GraphQL API at blue-api.morpho.org/graphql. We index every whitelisted MetaMorpho (V1) vault and every listed Morpho V2 vault across the eleven chains Morpho is deployed on — Ethereum, Base, Arbitrum, Optimism, Polygon, Unichain, Monad, HyperEVM, Katana, Stable, Tempo — ordered by total assets in USD. The dataset refreshes every 5 minutes (Next.js ISR), and the getDashboardData orchestrator falls back to bundled fixtures if the API is unreachable so the surface stays usable offline.
Vault history (APY, TVL) is taken directly from the API; we median-filter spike points above 50% APY because the underlying time-series occasionally carries IRM-snapshot artifacts. Risk and complexity scores are derived locally — composition logic is open in lib/risk-model.ts, exposure derivation in lib/morpho.ts. The depeg system reads CoinGecko spot, on-chain Chainlink USD aggregators, and issuer-side stress (Aave V3 GHO facilitator buckets, FRAX collateral ratio, USDe funding rate) directly via viem.
Morpho V2 vaults wrap V1 MetaMorpho positions through adapter contracts. We resolve those adapters through the V2 GraphQL fragment MetaMorphoAdapter.metaMorpho.address, look up the underlying V1 vault in the same fetch, and inherit its allocations weighted by adapter share. A V2 vault whose adapters don't resolve (e.g. direct MorphoMarketV1 adapters whose per-position detail exceeds the API's complexity cap in bulk) is scored with a maturity-factor opacity surcharge — never as a flat placeholder.
Morpho vaults are managed mandates, not fixed instruments. Curators rebalance allocations continuously, so a trailing multi-month average of a vault's APY is the realized yield of a portfolio composition that no longer exists. We deliberately do not use trailing windows as decision inputs.
Each vault has two present-tense yield numbers:
- Spot APY — state.netApy at query time, net of vault fees, including active reward token emissions. The number on the headline. Volatile, but accurate for "what's earning right now."
- Base APY (ex-rewards) — state.netApyWithoutRewards at query time, same instant and same allocations as the spot, just with the emission slice stripped. The rate that survives an active reward program ending. Stored on every vault as sustainableApy for historical-naming reasons.
Scoring, weighting, and recommendations use Base APY, not Spot. A short-term incentive that doubles a vault's headline doesn't double the value of holding the vault, and we don't want the universe-weighted APY in the topbar to flatter the surface every time a curator launches an emission campaign. Spot is still shown wherever we display yield, and the gap (the rewards slice) is surfaced explicitly.
A vault is flagged boosted when spot is more than 1.5× base — i.e. rewards account for more than ~33% of total yield. The boosted chip appears alongside the headline; the tooltip explains the split.
Trailing 30d and 90d averages remain on the vault detail page as historical context, labelled as backward-looking. They do not feed any scoring, ranking, recommendation, or aggregation.
Source-of-truth fields: lib/morpho.ts → computeSustainableApy, lib/morpho.ts → isApyBoosted.
Risk is the expected magnitude of principal loss under stress. It is computed as a composite of seven weighted factors plus two non-negotiable floors. Every vault renders the full decomposition on its detail page — the composite number is just max(weightedSum, warningFloor, depegFloor).
The model is built on a single principle: in an efficient lending market, excess yield is risk someone is bearing. A risk model that cannot explain the yield surface is wrong. We treat the protocol's own warnings as hard floors (Morpho's risk team has more context than us), we treat persistent yield anomalies relative to peer cohorts as risk we may not yet model, and we treat Morpho V2 vaults whose adapters can't be resolved at bulk as opaque — not zero-risk.
Bands. < 20 = blue-chip; 20–35 = mainstream; 35–55 = elevated; 55–75 = high; 75+ = critical.
Cross-protocol: same model, different inputs. The factor decomposition is protocol-agnostic. For Morpho the collateral-quality input is the per-market collateral asset (vaults route across many markets, each with its own collateral). For Aave V3 the supplier doesn't see per-borrow collateral — v1 approximates the basket as the reserve's own asset tier and the framework will swap in true basket-weighted scoring (per-collateral debt outstanding from UiPoolDataProvider) when the cross-protocol view ships. Same taxonomy, same 0..100 scale; the universe cohort math (USDC on Aave vs USDC on Morpho) stays apples-to-apples.
A second risk signal derived purely from what borrowers actually pay. In an efficient market, supply APY reflects the demand for leverage on that collateral — higher APY means borrowers are willing to pay more, which is a market-implied risk premium.
Fixed scale (12.5% APY caps at 100) so the number is comparable across time, not just relative to the current universe.
Divergence is the diagnostic. When our structural risk score is higher than the market signal, our model thinks the vault is riskier than borrowers do — either we're over-counting risk on under-utilized collateral, or the market is mispricing. When the market signal is higher than the structural score, the vault is yielding more than its collateral composition would suggest — usually a sign of strong borrower demand for that loan asset (not exotic strategy risk).
Every Morpho vault is denominated in a single loan asset (USDC, USDT, DAI, …). If that asset drifts off its peg, every depositor eats the move — and worse, Morpho's lending markets price collateral against a Chainlink oracle, not the open market. So a depeg can move LTVs and trigger liquidations before the spot price even reflects the stress.
For each USD-pegged loan asset we read two sources every cache cycle and surface the dominant deviation:
- CoinGecko spot — centralized aggregate of CEX and DEX prices. The "market" price.
- Chainlink USD aggregator — the oracle price. This is what Morpho actually liquidates against; the value used to compute every market's LTV.
Score saturates at 200 bps (2%) deviation. Bands: healthy below 30, watch 30–60, warning 60–80, critical 80+. When spot and oracle diverge by more than 30 bps the dashboard surfaces the gap explicitly — that's the dangerous regime where the market knows something the oracle hasn't priced in (or vice versa).
Why this is separate from the risk score. Asset-quality penalty (in the risk score above) is a static assumption — vanilla stables get 0.03 because they're usually on peg. The peg health signal is the live override: it tells you whether the assumption holds right now.
Vault-health sub-score. On top of the spot/oracle price comparison, we read issuer-side contracts directly so the score reflects backing stress, not just market price:
- GHO — Aave V3 facilitator bucket utilization. Below 85% = 0; ramps linearly to max when the bucket is fully drawn.
- FRAX — global_collateral_ratio(). 20% under-collateralization saturates risk (deficit × 5).
- USDC / USDT — paused() state on the issuer contract. Paused = instant max (this is the kill-switch Circle and Tether can hit unilaterally).
The composite peg score is max(priceScore, vaultHealthScore) — either signal flashing pulls overall severity up.
Per-market VaR-style estimate of P(vault eats bad debt) over a 30-day horizon. Two thresholds matter: dropToLiquidation = 1 − ltv/lltv (when liquidators take over) and dropToBadDebt = 1 − ltv (when collateral value falls below debt). The 14%-to-26% band between them on a typical wstETH market is the liquidator's window — whether bad debt occurs there depends on liquidator efficacy, not σ alone.
Probability under log-normal returns — σ_30d = σ_annual × √(30/365). For a target drop d, z = ln(1 − d) / σ_30d (negative for any drop), and P_normal(T=30) = Φ(z). σ-headroom = |z| — the TradFi-interpretable distance to bad debt. 3σ headroom ≈ 0.13% probability, 1.5σ ≈ 6.7%, < 1σ ≈ 16%+. The combined output, labeled E[loss] (30d), is a SEVERITY-WEIGHTED EXPECTED LOSS, not a probability: E[loss] = P(drop ∈ [liq, bd]) · (1 − L) · LGD_window + P(drop > bd) · 1.0. The first term captures liquidator failure inside the window with mid-window LGD (≈ ½ · (drop_bd − drop_liq)); the second is the insolvency tail with assumed 100% loss. Result is a fraction of position value, not a probability — see the caveats below.
Liquidator efficacy decomposition — L = oracleFactor · profitMarginFactor · liquidityFactor · keeperFactor · chainFactor. Multiplicative because being weak on any single axis crushes liquidator success (reliability-chain logic). oracleFactor: Chainlink reference = 0.95, proxy = 0.88, internal accountant = 0.70, hardcoded = 0.10. profitMarginFactor: liquidation bonus minus expected slippage at a $5M trade — slippage is derived from marketCap directly (USDC at $50B+ → ~0.05% slip; mF-ONE at ~$30M → ~15%) so the liquidity axis doesn't double-count into the margin. liquidityFactor: maps the same liquidityScore the collateralQuality factor uses (turnover + size proxy) into a multiplicative factor — inverse-related to depth. keeperFactor: chain-baseline today (Ethereum 0.95, L2s 0.85, newer chains 0.55) — refined by per-market `Liquidate` event counts in Phase 2. chainFactor: per-chain rail reliability from historical bad-debt incidents (Ethereum 0.95, Base / Arbitrum / Optimism 0.92, Polygon 0.88, Unichain 0.78, newer chains 0.70). The bottleneck axis is reported only when the weakest factor is < 0.85 AND ≥ 0.10 below the second-weakest — otherwise the system is labeled balanced. The headline E[loss] uses L's conservative lower bound (L − 0.15 · (1 − L)).
Fat-tail adjustment — Normal log-returns under-state crypto tails by a factor of 2–5× depending on regime — empirically log returns on ETH/BTC/LSTs are closer to Student-t with df≈3-5. We label P_normal and P_stressed = P_normal × 3 separately rather than folding the multiplier silently. Both are shown; the reader picks the framing. KNOWN LIMITATION: the 3× multiplier is applied uniformly across asset classes — RWA T-bill NAV tokens have genuinely normal-shaped tails, so the ×3 over-states their tail probability; LRTs and exotic strategies may need ×4-5. Per-tier multipliers are on the Phase 2 roadmap once more realized-drawdown data is collected.
Honest caveats — (1) Ltv per market is the aggregate, not per-borrower — the worst individual borrower is typically pinned just below lltv. (2) Bad debt during exchange / oracle outages is excluded — the L factor captures normal-operations failure, not infrastructure unavailability. (3) Correlated failure (same collateral class dropping across multiple markets simultaneously) is captured by the worst-market headline, not the share-weighted aggregate. (4) The 30d window is fixed at the CoinGecko sample size; longer horizons would need exponentially-tilted σ estimates we don't yet pull.
Complexity measures strategy intricacy independent of risk. A vault can be simple and risky (one collateral at 95% LLTV), or complex and safe (well-managed split across many low-risk markets). The score reflects how many independent moving parts the strategy depends on.
Two surfaces in the app proactively recommend vaults: the auto-rendered "Best vault per loan asset" row on the dashboard and portfolio pages, and the wallet-gated "Upgrade your positions" cards on the portfolio page. Both share a common selection pipeline — a deterministic investability gate, the same scoring formula, then surface-specific thresholds.
1. Investability gate
A vault must clear every check below before it's eligible for any recommendation surface. The gate is the same code path everywhere (lib/investable.ts).
- Deposits open — Morpho's deposit_disabled flag clears the supply queue; we hard-fail those.
- No RED warning — riskFactors.warning < 50 (excludes bad_debt_realized/unrealized, incompatible_oracle_feeds, unsafe_vault_as_market_collateral, and every other RED-tier flag from lib/risk-model.ts).
- No critical depeg — loan-asset depeg.score < 60 (excludes watch-band and above; details on the depeg card).
- No issuer pause — if the issuer-side stress sub-signal reports paused (Aave facilitator off, Circle minting paused, etc.) the vault is ineligible regardless of yield.
- Minimum TVL — $10M for major assets (USDC, USDT, DAI, WETH, wstETH, cbBTC, WBTC), $2M for niche assets.
- Utilization < 95% — above that, deposits route into a market structurally locked for withdrawals.
- Liquidity headroom — liquidityUsd / tvl ≥ 3% instant-redeem floor. When a wallet position size is in scope, the destination must also hold liquidityUsd ≥ 2× positionUsd so the move is executable AND leaves the next exiting depositor room.
- Minimum age — 1 month on-chain operational history.
- V2 opacity — Morpho V2 vaults whose adapters didn't resolve through GraphQL are excluded from recommendations. We don't surface what we can't audit.
2. Risk-adjusted score
Every candidate that clears the gate is scored with the same formula:
- baseApy — the vault's present-tense yield with active reward emissions stripped (Morpho's netApyWithoutRewards). See the "How we measure yield" section above. We never use trailing-window averages for scoring — a managed vault's past composition is not a forward signal.
- risk discount — full weight. A vault at risk=50 gets half credit on yield.
- complexity discount — half weight (×200, not ×100). Complexity is a soft penalty: "you have to read more docs", not "you might lose money".
We deliberately do not add a liquidity multiplier here — liquidity is already a sub-factor inside riskScore and the gate has already filtered thin-float vaults, so a boost would triple-count.
3. Ranking, tiebreaks, and stability
- Primary sort — score descending.
- Tiebreak 1 — TVL descending. Larger vaults generally offer more headroom and curator skin.
- Tiebreak 2 — vault id ascending. Stable identity prevents API-side reorderings from swapping near-ties between ISR cycles.
- Boost demotion — if the top pick is boosted (spot > 1.5× base, i.e. rewards account for >33% of yield) AND its base APY is itself > 1.25× the cohort base-APY median, we swap it with the first non-boosted alternate. Two filters: rewards inflate spot, and the vault's base rate is already abnormally high.
- Curator diversification — no curator appears more than once in the top-N for an asset. Alternates are picked from distinct curators so an allocator comparing "winner + alternates" sees alternative desks, not three vaults from the same shop.
- Winner stability gap — we compute (score₁ − score₂) / score₁ and surface "near tie" framing in the UI when it's small.
4. Upgrade finder (wallet-aware)
When a wallet is connected, every held position is scored with the same formula and matched against candidates of the same loan asset that pass the gate at the user's actual position size. A candidate is surfaced only if it clears one of two lenses:
- Yield upgrade — same-chain: ≥20% relative score lift AND ≥0.3 absolute score points (guards against noise on a low-score source). Cross-chain: ≥35% relative score AND ≥75 bps on sustainable APY — bridges have real costs that flat percentage improvements ignore.
- Safety upgrade — same-chain only: ≥8 absolute AND ≥20% relative risk-points reduction, while preserving ≥95% of the source's base APY. Both conditions apply because 8 absolute points off a 70 is only 11% relative — not "much safer".
- Source-stuck flag — if the user's CURRENT vault fails the position-sized gate (thin float, closed deposits), the card surfaces a warning so the user knows their exit may queue before they initiate the move.
5. Additions — fill structural gaps in your portfolio
When a wallet is connected, we deterministically scan the connected positions for three kinds of gaps and surface one vault per gap that closes it. No AI involvement in selection — the gap signal is read directly from the user's actual breakdown.
- Asset gap — major loan assets (USDC, USDT, DAI, WETH, wstETH, cbBTC, WBTC) where the holder's allocation is < 5% of NAV. First such asset in display order produces one suggestion: the highest risk-adjusted investable vault for that asset.
- Chain gap — fires when the holder sits on fewer than 2 distinct chains. Recommends the top vault on the first uncovered chain in [Ethereum, Base, Arbitrum].
- Curator diversification — fires when one curator represents > 50% of NAV. Recommends a top vault from a DIFFERENT curator in the same asset class as the concentrated holding.
6. Goal-driven AI portfolio builder
You type a 1-line goal + capital + risk ceiling; the AI translates that into a STRICT JSON filter spec (asset universe, risk/complexity ceilings, exposure exclusions, diversification caps); deterministic code applies the spec against the investability-gated universe and returns an equal-weighted portfolio.
The AI never picks vault names or curator names — only constraint values. Every vault that lands in the output passes the same investability gate documented above. The same goal + constraints always produces the same spec (1-hour cache); the spec is re-resolved against the live universe on every request so the picks track current allocations.
Model: Claude Sonnet 4.6, max 400 output tokens, schema-validated. Rate-limited per IP at 5 req/min, capped at 300 requests per UTC day across all users. Source: lib/ai/portfolio-builder.ts + app/api/ai/portfolio-build/route.ts.
- Curator track record. We display vault counts and aggregate TVL per curator, but the risk score doesn't penalize new / unknown curators yet.
- Timelocks and guardian quality. These are governance signals the API exposes (we surface guardian on the vault page) but don't factor into the score.
- Vault cap utilization. V1 vaults don't expose per-market caps on the GraphQL API, so the capacity column is displayed as 1.5× current TVL — a defensible placeholder, not a measurement. V2 exposes hard caps; we're not yet pulling them.
- Per-position LTV distribution. We use LLTV (protocol parameter) as a proxy for liquidation proximity. The actual current LTV per borrower position is determined by borrower behavior, which the API doesn't aggregate cheaply. The liquidation factor uses LLTV-headroom in σ-units, which is the right structural signal — but it can't tell you whether borrowers are *currently* sitting close to the liquidation line.
- V2 adapter resolution at full depth. We resolve V2 → V1 MetaMorpho adapters through GraphQL. We do not yet walk into MorphoMarketV1Adapter positions at bulk — the GraphQL complexity cap blocks it (a single page of position-resolved V2 vaults exceeds 1.5M of the 1M ceiling). Those V2 vaults are scored with the opacity surcharge until per-vault adapter walks land.
Aave V3 reserves are scored on the same 0–100 composite scale as Morpho vaults, with per-factor inputs adapted to Aave's structurally different shape (per-asset reserves vs curator-allocated markets, no allocation surface, governance-set parameters instead of curator discretion).
Cross-protocol cohort math
For each asset, the cohort spans both Aave V3 reserves and Morpho vaults on that asset. APYs are normalized to percent and compared like-for-like. This is the read U1/B2 personas can't get anywhere else — USDC on Aave (the conservative floor) sitting next to USDC on Steakhouse / Re7 / Felix, with z-scores telling you exactly how much spread the curated vaults are paying for their concentration risk.
Factor-by-factor adaptations
Composite
score = max( warningFloor, depegFloor, 0.22·collateralQuality + 0.14·structural + 0.16·liquidation + 0.18·liquidity + 0.14·yieldAnomaly + 0.06·concentration + 0.04·maturity + 0.06·depeg )
Same hard-floor logic as Morpho: when a status flag or depeg signal binds, it overrides the weighted blend. This makes the score conservative under structural stress, regardless of factor-level readings.