A protocol's quoted APY can be split into a base rate (what the market pays for the collateral) and an incentive rate (token-denominated rewards). These are economically different — incentives can be diluted, vest, or be pulled — so any consumer of yield should value the incentive part at a haircut, never at face value.
Pure core: src/analytics/yieldComposition.ts.
INCENTIVE_HAIRCUT = 0.15— incentives are valued at 85c on the dollar.effectiveApy = baseApy + incentiveApy × (1 − INCENTIVE_HAIRCUT), floored at 0. Fallback hierarchy, always non-null:- both parts known → the haircuted sum;
- only one part known → that part at face value;
- only
supplyApy→supplyApy(no split → no haircut possible); - nothing known →
null(null-on-degenerate, never 0).
incentiveShare(baseApy, incentiveApy)→ 0–1 share of yield from incentives,nullwhen the split is unknown (fail-safe).shouldUseEffectiveApy(env)→USE_EFFECTIVE_APY === 'true', default OFF.YIELD_CAVEATtravels with every yield-breakdown response.
ProtocolRate (and the new migration 20260829000000_protocol_yield_composition)
gains three columns, all stored raw exactly as the adapter returned them — the
DB never stores a haircuted value, so the consumption flag can toggle without a
rebuild:
baseApy Decimal(10,6)?incentiveApy Decimal(10,6)?rewardTokens Json?—[{ symbol, address?, apy? }]
YieldProtocol (src/agent/types.ts) and src/agent/scanner.ts carry and persist
these fields.
USE_EFFECTIVE_APY=true switches optimizer/agent consumption from quoted APY to
haircuted effective APY. Two boundaries, both off by default (byte-for-byte
unchanged until set):
- Agent:
scanAllProtocolsranks protocols byeffectiveApyin memory; the DB keeps the raw quoted split. - Optimizer:
src/analytics/service.tsbuilds itsRawRateObservationrates fromeffectiveApywhen the flag is on.
Emissions-heavy yield is riskier. applyEmissionsPenalty(score, incentiveShare)
(src/agent/riskScoring.ts) subtracts up to EMISSIONS_MAX_PENALTY (8 points)
as the incentive share rises from EMISSIONS_LOW_SHARE (0.4) toward
EMISSIONS_HIGH_SHARE (0.8). Unknown/absent share → no penalty (fail-safe).
computeRiskScore accepts an optional incentiveShare argument and applies it.
GET /api/v1/analytics/yield-breakdown (JWT-authenticated) returns the per-held-
protocol split, incentiveShare, effectiveApy, and the caveat, plus
effectiveApyEnabled. Falls back to all protocols when the user holds none.