The Pharos API is a REST API served by a Cloudflare Worker backed by a D1 database. It powers the pharos.watch stablecoin analytics dashboard through a split website-data lane plus an external integration API. On https://api.pharos.watch, all public routes are API-key protected unless this reference explicitly marks them as exempt.
Base URL: https://api.pharos.watch
Unless noted otherwise, responses are Content-Type: application/json. Exceptions: GET /api/og/* returns image/png, and POST /api/telegram-webhook returns a plain-text ok body. CORS headers are added to every response, but Access-Control-Allow-Origin is restricted by the Worker CORS_ORIGIN allowlist (production repo config: https://pharos.watch,https://ops.pharos.watch). When the request Origin matches an allowlisted entry, the Worker echoes that origin and sets Vary: Origin; when a request includes a foreign Origin, the worker omits Access-Control-Allow-Origin, and OPTIONS preflights from foreign origins receive 403. Requests without an Origin header keep the existing first-allowlisted-origin fallback. Protected public /api/* traffic on api.pharos.watch is API-key-gated via X-API-Key; production runs with PUBLIC_API_AUTH_MODE=enforce, so missing or invalid keys normally receive 401 Unauthorized after public-limiter checks. Rate-limit overages return 429, and limiter/auth dependency failures can return 503.
Surface Split
The runtime now uses three HTTP lanes:
https://api.pharos.watchis the external integration API. Protected public routes requireX-API-Key.https://site-api.pharos.watchis the website-internal Worker host. It accepts only allowlistedGETreads plusX-Pharos-Site-Proxy-Secret./_site-data/*is the same-origin Pages Functions proxy used by browsers onpharos.watch,ops.pharos.watch, and Pages preview hosts.
Browser consumers should use same-origin /_site-data/* via the frontend helpers in src/lib/api.ts. In production, that Pages proxy targets https://site-api.pharos.watch through SITE_API_ORIGIN. Direct integrations, CI smoke, and build-time sync scripts should target https://api.pharos.watch.
The direct Worker cache profiles below describe responses from api.pharos.watch / site-api.pharos.watch. The Pages /_site-data/* proxy adds a separate same-origin Cache API layer for successful responses without Set-Cookie; this can cache even a direct Worker route whose own profile is no-store, such as /api/health.
Public API Auth
Unless a route is explicitly called out below as exempt, requests to https://api.pharos.watch must send:
- header:
X-API-Key: ph_live_<16 hex prefix>_<32 char base64url secret> - example shape:
ph_live_0123456789abcdef_abcdefghijklmnopqrstuvwxyzABCDEF
Public, non-admin routes on https://api.pharos.watch that do not require X-API-Key are limited to:
GET /api/healthGET /api/og/*POST /api/feedbackPOST /api/telegram-webhook
POST /api/telegram-webhook is externally reachable but not anonymous: it requires X-Telegram-Bot-Api-Secret-Token instead of X-API-Key.
Admin/operator routes are also outside the public API-key gate, but they remain Cloudflare-Access-gated and are supported only through ops-api.pharos.watch or the ops.pharos.watch/api/admin/* Pages proxy.
The worker stores only the key prefix plus a peppered HMAC of the secret portion. Admin callers create, rotate, and deactivate keys through the operator lane (ops.pharos.watch / ops-api.pharos.watch); plaintext tokens are returned only once at creation/rotation time.
Stablecoin IDs
Most endpoints use the Pharos stablecoin ID in ticker-issuer format (e.g. usdt-tether). IDs are checked through the shared stablecoin-ID registry (shared/lib/stablecoin-id-registry.ts). Unknown or non-canonical IDs return 404.
Canonical IDs use ticker-issuer format — lowercase ticker symbol hyphenated with the issuer/protocol name:
| Example | Asset |
|---|---|
"usdt-tether" | Tether (USDT) |
"usdc-circle" | USD Coin (USDC) |
"paxg-paxos" | PAX Gold (PAXG) |
"ustb-superstate" | Superstate USTB |
"gyen-gyen" | GYEN |
The full list is exported from shared/lib/stablecoins/index.ts, with the raw metadata stored in shared/data/stablecoins/*.json and validated by shared/lib/stablecoins/schema.ts. The API currently accepts canonical IDs only; legacy route redirects are handled at the frontend/static-export layer, not by the worker ID resolver.
Response Headers
Endpoints backed by the cron cache include these additional headers:
| Header | Description |
|---|---|
X-Data-Age | Seconds elapsed since the cron last wrote this data to D1 |
Warning | Freshness warning (110) when cached data is older than the generic freshness runway, plus endpoint-specific advisory warnings (199) on a few compute-on-read routes |
Generic freshness status is fresh through 8x maxAge, degraded through 12x maxAge, then stale. Generic freshness headers emit Warning and downgrade Cache-Control to no-store after age > 8x maxAge so edge/browser caches do not keep serving an old payload after the underlying cron data recovers. Some routes also use Warning for dependency or quality advisories even when the age is still inside that runway; clients should treat body _meta.status as authoritative when it exists.
Response Body Freshness (_meta)
Endpoints that emit _meta into plain-object (non-array) response bodies do so through createCacheHandler() or route-specific manual injection, alongside the HTTP freshness headers above. This provides inline freshness metadata for consumers that prefer not to parse response headers.
Shape:
{
"_meta": {
"updatedAt": 1710500000,
"ageSeconds": 42,
"status": "fresh"
}
}
| Field | Type | Description |
|---|---|---|
updatedAt | number | Unix epoch seconds when the cron last wrote this data to D1 |
ageSeconds | number | floor(now / 1000) - updatedAt |
status | string | "fresh" (age/max <= 8.0), "degraded" (8.0 < ratio <= 12.0), or "stale" (ratio > 12.0) |
Route-specific manual _meta injectors can be stricter. GET /api/chains uses its 1800-second budget directly (fresh <= 1x, degraded <= 2x, then stale) and switches its response to no-store whenever the chain snapshot is not fresh.
Endpoints with _meta:
| Endpoint | Max Age (sec) | Source |
|---|---|---|
GET /api/stablecoins | 600 | createCacheHandler |
GET /api/chains | 1800 | worker/src/api/chains.ts |
GET /api/bluechip-ratings | 43200 | Custom cache reader in worker/src/api/cache-handlers.ts |
GET /api/usds-status | 86400 | createCacheHandler |
GET /api/yield-rankings | 3600 | Manual injection after live safety hydration |
Array-typed responses (e.g., endpoints returning a JSON array at the top level) do not include _meta. They receive X-Data-Age / Warning only when their handler wires freshness metadata explicitly; history endpoints such as supply history, DEX liquidity history, and non-USD share currently expose cache headers but no freshness headers.
The frontend apiFetchWithMeta() helper (in src/lib/api.ts) reads _meta from the response body when present, falling back to the X-Data-Age header for endpoints that do not include it.
Cache-Control Profiles
These profiles apply while the dataset is within its generic freshness runway. Once a cache-backed response exceeds 8x its endpoint max age, the worker overrides that response to Cache-Control: no-store until a fresh response is generated.
| Profile | Cache-Control | Used by |
|---|---|---|
| realtime | public, s-maxage=60, max-age=10 | stablecoins, stablecoin-summary, blacklist, blacklist-summary, depeg-events, peg-summary, mint-burn-events, chains |
| standard | public, s-maxage=300, max-age=60 | stablecoin-charts, redemption-backstops, usds-status, daily-digest, digest-archive, report-cards, stability-index, yield-rankings, mint-burn-flows, stress-signals |
| custom | public, s-maxage=300, max-age=300 | dex-liquidity (browser-side max-age extended to match CDN TTL) |
| per-coin | public, s-maxage=300, max-age=10 | stablecoin/:id (cache-aside with 5-min per-coin TTL in D1) |
| slow | public, s-maxage=3600, max-age=300 | supply-history, dex-liquidity-history, bluechip-ratings, yield-history, safety-score-history, non-usd-share |
| archive | public, s-maxage=86400, max-age=3600 | digest-snapshot |
| no-store | no-store | health plus all admin GET routes after router override (status, status-history, request-source-stats, API key inventory/audit routes, admin-action-log, debug-sync-state, backfill-dews, backfill-dews?repair=...&dry-run=true, audit-depeg-history?dry-run=true, discovery-candidates, status-probe-history) |
POST /api/feedback, POST /api/telegram-webhook, and admin POST endpoints bypass edge caching because they are non-GET request paths. They are not part of a cacheable Cache-Control profile and do not currently rely on an emitted Cache-Control: no-store header.
Polling Guidance
Recommended minimum polling cadence for external integrations:
| Cache profile | Minimum poll interval | Notes |
|---|---|---|
| realtime | 60 seconds | Polling faster usually re-fetches the same edge-cached payload |
| standard | 300 seconds | Preferred baseline for most dashboards |
| per-coin | 300 seconds | GET /api/stablecoin/:id is history-heavy; avoid short loops |
| slow | 3600 seconds | Historical/timeline endpoints should generally be polled hourly |
| archive | 86400 seconds | Historical snapshot payload for digest SSG and recap pages |
| no-store | On-demand only | Health/admin diagnostics; avoid high-frequency polling |
Client best practices:
- Add interval jitter (
±10%) to avoid synchronized bursts. - Read
X-Data-Age+Warningfor freshness/stale decisions when those optional headers are present. - Back off exponentially on
429and5xxresponses.
Rate Limits
Public API traffic enforces rate limiting to ensure fair usage. The Telegram webhook is exempt from the public IP limiter because Telegram sends from fixed infrastructure and is authenticated separately with X-Telegram-Bot-Api-Secret-Token; valid API-key traffic uses the per-key limiter instead of the IP limiter.
Global Limit
| Scope | Limit | Window |
|---|---|---|
| Per IP (unauthenticated) | 300 requests | 60 seconds |
| Per API key | Varies (default 120) | 60 seconds |
When the public IP limiter or per-key public API limiter is exceeded, the API returns 429 Too Many Requests:
{
"error": "Rate limit exceeded"
}
Rate-limited responses include the retry delay in the HTTP Retry-After header when the worker can compute one.
POST /api/feedback also has a form-specific limiter. Its 429 body is { "error": "Too many submissions. Please wait a few minutes." }, and it should be handled as a local submission throttle rather than as a public API quota response.
If global public-IP limiter bookkeeping fails repeatedly, the worker fails closed after 3 consecutive limiter errors and returns 503 Service Unavailable with { "error": "Public API temporarily unavailable" } plus Retry-After: 60. Treat this as an emergency limiter-health condition, not as successful quota exhaustion. API-key traffic uses a separate per-key limiter; quota overages return 429 with Retry-After when available, and dependency failures do not use the same 3-strike public-IP emergency state unless the implementation changes.
Retry Guidance
- Respect the
Retry-Afterheader when present - Add random jitter (0–2 seconds) to avoid thundering-herd retries
- Use exponential backoff for sustained 429 responses
- Combine with the polling cadences in the section above to stay well under limits
Error Response Conventions
JSON API handlers use { "error": "message" } JSON format. GET /api/og/* returns image/png on success; unknown OG route patterns return the normal JSON error body, while OG data/render failures inside known image routes can return text/plain.
| Status | Meaning | When |
|---|---|---|
| 400 | Bad Request | Missing required parameters, invalid enum values, malformed numeric input, or out-of-range numeric/filter values on handlers that opt into rejection (rangePolicy: "reject"). Some endpoints intentionally clamp or default selected numeric params; endpoint sections call this out where it is part of the contract. |
| 401 | Unauthorized | Protected public endpoint called without a valid X-API-Key, or admin endpoint called without a valid ops-api Access JWT (typically obtained through Cloudflare Access user login or service-token auth) |
| 403 | Forbidden | Disallowed CORS preflight from a foreign Origin, Pages ops proxy mutating request without a matching same-origin Origin, or mutating admin request missing X-Pharos-Admin: 1 |
| 404 | Not Found | Unknown stablecoin ID or missing resource |
| 429 | Too Many Requests | Rate limit exceeded (global public IP limiter, per-key public API limiter, or feedback-specific limiter; feedback uses its own message body) |
| 500 | Internal Server Error | Unhandled exception (caught by withErrorHandler) |
| 502 | Bad Gateway | Upstream fetch failed (external data provider or Pages proxy upstream), or the ops proxy received a Cloudflare Access login redirect from ops-api |
| 503 | Service Unavailable | Cache-passthrough endpoint where cache has never been populated, cached payload is corrupt / rejected by validation, limiter storage fails closed after repeated D1 errors, or MAINTENANCE_MODE=true (global kill switch via wrangler secret put) |
| 504 | Gateway Timeout | Pages /_site-data/* or /api/admin/* proxy timed out waiting for its Worker upstream (10 s default; 20 s for ops /api/status and /api/status-history) |
Rule: Cache-passthrough handlers return 503 when data hasn't been populated yet or when the stored cache payload is malformed and rejected at read time. Query handlers that find no matching rows return 200 with empty results (e.g., { events: [], total: 0 }). When MAINTENANCE_MODE is set to "true", all non-OPTIONS requests immediately return 503 with { "error": "maintenance", "message": "..." } — used during DB migrations. OPTIONS CORS preflights are handled before the maintenance gate.
Method Gating Policy
HTTP method allowance is defined centrally in shared/lib/api-endpoints/ and enforced by worker/src/router.ts (validateEndpointMethod).
GETis accepted for read endpoints (plus admin debug/status endpoints,GET /api/backfill-dews, and dry-run repair previews forGET /api/backfill-dews?repair=...&dry-run=true).POSTis accepted for mutating admin endpoints,POST /api/feedback, andPOST /api/telegram-webhook.GET, POSTis accepted on/api/api-keysso operators can list keys and create a new key through the same route.POSTis accepted on/api/api-keys/:id/update,/api/api-keys/:id/deactivate, and/api/api-keys/:id/rotate./api/audit-depeg-historyallowsGETonly with?dry-run=true; otherwise it isPOST-only./api/backfill-dewsallowsGETfor the historical backtest and forrepair=...&dry-run=truepreviews; mutating repair runs arePOST-only.- Unmatched unknown paths return
404before method validation. Once a static or dynamic route family is registered, known paths with disallowed methods return405withAllow; unsupported verbs on known endpoint families return405withAllow: GET, POST.
The same shared endpoint descriptors now also carry static worker dependency-hydration hints consumed by worker/src/routes/registry.ts, where the worker binds shared endpoint keys directly to handlers through a single static route-definition list. That keeps endpoint metadata, router behavior, method guards, admin status-page actions, and worker-side static route wiring aligned from one source of truth plus one worker binding table.
Public Endpoints
Unless an endpoint section explicitly says Authentication: exempt, routes in this section require X-API-Key when called on https://api.pharos.watch.
GET /api/stablecoins
Full stablecoin list with current supply, price, chain breakdown, and FX rates. Data is refreshed by cron every 15 minutes; the cache entry has a 10-minute max-age.
Cache: realtime — X-Data-Age and Warning headers included.
Response
{
"peggedAssets": [StablecoinData, ...],
"fxFallbackRates": { "peggedEUR": 1.082, "peggedGBP": 1.26 },
"_meta": { "updatedAt": 1710500000, "ageSeconds": 42, "status": "fresh" }
}
fxFallbackRates is present when the ECB FX-rate cron has run; keys are pegType strings (e.g. "peggedEUR"), values are rates in USD.
StablecoinData fields
| Field | Type | Description |
|---|---|---|
id | string | Pharos stablecoin ID |
name | string | Full name (e.g. "Tether") |
symbol | string | Ticker (e.g. "USDT") |
geckoId | string | null | CoinGecko ID (normalized output key; upstream DefiLlama uses gecko_id) |
pegType | string | DefiLlama peg type (e.g. "peggedUSD", "peggedEUR") |
pegMechanism | string | "fiat-backed", "crypto-backed-algorithmic", etc. |
priceSource | string | Source label for the current price ("defillama-list", "coingecko", composite agreement labels such as "binance+coingecko+kraken", "geckoterminal", "protocol-redeem", "dexscreener", etc.). For high-confidence consensus this label can describe the full agreeing cluster even when the published price is the cluster median rather than one member's raw mark. When no usable current price survives validation, the cache keeps price = null and serializes priceSource = "missing" for contract stability. |
priceConfidence | string | null | Price confidence level: "high" (cross-validated agreement), "single-source", "low" (sources diverge), "fallback" (enrichment pipeline) |
priceUpdatedAt | number | null | Compatibility timestamp for the current price; mirrors the effective observation time when available |
priceObservedAt | number | null | Unix seconds for the effective observation time attached to the selected source price; interpret alongside priceObservedAtMode |
priceObservedAtMode | "upstream" | "local_fetch" | "unknown" | null | Whether priceObservedAt came from source-native freshness metadata, local fetch time, or legacy/unknown provenance |
priceSyncedAt | number | null | Unix seconds when Pharos selected and wrote the current price during the sync |
supplySource | string | undefined | Supply data source: "defillama", "defillama-history-gap-fill" (used when a tracked DefiLlama live row collapses to zero supply but recent DefiLlama chart history still has a fresh non-zero total), "coingecko-gap-fill" (used when tracked deployments are missing from DefiLlama chain coverage and CoinGecko repairs the total/history buckets), "coingecko-fallback", or "onchain-total-supply" (used when a supplemental asset is normalized from on-chain total supply instead of an upstream market-cap field) |
price | number | null | Current price in USD. For high-confidence consensus this is the median of the winning agreeing cluster; for single-source, low-confidence, or fallback outcomes it is the selected source price. |
circulating | Record<string, number> | Current supply in USD, keyed by pegType (e.g. { "peggedUSD": 138000000 }) |
circulatingPrevDay | Record<string, number> | Supply 24 h ago |
circulatingPrevWeek | Record<string, number> | Supply 7 days ago |
circulatingPrevMonth | Record<string, number> | Supply ~30 days ago |
chainCirculating | Record<string, ChainCirculating> | Per-chain breakdown. For "coingecko-gap-fill" and "defillama-history-gap-fill" assets this remains DefiLlama-led unless the missing total can be allocated safely to one tracked chain, so the per-chain sum may be a lower bound on total supply. |
chains | string[] | List of chain names where the token is deployed |
consensusSources | string[] | Source names that returned a valid price for this coin during the sync cycle. Defaults to [] when absent. |
agreeSources | string[] | undefined | Compatibility alias for agreeing/current price sources when present |
ChainCirculating
{
"current": 50000000,
"circulatingPrevDay": 49000000,
"circulatingPrevWeek": 47000000,
"circulatingPrevMonth": 44000000
}
All circulating values are already in USD (the list endpoint does not return native-currency values for non-USD pegs). Do not multiply by price.
GET /api/stablecoin/:id
Historical price and supply chart data for a single stablecoin. Proxies DefiLlama (or CoinGecko for commodity/CG-only tokens) with a 5-minute server-side cache.
All upstream calls use fetchWithRetry with explicit per-request timeouts; on upstream/parse failures, or when CoinGecko-derived history is empty/stale, logs include source tags and stablecoin ID before stale-cache fallback or supply_history reconstruction. CoinGecko history is treated as stale when its newest point is more than 72 hours old.
Path parameter: :id — Pharos stablecoin ID.
Cache: per-coin — custom Cache-Control with a 5-minute server-side D1 TTL (public, s-maxage<=300, max-age=10)
Response
{
"tokens": [TokenPoint, ...]
}
TokenPoint
| Field | Type | Description |
|---|---|---|
date | number | Unix timestamp (seconds) |
totalCirculatingUSD | Record<string, number> | Supply in USD per pegType key |
totalCirculating | Record<string, number> | Supply in native units per pegType key |
For regular stablecoins the response still includes the raw DefiLlama detail fields, but the worker now also materializes totalCirculatingUSD and totalCirculating on each token row for contract consistency. Commodity and CG-only tokens are returned directly in the normalized shape above.
For non-USD pegs, totalCirculating remains in native units while totalCirculatingUSD is converted to USD using the current token price before caching, so the USD field always reflects market cap regardless of peg type.
GET /api/stablecoin-summary/:id
Lightweight per-coin snapshot sourced from cached stablecoins data. Designed for integrators that need current price/supply context without transferring full /api/stablecoin/:id history payloads.
Browser surfaces on pharos.watch and ops.pharos.watch should reach this route through same-origin /_site-data/stablecoin-summary/:id, which proxies onto the internal site-api lane instead of the external API-key lane.
Path parameter: :id — Pharos stablecoin ID.
Cache: realtime — X-Data-Age and Warning headers included.
Error responses: 503 when the shared stablecoins cache is missing or structurally corrupt; 404 when the requested coin ID is absent from an otherwise valid cache snapshot.
Response
{
"id": "usdt-tether",
"name": "Tether",
"symbol": "USDT",
"pegType": "peggedUSD",
"pegMechanism": "fiat-backed",
"priceUsd": 1.0001,
"priceSource": "coingecko+defillama-list",
"priceConfidence": "high",
"supplySource": "defillama",
"supplyByPegUsd": { "peggedUSD": 183883564940.52 },
"supplyUsd": {
"current": 183883564940.52,
"prevDay": 183697699496.48,
"prevWeek": 183673067145.19,
"prevMonth": 185316486043.16,
"change1d": 185865444.03,
"change7d": 210497795.33,
"change30d": -1432921102.64
},
"chainCount": 17,
"updatedAt": 1772718367
}
| Field | Type | Description |
|---|---|---|
id | string | Pharos stablecoin ID |
name | string | Asset name |
symbol | string | Ticker symbol |
pegType | string | Peg type key (peggedUSD, peggedEUR, etc.) |
pegMechanism | string | Backing/mechanism classification |
priceUsd | number | null | Current price in USD |
priceSource | string | Price source identifier. When priceUsd is null, this may be "missing" to indicate that no usable current price survived validation. |
priceConfidence | string | null | Price confidence label |
supplySource | string | null | Supply source identifier |
supplyByPegUsd | Record<string, number> | Current supply by peg bucket (USD) |
supplyUsd | object | Aggregate USD supply values and deltas (current, prevDay, prevWeek, prevMonth, change1d, change7d, change30d) |
chainCount | number | Number of chains where the asset is deployed |
updatedAt | number | Unix seconds of the stablecoins snapshot used for this response |
GET /api/non-usd-share
Returns historical non-USD stablecoin market share data from supply_history, split into commodity-pegged (gold/silver) and fiat non-USD buckets. Data is downsampled: daily for the last 90 days, weekly for the last 2 years, monthly beyond that.
Cache: slow — public, s-maxage=3600, max-age=300
| Param | Type | Default | Constraints | Description |
|---|---|---|---|---|
days | number | 1825 | min 30, max 1825 | Lookback window in days |
Unlike most numeric-query handlers, this endpoint defaults missing or malformed days values to 1825 and clamps most out-of-range values into 30..1825 instead of returning 400. Current parser quirk: days=0 is treated like a missing value and returns the default 1825 rather than the minimum 30.
Response: Array<{ date, commodityShare, fiatNonUsdShare, commodity, fiatNonUsd, total }>
| Field | Type | Description |
|---|---|---|
date | number | Unix seconds (snapshot date) |
commodityShare | number | Commodity-pegged share as % of total supply |
fiatNonUsdShare | number | Fiat non-USD share as % of total supply |
commodity | number | Commodity-pegged circulating USD |
fiatNonUsd | number | Fiat non-USD circulating USD |
total | number | Total circulating USD across all tracked coins |
GET /api/chains
Returns chain-level stablecoin aggregates with Chain Health Scores. Computed on-the-fly from the stablecoins cache and report-card cache (two D1 reads) — no dedicated chain table is required for the live leaderboard. The response body also carries _meta, so the frontend can distinguish fresh, degraded, and missing-dependency states without inferring freshness from fetch timing alone.
Cache: realtime — public, s-maxage=60, max-age=10
Freshness threshold: 1800 seconds. Returns 503 when the stablecoins cache is unavailable or structurally corrupt. When dependent snapshots lag, the endpoint stays readable but the body _meta.status degrades and the frontend surfaces stale-data warnings.
Status codes:
| Status | Meaning |
|---|---|
| 200 | Chain aggregates computed successfully; freshness may still be degraded in _meta |
| 503 | Stablecoins cache unavailable (missing or structurally corrupt) |
Response (ChainsResponse):
{
"_meta": {
"updatedAt": 1710500000,
"ageSeconds": 42,
"status": "fresh",
"dependencies": {
"reportCards": {
"updatedAt": 1710499800,
"ageSeconds": 242,
"status": "fresh"
}
}
},
"chains": [ChainSummary, ...],
"globalTotalUsd": 230000000000,
"updatedAt": 1710500000,
"healthMethodologyVersion": "1.2"
}
| Field | Type | Description |
|---|---|---|
chains | ChainSummary[] | Chains sorted by totalUsd descending |
globalTotalUsd | number | Sum of all chain supply in USD |
updatedAt | number | Unix epoch seconds of the underlying stablecoins snapshot |
healthMethodologyVersion | string | Chain Health Score methodology version (currently "1.2") |
_meta.dependencies.reportCards is present when the endpoint can determine report-card freshness. When that dependency is stale or unavailable, healthScore degrades to null and the route UI surfaces the dependency reason instead of pretending the chain is fully fresh.
ChainSummary fields:
| Field | Type | Description |
|---|---|---|
id | string | Canonical chain identifier (DefiLlama chain name) |
name | string | Human-readable chain name |
logoPath | string | null | Path to chain logo asset |
type | "evm" | "tron" | "other" | Chain runtime family from CHAIN_META |
totalUsd | number | Total stablecoin supply on this chain in USD |
change24h | number | Absolute 24h supply change in USD |
change24hPct | number | 24h supply change as a percentage |
change7d | number | Absolute 7d supply change in USD |
change7dPct | number | 7d supply change as a percentage |
change30d | number | Absolute 30d supply change in USD |
change30dPct | number | 30d supply change as a percentage |
stablecoinCount | number | Number of distinct stablecoins on this chain |
dominantStablecoin | { id, symbol, share } | Largest stablecoin by supply on the chain |
dominanceShare | number | Chain share of the global tracked stablecoin supply (0–1) |
healthScore | number | null | Chain Health Score 0–100, or null if insufficient data |
healthBand | string | null | Health band label: "robust" (80–100), "healthy" (60–79), "mixed" (40–59), "fragile" (20–39), "concentrated" (0–19) |
healthFactors | ChainHealthFactors | Raw sub-factor scores (0–100 each; quality may still be null) |
ChainHealthFactors fields:
| Field | Type | Description |
|---|---|---|
concentration | number | HHI-based supply concentration score (higher = more diverse) |
quality | number | null | Supply-weighted average stablecoin quality from report-card grades; null when rated supply coverage is below 50% by value |
chainEnvironment | number | Resilience-tier score for the chain itself (100 tier 1, 60 tier 2, 20 tier 3) |
pegStability | number | Supply-weighted average peg deviation score |
backingDiversity | number | Shannon entropy of the active backing split across the chain (rwa-backed vs crypto-backed) |
GET /api/stablecoin-reserves/:id
Returns the resolved reserve presentation for a stablecoin with liveReservesConfig.
- Unknown IDs or coins without live reserve support return
404. - Live-enabled coins return
200even before the first successful sync; the payload includes fallback mode + sync state. - This endpoint powers the stablecoin detail-page reserve card. The same underlying live-reserve dataset also feeds report-card collateral quality, reserve-drift monitoring, and
/status, but those surfaces read D1-backed reserve snapshots directly rather than calling this endpoint. - A response is treated as
liveonly when the stored reserve snapshot matches the latest successful sync state and passes strict integrity validation; orphaned partial writes or corrupt stored snapshots fall back to the curated/template presentation instead of presenting malformed live data as authoritative.
Cache: dynamic
- Live snapshots: slow (
public, s-maxage=3600, max-age=300) live-stalesnapshots:public, s-maxage=1800, max-age=120- Bootstrap / fallback / unavailable presentations: shorter (
public, s-maxage=300, max-age=60) so pre-sync fallback responses do not stay pinned at the edge after the first successful live sync
Response (200):
| Field | Type | Description |
|---|---|---|
stablecoinId | string | Pharos coin ID |
mode | string | One of live, live-stale, curated-fallback, template-fallback, unavailable. This is snapshot transport/freshness state, not the user-facing reserve badge semantics. |
reserves | ReserveSlice[] | Reserve slices currently being shown to the user |
estimated | boolean | true only when using the classification template fallback |
liveAt | number? | Unix seconds of the last successful live snapshot. Present only when live data exists |
source | string? | Adapter key (for example "infinifi", "m0", "openeden-usdo", or "accountable"). Present only when live data exists |
displayUrl | string? | Human-readable source link shown in the UI. Present only when configured |
displayBadge | object? | User-facing reserve badge semantics for authoritative live snapshots (live, curated-validated, or proof) |
metadata | object? | Adapter snapshot metadata for authoritative live snapshots. This can include feed-specific context such as yieldBasisCollateralPct for crvusd |
provenance | object? | Evidence-quality envelope for authoritative live snapshots (evidenceClass, sourceModel, optional freshnessMode, scoringEligible) |
sync | object? | Live sync state (status, bootstrap, stale, lastAttemptedAt, lastSuccessAt, warnings, lastError). Present only when live-enabled |
sync.warnings can include both adapter-emitted warnings from the latest attempt and storage-integrity warnings when a stored live snapshot is rejected and the endpoint fails closed to a fallback presentation.
When present, displayBadge has:
| Field | Type | Description |
|---|---|---|
kind | "live" | "curated-validated" | "proof" | User-facing reserve badge classification |
label | string | Badge label rendered by the frontend (Live, Curated-Validated, or Proof) |
When present, provenance has:
| Field | Type | Description |
|---|---|---|
evidenceClass | "independent" | "static-validated" | "weak-live-probe" | Evidence class used for scoring and provenance. This is related to, but not identical to, the UI badge semantics. |
sourceModel | "dynamic-mix" | "validated-static" | "single-bucket" | Structural shape of the reserve feed |
freshnessMode | "verified" | "unverified" | "not-applicable" | undefined | Explicit freshness policy when the adapter emits one |
scoringEligible | boolean | Whether this exact snapshot is currently eligible for collateral-quality passthrough |
Response (404): { "error": "Not found" }
GET /api/stablecoin-charts
Aggregate historical supply chart data across all stablecoins, broken down by peg type. sync-stablecoin-charts is triggered every 30 minutes, but a stablecoin-charts:last-write cooldown caps successful refreshes at once per hour; /api/health treats the cache as healthy for up to 1 hour.
Cache: standard — X-Data-Age and Warning headers included. This array response gets freshness headers only; it does not receive a response-body _meta envelope.
Response: A top-level array.
[
{
"date": 1511913600,
"totalCirculatingUSD": {
"peggedUSD": 110105,
"peggedEUR": 14967600
}
}
]
| Field | Type | Description |
|---|---|---|
date | number | Unix timestamp (seconds) |
totalCirculatingUSD | Record<string, number> | Aggregate supply in USD per peg type |
GET /api/blacklist
Freeze, blacklist, block/unblock, account-pause, and token-destruction events for symbols in the shared BLACKLIST_STABLECOINS set. EURC mirror-zero rows are preserved with suppression metadata and excluded from public aggregates. Data is sourced from on-chain logs via Etherscan, Tron, and EVM RPCs.
Cache: realtime
Freshness note: X-Data-Age / Warning track the latest successful 6-hourly sync-blacklist writer timestamp. Public freshness stays fresh through that 6-hour budget and only degrades once the scheduled blacklist sync is actually late.
Query parameters
| Param | Type | Default | Description |
|---|---|---|---|
stablecoin | string | — | Filter by token symbol from the full BLACKLIST_STABLECOINS set in shared/types/market.ts |
chain | string | — | Filter by chain name (e.g. Ethereum, Tron) |
eventType | string | — | Filter by type: blacklist, unblacklist, destroy |
q | string | — | Case-insensitive address substring search |
sortBy | string | date | Sort field: date, stablecoin, chain, event |
sortDirection | string | desc | Sort direction: asc, desc |
limit | integer | 1000 | Max results (1–1000; 0 maps to default 1000) |
offset | integer | 0 | Pagination offset |
Response
{
"events": [BlacklistEvent, ...],
"total": 13422,
"methodology": {
"version": "3.9",
"versionLabel": "v3.9",
"currentVersion": "3.97",
"currentVersionLabel": "v3.97",
"changelogPath": "/methodology/blacklist-tracker-changelog/",
"asOf": 1772606400,
"isCurrent": true
}
}
BlacklistEvent
| Field | Type | Description |
|---|---|---|
id | string | Composite ID: {chainId}-{txHash}-{logIndex} |
stablecoin | string | Token symbol (USDC, USDT, etc.) |
chainId | string | Chain identifier (e.g. "ethereum", "tron") |
chainName | string | Human-readable chain name (e.g. "Ethereum") |
eventType | string | "blacklist", "unblacklist", or "destroy" |
address | string | Affected address (EVM 0x… or Tron T…) |
amountNative | number | null | Canonical token-native amount recovered from event data or historical balance lookup |
amountUsdAtEvent | number | null | Event-time USD value when Pharos can justify one |
amountSource | string | event, historical_balance, current_balance_snapshot, derived, legacy_migration, or unavailable |
amountStatus | string | resolved, recoverable_pending, permanently_unavailable, provider_failed, ambiguous |
txHash | string | Transaction hash |
blockNumber | number | Block number |
timestamp | number | Unix seconds |
methodologyVersion | string | Methodology version attributed to this event row |
contractAddress | string | null | Emitting token contract when known |
configKey | string | null | Internal tracker config identity ({chainId}-{contract}) |
eventSignature | string | null | Human-readable event signature/name when known |
eventTopic0 | string | null | Raw EVM topic0 when applicable |
suppressionReason | string | null | Always null or absent on public rows because /api/blacklist filters suppression_reason IS NULL; non-null reasons are retained only on internal/audit DB rows excluded from public aggregates/events |
explorerTxUrl | string | Block explorer URL for the transaction |
explorerAddressUrl | string | Block explorer URL for the address |
methodology
| Field | Type | Description |
|---|---|---|
version | string | Methodology version of the latest returned event in this response |
versionLabel | string | Display label (e.g. "v3.2") |
currentVersion | string | Latest methodology version |
currentVersionLabel | string | Display label for latest methodology version |
changelogPath | string | Relative URL to the methodology changelog page |
asOf | number | Unix timestamp of latest event used for freshness |
isCurrent | boolean | Whether version matches currentVersion |
GET /api/blacklist-summary
Server-side aggregates for the Blacklist Tracker overview cards, chart, and filter options. This lets the frontend render summary state without hydrating the full blacklist history first.
stats.destroyedTotal remains an event-history total. stats.activeFrozenTotal reflects Pharos' local active blacklist state machine. stats.trackedFrozenTotal is the persistent freeze-ledger total sourced from blacklist_current_balances, including reconciled historical bootstrap rows where later seizures or unblacklists would otherwise hide the frozen amount. stats.recentCount covers the last 30 days, while stats.recentCount24h is the last-24-hours subset used by chrome-level monitoring surfaces. The chart now uses that same freeze ledger and attributes each tracked balance back to its latest recorded blacklist quarter, so the quarterly buckets explain the trackedFrozenTotal headline rather than raw event-time intake.
The four perCoin* maps power the per-coin "Blacklist Activity" block on stablecoin detail pages. perCoinFrozenAddressCount counts addresses whose latest event is blacklist (net-frozen). perCoinFrozenTotal sums blacklist_current_balances.balance_usd per coin. perCoinDestroyedTotal sums amount_usd_at_event over destroy events per coin. perCoinQuarterlyEventTypes contains each coin's quarterly breakdown of event-type counts, zero-filled between the coin's first and last event quarters so bars render contiguously. All per-coin aggregations exclude rows where suppression_reason is set (e.g. EURC mirror zero-balance entries).
Cache: realtime
Freshness note: Shares the same 6-hourly freshness headers as GET /api/blacklist, keyed to the latest successful sync-blacklist write rather than the request time of the summary endpoint itself.
Response
{
"stats": {
"usdcBlacklisted": 1204,
"usdtBlacklisted": 3881,
"goldBlacklisted": 19,
"frozenAddresses": 5071,
"destroyedTotal": 158938221.19,
"activeAddressCount": 5071,
"activeFrozenTotal": 2120456789.42,
"activeAmountGapCount": 17,
"trackedAddressCount": 9466,
"trackedFrozenTotal": 3235360796.7,
"trackedAmountGapCount": 0,
"recentCount": 42,
"recentCount24h": 3,
"recoverableGapCount": 17,
"perCoinBlacklistCounts": { "USDC": 1204, "USDT": 3881 },
"perCoinTotalEvents": { "USDC": 1210, "USDT": 3945 },
"perCoinFrozenAddressCount": { "USDC": 1151, "USDT": 3794 },
"perCoinFrozenTotal": { "USDC": 143000000, "USDT": 1800000000 },
"perCoinDestroyedTotal": { "USDC": 0, "USDT": 158900000 },
"perCoinQuarterlyEventTypes": {
"USDC": [{ "quarter": "Q1 '26", "blacklist": 42, "unblacklist": 0, "destroy": 1 }]
}
},
"chart": [
{ "quarter": "Q1 '24", "USDT": 1200000, "USDC": 850000, "PAXG": 0, "XAUT": 0, "total": 2050000 }
],
"chains": [
{ "id": "ethereum", "name": "Ethereum" },
{ "id": "tron", "name": "Tron" }
],
"totalEvents": 13422,
"methodology": {
"version": "3.9",
"versionLabel": "v3.9",
"currentVersion": "3.97",
"currentVersionLabel": "v3.97",
"changelogPath": "/methodology/blacklist-tracker-changelog/",
"asOf": 1772606400,
"isCurrent": true
}
}
GET /api/depeg-events
Peg deviation events (≥ 100 bps for USD-pegged, ≥ 150 bps for non-USD pegs). Events are detected every 15 minutes by the cron.
Cache: realtime
Query parameters
| Param | Type | Default | Description |
|---|---|---|---|
stablecoin | string | — | Filter by Pharos stablecoin ID |
active | "true" | — | When "true", return only ongoing (unresolved) depeg events |
limit | integer | 100 | Max results (1–1000) |
offset | integer | 0 | Pagination offset |
Response
{
"events": [DepegEvent, ...],
"total": 4080,
"methodology": {
"version": "5.95",
"versionLabel": "v5.95",
"currentVersion": "5.95",
"currentVersionLabel": "v5.95",
"changelogPath": "/methodology/depeg-changelog/",
"asOf": 1772606400,
"isCurrent": true
}
}
Results are ordered by startedAt descending (most recent first).
DepegEvent
| Field | Type | Description |
|---|---|---|
id | number | Auto-increment DB ID |
stablecoinId | string | Pharos stablecoin ID |
symbol | string | Token symbol |
pegType | string | DefiLlama peg type (e.g. "peggedUSD") |
direction | "above" | "below" | Whether the price was above or below the peg |
peakDeviationBps | number | Largest deviation observed (basis points, signed; negative = below peg, positive = above peg) |
startedAt | number | Unix seconds when depeg was first detected |
endedAt | number | null | Unix seconds when price returned to peg; null if still active |
startPrice | number | Price at event start (USD) |
peakPrice | number | null | Price at worst deviation |
recoveryPrice | number | null | Price at recovery |
pegReference | number | Reference peg value used (USD) |
source | "live" | "backfill" | Detection method |
confirmationSources | string | null | Composite provenance tag recorded when a pending depeg was promoted. Components (joined with +): the off-chain source label (CoinGecko, DefiLlama, or NativePeg(<currency>)), DEX, CEX, Pool. Example: "DEX+CEX" or "CoinGecko+Pool". null for events that bypassed the pending lane (small-cap authoritative direct-insert and historical backfill rows). |
pendingReason | string | null | Composite reason the incident entered the pending lane, e.g. "large-cap", "low-confidence", "large-cap+low-confidence", "extreme-move". null when the event did not enter pending. |
methodology
| Field | Type | Description |
|---|---|---|
version | string | Methodology version attributed from the latest returned event timestamp |
versionLabel | string | Display label (e.g. "v5.93") |
currentVersion | string | Latest methodology version |
currentVersionLabel | string | Display label for latest methodology version |
changelogPath | string | Relative URL to the methodology changelog page |
asOf | number | Unix timestamp used for methodology attribution |
isCurrent | boolean | Whether version matches currentVersion |
GET /api/peg-summary
Composite peg scores and aggregate statistics for tracked stablecoins. Scores are computed over a 4-year window from live depeg events, DEX prices, and current prices. The coins array can still include NAV / other non-peg rows with currentDeviationBps = null, while the summary counters only cover rows with a live peg-status deviation.
Cache: realtime
Response
{
"coins": [PegSummaryCoin, ...],
"summary": PegSummaryStats,
"methodology": {
"version": "5.95",
"versionLabel": "v5.95",
"currentVersion": "5.95",
"currentVersionLabel": "v5.95",
"changelogPath": "/methodology/depeg-changelog/",
"asOf": 1772606400,
"isCurrent": true
}
}
PegSummaryCoin
| Field | Type | Description |
|---|---|---|
id | string | Pharos stablecoin ID |
symbol | string | Token symbol |
name | string | Full name |
pegType | string | DefiLlama peg type |
pegCurrency | string | Peg currency code (USD, EUR, GOLD, etc.) |
governance | string | "centralized", "centralized-dependent", "decentralized" |
currentDeviationBps | number | null | Live price deviation from peg (basis points, signed). null for NAV / non-fixed-peg rows, for coins with current supply below the live depeg-event floor, or when price / peg-reference inputs are missing. |
depegEventCoverageLimited | boolean | Present when the coin's current supply is below the live depeg-event floor ($1M). Use this to distinguish "below coverage floor" from generic missing-price cases when currentDeviationBps is null. |
priceSource | string | Primary price source label used for current deviation (defillama-list, coingecko, composite agreement labels such as binance+coingecko+kraken, protocol-redeem, defillama-contract, coinmarketcap, dexscreener, cached, etc.). High-confidence consensus can expose the agreeing cluster label even when the published price is the cluster median. |
priceConfidence | "high" | "single-source" | "low" | "fallback" | null | Confidence tier attached to the primary price input |
priceUpdatedAt | number | null | Compatibility timestamp for the primary price; now mirrors the effective observation time rather than the cache-write time |
priceObservedAt | number | null | Unix seconds for the effective observation time attached to the selected primary price; interpret alongside priceObservedAtMode |
priceObservedAtMode | "upstream" | "local_fetch" | "unknown" | null | Whether priceObservedAt came from source-native freshness metadata, local fetch time, or legacy/unknown provenance |
priceSyncedAt | number | null | Unix seconds when Pharos selected and wrote the primary price during the sync |
primaryTrust | "authoritative" | "confirm_required" | "unusable" | Whether the current primary price is trusted to mutate live depeg state directly |
pegScore | number | null | Composite peg score 0–100 (higher = more stable) |
pegPct | number | % of tracked time within ±100 bps |
severityScore | number | Severity sub-score (0–100) |
spreadPenalty | number | Spread/liquidity penalty applied to score |
eventCount | number | Number of depeg events in the 4-year window |
worstDeviationBps | number | null | Worst single deviation seen (basis points) |
activeDepeg | boolean | Whether a depeg event is currently open |
lastEventAt | number | null | Unix seconds of most recent depeg event |
trackingSpanDays | number | Days of history used for score computation |
methodologyVersion | string | Methodology version attributed to this coin snapshot |
dexPriceCheck | DexPriceCheck | null | Optional cross-validation against DEX price (shown when coin supply is at or above the live depeg-event floor, DEX data is ≤ 60 minutes old, and aggregate source TVL is ≥ $250K) |
consensusSources | string[] | Source names that returned a valid price for this coin. Defaults to [] when absent. |
agreeSources | string[] | undefined | Compatibility alias for agreeing/current price sources when present |
DexPriceCheck
| Field | Type | Description |
|---|---|---|
dexPrice | number | DEX-derived price (USD) |
dexDeviationBps | number | DEX price deviation from peg (basis points, signed) |
agrees | boolean | Whether primary and DEX prices are within 50 bps |
sourcePools | number | Number of DEX pools contributing to the price |
sourceTvl | number | Combined TVL of those pools (USD) |
PegSummaryStats
| Field | Type | Description |
|---|---|---|
activeDepegCount | number | Coins with an open depeg event |
medianDeviationBps | number | Median absolute deviation across rows with a live current deviation |
worstCurrent | { id, symbol, bps } | null | Coin with the largest current deviation among rows with a live current deviation |
coinsAtPeg | number | Rows with a live current deviation that are below their live depeg threshold (100 bps for USD pegs, 150 bps for non-USD pegs) |
totalTracked | number | Rows included in the live peg-status aggregate (currentDeviationBps !== null) |
depegEventsToday | number | Number of depeg events whose startedAt is in the current UTC day |
depegEventsYesterday | number | Number of depeg events whose startedAt is in the previous UTC day |
fallbackPegRates | string[] | (optional) pegType keys using stale FX fallback rates |
methodology — same fields and semantics as /api/depeg-events
GET /api/usds-status
Sky/USDS protocol status — whether the freeze module is currently active.
Cache: standard — X-Data-Age and Warning headers included.
Response
{
"freezeActive": false,
"implementationAddress": "0x1923dfee706a8e78157416c29cbccfde7cdf4102",
"lastChecked": 1771809338,
"_meta": { "updatedAt": 1710500000, "ageSeconds": 42, "status": "fresh" }
}
| Field | Type | Description |
|---|---|---|
freezeActive | boolean | Whether the USDS freeze module is currently enabled |
implementationAddress | string | Address of the current USDS implementation contract |
lastChecked | number | Unix seconds when this was last fetched on-chain |
GET /api/bluechip-ratings
Safety ratings from bluechip.org for covered stablecoins. Updated daily at 08:05 UTC.
Cache: slow — X-Data-Age and Warning headers included.
Response: Object keyed by Pharos stablecoin ID, plus top-level _meta freshness metadata.
{
"usdt-tether": BluechipRating,
"usdc-circle": BluechipRating,
"_meta": { "updatedAt": 1710500000, "ageSeconds": 42, "status": "fresh" }
}
BluechipRating
| Field | Type | Description |
|---|---|---|
grade | string | Letter grade: "A+", "A", "A-", "B+" … "F" |
slug | string | Bluechip report slug (e.g. "usdt") |
collateralization | number | Collateralization percentage |
smartContractAudit | boolean | Whether an audit exists |
dateOfRating | string | ISO 8601 date of rating |
dateLastChange | string | null | ISO 8601 date of last grade change |
smidge | BluechipSmidge | Plain-text evaluation summaries (HTML stripped) |
BluechipSmidge — each field is string | null:
| Field | Description |
|---|---|
stability | Reserves management and stabilization mechanisms |
management | Personnel restrictions and track records |
implementation | Smart contract implementation assessment |
decentralization | Decentralization posture |
governance | Governance and redemption terms |
externals | External risk factors |
GET /api/dex-liquidity
DEX liquidity scores, pool breakdowns, source-confidence metadata, and on-chain DEX price data for all tracked stablecoins. Updated every 30 minutes. Trend data is only returned when a trusted historical baseline exists.
Cache: custom — public, s-maxage=300, max-age=300
Freshness note: In addition to stale-data warnings, this endpoint can also emit a Warning header when the latest sync-dex-liquidity run finished in degraded or error state and the API is serving the last successful dataset.
Response: Object keyed by Pharos stablecoin ID plus a __global__ aggregate sentinel row.
{
"usdt-tether": DexLiquidityData,
"usdc-circle": DexLiquidityData,
"__global__": DexLiquidityData
}
DexLiquidityData
| Field | Type | Description |
|---|---|---|
totalTvlUsd | number | Total DEX TVL (USD) |
totalVolume24hUsd | number | 24 h trading volume (USD) |
totalVolume7dUsd | number | 7-day trading volume (USD) |
poolCount | number | Number of liquidity pools |
pairCount | number | Number of unique trading pairs |
chainCount | number | Number of chains with active pools |
protocolTvl | Record<string, number> | TVL per DEX protocol (e.g. { "uniswap-v3": 100000 }) |
chainTvl | Record<string, number> | TVL per chain (e.g. { "Ethereum": 500000 }) |
topPools | DexLiquidityPool[] | Top 10 retained pools sorted by 24h volume, then TVL |
liquidityScore | number | null | Composite liquidity score 0–100 |
concentrationHhi | number | null | Herfindahl–Hirschman Index for pool concentration (0–1; lower = more distributed), computed from the full retained pool set before top-10 truncation |
depthStability | number | null | Pool depth stability metric |
tvlChange24h | number | null | % TVL change vs. 24 h ago |
tvlChange7d | number | null | % TVL change vs. 7 days ago |
updatedAt | number | Unix seconds of last cron update |
dexPriceUsd | number | null | DEX-derived price (USD) |
dexDeviationBps | number | null | DEX price deviation from peg (basis points, signed) |
priceSourceCount | number | null | Number of pools used for DEX price (all must meet the shared $50K observation floor) |
priceSourceTvl | number | null | Combined TVL of price-source pools (USD) |
priceSources | DexPriceSource[] | null | Aggregated price sources by protocol (for example one balancer or raydium entry per asset) |
effectiveTvlUsd | number | TVL after applying quality multipliers |
avgPoolStress | number | null | Average pool stress index on a 0–100 scale (0 = balanced, 100 = maximally stressed / imbalanced) |
weightedBalanceRatio | number | null | TVL-weighted balance ratio across pools |
organicFraction | number | null | Fraction of TVL from organic (non-incentivized) pools |
durabilityScore | number | null | Score for pool maturity and reliability |
coverageClass | "primary" | "mixed" | "fallback" | "legacy" | "unobserved" | null | Coverage-confidence classification for the retained pool set; primary includes pure dl and pure direct_api rows. The __global__ aggregate sentinel uses null. |
coverageConfidence | number | Evidence-weighted confidence (0-1) derived from retained-pool breadth, measured TVL share, and synthetic/decayed dependence |
liquidityEvidenceClass | "unobserved" | "measured" | "partial_measured" | "observed_unmeasured" | Explicit classification of whether liquidity is balance-measured versus only observed from TVL / price evidence |
hasMeasuredLiquidityEvidence | boolean | Whether any retained liquidity evidence for the row includes measured pool balances |
trendworthy | boolean | Whether this row is suitable for trend baselines (coverageConfidence >= 0.75, positive TVL, and primary/mixed coverage) |
sourceMix | Record<string, { poolCount: number; tvlUsd: number }> | TVL/pool-count mix across source families (dl, direct_api, cg_onchain, gecko_terminal, dexscreener, cg_tickers) |
balanceMeasuredTvlUsd | number | TVL denominator actually used for weightedBalanceRatio |
organicMeasuredTvlUsd | number | TVL denominator actually used for organicFraction |
scoreComponents | ScoreComponents | null | Breakdown of the composite liquidity score |
lockedLiquidityPct | number | null | TVL-weighted fraction of liquidity reported as locked by source pools |
methodologyVersion | string | Methodology version attributed to this row |
ScoreComponents
| Field | Type | Description |
|---|---|---|
tvlDepth | number | TVL depth sub-score |
volumeActivity | number | Volume activity sub-score |
poolQuality | number | Pool quality sub-score |
durability | number | Durability sub-score |
pairDiversity | number | Pair diversity sub-score |
DexLiquidityPool
| Field | Type | Description |
|---|---|---|
project | string | Protocol slug (e.g. "curve-dex", "uniswap-v3") |
chain | string | Chain name |
tvlUsd | number | Pool TVL (USD) |
symbol | string | Pool pair name (e.g. "USDC-USDT"), normalized to tracked tickers when direct-API sources only provide token addresses |
volumeUsd1d | number | 24 h volume (USD) |
poolType | string | Pool type (e.g. "curve-stableswap", "uniswap-v3-5bp") |
source | string | undefined | Canonical source family for this retained pool |
extra | object | undefined | Optional detailed pool metrics (A-factor, balance ratio, measurement flags, etc.) |
extra may include:
| Field | Type | Description |
|---|---|---|
amplificationCoefficient | number | undefined | Curve amplification coefficient (A) |
balanceRatio | number | undefined | Measured pool balance ratio from 0 to 1; Balancer weighted pools normalize against weights and Fluid uses official DexReservesResolver balances where deployed |
feeTier | number | undefined | Normalized fee tier in basis points |
balanceDetails | Array<{ symbol: string; balancePct: number; isTracked: boolean }> | undefined | Per-token USD composition shares used for balance tooltips/detail |
measurement | object | undefined | Per-pool provenance flags such as tvlMeasured, volumeMeasured, balanceMeasured, maturityMeasured, priceMeasured, synthetic, decayed, and capped |
DexPriceSource
| Field | Type | Description |
|---|---|---|
protocol | string | DEX protocol name |
chain | string | Chain name |
price | number | Price from this source |
tvl | number | TVL of this pool (USD) |
GET /api/dex-liquidity-history
Per-coin historical DEX liquidity snapshots. Snapshots are recorded daily (UTC midnight, first sync after day rollover). Baseline consumers should use coverageClass / coverageConfidence before treating a history point as trend-worthy.
Cache: slow — public, s-maxage=3600, max-age=300
Required query parameter
| Param | Type | Description |
|---|---|---|
stablecoin | string | Pharos stablecoin ID (required) |
Optional query parameters
| Param | Type | Default | Bounds | Description |
|---|---|---|---|---|
days | integer | 90 | 1–365 | Lookback window in days |
Response: Array sorted by date ascending.
[
{
"tvl": 1658000000,
"volume24h": 1700000000,
"score": 93,
"date": 1771500000,
"coverageClass": "mixed",
"coverageConfidence": 0.85,
"liquidityEvidenceClass": "partial_measured",
"hasMeasuredLiquidityEvidence": true,
"trendworthy": true,
"methodologyVersion": "3.1"
}
]
| Field | Type | Description |
|---|---|---|
tvl | number | Total DEX TVL snapshot (USD) |
volume24h | number | 24 h volume at time of snapshot (USD) |
score | number | null | Liquidity score at time of snapshot |
date | number | Unix seconds |
coverageClass | string | Snapshot confidence class (primary, mixed, fallback, legacy, unobserved) |
coverageConfidence | number | Snapshot confidence score |
liquidityEvidenceClass | string | Snapshot evidence class (measured, partial_measured, observed_unmeasured, unobserved) |
hasMeasuredLiquidityEvidence | boolean | Whether the snapshot qualifies as balance-measured liquidity evidence |
trendworthy | boolean | Whether the snapshot is suitable for trend baselines rather than informational use |
methodologyVersion | string | Methodology version attributed to this snapshot |
GET /api/supply-history
Per-coin circulating supply and price history, snapshotted once daily at 08:00 UTC.
Cache: slow — public, s-maxage=3600, max-age=300
Required query parameter
| Param | Type | Description |
|---|---|---|
stablecoin | string | Pharos stablecoin ID (required) |
Optional query parameters
| Param | Type | Default | Bounds | Description |
|---|---|---|---|---|
days | integer | 365 | 1–1825 | Lookback window in days |
Response: Array sorted by date ascending.
[
{
"date": 1771500000,
"circulatingUsd": 138000000000,
"price": 1.0001
}
]
| Field | Type | Description |
|---|---|---|
date | number | Unix seconds |
circulatingUsd | number | Circulating supply in USD |
price | number | null | Price at snapshot time (USD); may be null for older records |
GET /api/daily-digest
Latest AI-generated market summary, produced daily at 08:05 UTC via the Claude API.
Cache: standard. When a digest exists, the response includes X-Data-Age and Warning freshness headers (max 2 h). The bootstrap { "digest": null } response carries only Cache-Control.
Response
{
"digest": "USDC absorbed $812M of the market's $1.36B weekly inflow…",
"editionNumber": 214
}
If no digest exists yet, the endpoint returns only { "digest": null }.
| Field | Type | Description |
|---|---|---|
digest | string | null | Tweet-ready summary (≤ 240 characters). null if no digest has been generated yet. |
digestTitle | string | null | Short headline for the digest |
digestExtended | string | null | Extended commentary for the website view |
generatedAt | number | Unix seconds when this digest was generated (present only when digest is non-null) |
editionNumber | number | null | Sequential daily digest number (present only when digest is non-null) |
GET /api/digest-archive
Newest-first archive of up to 365 daily and weekly digests.
Cache: standard
Response
{
"digests": [
{
"digestText": "USDC absorbed $812M…",
"digestTitle": "USDC Eats the Week",
"digestExtended": "Longer editorial…",
"generatedAt": 1771839719,
"psiScore": 81.1,
"psiBand": "STEADY",
"totalMcapUsd": 234500000000,
"digestType": "daily",
"editionNumber": 214
}
]
}
Each element uses digestText (note: differs from the singular /api/daily-digest which uses digest).
| Field | Type | Description |
|---|---|---|
digestText | string | Tweet-ready summary |
digestTitle | string | null | Short headline |
digestExtended | string | null | Extended commentary |
generatedAt | number | Unix seconds of generation time |
psiScore | number | null | PSI score parsed from archived digest input data |
psiBand | string | null | PSI condition band parsed from archived digest input data |
totalMcapUsd | number | null | Ecosystem market cap parsed from archived digest input data |
digestType | "daily" | "weekly" | Digest cadence for this archived entry |
editionNumber | number | Sequential edition number within that digest cadence |
GET /api/digest-snapshot
Contextual data snapshot for a specific digest date — includes the digest's input data, active depeg events, and blacklist events for that day. Used by SSG builds for individual digest pages.
Cache: archive
Required query parameter
| Param | Type | Description |
|---|---|---|
date | string | Date in YYYY-MM-DD format, or YYYY-MM-DD-weekly for weekly recap pages (required) |
Response
{
"date": "2026-02-27",
"inputData": { "totalMcapUsd": 230000000000, "mcap7dDelta": 0.012, ... },
"prevInputData": { ... },
"depegEvents": [{ "stablecoinId": "usdt-tether", "symbol": "USDT", "direction": "below", "peakDeviationBps": -150, ... }],
"blacklistEvents": [{ "stablecoin": "USDT", "chainName": "Ethereum", "eventType": "blacklist", ... }]
}
| Field | Type | Description |
|---|---|---|
date | string | The requested date |
inputData | object | null | Digest input data (mcap, depegs, supply changes, PSI) for this date |
prevInputData | object | null | Previous day's input data for delta computation |
depegEvents | array | Up to 20 depeg events active on that date, ordered by severity |
blacklistEvents | array | Up to 50 blacklist events on that date |
Error responses: 400 for missing/invalid date, 404 if no digest exists for that date.
GET /api/health
Worker health check. Reports cache freshness, blacklist integrity, mint/burn freshness, and circuit-breaker states. Not served from Cloudflare edge cache (no-store).
Cache freshness in /api/health separates producer cadence, endpoint freshness, and availability impact. caches[*].maxAge is the availability budget used by /api/health, /api/status, and the public/admin status pages. endpointMaxAge is the endpoint freshness basis used for _meta, X-Data-Age, and the generic freshness warning runway when it differs. producerIntervalSec is the expected writer cadence.
Authentication: exempt
Response
{
"status": "healthy",
"timestamp": 1771856453,
"warnings": [],
"caches": {
"stablecoins": { "ageSeconds": 323, "maxAge": 600, "healthy": true, "producerJob": "sync-stablecoins", "producerIntervalSec": 900, "endpointMaxAge": 600, "availabilityMaxAge": 600 },
"stablecoin-charts": { "ageSeconds": 323, "maxAge": 3600, "healthy": true, "producerJob": "sync-stablecoin-charts", "producerIntervalSec": 3600, "endpointMaxAge": 3600, "availabilityMaxAge": 3600 },
"usds-status": { "ageSeconds": 47118, "maxAge": 86400, "healthy": true, "producerJob": "sync-usds-status", "producerIntervalSec": 86400, "endpointMaxAge": 86400, "availabilityMaxAge": 86400 },
"fx-rates": {
"ageSeconds": 1223,
"maxAge": 1800,
"healthy": true,
"producerJob": "sync-fx-rates",
"producerIntervalSec": 1800,
"endpointMaxAge": 1800,
"availabilityMaxAge": 1800,
"mode": "live",
"sourceUpdatedAt": 1771855200,
"sourceAgeSeconds": 323,
"sourceStatus": "fresh",
"warning": null,
"consecutiveFallbackRuns": 0
},
"bluechip-ratings": { "ageSeconds": 22815, "maxAge": 86400, "healthy": true, "producerJob": "sync-bluechip", "producerIntervalSec": 86400, "endpointMaxAge": 43200, "availabilityMaxAge": 86400 },
"dex-liquidity": { "ageSeconds": 290, "maxAge": 43200, "healthy": true, "producerJob": "sync-dex-liquidity", "producerIntervalSec": 1800, "endpointMaxAge": 3600, "availabilityMaxAge": 43200 },
"yield-data": { "ageSeconds": 820, "maxAge": 3600, "healthy": true, "producerJob": "sync-yield-data", "producerIntervalSec": 3600, "endpointMaxAge": 3600, "availabilityMaxAge": 3600 },
"dews": { "ageSeconds": 240, "maxAge": 1800, "healthy": true, "producerJob": "compute-dews", "producerIntervalSec": 1800, "endpointMaxAge": 1800, "availabilityMaxAge": 1800 }
},
"blacklist": {
"totalEvents": 13422,
"missingAmounts": 0,
"recentMissingAmounts": 0,
"recentWindowSec": 86400,
"missingRatio": 0
},
"mintBurn": {
"totalEvents": null,
"latestEventTs": null,
"latestHourlyTs": null,
"freshnessAgeSec": null,
"majorStaleCount": 0,
"staleMajorSymbols": [],
"sync": {
"lastSuccessfulSyncAt": 1771856400,
"freshnessStatus": "fresh",
"warning": null,
"criticalLaneHealthy": true
}
},
"circuits": {
"defillama-stablecoins": { "state": "closed", "consecutiveFailures": 0, "lastSuccessAt": 1772190029 },
"coingecko-prices": { "state": "closed", "consecutiveFailures": 0, "lastSuccessAt": 1772190030 }
},
"telegramSummary": {
"totalChats": 142,
"pendingDeliveries": 0,
"lastDispatchAt": 1771856400,
"lastDispatchStatus": "success"
}
}
| Field | Type | Description |
|---|---|---|
status | string | "healthy" / "degraded" / "stale" |
timestamp | number | Unix seconds at time of response |
warnings | string[] | Best-effort machine-readable warnings when health subqueries fail but the endpoint can still return a non-500 payload. Messages are sanitized for public output and do not include raw exception text, SQL fragments, or table names. |
caches | Record<string, CacheStatus> | Per-cache freshness status |
caches["fx-rates"] | CacheStatus | FX cache freshness plus source-cadence diagnostics (mode, sourceUpdatedAt, sourceAgeSeconds, sourceStatus, warning, consecutiveFallbackRuns) |
blacklist.totalEvents | number | Total events in blacklist table |
blacklist.missingAmounts | number | Events where amount is null (should be 0) |
blacklist.recentMissingAmounts | number | Missing-amount events inside the recent monitoring window used by status logic |
blacklist.recentWindowSec | number | Size of the recent monitoring window in seconds |
blacklist.missingRatio | number | missingAmounts / totalEvents (0 when no blacklist rows exist yet) |
telegramSummary | TelegramHealthSummary | null | Lightweight Telegram delivery health summary. null when the Telegram tables are unavailable or not yet migrated |
telegramSummary.totalChats | number | Total subscribed Telegram chats currently stored |
telegramSummary.pendingDeliveries | number | Pending overflow alert deliveries waiting in telegram_pending_alerts |
telegramSummary.lastDispatchAt | number | null | Unix seconds of the most recent dispatch-telegram-alerts cron run, if available |
telegramSummary.lastDispatchStatus | string | null | Status of the most recent dispatch-telegram-alerts cron run, if available |
mintBurn.totalEvents | number | null | Legacy advisory total. null on the budget-capped health path because /api/health no longer scans mint_burn_hourly; use /api/mint-burn-flows or /api/mint-burn-events for mint/burn data views. |
mintBurn.latestEventTs | number | null | Legacy advisory timestamp. null on the budget-capped health path because /api/health no longer scans mint_burn_events; freshness is represented by mintBurn.sync.lastSuccessfulSyncAt. |
mintBurn.latestHourlyTs | number | null | Legacy advisory timestamp. null on the budget-capped health path because /api/health no longer scans mint_burn_hourly. |
mintBurn.freshnessAgeSec | number | null | Legacy advisory age. null on the budget-capped health path; derive critical-lane age from mintBurn.sync.lastSuccessfulSyncAt. |
mintBurn.majorStaleCount | number | Legacy advisory count. Always 0 on the budget-capped health path because per-symbol stale checks are intentionally not scanned from D1. |
mintBurn.staleMajorSymbols | string[] | Legacy advisory list. Always empty on the budget-capped health path because per-symbol stale checks are intentionally not scanned from D1. |
mintBurn.sync | object | Critical-lane sync freshness summary used for public health evaluation |
mintBurn.sync.lastSuccessfulSyncAt | number | null | Unix seconds of the latest successful sync-mint-burn run |
mintBurn.sync.freshnessStatus | "fresh" | "degraded" | "stale" | Public freshness state keyed to the 30-minute critical-lane cadence (fresh <= 60m, degraded <= 90m, stale > 90m) |
mintBurn.sync.warning | string | null | Human-readable warning when the critical lane is stale, degraded, or errored |
mintBurn.sync.criticalLaneHealthy | boolean | true when the latest critical-lane run is ok, degraded, or skipped_locked |
circuits | Record<string, CircuitRecord> | Per-source circuit breaker states. Keys include defillama-stablecoins, defillama-stablecoin-detail, defillama-coins, defillama-yields, defillama-protocols, coingecko-prices, coingecko-detail-platforms, coingecko-mcap, coingecko-discovery, coinmarketcap-prices, dexscreener-prices, dexscreener-search, treasury-rates, etherscan, alchemy, twitter-api, telegram-api, pyth-prices, binance-prices, coinbase-prices, redstone-prices, curve-onchain, curve-liquidity-api, fx-realtime |
CacheStatus
| Field | Type | Description |
|---|---|---|
ageSeconds | number | null | Seconds since last cron update; null if never populated |
maxAge | number | Availability budget in seconds for this cache key; same value as availabilityMaxAge for current workers |
healthy | boolean | true when ageSeconds / maxAge ≤ 12.0; status-page bands use >8.0x for degraded and >12.0x for stale |
producerJob | string | null | undefined | Cron job that produces the cache freshness signal |
producerIntervalSec | number | null | undefined | Expected producer cadence in seconds |
endpointMaxAge | number | null | undefined | Endpoint freshness basis used by _meta, X-Data-Age, and generic freshness warning runway when available |
availabilityMaxAge | number | null | undefined | Availability budget used by /api/health, /api/status, and status-page cache ratios |
endpointBudgetReason | string | null | undefined | Short explanation when endpoint freshness differs from producer cadence or availability budget |
availabilityBudgetReason | string | null | undefined | Short explanation for the availability budget |
mode | "live" | "cached-fallback" | undefined | FX cache only: whether the latest usable sync came from a live fetch or cached fallback |
sourceUpdatedAt | number | null | undefined | FX cache only: Unix seconds for the source currently driving sourceStatus |
sourceAgeSeconds | number | null | undefined | FX cache only: age of the source currently driving sourceStatus |
sourceStatus | "fresh" | "degraded" | "stale" | "none" | FX cache only: cadence-aware source freshness status |
warning | string | null | undefined | Human-readable warning when a lane is running on degraded freshness evidence (FX fallback/source cadence, or cache freshness fallback from sentinel to table/cron evidence) |
consecutiveFallbackRuns | number | undefined | FX cache only: number of back-to-back cached-fallback runs |
The /status/ page consumes the richer blacklist fields directly so it can distinguish long-tail historical cleanup from fresh incoming amount gaps.
Blacklist amount-gap severity is intentionally tolerant of isolated parser/provider misses: data-quality degrades when the missing-amount ratio reaches 1% or when at least 5 recent events are missing amounts, and becomes stale at 2% or at 25 recent missing events.
dex-liquidity, yield-data, and dews now compute freshness from producer-owned cache sentinels first (freshness:dex-liquidity, freshness:yield-data, freshness:dews). If a sentinel is missing, the worker temporarily falls back to the legacy table query; if the freshness diagnostic still fails, it can fall back again to the latest successful producer cron timestamp and emits a warning/info cause instead of treating the diagnostic miss itself as public stale.
Overall status logic:
healthy— every cache impact is healthy, the public mint/burn lane is healthy, fewer than 3 public-impact circuit groups are open, and the health subqueries all resolved cleanlydegraded— any cache impact is degraded (including FX cached-fallback or source-cadence lag), any of the blacklist/mint-burn/circuit health subqueries failed, the public mint/burn lane is warning-only, or 3+ public-impact circuit groups are openstale— any cache impact is stale, or the public mint/burn lane is stale versus its critical-lane cadence
/api/health still emits every circuit record under circuits, including dynamic per-coin live-reserves:* scopes, but those reserve-specific breakers do not change the top-level public status on their own; reserve sync health is evaluated on the dedicated reserve/data-quality lanes instead.
Blacklist ratio fields are still emitted here for the public surface, but threshold-based blacklist severity lives under /api/status data-quality; /api/health only escalates its top-level status when the blacklist health loader itself fails.
GET /api/public-status-history
Public transition history for the read-only /status/ page. Returns the current public status plus recent state transitions within a requested time window. Not edge-cached beyond the standard 60-second response cache.
Query parameters
| Param | Type | Default | Description |
|---|---|---|---|
window | "24h" | "7d" | "30d" | "30d" | Transition time window applied server-side before rows are returned |
limit | integer | 50 | Max transitions returned after the time-window filter (1–200) |
Response
{
"timestamp": 1771856453,
"currentStatus": "healthy",
"lastChangedAt": 1771770000,
"transitions": [
{
"id": 418,
"from": "degraded",
"to": "healthy",
"transitionType": "recover",
"reason": "raw-healthy-recovery-threshold",
"at": 1771770000
}
]
}
| Field | Type | Description |
|---|---|---|
timestamp | number | Unix seconds at time of response |
currentStatus | "healthy" | "degraded" | "stale" | Current public status, sourced from assessPublicHealth (matches /api/health.status) |
lastChangedAt | number | null | Unix seconds for the latest admin status-machine change, if known |
transitions | PublicStatusTransition[] | Recent public-impact incident transitions, inside the requested window, newest first |
This endpoint powers two separate public /status/ views: the hero Status runway always uses window=30d, while the transition table owns its own user-selected 24h / 7d / 30d filter.
Browser consumers on pharos.watch and ops.pharos.watch should use same-origin /_site-data/public-status-history, which proxies onto the internal website lane instead of calling the external API host directly.
Public-impact filtering (2026-04-13): The endpoint filters the admin state-machine transitions down to incidents opened by at least one public-facing impact code (cache_ratio_*, cache_freshness_query_failed, fx_source_*, fx_cached_fallback, mint_burn_public_*, open_circuit_groups, circuit_query_failed, cron_error_runs, multiple_unhealthy_crons, unhealthy_crons_present, db_unhealthy). Admin-only data-quality causes (missing_prices_*, blacklist_gaps_*, reserve_sync_*, onchain_*, watch_*) are excluded, and info-severity causes cannot open a public incident. Once a public-impact incident is retained, the endpoint also retains the recovery path needed to return that incident to healthy, even when those recovery rows only carry info-level causes. This ensures the public /status/ hero (driven by /api/health) and the uptime bar / transition timeline (driven by this endpoint) always agree. The unfiltered admin view is still available via the admin /api/status endpoint.
GET /api/telegram-pulse
Lightweight Telegram adoption metrics for the public /telegram/ landing page. Returns only aggregate watcher/subscription counts plus the most subscribed coin symbols.
Cache: public, max-age=300, s-maxage=300
Response
{
"activeWatchers": 1842,
"coinSubscriptions": 5621,
"topCoins": ["USDT", "USDC", "USDe"]
}
| Field | Type | Description |
|---|---|---|
activeWatchers | number | Subscribers with at least one active alert type or per-coin alert |
coinSubscriptions | number | Total active per-coin subscription rows |
topCoins | string[] | Up to five most subscribed coin tickers, ordered by subscription count |
Browser consumers on pharos.watch and ops.pharos.watch should use same-origin /_site-data/telegram-pulse, which proxies onto the internal website lane instead of calling the external API host directly.
GET /api/stability-index
Latest Pharos Stability Index (PSI) sample plus daily history. The PSI is a composite ecosystem health score (0–100) computed from active depeg severity, affected-market breadth, DEWS stress breadth, and 7-day ecosystem trend across the PSI-eligible universe (tracked coins plus shadow assets used for historical continuity). If a dependency failure prevents a safe fresh sample, the endpoint continues serving the last healthy stored PSI sample instead of publishing a degraded substitute.
Cache: standard — X-Data-Age and Warning headers included.
Error responses: 503 when the canonical current PSI components or input_snapshot payload is missing or malformed.
Optional query parameters
| Param | Type | Default | Description |
|---|---|---|---|
detail | "true" | — | When "true", returns full history with per-day component breakdowns instead of last 91 days |
Response
{
"current": {
"score": 81.1,
"band": "STEADY",
"components": { "severity": 4.59, "breadth": 15, "stressBreadth": 1.8, "trend": 0.65 },
"computedAt": 1771977600,
"methodologyVersion": "3.2"
},
"history": [{ "date": 1771891200, "score": 81.0, "band": "STEADY", "methodologyVersion": "2.1" }],
"methodology": {
"version": "3.2",
"versionLabel": "v3.2",
"currentVersion": "3.2",
"currentVersionLabel": "v3.2",
"changelogPath": "/methodology/stability-index-changelog/",
"asOf": 1771977600,
"isCurrent": true
}
}
| Field | Type | Description |
|---|---|---|
current | object | null | Latest PSI score and components. null if cron has not yet run |
current.score | number | PSI score 0–100 |
current.band | string | Condition band: "BEDROCK", "STEADY", "TREMOR", "FRACTURE", "CRISIS", "MELTDOWN" |
current.avg24h | number | undefined | Rolling 24 h average PSI score |
current.avg24hBand | string | undefined | Condition band for avg24h |
current.components | object | Component breakdown: severity, breadth, stressBreadth, trend |
current.contributors | array | Top per-coin contributors from input_snapshot.contributors (empty when unavailable) |
current.inputDegradation | object | undefined | Dependency-loss metadata carried by the served sample when the stored input snapshot recorded degraded upstream inputs |
current.totalMcapUsd | number | Total ecosystem market cap from the latest input snapshot (0 when unavailable) |
current.computedAt | number | Unix seconds of computation |
current.methodologyVersion | string | Methodology version used to compute the current score |
history | array | Historical scores, newest first. With detail=true, each entry includes components |
malformedRows | number | Count of historical rows dropped from detail=true because persisted components JSON was malformed |
history[].methodologyVersion | string | Methodology version used for that history point |
methodology | object | Version metadata for current PSI methodology context |
methodology.version | string | Methodology version used by current score |
methodology.changelogPath | string | Relative path to full methodology changelog |
GET /api/og/*
Dynamic Open Graph PNG images used by share buttons and page metadata.
Authentication: exempt
Supported routes
/api/og/stablecoin/:id/api/og/safety-scores/api/og/depeg/api/og/stability-index
Content-Type: image/png
Cache: public, max-age=900, s-maxage=900
Error cases
404withtext/plainfor unknown coin IDs inside/api/og/stablecoin/:id; unknown OG route patterns return the standard JSON{ "error": "Unknown OG route" }503when required cached data is not yet available400for malformed URI encoding in/api/og/stablecoin/:id500withtext/plainbody when OG image rendering fails
/api/og/stablecoin/:id accepts tracked public stablecoin IDs only. The renderer assembles each card from cached stablecoin, DEWS, PSI, report-card, depeg, liquidity, and mint/burn data on the worker.
GET /api/report-cards
Stablecoin risk grade cards with dimension-level scores. Output includes 5 dimensions; overall score is the weighted base (exit-liquidity/resilience/decentralization/dependency) plus peg-multiplier adjustment.
Cache: standard
Response
{
"cards": [ReportCard, ...],
"dependencyGraph": {
"edges": [{ "from": "usde-ethena", "to": "usdc-circle", "weight": 0.9, "type": "collateral" }, ...]
},
"methodology": {
"version": "7.07",
"weights": { "pegStability": 0, "liquidity": 0.30, "resilience": 0.20, "decentralization": 0.15, "dependencyRisk": 0.25 },
"pegMultiplierExponent": 0.4,
"activeDepegSeveritySource": "open-event-peak",
"activeDepegCaps": {
"d": { "thresholdBps": 1000, "score": 49 },
"f": { "thresholdBps": 2500, "score": 39 }
},
"thresholds": [{ "grade": "A+", "min": 87 }, { "grade": "A", "min": 83 }, ...]
},
"liquidityStale": false,
"redemptionStale": false,
"inputFreshness": {
"dexLiquidity": { "updatedAt": 1771977600, "ageSeconds": 120, "stale": false },
"redemptionBackstops": { "updatedAt": 1771977600, "ageSeconds": 300, "stale": false }
},
"updatedAt": 1771977600
}
The Liquidity dimension now represents effectiveExitScore: the public DEX liquidity score remains the floor, while redeemable assets can receive uplift from redemptionBackstopScore when a meaningful direct exit path exists. Documented offchain issuer exits with eventual-only capacity can add only a DEX-gated primary-market bonus; they do not replace missing DEX liquidity. Last-known DEX liquidity remains usable even after its freshness runway, with staleness surfaced through liquidityStale and inputFreshness.dexLiquidity.stale. Low-confidence redemption routes stay visible but do not uplift the score, and materially stale redemption inputs are not blended. Report-card redemption inputs are treated as materially stale after more than twice the 4-hourly redemption sync cadence, so normal cron lag does not globally remove medium- or high-confidence redemption uplift.
For peg handling, rawInputs.pegScore is the effective peg input used by report-card scoring. Most coins use their direct peg-summary value. Configured NAV wrappers can inherit peg stability from a referenced base stablecoin when the wrapper share price is not the right peg-tracking surface; pure NAV tokens without a configured reference remain null and keep neutral handling. rawInputs.activeDepegBps is the open active depeg event's absolute peak deviation used for final Safety Score caps; it is not the latest spot deviation.
GET /api/report-cards treats the stablecoins cache and readable redemption-backstop table as hard dependencies. DEX liquidity, bluechip ratings, live-reserve inputs, and materially stale redemption rows are soft dependencies: if one of those loaders is temporarily unavailable or stale beyond its scoring freshness runway, the endpoint continues serving a degraded snapshot instead of failing closed, with stale inputs suppressed from scoring.
dependencyGraph.edges: Pre-computed forward edges. from = upstream stablecoin ID, to = dependent stablecoin ID. weight and type carry the worker's canonical dependency metadata, so frontend graph consumers can use the snapshot directly instead of re-deriving edge semantics from static stablecoin metadata.
ReportCard
| Field | Type | Description |
|---|---|---|
id | string | Pharos stablecoin ID |
name | string | Full name |
symbol | string | Ticker |
overallGrade | string | Letter grade: "A+" through "F", or "NR" |
overallScore | number | null | Weighted score 0–100. null for unrated coins |
baseScore | number | null | Pre-peg-multiplier/no-liquidity/active-depeg-cap score after base dimension blending |
dimensions | Record<DimensionKey, DimensionScore> | Per-dimension grade, score, and detail text |
ratedDimensions | number | Number of dimensions with data (max 5) |
rawInputs | RawDimensionInputs | Raw scoring inputs for client-side grade recomputation (stress testing) |
isDefunct | boolean | true for cemetery coins (permanent F grade) |
DependencyWeight: { id: string, weight: number, type?: DependencyType } — upstream stablecoin ID + fraction of collateral from that source (0–1), with optional dependency category. Weights sum to ≤ 1.0; the remainder represents non-stablecoin collateral.
RawDimensionInputs
| Field | Type |
|---|---|
pegScore | number | null |
activeDepeg | boolean |
activeDepegBps | number | null |
depegEventCount | number |
lastEventAt | number | null |
liquidityScore | number | null |
effectiveExitScore | number | null |
redemptionBackstopScore | number | null |
redemptionRouteFamily | RedemptionRouteFamily | null |
redemptionModelConfidence | "high" | "medium" | "low" | null |
redemptionUsedForLiquidity | boolean |
redemptionImmediateCapacityUsd | number | null |
redemptionImmediateCapacityRatio | number | null |
concentrationHhi | number | null |
bluechipGrade | BluechipGrade | null |
canBeBlacklisted | boolean | "possible" | "inherited" |
chainTier | ChainTier |
deploymentModel | DeploymentModel |
collateralQuality | CollateralQuality |
custodyModel | CustodyModel |
governanceTier | GovernanceType |
governanceQuality | GovernanceQuality |
dependencies | DependencyWeight[] |
navToken | boolean |
collateralFromLive | boolean |
rawInputs.canBeBlacklisted is the canonical resolved blacklist status used by report-card-backed product surfaces. It can therefore differ from the raw StablecoinMeta.canBeBlacklisted override field, which only carries manual metadata and never stores computed "inherited" values.
rawInputs.collateralFromLive is true when score-grade live reserve data drove collateral scoring for the card.
Dimensions: pegStability, liquidity, resilience, decentralization, dependencyRisk
GET /api/redemption-backstops
Current redemption-backstop dataset for redeemable assets.
Cache: standard
Error responses: 503 when redemption_backstop has no rows yet, or when the current snapshot cannot be read cleanly.
Rows written by the current worker are grouped by a completed snapshot run manifest. The API serves the latest completed run when one exists, which prevents a partially written hourly sync from being treated as a fresh complete dataset. Legacy rows without a completed run remain readable during bootstrap and migration fallback.
Response
{
"coins": {
"cusd-cap": {
"stablecoinId": "cusd-cap",
"score": 88,
"effectiveExitScore": 56,
"dexLiquidityScore": 29,
"routeFamily": "basket-redeem",
"accessModel": "permissionless-onchain",
"settlementModel": "atomic",
"outputAssetType": "stable-basket",
"immediateCapacityUsd": null,
"immediateCapacityRatio": null,
"sourceMode": "estimated",
"resolutionState": "resolved",
"routeStatus": "open",
"routeStatusSource": "static-config",
"holderEligibility": "any-holder",
"capacityConfidence": "heuristic",
"capacitySemantics": "eventual-only",
"feeConfidence": "undisclosed-reviewed",
"feeModelKind": "undisclosed-reviewed",
"modelConfidence": "low",
"updatedAt": 1773350400,
"methodologyVersion": "3.98"
}
},
"methodology": {
"version": "3.98",
"versionLabel": "v3.98",
"currentVersion": "3.98",
"currentVersionLabel": "v3.98",
"changelogPath": "/methodology/#safety-scores-methodology",
"asOf": 1773350400,
"isCurrent": true,
"componentWeights": {
"access": 0.2,
"settlement": 0.15,
"executionCertainty": 0.15,
"capacity": 0.25,
"outputAssetQuality": 0.15,
"cost": 0.1
},
"effectiveExitModel": {
"model": "best-path",
"diversificationFactor": 0.1
}
},
"updatedAt": 1773350400
}
score is the direct redemption-quality score.
effectiveExitScore is the raw best-path exit score written into the redemption snapshot when the route resolved cleanly and is not currently impaired. It reuses last-known DEX liquidity even when the DEX input is stale; the cron records that operational state through metadata.liquidityStale. When both DEX liquidity and redemption exist, the model uses min(100, max(dex, redemption) + min(dex, redemption) × 0.10). Report cards may still recompute liquidity from the same underlying redemption score with additional confidence, eligibility, and active-depeg gating, so this raw endpoint value can differ numerically from dimensions.liquidity.score.
methodology.version is attributed from the latest completed redemption snapshot run, falling back to the latest stored row for legacy snapshots. methodology.currentVersion remains the live code version when the API is serving an older snapshot that has not yet been recomputed.
sourceMode:
dynamic= live reserve/protocol telemetryestimated= modelled from current supply and conservative route assumptionsstatic= route remains configured, but current runtime inputs did not resolve a usable score
resolutionState:
resolved= the route produced a usable scoremissing-cache= the stablecoins snapshot did not include the asset or its current supplymissing-capacity= the route is configured, but the snapshot could not resolve enough capacity to score itfailed= a route-specific resolver failedimpaired= the route shape is known but current market or route-availability evidence contradicts broad par redemption
routeStatus / routeStatusSource describe current route availability separately from the static route shape. Normal rows use routeStatus: "open" and routeStatusSource: "static-config". A severe active depeg (>=2500 bps) can publish routeStatus: "degraded" and routeStatusSource: "market-implied" for static or non-live-direct routes; those impaired rows have score = null, effectiveExitScore = null, and modelConfidence = "low". holderEligibility describes the modeled holder cohort, such as any-holder, verified-customer, whitelisted-primary, pre-incident-holder, issuer-discretionary, or unknown.
Top-level fields:
| Field | Type | Description |
|---|---|---|
coins | Record<string, RedemptionBackstopEntry> | Current snapshot keyed by Pharos stablecoin ID |
methodology | object | Version metadata plus component weights, effective-exit blend weights, and route-family caps |
updatedAt | number | Freshest updated_at timestamp for the served completed run, or freshest current row for legacy snapshots |
RedemptionBackstopEntry highlights:
| Field | Type | Description |
|---|---|---|
score | number | null | Direct redemption-quality score after route-family/config caps |
effectiveExitScore | number | null | Blended exit score used by report cards |
dexLiquidityScore | number | null | DEX liquidity input used in the blend |
routeFamily | string | stablecoin-redeem, basket-redeem, collateral-redeem, psm-swap, queue-redeem, or offchain-issuer |
accessModel | string | permissionless-onchain, whitelisted-onchain, issuer-api, or manual |
settlementModel | string | atomic, immediate, same-day, days, or queued |
outputAssetType | string | stable-single, stable-basket, bluechip-collateral, mixed-collateral, or nav |
sourceMode | string | dynamic, estimated, or static capacity provenance |
resolutionState | string | resolved, missing-cache, missing-capacity, failed, or impaired |
routeStatus | string | Current route availability: open, degraded, paused, cohort-limited, or unknown |
routeStatusSource | string | Source for current route availability: static-config, market-implied, operator-notice, protocol-api, or onchain |
routeStatusReason | string | undefined | Human-readable explanation when current availability impairs scoring |
routeStatusReviewedAt | string | undefined | UTC date (YYYY-MM-DD) for the current route-status assessment |
holderEligibility | string | Modeled holder cohort: any-holder, verified-customer, whitelisted-primary, pre-incident-holder, issuer-discretionary, or unknown |
capacityConfidence | string | live-direct, live-proxy, documented-bound, heuristic, or legacy dynamic fidelity tag for the capacity model |
capacityBasis | string | undefined | Typed basis for the modeled capacity, such as issuer-term-redemption, full-system-eventual, psm-balance-share, strategy-buffer, hot-buffer, daily-limit, live-direct-telemetry, or live-proxy-buffer |
capacitySemantics | string | immediate-bounded or eventual-only, distinguishing current redeemable buffer from eventual redeemability |
feeConfidence | string | fixed, formula, or undisclosed-reviewed fidelity tag for the fee model |
feeModelKind | string | fixed-bps, formula, documented-variable, or undisclosed-reviewed |
modelConfidence | string | Overall route-fidelity rollup: high, medium, or low |
immediateCapacityUsd | number | null | Immediate redeemable capacity in USD. null when the model is eventual-only or currently unrated |
immediateCapacityRatio | number | null | Immediate redeemable capacity as a share of supply. null when not separately quantified |
feeBps | number | null | Explicit bounded fee when configured |
feeDescription | string | undefined | Docs-backed fee description for variable, conditional, flat-minimum, or undisclosed redemption schedules |
queueEnabled | boolean | Whether the modeled route is explicitly queued/serial |
docs | { label?: string, url?: string, reviewedAt?: string, provenance?: string, sources?: { label: string, url: string, supports?: string[] }[] } | undefined | Optional documentation / transparency metadata. reviewedAt is the route-review date, while provenance is config-reviewed, live-reserve-display, proof-of-reserves, or preferred-link |
notes | string[] | undefined | Runtime notes such as stale reserve metadata fallback |
capsApplied | string[] | undefined | Applied score caps (queue-route-cap, offchain-route-cap, config-cap) |
Response (503): { "error": "Data not yet available" } or { "error": "Redemption backstop snapshot unavailable" }
GET /api/safety-score-history
Per-coin Safety Score grade transition history (seed row + grade changes only). Rows are written by the daily snapshot-safety-grade-history cron and returned in ascending date order.
Cache: slow
Required query parameter
| Param | Type | Description |
|---|---|---|
stablecoin | string | Pharos stablecoin ID (required) |
Optional query parameters
| Param | Type | Default | Bounds | Description |
|---|---|---|---|---|
days | integer | 365 | 1–3650 | Lookback window in days |
Response: Array sorted by date ascending.
[
{
"date": 1771977600,
"grade": "B+",
"score": 78,
"prevGrade": "B",
"prevScore": 74,
"methodologyVersion": "5.5"
}
]
| Field | Type | Description |
|---|---|---|
date | number | UTC day bucket (Unix seconds) when the event was recorded |
grade | string | Current Safety Score letter grade at date |
score | number | null | Current numeric score (0–100); null when grade is NR |
prevGrade | string | null | Previous grade before this event; null for the seed row |
prevScore | number | null | Previous score before this event; null for the seed row |
methodologyVersion | string | Safety Score methodology version used for this event row |
GET /api/yield-rankings
Cache-backed yield rankings written by the sync-yield-data cron. The endpoint rehydrates safetyScore, safetyGrade, yieldToRisk, and pharosYieldScore from the current report-card snapshot at read time so Yield Intelligence stays aligned with /api/report-cards. PYS is benchmark-aware: it starts from cached APY inputs, adds a weighted slice of the row's benchmark spread, and then applies the current Safety Score. The response also includes source-selection provenance, the default USD benchmark (riskFreeRate), and the structured benchmark registry used for row-level excess-yield selection. If a ranking row has no matching live report-card snapshot, the API now retains the row and falls back to DEFAULT_SAFETY_SCORE (40) and grade NR instead of dropping coverage.
Cache: standard — X-Data-Age and Warning headers included. Freshness threshold: 3600 s (1 hour, aligned to the hourly sync-yield-data publisher).
Error responses: 503 when the cached rankings payload is missing or unparseable JSON. If a parseable payload fails the live schema validation path, the handler logs the validation issue and still serves the cached object with _meta, without live safety hydration.
Response
{
"rankings": [YieldRanking, ...],
"riskFreeRate": 4.25,
"benchmarks": {
"USD": { "key": "USD", "label": "USD 3M T-Bill", "currency": "USD", "rate": 4.25, "recordDate": "2026-03-25", "fetchedAt": 1774425600, "ageSeconds": 0, "source": "fred-dgs3mo", "isFallback": false, "fallbackMode": null, "isProxy": false },
"EUR": { "key": "EUR", "label": "EUR 3M compounded €STR", "currency": "EUR", "rate": 1.9358, "recordDate": "2026-03-26", "fetchedAt": 1774425600, "ageSeconds": 0, "source": "ecb-estr-3m", "isFallback": false, "fallbackMode": null, "isProxy": false },
"CHF": { "key": "CHF", "label": "CHF 3M compounded SARON", "currency": "CHF", "rate": -0.0539, "recordDate": "2026-03-25", "fetchedAt": 1774425600, "ageSeconds": 0, "source": "six-sar3mc", "isFallback": false, "fallbackMode": null, "isProxy": false }
},
"scalingFactor": 8,
"medianApy": 4.21,
"updatedAt": 1772000000,
"provenance": {
"selectionMethod": "confidence-weighted",
"benchmark": { "key": "USD", "label": "USD 3M T-Bill", "currency": "USD", "rate": 4.25, "recordDate": "2026-03-25", "fetchedAt": 1774425600, "ageSeconds": 0, "source": "fred-dgs3mo", "isFallback": false, "fallbackMode": null, "isProxy": false },
"benchmarks": {
"USD": { "key": "USD", "label": "USD 3M T-Bill", "currency": "USD", "rate": 4.25, "recordDate": "2026-03-25", "fetchedAt": 1774425600, "ageSeconds": 0, "source": "fred-dgs3mo", "isFallback": false, "fallbackMode": null, "isProxy": false },
"EUR": { "key": "EUR", "label": "EUR 3M compounded €STR", "currency": "EUR", "rate": 1.9358, "recordDate": "2026-03-26", "fetchedAt": 1774425600, "ageSeconds": 0, "source": "ecb-estr-3m", "isFallback": false, "fallbackMode": null, "isProxy": false },
"CHF": { "key": "CHF", "label": "CHF 3M compounded SARON", "currency": "CHF", "rate": -0.0539, "recordDate": "2026-03-25", "fetchedAt": 1774425600, "ageSeconds": 0, "source": "six-sar3mc", "isFallback": false, "fallbackMode": null, "isProxy": false }
},
"dlPools": { "mode": "dex-cache", "ageSeconds": 240, "poolCount": 812 },
"safetySnapshot": { "kind": "ok", "coverageRatio": 0.98 }
},
"_meta": { "updatedAt": 1710500000, "ageSeconds": 42, "status": "fresh" }
}
| Field | Type | Description |
|---|---|---|
rankings | YieldRanking[] | All ranked stablecoins, sorted by Pharos Yield Score descending |
riskFreeRate | number | Default USD benchmark rate (%) retained for backward compatibility and mixed-view fallback |
benchmarks | object | null | Benchmark registry keyed by currency (USD, EUR, CHF) with label, rate, freshness, fallback, and proxy metadata |
scalingFactor | number | Scaling factor applied in yield score computation |
medianApy | number | TVL-weighted median APY (30d) across best-source rows, used as a peer reference in warning heuristics |
updatedAt | number | Unix seconds when the rankings were last computed |
provenance | object | null | Snapshot-level provenance for default benchmark freshness, full benchmark registry, DeFiLlama pool input freshness, safety coverage, and selection method |
YieldRanking
| Field | Type | Description |
|---|---|---|
id | string | Pharos stablecoin ID |
symbol | string | Token symbol |
name | string | Full name |
currentApy | number | Current APY (%) |
apy7d | number | 7-day average APY (%) |
apy30d | number | 30-day average APY (%) |
apyBase | number | null | Base APY component (%) |
apyReward | number | null | Reward APY component (%), null if none |
yieldSource | string | Human-readable yield source description |
yieldSourceUrl | string | null | Official URL for the selected source when Pharos has a curated or metadata-derived link |
yieldType | string | Yield type classification (e.g. "lending-vault", "staking") |
dataSource | string | Data source identifier (e.g. "defillama") |
sourceTvlUsd | number | null | TVL of the yield source pool (USD) |
pharosYieldScore | number | null | Composite Pharos Yield Score (0–100), recomputed at read time from cached APY + benchmark inputs plus the current Safety Score |
safetyScore | number | null | Current Safety Score input used by Yield Intelligence. Rated coins match /api/report-cards; unrated coins use the default NR penalty input (40) |
safetyGrade | string | null | Current Safety Score letter grade ("A+" through "F", or "NR") from /api/report-cards |
yieldToRisk | number | null | Yield-to-risk ratio recomputed at read time from cached APY inputs plus the current Safety Score |
excessYield | number | null | APY above risk-free rate (percentage points) |
benchmarkKey | "USD" | "EUR" | "CHF" | undefined | Benchmark selected for this row's excessYield and any rate-derived APY logic |
benchmarkLabel | string | undefined | Human-readable benchmark label for the row |
benchmarkCurrency | string | undefined | Benchmark currency code used for the row |
benchmarkRate | number | undefined | Benchmark rate (%) applied to this row |
benchmarkRecordDate | string | null | undefined | Market or policy record date for the selected benchmark |
benchmarkIsFallback | boolean | undefined | Whether the row benchmark is currently on a fallback path |
benchmarkFallbackMode | string | null | undefined | Fallback reason for the row benchmark when applicable |
benchmarkSelectionMode | "native" | "fallback-usd" | "manual-override" | undefined | How the row benchmark was selected |
benchmarkIsProxy | boolean | undefined | True when the selected benchmark is an explicit proxy rather than the exact reference rate |
yieldStability | number | null | Yield stability metric (0–1; higher = more stable) |
apyVariance30d | number | null | 30-day APY variance |
apyMin30d | number | null | Minimum APY in last 30 days (%) |
apyMax30d | number | null | Maximum APY in last 30 days (%) |
warningSignals | string[] | Active warning-signal flags for the selected best source |
altSources | AltYieldSource[] | Additional non-selected source rows for the same coin |
provenance | object | null | Source-level provenance: confidence tier, selection reason, benchmark state, source-switch metadata, source freshness, and optional anchor timing |
When present, YieldRanking.provenance includes:
sourceObservedAt/sourceAgeSeconds: the timestamp and age of the latest observation actually backing the rowcomparisonAnchorObservedAt/comparisonAnchorAgeSeconds: optional prior-anchor timing for APYs derived from two observations, such as price-derived and on-chain exchange-rate rowsbenchmarkKey,benchmarkLabel,benchmarkRate,benchmarkIsFallback,benchmarkSelectionMode, and related fields for the exact benchmark applied to that row
GET /api/yield-history
Historical yield data for a single stablecoin. If a stored warning_signals payload is malformed, the API treats it as an empty array rather than failing the entire response. Returned rows are capped at the latest published /api/yield-rankings snapshot so history cannot advance past an unpublished yield cache state. If the cached rankings payload is missing or malformed, the cap degrades to the latest successful sync-yield-data cron timestamp instead of wall-clock now.
Cache: slow — X-Data-Age and Warning headers included. Freshness threshold: 3600 s (1 hour, aligned to the hourly sync-yield-data publisher).
Required query parameter
| Param | Type | Description |
|---|---|---|
stablecoin | string | Pharos stablecoin ID (required) |
Optional query parameters
| Param | Type | Default | Bounds | Description |
|---|---|---|---|---|
days | integer | 90 | 1–365 | Lookback window in days |
mode | string | best | — | best for historically selected best-source rows |
sourceKey | string | — | — | When present, returns source-specific history for that source key |
Response
{
"current": {
"date": 1772000000,
"apy": 4.21,
"sourceKey": "onchain:usde-ethena",
"yieldSource": "Ethena staking (sUSDe)"
},
"history": [YieldHistoryPoint, "..."],
"methodology": {
"version": "7.4",
"currentVersion": "7.4",
"changelogPath": "/methodology/yield-changelog/"
}
}
| Field | Type | Description |
|---|---|---|
current | YieldHistoryPoint|null | Latest row in the returned history window, or null when no history exists |
history | YieldHistoryPoint[] | History rows sorted by date ASC |
methodology | object | Yield methodology envelope for the response |
YieldHistoryPoint
{
"date": 1771500000,
"apy": 12.4,
"apyBase": 10.2,
"apyReward": 2.2,
"exchangeRate": 1.052,
"sourceTvlUsd": 5200000000,
"warningSignals": [],
"sourceKey": "rate-derived",
"yieldSource": "T-bill proxy",
"yieldSourceUrl": "https://ondo.finance/usdy",
"yieldType": "nav-appreciation",
"dataSource": "rate-derived",
"isBest": true,
"sourceSwitch": false
}
| Field | Type | Description |
|---|---|---|
date | number | Unix seconds |
apy | number | Total APY at snapshot time (%) |
apyBase | number | null | Base APY component (%) |
apyReward | number | null | Reward APY component (%); null if none |
exchangeRate | number | null | Exchange rate at snapshot time (e.g. sUSDe/USDe); null if not applicable |
sourceTvlUsd | number | null | TVL of the yield source pool at snapshot time (USD) |
warningSignals | string[] | Active warning-signal flags at that snapshot |
sourceKey | string | null | Stable source identifier for this history row (for example a DL pool UUID or onchain:<stablecoinId>) |
yieldSource | string | null | Human-readable source label at that snapshot |
yieldSourceUrl | string | null | Official URL for that source when Pharos has a curated or metadata-derived link |
yieldType | string | null | Yield type classification at that snapshot |
dataSource | string | null | Underlying data-source family |
isBest | boolean | Whether this row was the selected best source at that timestamp |
sourceSwitch | boolean | True when the historically selected best source changed at this row |
GET /api/mint-burn-flows
Mint/burn flow data across tracked stablecoins — aggregate gauge score, per-coin net-flow + pressure-shift signals, and hourly timeseries. Updated every 30 minutes by the sync cron.
Cache: standard
Error responses: 503 when the cached fallback payload is missing or malformed and live recomputation cannot satisfy the request. Malformed embedded freshness fields inside an otherwise valid cached payload no longer reset freshness to synthetic values; the API logs the corruption and falls back to the cache row timestamp.
Optional query parameters
| Param | Type | Default | Bounds | Description |
|---|---|---|---|---|
stablecoin | string | — | — | Filter to a single stablecoin ID. Changes response shape to per-coin mode |
hours | integer | 24 | 1–720 | Lookback window for the returned hourly[] series |
Response (aggregate mode — no stablecoin param)
{
"gauge": {
"score": 2.3,
"band": "NEUTRAL",
"flightToQuality": false,
"flightIntensity": 0,
"trackedCoins": 8,
"trackedMcapUsd": 215000000000,
"intensitySemantics": "signed-v2",
"classificationSource": "report-card-cache"
},
"coins": [CoinFlow, ...],
"hourly": [HourlyFlow, ...],
"updatedAt": 1772000000,
"windowHours": 24,
"scope": { "chainIds": ["ethereum", "arbitrum"], "label": "Configured issuance chains" },
"sync": { "lastSuccessfulSyncAt": 1772000200, "freshnessStatus": "fresh", "warning": null, "criticalLaneHealthy": true }
}
gauge
| Field | Type | Description |
|---|---|---|
score | number | null | Market-cap-weighted pressure-shift composite (-100 to +100). null when insufficient data |
band | string | null | Gauge band: "CRISIS", "STRESS", "CAUTIOUS", "NEUTRAL", "HEALTHY", "CONFIDENT", "SURGE" |
flightToQuality | boolean | Whether flight-to-quality conditions are active |
flightIntensity | number | Flight-to-quality intensity (0–100). 0 when not active |
trackedCoins | number | Number of stablecoins tracked for mint/burn flows |
trackedMcapUsd | number | Combined market cap of tracked coins (USD) |
intensitySemantics | string | Scoring semantics version identifier (currently "signed-v2") |
classificationSource | string | Source of flight-to-quality classification ("report-card-cache" or "unavailable") |
Top-level metadata
| Field | Type | Description |
|---|---|---|
windowHours | number | Requested chart window for hourly[] |
scope | object | Current ingestion scope, for example { chainIds: ["ethereum", "arbitrum"], label: "Configured issuance chains" } |
sync | object | Latest critical-lane freshness metadata, warning state, and optional classificationWarning |
CoinFlow
| Field | Type | Description |
|---|---|---|
stablecoinId | string | Pharos stablecoin ID |
symbol | string | Token symbol |
flowIntensity | number | null | Deprecated alias for pressureShiftScore; retained for compatibility |
pressureShiftScore | number | null | Canonical baseline-relative pressure score (-100 to +100). null if < 7 days of data or no current activity |
pressureShiftState | "improving" | "stable" | "worsening" | "nr" | Interpreted pressure state from pressureShiftScore |
netFlowDirection24h | "minting" | "burning" | "flat" | "inactive" | Current 24h direction derived from raw net flow + activity |
has24hActivity | boolean | Whether any 24h mint/burn events were recorded for the coin |
baselineDailyNetUsd | number | null | Average daily net flow over the baseline window used for scoring |
baselineDailyAbsUsd | number | null | Average daily absolute flow over the baseline window used for scoring |
baselineDataDays | number | null | Number of tracked days contributing to the baseline window |
netFlow24hUsd | number | Raw 24h net flow (USD, positive = net minting, negative = net burning). Fixed to the canonical 24h window even when hours changes |
mintVolume24hUsd | number | Total mint volume in the canonical 24h window (USD) |
burnVolume24hUsd | number | Total burn volume in the canonical 24h window (USD) |
mintCount24h | number | Number of mint events in the canonical 24h window |
burnCount24h | number | Number of burn events in the canonical 24h window |
netFlow7dUsd | number | 7-day net flow (USD) |
netFlow30dUsd | number | 30-day net flow (USD) |
netFlow90dUsd | number | 90-day net flow (USD) |
largestEvent24h | object | null | Largest event in the last 24h: { direction, amountUsd, txHash, timestamp } |
coverage | object | undefined | Coverage metadata: startBlock, lastSyncedBlock, lagBlocks, historyStartAt, window booleans, adapter provenance (adapterKinds, startBlockSource, startBlockConfidence), and status |
HourlyFlow
| Field | Type | Description |
|---|---|---|
hourTs | number | Unix seconds (start of hour) |
netFlowUsd | number | Net flow for this hour (USD) |
mintVolumeUsd | number | Mint volume for this hour (USD) |
burnVolumeUsd | number | Burn volume for this hour (USD) |
Response (per-coin mode — with stablecoin param)
Returns per-chain breakdown and hourly timeseries for a single coin. Returns 404 if the stablecoin is not tracked for mint/burn flows.
{
"stablecoinId": "usdt-tether",
"symbol": "USDT",
"mintVolumeUsd": 50000000,
"burnVolumeUsd": 30000000,
"netFlowUsd": 20000000,
"mintCount": 12,
"burnCount": 8,
"chains": [{ "chainId": "ethereum", "mintVolumeUsd": 40000000, ... }],
"hourly": [HourlyFlow, ...],
"updatedAt": 1772000000,
"windowHours": 24,
"scope": { "chainIds": ["ethereum"], "label": "Ethereum-only" },
"sync": { "lastSuccessfulSyncAt": 1772000200, "freshnessStatus": "fresh", "warning": null, "criticalLaneHealthy": true }
}
GET /api/mint-burn-events
Paginated list of individual mint/burn events for a specific stablecoin. Events are sourced from on-chain logs via Alchemy JSON-RPC.
Cache: realtime
Required query parameter
| Param | Type | Description |
|---|---|---|
stablecoin | string | Pharos stablecoin ID (required) |
Optional query parameters
| Param | Type | Default | Bounds | Description |
|---|---|---|---|---|
direction | string | — | "mint" or "burn" | Filter by direction |
chain | string | — | tracked chain IDs for the requested stablecoin | Filter by chain ID within the stablecoin's configured issuance scope |
burnType | string | — | "effective_burn", "bridge_burn", "review_required" | Filter burn rows by classification |
scope | string | "all" | "all" or "counted" | counted returns only rows used in economic-flow aggregates (flow_type='standard' and mint/effective-burn semantics) |
minAmount | number | — | — | Minimum USD amount; unpriced rows are excluded when this filter is used |
limit | integer | 50 | 1–500 | Max results |
offset | integer | 0 | — | Pagination offset |
Response
{
"events": [MintBurnEvent, ...],
"total": 1234
}
Results are ordered by timestamp descending (most recent first).
MintBurnEvent
| Field | Type | Description |
|---|---|---|
id | string | Composite ID: {chainId}-{txHash}-{logIndex} |
stablecoinId | string | Pharos stablecoin ID |
symbol | string | Token symbol |
chainId | string | Chain identifier (e.g. "ethereum") |
direction | "mint" | "burn" | Whether tokens were minted or burned |
flowType | "standard" | "bridge_transfer" | "atomic_roundtrip" | Flow-noise classification; bridge_transfer and atomic_roundtrip rows are excluded from aggregate flow metrics |
amount | number | Amount in native token units |
amountUsd | number | null | USD value at time of event |
burnType | "effective_burn" | "bridge_burn" | "review_required" | null | Burn classification; null for mint rows |
burnReviewReason | string | null | Reason emitted when a burn requires manual review classification |
counterparty | string | null | Non-zero address (recipient for mint, sender for burn) |
txHash | string | Transaction hash |
blockNumber | number | Block number |
timestamp | number | Unix seconds |
explorerTxUrl | string | Block explorer URL for the transaction |
priceUsed | number | null | Price used to derive amountUsd |
priceTimestamp | number | null | Unix seconds of the price snapshot used |
priceSource | string | null | Valuation provenance (supply_history, price_cache, price_cache_heal, etc.) |
GET /api/stress-signals
Returns Depeg Early Warning Score (DEWS) data for active tracked stablecoins.
All coins (no params): Latest DEWS score + signal breakdown per coin.
Single coin: Add ?stablecoin=ID&days=30 for latest + daily history.
stablecoin must be an active tracked Pharos stablecoin ID. Unknown IDs return 404 with { "error": "Unknown stablecoin" }; tracked-but-non-active IDs return 404 with { "error": "Stablecoin not tracked" }.
Cache: standard (public, s-maxage=300, max-age=60). Freshness threshold: 1800 s (30 minutes, aligned to compute-dews).
Query parameters
| Param | Type | Default | Description |
|---|---|---|---|
stablecoin | string | — | Single coin mode: return latest + daily history |
days | integer | 30 | History lookback (max 365) |
Aggregate responses are filtered to active tracked stablecoin IDs only, even if stale rows for non-active or de-tracked IDs still exist in storage. The aggregate response keeps updatedAt as the newest returned current row, but X-Data-Age / Warning freshness headers are based on oldestComputedAt so a stale per-coin row cannot be hidden by fresher rows for other coins.
Response (all coins)
{
"signals": {
"usdt-tether": {
"score": 5,
"band": "CALM",
"signals": {
"supply": { "value": 2, "available": true },
"price": { "value": 1, "available": true }
},
"amplifiers": { "psi": 1, "contagion": 1 },
"computedAt": 1740000000,
"methodologyVersion": "5.95"
}
},
"updatedAt": 1740000000,
"oldestComputedAt": 1740000000,
"malformedRows": 0,
"methodology": {
"version": "5.95",
"versionLabel": "v5.95",
"currentVersion": "5.95",
"currentVersionLabel": "v5.95",
"changelogPath": "/methodology/depeg-changelog/",
"asOf": 1740000000,
"isCurrent": true
}
}
Response (single coin)
{
"current": {
"score": 5,
"band": "CALM",
"signals": {
"supply": { "value": 2, "available": true },
"price": { "value": 1, "available": true }
},
"amplifiers": { "psi": 1, "contagion": 1 },
"computedAt": 1740000000,
"methodologyVersion": "5.95"
},
"history": [
{
"date": 1739900000,
"score": 3,
"band": "CALM",
"signals": {
"supply": { "value": 1, "available": true },
"price": { "value": 1, "available": true }
},
"amplifiers": { "psi": 1, "contagion": 1 },
"methodologyVersion": "5.95"
}
],
"malformedRows": 0,
"methodology": {
"version": "5.95",
"versionLabel": "v5.95",
"currentVersion": "5.95",
"currentVersionLabel": "v5.95",
"changelogPath": "/methodology/depeg-changelog/",
"asOf": 1740000000,
"isCurrent": true
}
}
malformedRows — count of DB rows with unparseable JSON signal data (expected 0 under normal operation)
oldestComputedAt — aggregate mode only; oldest returned current row and the timestamp used for response freshness headers
amplifiers — clamped multipliers that were applied on top of the base weighted score. psi is the systemic PSI amplifier (range [1.0, 1.3]); contagion is the per-peg-type cross-asset amplifier (range [1.0, 1.2]). Both default to 1.0 for legacy cached rows written before v5.95.
methodology — same fields and semantics as /api/depeg-events
POST /api/feedback
Public feedback ingestion endpoint used by the in-app feedback modal. Validates payloads, applies IP-based rate limiting, and forwards submissions to GitHub Issues.
Authentication: exempt
Cache: no edge cache (POST passthrough)
Rate limits
- Global public API limiter: D1-backed per-IP-hash limiter (
300 requests / 60 seconds) for non-admin requests. If the distributed limiter path fails, the worker fails open for the first two consecutive storage failures, then returns503withRetry-After: 60until limiter storage recovers or the emergency counter decays. - Feedback endpoint limiter:
3 submissions / 10 minutesper salted IP hash in D1.
Request body
{
"type": "bug",
"title": "Optional short title",
"description": "Required, 10-2000 characters",
"expectedValue": "Optional expected behavior/value",
"stablecoinId": "Optional canonical stablecoin id",
"stablecoinName": "Optional stablecoin name",
"pageUrl": "/stablecoin/usdt-tether",
"pegValue": "Optional UI value snapshot",
"contactHandle": "@pharos_user",
"website": ""
}
| Field | Type | Required | Notes |
|---|---|---|---|
type | "bug" | "data-correction" | "feature-request" | Yes | Submission category |
title | string | Conditional | Required for bug and feature-request (3–100 chars); optional for data-correction |
description | string | Yes | 10–2000 chars |
pageUrl | string | Yes | Relative app path (must start with /) |
website | string | No | Honeypot field; non-empty is silently accepted/dropped |
expectedValue, stablecoinId, stablecoinName, pegValue | string | No | Optional metadata |
contactHandle | string | No | Optional Telegram/X handle that appears publicly on GitHub |
Response
{ "ok": true }
Error responses
400invalid payload429rate limited (3 submissions / 10 minutes per salted IP hash)500forwarding/processing failure503service misconfigured (missingFEEDBACK_IP_SALTorGITHUB_PAT)
POST /api/telegram-webhook
Telegram Bot API webhook endpoint. Receives user messages, processes bot commands, and manages subscriptions.
Authentication: exempt from X-API-Key; requires X-Telegram-Bot-Api-Secret-Token instead. Not the standard X-Admin-Key.
Rate limiting: Exempt from IP rate limiter (Telegram sends from fixed IPs).
Cache: no edge cache (POST passthrough)
Request body: Telegram Update object (JSON, sent by Telegram servers).
Response: Always 200 OK with plain-text body ok (Telegram retries on non-2xx).
Commands handled:
/start— Welcome message with onboarding examples plus@pharoswatchand@pharoswatcherslinks/presets— List the preset watchlist catalog and example commands/subscribe <types> <targets>— Subscribe to alerts for explicit coins or preset watchlists (types: dews, depeg, safety, launch)/subscribe <types> all— Enable one or more alert types across all tracked stablecoins/unsubscribe <targets>— Remove explicit coin subscriptions or the concrete coin rows covered by a preset watchlist/unsubscribe all— Remove all per-coin subscriptions and disable every current alert flag, including launch/set <ticker> <setting> <value>— Tune per-coin thresholds and modes/set all <setting> <value>— Toggle global all-stablecoin alert types/mute <start>-<end>— Enable UTC quiet hours/unmutehours— Disable quiet hours/status <ticker>— Read-only per-coin status summary/cancel— Cancel a pending disambiguation flow/list— Show current subscriptions, per-coin settings, and quiet hours/help— Command reference
Preset watchlists currently resolve at command time into standard telegram_subscriptions rows. Supported aliases are usd-top10, usd-top25, usd-top50, eur-top10, gold-top5, mcap-ge-1b, and mcap-ge-100m. Presets are supported for dews, depeg, and safety; launch still requires explicit tickers or Pharos coin IDs.