Skip to main content
Pharos
PHAROSlive stablecoin signals

DEWS

DEWS formula, sub-signals, bands, and API contract.

Per-coin, forward-looking stress score (0-100) estimating depeg 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: v5.95
  • Public changelog page: /methodology/depeg-changelog/
  • Canonical source: shared/lib/depeg-dews-version.ts

Each API response includes a methodology envelope with version, changelogPath, 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.

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_dataYield warning signals (spike, divergence, TVL outflow, negative trend, reward-heavy)

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#ef4444All precursors firing

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

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 symbols in the shared BLACKLIST_STABLECOINS set (shared/types/market.ts). Coverage is derived from the same supported blacklist symbol set as the live blacklist tracker instead of maintaining a separate DEWS-local coin list. 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 only for yield-bearing coins with warning signals in yield_data. 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).


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

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. Compute DEWS per PSI-eligible coin
  7. Batch write to stress_signals (only for coins where computeDEWS() returned a score)
  8. Retire current stress_signals rows for PSI-eligible assets that are explicitly present in the stablecoins cache with zero current circulating supply
  9. Daily snapshot to stress_signal_history (first run of UTC day)
  10. 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)
  11. Prune old data

API Endpoint

GET /api/stress-signals

All coins (no params): Returns latest DEWS for active tracked stablecoins only. Pre-launch tracked entries are excluded because the handler gates on ACTIVE_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": "5.95" },
    ...
  },
  "updatedAt": 1740000000,
  "oldestComputedAt": 1740000000,
  "malformedRows": 0,
  "methodology": { "version": "5.95", "versionLabel": "...", "currentVersion": "5.95", "currentVersionLabel": "...", "changelogPath": "/methodology/depeg-changelog/", "asOf": 1740000000 }
}

updatedAt is the newest current row in the aggregate response. oldestComputedAt is the oldest returned current row and is the timestamp used for aggregate X-Data-Age / Warning freshness headers, so one stale per-coin row cannot be hidden by newer rows for other coins.

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": "5.95" },
  "history": [
    { "date": 1739900000, "score": 3, "band": "CALM", "signals": { ... }, "methodologyVersion": "5.95" },
    ...
  ],
  "malformedRows": 0,
  "methodology": { "version": "5.95", "versionLabel": "...", "currentVersion": "5.95", "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. Reports TP rate and lead time.

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)