Per-coin, forward-looking stress score (0-100) for depeg stress. It is not a calibrated probability. Computed every 30 minutes from 8 sub-signals.
Methodology Versioning
DEWS shares its methodology versioning with the Depeg Tracker pipeline. Both are tracked together in shared/lib/depeg-dews-version.ts.
- Current methodology version:
v6.08 - Public changelog page:
/methodology/depeg-changelog/ - Canonical source:
shared/lib/depeg-dews-version.ts
Each API response includes the shared methodology envelope with version, versionLabel, currentVersion, currentVersionLabel, changelogPath, asOf, and isCurrent fields.
Score Formula
base = sum(W_i * S_i) / sum(W_i) # available signals only
psiAmp = PSI < 75 ? 1 + ((75 - PSI) / 75) * 0.3 : 1.0
contagionAmp = same-peg first-pass bump, currently 1.15 for DANGER or 1.08 for WARNING, clamped to 1.2
DEWS = round(clamp(0, 100, base * psiAmp * contagionAmp))
Only signals where available = true participate. Weights are redistributed proportionally across available signals.
Minimum signal requirement: At least 2 available signal sources (total weight >= 0.30). If weight is below 0.30, computeDEWS() returns null (insufficient data) instead of emitting 0/CALM.
Evidence-quality WATCH cap: After the amplifier formula, computeDEWS() caps preliminary scores above WATCH_MAX_SCORE = 35 back to WATCH when the evidence set has neither market-price evidence nor DEX-liquidity evidence and there is no severe issuer-control signal. Severe issuer-control evidence is a blacklist sub-signal at or above the configured severe threshold, so a real freeze/blacklist surge can exceed WATCH even without market or DEX corroboration. Capped rows carry insufficientEvidenceReason = "data_quality_only" when the stress is only data quality plus systemic backdrop, or "missing_market_or_liquidity_evidence" when other non-market evidence exists but market/DEX corroboration is missing.
Systemic backdrop amplifier: When PSI drops below 75 (STEADY band), individual DEWS scores are amplified by up to 30%. At PSI=40, amplification is ~14%. At PSI=0, amplification is 30%. This reflects that individual coin stress is more dangerous during systemic instability.
Contagion Amplifier: On top of the PSI amplifier, DEWS applies a bounded per-peg-type contagion amplifier derived from the same cycle's first-pass results. If any tracked stablecoin's first-pass band is DANGER (bump 1.15x) or WARNING (bump 1.08x), the other coins sharing its pegType are re-scored with that multiplier — the amplifier takes the largest qualifying bump, not a sum, so multiple DANGER coins on one peg type still cap at 1.15x. The hard defensive cap is 1.2x (CONTAGION_AMPLIFIER_CAP) for future bump values; different peg types do not share contagion risk. A coin that is itself DANGER or WARNING on the first pass does not contagion-amplify itself — its first-pass result carries forward unchanged. The resulting per-coin amplifier is surfaced on /api/stress-signals as amplifiers.contagion (default 1.0 when no contagion is detected or for legacy cached rows).
Sub-Signals & Weights
| Signal | Key | Weight | Data Source | What It Detects |
|---|---|---|---|---|
| Supply Velocity | supply | 0.25 | stablecoins cache | Rapid redemptions (bank run) |
| Pool Balance Drift | pool | 0.20 | dex_liquidity | One-sided selling pressure in DEX pools |
| Liquidity Erosion | liq | 0.15 | dex_liquidity_history | LPs fleeing |
| Price Confidence | price | 0.15 | stablecoins cache | Oracle/data source failures |
| Cross-Source Divergence | diverg | 0.15 | dex_prices + cache | Fragmented pricing, trust breakdown |
| Blacklist Activity | black | 0.10 | blacklist_events | Issuer emergency freeze surge |
| Mint/Burn Flow | flow | 0.10 | mint_burn_hourly | Redemption surge vs minting |
| Yield Anomaly | yield | 0.05 | yield_data + yield-rankings cache | Yield warning signals plus populated source-risk and rank-attribution stress evidence |
Weights sum to 1.15 but only available signals participate, so redistribution normalizes by actual available weight. When S_flow and S_yield are both unavailable (most coins), effective weight is 1.00 across the 6 original signals.
Threat Bands
| Range | Band | Hex | Description |
|---|---|---|---|
| 0-15 | CALM | #22c55e | No stress signals detected |
| 16-35 | WATCH | #14b8a6 | Mild stress on 1-2 indicators |
| 36-55 | ALERT | #eab308 | Multiple indicators elevated |
| 56-75 | WARNING | #f97316 | Strong stress signals, depeg plausible |
| 76-100 | DANGER | #ef4444 | Severe stress across available weighted signals |
Sub-Signal Details
S_supply — Supply Velocity
Measures supply contraction rate. Only negative changes contribute stress.
- 1d normalization:
[0%, 0] → [1%, 15] → [3%, 40] → [5%, 65] → [10%, 85] → [20%, 100] - 7d normalization:
[0%, 0] → [3%, 15] → [7%, 40] → [15%, 70] → [30%, 100] - Blend:
0.6 * norm1d + 0.4 * norm7d - Size dampening:
sizeFactor = min(1, log10(max(mcap, $1M) / $1M) / 3)— small coins (<$50M) get reduced signal - Anchor availability: if both previous-day and previous-week supply anchors are absent, the sub-signal is unavailable and its weight is redistributed. If one anchor is present, the present side is scored and the missing side contributes zero velocity. Explicit finite zero anchors stay available and produce zero velocity stress rather than a divide-by-zero or a false contraction
S_pool — Pool Balance Drift
DEX pool imbalances from dex_liquidity. Blends:
- 40% balance stress (1 - weighted_balance_ratio)
- 35% pool stress score (avg_pool_stress)
- 25% worst single pool imbalance (from top_pools_json)
Smoothed with previous reading when available.
S_liq — Liquidity Erosion
7-day change in liquidity score and TVL from dex_liquidity_history.
- Score erosion anchors:
[0%, 0] → [5%, 15] → [15%, 40] → [30%, 70] → [50%, 100] - TVL erosion anchors:
[0%, 0] → [10%, 15] → [25%, 40] → [50%, 70] → [75%, 100] - 50/50 blend
- Fail-closed: current liquidity score is required, and at least one of the score or TVL 7-day anchors must be available. If both anchors are missing, the sub-signal is unavailable; if one anchor is missing, that side contributes 0 to the 50/50 blend.
S_price — Price Confidence Degradation
Maps priceConfidence field: high=0, single-source=25, low=60, fallback=80, null price=100.
+15 transition bonus when confidence degrades from previous reading.
S_diverg — Cross-Source Price Divergence
Max of: primary deviation from peg, DEX deviation from peg, cross-source spread (all in bps).
- DEX input comes only from
dex_pricesrows refreshed within the live depeg trust window (DEX_FRESHNESS_SEC = 2100, currently 35 minutes) and backed by at least$1Mof aggregate source TVL, matching the live depeg trust floor - Anchors:
[0bps, 0] → [25bps, 10] → [50bps, 25] → [75bps, 50] → [100bps, 75] → [200bps, 90] → [500bps, 100] - Non-USD peg dampening:
value *= 0.7 - Smoothed with previous reading.
Historical stress_signal_history rows do not retain the underlying DEX trust metadata (source_total_tvl, per-row freshness context) needed to replay this gate exactly. The Wave 5.9 repair path therefore refreshes current rows and prunes unrecomputable daily history from the Mar 9, 2026 trust-floor boundary onward instead of pretending those stored snapshots can be deterministically recomputed.
S_black — Blacklist Activity
Only for stablecoin IDs with direct live blacklist tracker configs. Recent blacklist_events rows are resolved through tracker provenance (config_key / contract_address) to the owning canonical stablecoin ID before scoring, so same-symbol siblings do not inherit each other's freeze events. Legacy rows without provenance fall back only when the symbol maps to a single tracker-owned stablecoin ID. Uses 24h event count with spike detection relative to 7d daily average.
S_flow — Mint/Burn Flow
Available only when mint_burn_hourly data exists and is >= 7 days old. A mature 30-day baseline with zero mint/burn activity in the latest 24h window still counts as available data and contributes zero flow stress. Measures:
- Burn surge: 24h burn volume / 30d daily average
- Burn-to-mint ratio: 24h burns / 24h mints
- 60/40 blend of surge and ratio scores
S_yield — Yield Anomaly
Available for yield-bearing coins with warning signals in yield_data or populated structured yield stress in the published yield-rankings cache. Maps active warning signals to stress points:
| Warning Signal | Points |
|---|---|
yield-spike | 30 |
yield-divergence | 25 |
tvl-outflow | 35 |
negative-trend | 15 |
reward-heavy | 20 |
Score = min(100, sum of active signal points).
Structured Yield Intelligence source-risk and rank-attribution evidence adds these stress points inside the same Yield Anomaly sub-signal:
| Structured input | Points |
|---|---|
sourceRisk.rewardShare > 0.5 | 20 |
sourceRisk.sourceDepthRatio < 0.001 | 35 |
sourceRisk.sourceAgeSeconds > 6h | 15 |
sourceRisk.sourceSwitchCount30d > 0 | 20 |
sourceRisk.sourceRiskPenalty >= 1.5 | 20 |
sourceRisk.venueRiskTier = "medium" | 10 |
sourceRisk.venueRiskTier = "high" | 25 |
rankChangeAttribution.primaryDriver = "source-switch" | 20 |
rankChangeAttribution.primaryDriver = "source-risk" | 20 |
Structured evidence is additive with warning-string evidence and the final Yield Anomaly sub-signal still caps at 100. Medium reviewed venue risk adds the bounded structured-medium-risk-venue warning, while high reviewed venue risk keeps the stronger structured-high-risk-venue warning. Neutral structured rows do not become available zero-stress signals; missing, malformed, or neutral source-risk evidence remains a no-op so legacy warning-only behavior is preserved.
Edge Cases
- NAV tokens (
flags.navToken): Excluded entirely (price appreciates, not pegged) - Non-USD pegs: S_diverg dampened by 0.7 factor (noisier FX pricing)
- Small coins (<$50M): S_supply dampened via size factor
- No DEX data: S_pool and S_liq marked unavailable, weight redistributed
- No blacklist tracking: S_black unavailable for most coins
- New coins / no history: Signals gracefully degrade to unavailable; Supply Velocity is unavailable until at least one previous-day or previous-week supply anchor exists
Data Pipeline
Tables
| Table | Pruning | Purpose |
|---|---|---|
stress_signals | 7 days | 30-minute rolling samples |
stress_signal_history | 365 days | Daily snapshots (first run of UTC day) |
Cron Schedule
Trigger: 26,56 * * * * — DB-only DEWS/PSI lane. It runs after the normal 10,40 * * * * DEX-liquidity slot, but it is a separate scheduled invocation so a DEX-liquidity CPU overrun cannot prevent DEWS publication.
Cron name: compute-dews
Run health semantics: DEWS records upstream read problems as structured cron metadata (sourceFailures, sourceCoverage, validationFailures). The cron returns status: "degraded" when non-bootstrap source dependencies fail. Bootstrap grace is now a one-time state transition, tracked by the dews:bootstrap-complete cache sentinel after the first successful publication. Before that first success, only explicitly optional missing tables are tagged bootstrapAllowed=true; once the sentinel exists, those same failures degrade the run normally. Stale dex_liquidity is treated as a core dependency failure and is recorded in metadata, but rows that meet signal-coverage requirements are still persisted.
Off-chain confirmation resilience: CoinGecko and DefiLlama confirmation fetches used by the pending-depeg pipeline are wrapped in a circuit breaker. A sustained provider outage trips the breaker and short-circuits subsequent confirmation lookups until it resets, so a single upstream failure no longer hammers the endpoint for 45 minutes per pending row.
Data flow:
- Read stablecoins cache, derive peg rates with cached
fxFallbackRatesfor thin non-USD groups - Read
dex_liquidity, live-depeg-trusteddex_prices, anddex_liquidity_history - Read
blacklist_eventscounts (24h + 7d) - Read previous
stress_signalsfor smoothing - Read
mint_burn_hourlyaggregates - Read
yield_data.warning_signalsand structuredsourceRisk/rankChangeAttributionevidence from the publishedyield-rankingscache - Compute DEWS per PSI-eligible coin
- Batch write to
stress_signals(only for coins wherecomputeDEWS()returned a score) - Retire current
stress_signalsrows for PSI-eligible assets that are explicitly present in the stablecoins cache with zero current circulating supply - Daily snapshot to
stress_signal_history(first run of UTC day) - Purge rows for IDs no longer in the current PSI-eligible universe (chunked ID deletes, 90 IDs/chunk, to stay under D1 bind-variable limits)
- Prune old data
API Endpoint
GET /api/stress-signals
All coins (no params): Returns latest DEWS for readable tracked stablecoins. The response-level freshness headers use the latest aggregate publication timestamp (updatedAt) so one retained frozen/long-tail row does not stale the entire /depeg surface; oldestComputedAt remains in the body for consumers that need to detect per-coin lag. Pre-launch tracked entries are excluded because the handler gates on readable tracked IDs.
When a coin has insufficient data in a cycle (computeDEWS() === null), that run skips writes for the coin, so this endpoint continues serving the last valid cached row.
{
"signals": {
"usdt-tether": { "score": 5, "band": "CALM", "signals": { ... }, "computedAt": 1740000000, "methodologyVersion": "6.07" },
...
},
"updatedAt": 1740000000,
"oldestComputedAt": 1740000000,
"malformedRows": 0,
"methodology": { "version": "6.07", "versionLabel": "...", "currentVersion": "6.07", "currentVersionLabel": "...", "changelogPath": "/methodology/depeg-changelog/", "asOf": 1740000000 }
}
updatedAt is the newest current row in the aggregate response and is used for aggregate X-Data-Age / Warning freshness headers. oldestComputedAt is the oldest returned current row and is body-only for consumers that need per-coin lag detection.
Single coin: ?stablecoin=usdt-tether&days=30 (default 30, min 1, max 365) — Returns latest + daily history.
Unknown IDs return 404 with Unknown stablecoin; tracked-but-non-active IDs return 404 with Stablecoin not tracked.
{
"current": { "score": 5, "band": "CALM", "signals": { ... }, "computedAt": 1740000000, "methodologyVersion": "6.07" },
"history": [
{ "date": 1739900000, "score": 3, "band": "CALM", "signals": { ... }, "methodologyVersion": "6.07" },
...
],
"malformedRows": 0,
"methodology": { "version": "6.07", "versionLabel": "...", "currentVersion": "6.07", "currentVersionLabel": "...", "changelogPath": "/methodology/depeg-changelog/", "asOf": 1740000000 }
}
Cache: standard (s-maxage=300, max-age=60)
GET /api/backfill-dews (admin)
Validates DEWS against historical depeg events. The primary calibration path uses stored stress_signal_history rows plus curated anchors and reports precision, recall, false-positive days, false-negative incidents, lead-time P50/P90, alert churn, band-transition stability, and cohort metrics. The older supply/liquidity reconstruction remains a diagnostic path because it cannot replay every live signal or source-trust gate.
GET /api/backfill-dews?repair=...&dry-run=true / POST /api/backfill-dews?repair=... (admin)
Repair modes:
repair=refresh-current: preview or immediately republish currentstress_signalsrows under the live$1MDEX trust floorrepair=prune-history: preview or delete boundedstress_signal_historywindows that cannot be deterministically recomputed because the retained daily snapshots do not store the underlying DEX trust metadata
GET is accepted for the read-only backtest path and for repair previews with dry-run=true. Mutating DEWS repair runs require POST.
Direct ops-api CLI example: CF-Access-Client-Id: <id> and CF-Access-Client-Secret: <secret>
Frontend Integration
| Component | File | Location |
|---|---|---|
DEWSBadge | src/components/dews-badge.tsx | Table rows (hidden when CALM) |
DEWSDetail | src/components/dews-detail.tsx | Stablecoin detail page; contextual methodology hint + footer links on the detail card |
DEWSSummary | src/components/dews-summary.tsx | Homepage widget / depeg-page hero radar; title-level contextual methodology hint |
Hook: useStressSignals() and useStressSignalDetail(id, days) in src/hooks/api-hooks.ts
Classification constants: ThreatBand, THREAT_BAND_COLORS, THREAT_BAND_HEX, THREAT_BAND_LABELS in shared/lib/classification.ts
Design tokens: --dews-calm through --dews-danger, plus radar contrast tokens (--dews-radar-spoke, --dews-radar-calm-boundary, --dews-radar-band-ring-opacity, --dews-radar-outer-ring-opacity, --dews-radar-calm-dot-bloom, --dews-radar-calm-dot-core) in src/styles/tokens/semantic.css
Radar Layout (DEWSSummary)
The radar is center-is-danger: higher threat bands occupy inner rings, CALM coins form an ambient starfield at the periphery.
| Zone | Radius range | Description |
|---|---|---|
| Center label | r 0–38 | SCANNING status label + total monitored count |
| DANGER | r 45–90 | Innermost elevated ring |
| WARNING | r 95–140 | |
| ALERT | r 143–175 | |
| WATCH | r 178–208 | Outermost elevated ring |
| CALM starfield | r 212–238 | Non-interactive ambient dots (r=2 core + r=5 bloom, theme-aware opacity tokens) |
| Outer boundary | r 240 | Radar edge |
Dashed ring boundaries are drawn at each zone's inner edge (r=45, 95, 143, 178) using the zone's threat color, plus a faint gray ring at r=212 delimiting the calm zone. Ring/spoke/calm-dot visibility is theme-aware via the DEWS radar tokens listed above. CALM dots are scattered deterministically using deterministicRadiusOffset(id, 26) from src/lib/dews-radar-utils.ts. The legend renders severity-order bands with live counts: Danger (n), Warning (n), Alert (n), Watch (n), Calm (n). The Calm legend marker uses the same faint bloom+core star-dot treatment as the calm outer starfield.
Alerting
DEWS has a dedicated outbound Telegram path via dispatch-telegram-alerts, scheduled every 5 minutes on the isolated dispatch-telegram-alerts cron. Subscriber filtering and dedupe behavior live in the Telegram alert subsystem, but DEWS remains surfaced through the normal read paths too:
GET /api/stress-signals- Telegram subscriber alerts (
dispatch-telegram-alerts) - Frontend components (
dews-badge,dews-detail,dews-summary)