Skip to main contentSkip to data table
Pharos
PHAROSlive stablecoin signals

DEWS

Depeg Early Warning System methodology: DEWS formula, stress sub-signals, bands, downgrade gates, and API response contract.

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

SignalKeyWeightData SourceWhat It Detects
Supply Velocitysupply0.25stablecoins cacheRapid redemptions (bank run)
Pool Balance Driftpool0.20dex_liquidityOne-sided selling pressure in DEX pools
Liquidity Erosionliq0.15dex_liquidity_historyLPs fleeing
Price Confidenceprice0.15stablecoins cacheOracle/data source failures
Cross-Source Divergencediverg0.15dex_prices + cacheFragmented pricing, trust breakdown
Blacklist Activityblack0.10blacklist_eventsIssuer emergency freeze surge
Mint/Burn Flowflow0.10mint_burn_hourlyRedemption surge vs minting
Yield Anomalyyield0.05yield_data + yield-rankings cacheYield 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

RangeBandHexDescription
0-15CALM#22c55eNo stress signals detected
16-35WATCH#14b8a6Mild stress on 1-2 indicators
36-55ALERT#eab308Multiple indicators elevated
56-75WARNING#f97316Strong stress signals, depeg plausible
76-100DANGER#ef4444Severe 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_prices rows refreshed within the live depeg trust window (DEX_FRESHNESS_SEC = 2100, currently 35 minutes) and backed by at least $1M of 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 SignalPoints
yield-spike30
yield-divergence25
tvl-outflow35
negative-trend15
reward-heavy20

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 inputPoints
sourceRisk.rewardShare > 0.520
sourceRisk.sourceDepthRatio < 0.00135
sourceRisk.sourceAgeSeconds > 6h15
sourceRisk.sourceSwitchCount30d > 020
sourceRisk.sourceRiskPenalty >= 1.520
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

TablePruningPurpose
stress_signals7 days30-minute rolling samples
stress_signal_history365 daysDaily 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:

  1. Read stablecoins cache, derive peg rates with cached fxFallbackRates for thin non-USD groups
  2. Read dex_liquidity, live-depeg-trusted dex_prices, and dex_liquidity_history
  3. Read blacklist_events counts (24h + 7d)
  4. Read previous stress_signals for smoothing
  5. Read mint_burn_hourly aggregates
  6. Read yield_data.warning_signals and structured sourceRisk / rankChangeAttribution evidence from the published yield-rankings cache
  7. Compute DEWS per PSI-eligible coin
  8. Batch write to stress_signals (only for coins where computeDEWS() returned a score)
  9. Retire current stress_signals rows for PSI-eligible assets that are explicitly present in the stablecoins cache with zero current circulating supply
  10. Daily snapshot to stress_signal_history (first run of UTC day)
  11. 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)
  12. 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 current stress_signals rows under the live $1M DEX trust floor
  • repair=prune-history: preview or delete bounded stress_signal_history windows 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

ComponentFileLocation
DEWSBadgesrc/components/dews-badge.tsxTable rows (hidden when CALM)
DEWSDetailsrc/components/dews-detail.tsxStablecoin detail page; contextual methodology hint + footer links on the detail card
DEWSSummarysrc/components/dews-summary.tsxHomepage 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.

ZoneRadius rangeDescription
Center labelr 0–38SCANNING status label + total monitored count
DANGERr 45–90Innermost elevated ring
WARNINGr 95–140
ALERTr 143–175
WATCHr 178–208Outermost elevated ring
CALM starfieldr 212–238Non-interactive ambient dots (r=2 core + r=5 bloom, theme-aware opacity tokens)
Outer boundaryr 240Radar 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)