---
title: "Pharos Stability Index"
canonical: "https://pharos.watch/docs/stability-index/"
description: "Pharos Stability Index methodology explaining the PSI formula, active universe, stress bands, storage model, API surface, and replay rules."
---

# Pharos Stability Index (PSI)

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.3`
- **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

| Component | Range | Formula | Purpose |
|-----------|-------|---------|---------|
| **Severity** | 0–68 | `min(68, Σ (abs(bps) / 100 × mcap_share × log₂(1 + mcap / $1B) × 60 × factor))` | Depeg impact weighted by market cap significance |
| **Breadth** | 0–17 | `min(17, Σ sqrt(mcap / $1B) × 3 × factor)` per unique depegged coin | Number of depegging coins, weighted so micro-caps barely register |
| **Stress Breadth** | 0–5 | `min(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 +5 | `clamp(-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)
```

| Age | Factor | Meaning |
|-----|--------|---------|
| 0–30 days | 100% | Full impact — fresh depeg, market-relevant |
| 45 days | 87.5% | Still significant |
| 60 days | 75% | Fading |
| 90 days | 50% | Half impact |
| 120 days | 25% | Floor reached |
| 120+ days | 25% | 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`:

```json
[{ "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

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

## Calibration Examples

| Scenario | Score | Band |
|----------|-------|------|
| 12 micro-coin depegs, +0.26% mcap | 96.4 | BEDROCK |
| USDT wobbles 10bps | 65.1 | TREMOR |
| USDT wobbles 30bps | 25.4 | CRISIS |
| USDT 50bps + USDC 20bps − 3% mcap | 12.0 | MELTDOWN |

## Input Data

| Input | Source |
|-------|--------|
| 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 cap | Sum of the PSI-eligible stablecoins present in the DefiLlama cache (`PSI_ELIGIBLE_IDS` = active tracked + shadow; pre-launch and frozen tracked rows excluded) |
| 7-day market cap change | Current vs previous week total from stablecoins cache |
| DEWS stress breadth | Latest `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` (baseline schema `0000_baseline.sql`) 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` (defined in `worker/migrations/0000_baseline.sql` after the D1 squash) — per-sample: `stored_at`, `score`, `band`, `components` (JSON), `input_snapshot` (JSON), `methodology_version`. `stability_index` (defined in `worker/migrations/0000_baseline.sql` after the D1 squash) — 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](https://pharos.watch/docs/api-reference/) for the full response shape.

## Frontend

- **Homepage widget**: `src/components/kpi-bar.tsx` — score, band, delta from yesterday, and 30-day sparkline in the market snapshot bar
- **Dedicated page**: `src/app/stability-index/client.tsx` — hero KPI bar focused on the lighthouse/current PSI signal and historical PSI measurements, score history chart with band-colored zones, Beam Dimmers for the current formula component pressure, 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`). Beam Dimmers use the current PSI component values and prior-sample deltas only; they are not a causal event timeline and do not change scoring.
- **Hook**: `src/hooks/api-hooks.ts` — `useStabilityIndex()` (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 PSI sample plus daily rows (current and yesterday) 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

| File | Purpose |
|------|---------|
| `worker/src/lib/stability-index.ts` | Pure compute function, band definitions, colors |
| `worker/src/cron/stability-index.ts` | 30-minute cron job |
| `worker/src/api/stability-index.ts` | API endpoint |
| `worker/src/api/backfill-stability-index.ts` | Admin backfill (replays formula over completed UTC days only, using the PSI-eligible supply universe plus same-day historical price replay where available) |
| `src/components/kpi-bar.tsx` | Homepage market snapshot PSI widget |
| `src/app/stability-index/client.tsx` | Full page with charts and methodology |
| `src/hooks/api-hooks.ts` | TanStack Query hook exports for `useStabilityIndex()` and `useStabilityIndexDetail()` |
