https://api.pegana.xyz
Endpoints at a glance
The full schema lives in the API Reference.
Stability contract
Thev1 read surface is a stable, versioned contract you can wire a risk engine
against. Going forward we make only additive changes within v1 — new fields (such as
risk_score) and new endpoints — never a
breaking rename or removal; any future breaking change would ship under a new prefix (v2)
with v1 left working.
One-time reshape (2026-07-24). Before the first external integration,
v1 had a single
coordinated cleanup to the shapes documented here — lists gained the { ok, generated_at, count, data } envelope, the peg feed became a data array, and a few fields were renamed
(as_of→updated_at, model_note/note→advisory_note, positions_affected→positions).
See the changelog for the migration. The additive-only promise above holds from
that point on./v1/assets/{symbol}/state as often as your logic needs (soft
~300 req/min per IP); it is the same object the dashboard, embed widget and bot read,
so it never disagrees with what a human sees on the site.
Pegana is purely observational — it classifies and proves, and never touches your
contracts or parameters. Reading Pegana adds no actuation and no new attack surface to
your protocol; you keep full control of any response.
Response format
Two wire conventions hold across every endpoint. They are part of the stability contract — values never change form in a breaking way.-
Monetary & ratio values are exact decimal strings.
discount,intrinsic_usd,market_usd,market_cap,loop_exposure_usd,sol_per_lst,worst_abs_24h,jitter_bps_24hand friends are JSON strings (e.g."3.58235002","-0.007405"), not floats — parse them with a decimal/BigNumber library so you never lose precision. Trailing zeros are trimmed, so the value is exact but compact. -
USD aggregates are cent-rounded; signal fields keep full precision. Large USD totals
(
market_cap,loop_exposure_usd,liquidatable_usd,collateral_usd,collateral_lev_usd,debt_usd) are rounded to two decimals — sub-cent noise on a hundreds-of-millions figure is meaningless. Price and discount fields (discount,intrinsic_usd,market_usd, …) are not rounded — they carry the full peg signal. -
Timestamps are RFC3339 UTC, millisecond precision,
Zsuffix — e.g.2026-07-24T15:33:14.545Z. Every*_at/ts/generated_atfield uses this single form; parse with any RFC3339/Dateparser. Resource freshness is alwaysupdated_at; the response’s own build time isgenerated_at. -
Lists are enveloped; single resources are bare. Every endpoint that returns a
collection (
/v1/assets,/v1/alerts,/v1/calibration,/v1/assets/{s}/history,/v1/peg/feed) wraps its rows in a uniform envelope so you always branch on the same shape and new top-level fields never break you:Read the rows fromdata. Single-resource endpoints (/v1/assets/{s},/v1/assets/{s}/state,/v1/assets/{s}/loop-exposure, …) return the resource object itself (the Stripe/GitHub convention); errors always use the{error, message}envelope.
Do not compare decimals or timestamps as exact byte strings — compare the parsed numeric
value or instant. Two representations that differ only in trailing zeros or sub-millisecond
precision are the same value.
Read one asset’s state
risk_score — one number to branch on
risk_score is a derived 0–100 integer for integrators who want a single
sortable / thresholdable value — if risk_score >= 75 { gate_market() } — alongside
(never replacing) the authoritative state. It is anchored to the committed state,
so it can never disagree with the classification:
Within each band the score interpolates by the effective, direction-sensitive discount
magnitude (a benign premium on an LST / yield token scores ~0). It is
null whenever
the state is UNKNOWN/absent, there is no fresh discount, or the asset uses
collateral-ratio thresholds (hyUSD) — Pegana never fabricates a number it does not have.
The score is a pure, deterministic function of the other published fields, so you can
recompute it yourself; it is a projection of the verifiable classification, not a
separate input and not part of any receipt hash.
List all assets
{ ok, generated_at, count, data } where data is an
array of AssetCard objects with optional series_24h (hourly avg discount).
Filter by class or peg query params. class is a case-sensitive exact match —
valid values are lst, stable_fiat, stable_dn, stable_cdp, stable_yield,
stable_fx, and synth_lev. An unknown value (e.g. stablecoin) returns an empty
data array with HTTP 200.
Asset history
bucket=raw— discount snapshots as recorded by the enginebucket=1m— 1-minute aggregate (use for charts; smaller payload)from,to— ISO 8601 timestamps; default last 24hlimit— clamped 1–5000, default 500
Systemic impact (Loop Intelligence)
A depeg rarely breaks in isolation — it breaks the leverage stacked on top of it. These endpoints answer “how much leveraged collateral rides on this asset across Solana lending, and how much of it would be liquidated if the asset depegs?” — a cross-protocol view no single lending dashboard shows.Everything here is an advisory model estimate — not a verifiable receipt. It is
namespaced away from the peg fields and carries its own
advisory_note. It is anonymous by
construction: only k-anonymised distributions are computed (a band with fewer than 5
positions, or one dominated by a single position, is suppressed), and no wallet, owner
or authority is ever read. Coverage today spans Kamino + Jupiter Lend — read the
numbers as an honest floor, not a ceiling.Exposure + cascade — /v1/assets/{symbol}/loop-exposure
cascade carries the whole depeg ladder in one call — liquidatable_usd is the
leveraged collateral crossing a liquidation threshold at each preset shock (in bps).
positions is a k-floored range, never a raw count. The array is empty (or
the object omitted) when the asset carries no tracked leverage.
Single shock — /v1/assets/{symbol}/simulate-depeg
shock_bps must be one of {100, 250, 500, 1000, 2000} (a fixed grid — continuous
sweeps are refused so the anonymised distribution can’t be differenced). Returns the
same estimate as one cascade rung, plus a by_protocol breakdown. coverage is
partial whenever risky bands were privacy-suppressed (the honest default).
Partner feed — /v1/peg/feed
The batch feed carries the full peg signal for every active asset as a data array
(each signal carries its own mint), including an optional systemic block — the same
exposure + cascade, packaged for a token page. The global methodology version is stamped
once on the envelope, not on every signal:
data[] by each signal’s mint. systemic is omitted (not null) when the token
carries no tracked leverage, and it never mixes into the verifiable peg fields (state,
discount, receipt_url). The feed supports conditional GETs (ETag / If-None-Match →
304), so high-volume pollers stay cheap.
Pagination
We do not paginate/v1/assets (a few dozen rows). For /v1/alerts and /history, use the
since / from + limit parameters and walk forward by adjusting the lower bound.
Errors
We return standard HTTP status codes:
Error bodies are
{"error": "...", "message": "..."} (plus an optional asset) — short, machine-readable. Branch on error, never on message.
CORS
https://pegana.xyz, https://www.pegana.xyz, and http://localhost:3000 are
allowed by default. Add more origins via the CORS_EXTRA_ORIGINS env var on
self-hosted instances.
Rate limit
Soft per-IP sliding-window: ~300 req/min in production (the repo default is 60 req/min — the production host raises it). The limit resets on a 60-second sliding window./v1/audit.csv is stricter at 10 req/min. /healthz and
/readyz are exempt.
The 429 response includes a Retry-After: 60 header (the sliding-window upper
bound) so clients know when to retry, but the API does not emit X-RateLimit-*
headers — your remaining budget is not client-discoverable, so handle 429
defensively and back off. If you hit 429, slow down or contact
rafael@pegana.xyz for a higher limit.
What about authenticated routes?
The/v1/me/*, /v1/auth/*, and webhook management endpoints require a JWT — see
authentication. These are user-scoped routes (your subscriptions,
your delivery history, your webhook configuration).
Next
WebSocket stream
Sub-second push for state changes — better than polling.
API Reference
Full OpenAPI spec with try-it-now.