Skip to main content
Pegana computes peg state from two numbers — intrinsic value (what the asset should be worth, per its own mechanism) and market value (what you’d actually receive swapping it). The spread between them, smoothed and bucketed, is the entire output.
Smoothed with an EWMA at α = 0.3. Compared against per-asset, per-class thresholds. Resolved through a 5-state FSM (PEGGED → DRIFT → DEPEG → CRITICAL → BLACK_SWAN) with asymmetric hysteresis so a noisy signal does not produce noisy alerts.

Reading order

Intrinsic value

What the asset is supposed to be worth. Per-class definition (LST, stable, CDP, NAV, FX).

Market value

What you’d get swapping it. Jupiter routed quote, repriced via Pyth.

The spread

Formula, EWMA smoothing, per-asset thresholds, the 5-state FSM with hysteresis.

Honesty about limits

What we don’t model. Where the signal can lie. When to mistrust an alert.

Sources, in one table

Each adapter lives in crates/indexer-rs/src/sources/. One source per asset class is a deliberate constraint — we do not “blend” prices.

The 5-state FSM, in one table

BLACK_SWAN is reachable from both the spread path (a discount beyond 2× critical by default, overridable per asset via black_swan / black_swan_bps) and the CR path (hyUSD, CR < 100%). It re-labels only the most extreme moves and auto-exits via the normal decay-down hysteresis like every other band — it is not a no-reset terminal state (ADR-0025). UNKNOWN is a non-alerting status, not a severity band: the engine publishes it at cold-start and as honest-dark when an anchor is detectably broken (a >10% premium on a NAV-priced asset → the NAV-sanity gate). On routine stale, low-confidence, or decoder-drift input it instead holds the last published state rather than flipping to UNKNOWN.

Update cadence

  • Indexers poll their sources every 15s (Sanctum, Solayer, Piggybank, Hylo, Jupiter)
  • Pyth pushes via SSE — sub-second arrivals
  • Engine recomputes on every source event, publishes to Redis + Postgres
  • API snapshots are <1s old; the WebSocket pushes within ~200ms of the engine
  • Alert delivery (Telegram / Webhook) fires within 1–3s of the state transition

How to verify

Every parameter — threshold, dwell time, EWMA α, source endpoint — is committed in assets.toml and crates/engine-rs/src/main.rs. The methodology is reproducible from raw inputs. To inspect any asset’s live parameters:
Or run the bot: