Skip to main content
Pharos
PHAROSlive stablecoin signals

Pharos Stability Index

PSI formula, bands, storage, and API surface.

Composite ecosystem health score (0–100) measuring how stable the stablecoin market is right now. Computed every 30 minutes.

Methodology Versioning

  • Current methodology version: v3.2
  • Public changelog page: /methodology/stability-index-changelog/
  • Canonical source: shared/lib/stability-index-version.ts

PSI versions are bumped when formula terms, caps, condition bands, or score-affecting input semantics change. Historical entries before formal versioning were reconstructed from git commit history and marked as such.

Formula

Score = 100 − severity − breadth − stressBreadth + trend

Clamped to [0, 100], rounded to 1 decimal place.

Components

ComponentRangeFormulaPurpose
Severity0–68min(68, Σ (abs(bps) / 100 × mcap_share × log₂(1 + mcap / $1B) × 60 × factor))Depeg impact weighted by market cap significance
Breadth0–17min(17, Σ sqrt(mcap / $1B) × 3 × factor) per unique depegged coinNumber of depegging coins, weighted so micro-caps barely register
Stress Breadth0–5min(5, dewsStressBreadth)DEWS-derived market-cap-weighted stress signal: each coin in ALERT+ band contributes sqrt(mcap / $1B) × 1.5, not a simple count
Trend−5 to +5clamp(-5, 5, mcap_7d_change_pct)7-day total market cap momentum

Severity and breadth iterate over active depegs only (unique coins currently outside their peg threshold), with depreciation applied to chronic depegs.

Severity scaling

  • K = 60 scaling constant, calibrated so a 10bps USDT wobble drops the score ~30 points. Multiplied by factor for depreciation.
  • log₂ amplifier makes mega-cap depegs disproportionately impactful: USDT ($145B) gets 7.2×, USDC ($60B) gets 5.9×, a $50M coin gets 0.07×
  • Cap at 68 prevents a single catastrophic event from consuming the entire score range

Breadth scaling

  • Uses sqrt(mcap / $1B) so 12 micro-coin depegs ≈ 3.8 points, but USDT alone ≈ 15 points (cap)

Deviation source

The cron computes live deviation from the current stablecoins snapshot price when available: bps = ((current_price / peg_reference) - 1) × 10000. It does not use peak_deviation_bps from the depeg event — a coin that peaked at 500bps but is currently at 120bps contributes only 120bps of severity.

For already-open depegs only, if the current stablecoins snapshot temporarily lacks a usable positive price, PSI falls back to the last replay-safe positive price_cache entry as long as it is no older than 6 hours. This preserves contributor continuity through transient price-validation gaps without anchoring PSI to arbitrarily stale prices.

The historical admin replay treats a depeg as active for any UTC day whose window overlaps the event interval. It canonicalizes legacy depeg IDs onto the current PSI universe before matching supply history (for example historical ust-terra-classic rows replay through the ust-terra shadow asset). When rebuilding a prior UTC day, it uses same-day supply_history.price to derive replayed deviation when that day price is usable, but it will not replay a daily deviation more severe than the event's recorded peak_deviation_bps. On the UTC day a depeg begins it keeps peak_deviation_bps as a floor only when the depeg materially persists past that UTC close and the daily snapshot undercaptures the shock by at least the configured depeg threshold. Same-day wicks that fully recover before the UTC close, near-midnight bleed-throughs, and moderate follow-on moves that the restored day price already captures use the daily historical price instead of the intraday peak. Replayed days whose restored daily price is back inside the configured depeg threshold now drop out of PSI entirely instead of still contributing breadth. Later days fall back to peak_deviation_bps when a usable historical day price is unavailable or when legacy peg-reference drift would otherwise exaggerate the daily replay. The restore path now also repairs replay-critical supply_history.price coverage for restored USD rows and PSI shadow assets when historical market series are available, so rebuilt history does not overuse peak fallback. This preserves real crisis windows without permanently peak-anchoring recovery days or turning stale peg references into synthetic daily collapses.

Depreciation

Chronically depegged coins have their severity and breadth contributions reduced over time to prevent zombie stablecoins from permanently dominating the score.

factor = depegAgeDays ≤ 30 ? 1.0 : max(0.25, 1.0 - (depegAgeDays - 30) / 120)
AgeFactorMeaning
0–30 days100%Full impact — fresh depeg, market-relevant
45 days87.5%Still significant
60 days75%Fading
90 days50%Half impact
120 days25%Floor reached
120+ days25%Permanent residual

Age is measured from the earliest started_at across all active depeg events for a coin.

Deduplication

A coin may have multiple overlapping depeg events (e.g., one event opened at 100bps that's still active when a second event opens at 200bps due to a peg reference change). To avoid double-counting:

  1. Events are grouped by stablecoin_id
  2. For each coin, the event with the worst current abs(bps) is used for severity
  3. The earliest started_at across all events determines the depreciation age
  4. Each coin contributes exactly once to both severity and breadth

Per-coin contributors

The cron captures a per-coin breakdown in input_snapshot.contributors:

[{ "id": "a7a5-old-vector", "symbol": "A7A5", "bps": -9871, "mcapUsd": 507000000, "ageDays": 61.2, "factor": 0.74 }]

The API surfaces this array in current.contributors (not in history). The frontend renders it as a "Top Contributors" table showing each coin's deviation, market cap, age, depreciation factor, and severity/breadth cost.

Condition Bands

RangeBandColorCharacter
90–100BEDROCK#22c55e (green)Boring. The way stablecoins should be
75–89STEADY#14b8a6 (teal)Minor noise, nothing systemic
60–74TREMOR#eab308 (yellow)Something real is happening. Pay attention
40–59FRACTURE#f97316 (orange)Multiple signals firing. DeFi Twitter is awake
20–39CRISIS#ef4444 (red)Active contagion risk. Last seen during SVB
0–19MELTDOWN#991b1b (dark red)UST-tier. Generational event

Calibration Examples

ScenarioScoreBand
12 micro-coin depegs, +0.26% mcap96.4BEDROCK
USDT wobbles 10bps65.1TREMOR
USDT wobbles 30bps25.4CRISIS
USDT 50bps + USDC 20bps − 3% mcap12.0MELTDOWN

Input Data

InputSource
Active depegs (bps + mcap)depeg_events where ended_at IS NULL, with current price from stablecoins cache or, for already-open depegs missing a current price, a replay-safe price_cache fallback ≤6h old
Total market capSum of the PSI-eligible stablecoins present in the DefiLlama cache (PSI_ELIGIBLE_IDS = tracked + shadow)
7-day market cap changeCurrent vs previous week total from stablecoins cache
DEWS stress breadthLatest stress_signals rows in warning bands (ALERT, WARNING, DANGER)

If the active-depeg query is unavailable, the cron fails closed and does not publish a fresh PSI sample for that cycle. If totalMcapUsd is missing or <= 0, computeStabilityIndex() returns null (insufficient data) and no new score is produced for that cycle.

Cron & Storage

  • 30-min samples: computeAndStoreStabilityIndex() in worker/src/cron/stability-index.ts — runs every 30 minutes (26,56 * * * *) after compute-dews on the DB-only DEWS/PSI lane. The lane is separate from the 10,40 * * * * DEX-liquidity trigger so a DEX-liquidity CPU overrun cannot prevent PSI publication. Computes severity/breadth from active depegs, stress breadth from DEWS, and trend from 7-day market cap change. If DEWS input is unavailable, the run records that dependency loss in cron metadata, returns status: "degraded", and does not publish a fresh PSI sample; the API keeps serving the last healthy stored value rather than understating stress breadth as zero. If the active-depeg query itself is unavailable, the run likewise skips the sample and leaves the last valid stored PSI untouched. For already-open depegs whose current stablecoins snapshot price is temporarily missing, the cron can reuse a replay-safe positive price_cache price if it is at most 6 hours old; otherwise that coin is skipped for that sample. If total market cap input is missing/zero, PSI compute returns null, the cron skips writing that sample, and the API continues serving the last valid stored value. Samples are stored in stability_index_samples (migration 0026) and pruned after 90 days.
  • Daily aggregation: snapshotPsiDaily() in worker/src/cron/snapshot-psi.ts — runs daily at 08:00 UTC. Averages all 30-minute samples from the previous UTC day and stores one row in the stability_index table using INSERT OR REPLACE on the midnight-keyed computed_at. If the prior UTC day has zero samples, the cron returns status: "degraded" with reason: "no-samples-for-yesterday" and skips the write.
  • Historical admin backfill: handleBackfillStabilityIndex() replays completed UTC days only. The replay path bounds market-cap denominators to the PSI-eligible universe, canonicalizes legacy depeg IDs into the current PSI universe before matching supply history, treats any depeg overlapping the UTC day as active, derives same-day depeg severity from supply_history.price when available, never lets that replayed daily deviation exceed the event's recorded peak_deviation_bps, keeps peak_deviation_bps as a start-day floor only when the event materially persists past that UTC close and the daily snapshot undercaptures the move by at least the configured depeg threshold, drops replay days whose restored daily price is already back inside the configured depeg threshold, and otherwise falls back to peak_deviation_bps for missing/invalid historical prices or stale peg references. The paired supply-history and historical-price repair jobs are expected to restore replay-critical supply_history.price coverage, including PSI-only shadow assets, before the PSI rebuild is rerun. For methodology v3.0+, it also derives historical stressBreadth from same-day stress_signal_history warning bands (ALERT, WARNING, DANGER). When a rebuild day cannot be replayed because archival inputs are missing, the endpoint preserves the existing stored row instead of deleting that day.
  • Pure compute: computeStabilityIndex() in worker/src/lib/stability-index.ts — stateless, deterministic
  • Tables: stability_index_samples (migration 0026, columns added by 0035) — per-sample: stored_at, score, band, components (JSON), input_snapshot (JSON), methodology_version. stability_index (migration 0022, columns added by 0035) — daily averages: computed_at, score, band, components (JSON), input_snapshot (JSON), methodology_version

API

GET /api/stability-index — latest score + recent history (default response uses the latest ~91 daily rows, with today's running average prepended when available). With ?detail=true, it returns the full stored history with per-day component breakdowns. The current sample can also expose inputDegradation when the stored snapshot carried degraded-input metadata. Cache: standard (5-min edge, 1-min browser).

See API Reference for the full response shape.

Frontend

  • Homepage widget: src/components/stability-index.tsx — score, band, delta from yesterday, 30-day sparkline, animated lighthouse icon
  • Dedicated page: src/app/stability-index/client.tsx — score history chart with band-colored zones, component breakdown stacked area chart, time range filter, methodology section, and contextual methodology hints on PSI plus the four component labels (Severity, Breadth, Stress Breadth, Trend)
  • Hook: src/hooks/api-hooks.tsuseStabilityIndex() (homepage), useStabilityIndexDetail() (page)
  • Route strategy (2026-03-05): legacy /stability-index-alt was retired after Tier 3A review (no nav/sitemap/internal product usage) and now redirects to /stability-index via public/_redirects

Digest Integration

The daily digest cron (08:05 UTC) queries the latest two stability index rows and passes PSI score, band, components, and yesterday's score into the Anthropic digest prompt. The digest opens with the current PSI band. The digest runs on its own 08:05 UTC trigger, five minutes after the daily PSI snapshot (snapshot-psi) at 08:00 UTC, so it reads today's stored row without an explicit promise chain.

Key Files

FilePurpose
worker/src/lib/stability-index.tsPure compute function, band definitions, colors
worker/src/cron/stability-index.ts30-minute cron job
worker/src/api/stability-index.tsAPI endpoint
worker/src/api/backfill-stability-index.tsAdmin backfill (replays formula over completed UTC days only, using the PSI-eligible supply universe plus same-day historical price replay where available)
src/components/stability-index.tsxHomepage widget + lighthouse SVG
src/app/stability-index/client.tsxFull page with charts and methodology
src/hooks/api-hooks.tsTanStack Query hook exports for useStabilityIndex() and useStabilityIndexDetail()