Skip to main contentSkip to data table
Pharos
PHAROSlive stablecoin signals

API Reference

Public Pharos API contracts for stablecoin data: endpoints, authentication, response schemas, pagination, freshness headers, and dataset exports.

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/* / HEAD /api/og/* return image/png for known image routes, 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. Non-exempt /api/* requests on api.pharos.watch require a valid X-API-Key; missing or invalid keys return 401 Unauthorized. Per-key rate-limit overages return 429, and cold auth/limiter dependency failures can still return 503.

Surface Split

The runtime now uses three HTTP lanes:

  • https://api.pharos.watch is the external integration API. Protected public routes require X-API-Key.
  • https://site-api.pharos.watch is the website-internal Worker host. It accepts only allowlisted GET reads plus X-Pharos-Site-Proxy-Secret.
  • /_site-data/* is the same-origin Pages Functions proxy used by browsers on pharos.watch, ops.pharos.watch, stablecoin-dashboard.pages.dev, and subdomains of stablecoin-dashboard.pages.dev.

Static dataset exports are served from the public website, not from the Worker API, and do not require X-API-Key. The Stablecoin Cemetery export is available as JSON at https://pharos.watch/datasets/stablecoin-cemetery.json and CSV at https://pharos.watch/datasets/stablecoin-cemetery.csv.

Machine-readable integration artifacts are also served from the public website for onboarding. The OpenAPI endpoint catalogue is available at https://pharos.watch/openapi.json, and Postman artifacts are available at https://pharos.watch/postman/pharos-api.postman_collection.json plus https://pharos.watch/postman/pharos-api.postman_environment.json. Import both Postman files, then replace the environment apiKey placeholder with a real X-API-Key. The generated OpenAPI artifact includes named schemas for the richer Yield Intelligence ranking and history payloads, and the Postman collection includes both best-source and source-key yield-history examples. These are public integration/read onboarding artifacts, not a complete dump of every no-key route; they intentionally exclude Cloudflare-Access-gated admin routes, self-serve key issuance POST endpoints, feedback submission, Telegram webhook ingestion, Telegram Mini App endpoints, and dynamic OG image routes. Request keys through https://pharos.watch/api/.

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 and send X-API-Key for protected public reads, including /api/telegram-pulse.

Production Pages does not proxy public self-serve /api/* POST requests. The public form at https://pharos.watch/api/ calls https://api.pharos.watch/api/api-key-requests and https://api.pharos.watch/api/api-key-requests/verify with normal CORS preflights for JSON POST requests.

Self-serve API-key request honeypot submissions are intentionally no-op accepted: POST /api/api-key-requests returns 200 { "ok": true } when the optional website field is non-empty, without creating an API-key request or sending email. Normal non-honeypot submissions return 202 Accepted with pending_verification.

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, without Cache-Control: no-store, and without freshness Warning: 110; it does not cache no-store routes 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/health
  • GET /api/og/*
  • HEAD /api/og/*
  • POST /api/feedback
  • POST /api/api-key-requests
  • POST /api/api-key-requests/verify
  • POST /api/telegram-webhook
  • POST /api/telegram-mini-app/session
  • POST /api/telegram-mini-app/mutate

POST /api/telegram-webhook is externally reachable but not anonymous: it requires X-Telegram-Bot-Api-Secret-Token instead of X-API-Key.

POST /api/telegram-mini-app/session and POST /api/telegram-mini-app/mutate are also externally reachable but not anonymous. They require Telegram Mini App initData signed for @PharosWatchBot; the worker validates the HMAC, auth_date, and user payload before any D1-backed state write. These endpoints are denied on the website-internal site-data lane and are intended only for the Mini App at https://pharos.watch/pharoswatchbot/app/.

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 public API host rejects registered admin paths and configured admin-like root families before API-key auth, so a public API key cannot be used to reach registered admin routes or malformed children of configured roots such as /api/api-keys*, /api/api-key-requests-admin*, and /api/discovery-candidates* on api.pharos.watch.

The public self-serve request form lives at https://pharos.watch/api/. It sends an email verification link, then exchanges that one-time token for a default key after verification. Default self-serve keys are tier="self-serve", trafficClass="external", limited to 30 requests per minute, expire after 60 days, and allow one active/pending self-serve claim per normalized email. Request details are available only in the private ops.pharos.watch/admin-api/ UI.

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. Self-serve issuance uses the same storage model and returns the plaintext token only once after verification.

For protected cacheable GET routes, the worker keeps a bounded isolate-local verified-key cache and a bounded isolate-local limiter. A recently verified non-self-serve key can use that local path for hot edge-cache hits, and can continue to read cached routes during a brief D1 auth/limiter outage. Self-serve, unknown, stale-cache, or not-yet-verified keys still fail closed.


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:

ExampleAsset
"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/registry.ts, with editable per-coin metadata stored in shared/data/stablecoins/coins/*.json, the checked-in generated aggregate at shared/data/stablecoins/coins.generated.json, and validation in shared/lib/stablecoins/schema.ts. The API accepts canonical IDs only. Non-canonical stablecoin detail URLs and legacy frontend route aliases are retired and unsupported.


Response Headers

Endpoints backed by the cron cache include these additional headers:

HeaderDescription
X-Data-AgeSeconds elapsed since the cron last wrote this data to D1
WarningFreshness 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"
  }
}
FieldTypeDescription
updatedAtnumberUnix epoch seconds when the cron last wrote this data to D1
ageSecondsnumberfloor(now / 1000) - updatedAt
statusstring"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:

EndpointMax Age (sec)Source
GET /api/stablecoins600createCacheHandler
GET /api/chains1800worker/src/api/chains.ts
GET /api/bluechip-ratings43200createCacheHandler
GET /api/usds-status86400createCacheHandler
GET /api/yield-rankings3600Manual injection after live safety hydration
GET /api/depeg-resolver900worker/src/api/depeg-resolver.ts
GET /api/depeg-resolver-review900worker/src/api/depeg-resolver-review.ts

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. Supply history, safety score history, and non-USD share are explicit history-endpoint exceptions that emit freshness headers; DEX liquidity history currently exposes 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.

All rows below are members of the centralized API_CACHE_PROFILES map (shared/lib/api-cache-profiles.ts) except immutable-snapshot, which is a route-local constant (IMMUTABLE_CACHE_CONTROL in worker/src/api/snapshot.ts) reused for the immutable public-snapshot routes.

ProfileCache-ControlUsed by
realtimepublic, s-maxage=60, max-age=10stablecoins, stablecoin-summary, blacklist, blacklist-summary, depeg-events, peg-summary, mint-burn-events, chains
standardpublic, s-maxage=300, max-age=60stablecoin-charts, depeg-resolver, depeg-resolver-review, redemption-backstops, usds-status, daily-digest, digest-archive, report-cards, stability-index, yield-rankings, mint-burn-flows, stress-signals
custompublic, s-maxage=300, max-age=300dex-liquidity (browser-side max-age extended to match CDN TTL)
per-coinpublic, s-maxage=300, max-age=10stablecoin/:id (cache-aside with 5-min per-coin TTL in D1)
slowpublic, s-maxage=3600, max-age=300supply-history, dex-liquidity-history, bluechip-ratings, yield-history, safety-score-history, non-usd-share
archivepublic, s-maxage=86400, max-age=3600digest-snapshot, snapshots-index
immutable-snapshotpublic, s-maxage=31536000, max-age=31536000, immutablesnapshots/:date.json, snapshot/:date/stablecoin/:id
public-statuspublic, max-age=60public-status-history
og-imagepublic, max-age=900, s-maxage=900dynamic Open Graph images
reserve-livepublic, s-maxage=3600, max-age=300stablecoin-reserves live mode
reserve-live-stalepublic, s-maxage=1800, max-age=120stablecoin-reserves live-stale mode
reserve-fallbackpublic, s-maxage=300, max-age=60stablecoin-reserves curated/template/unavailable fallback modes
no-storeno-storehealth plus all admin GET routes via the router override or admin route wrapper (status, status-history, request-source-stats, yield-source-decisions, 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, admin-telegram-chat/:chatId, status-probe-history)

POST /api/feedback, POST /api/api-key-requests, POST /api/api-key-requests/verify, POST /api/telegram-webhook, POST /api/telegram-mini-app/session, POST /api/telegram-mini-app/mutate, and admin POST endpoints bypass edge caching because they are non-GET request paths. The self-serve API-key endpoints and Telegram Mini App endpoints explicitly return no-store responses so verification tokens, plaintext API keys, and per-chat alert state are never cacheable.


Polling Guidance

Recommended minimum polling cadence for external integrations:

Cache profileMinimum poll intervalNotes
realtime60 secondsPolling faster usually re-fetches the same edge-cached payload
standard300 secondsPreferred baseline for most dashboards
per-coin300 secondsGET /api/stablecoin/:id is history-heavy; avoid short loops
slow3600 secondsHistorical/timeline endpoints should generally be polled hourly
archive86400 secondsHistorical digest snapshots and public snapshot index listings
immutable-snapshotOn-demand onlyDated public dataset snapshots are content-addressed and immutable
no-storeOn-demand onlyHealth/admin diagnostics; avoid high-frequency polling

Client best practices:

  • Add interval jitter (±10%) to avoid synchronized bursts.
  • Read X-Data-Age + Warning for freshness/stale decisions when those optional headers are present.
  • Back off exponentially on 429 and 5xx responses.

Rate Limits

Public API traffic enforces per-key rate limiting to ensure fair usage. Non-exempt /api/* requests require a valid X-API-Key; the no-key public exceptions are GET /api/health, GET /api/og/*, HEAD /api/og/*, POST /api/feedback, POST /api/api-key-requests, POST /api/api-key-requests/verify, POST /api/telegram-webhook, POST /api/telegram-mini-app/session, and POST /api/telegram-mini-app/mutate. The Telegram webhook is authenticated separately with X-Telegram-Bot-Api-Secret-Token; Telegram Mini App endpoints are authenticated with signed Telegram initData.

Per-key limit

ScopeLimitWindow
Per API keyVaries (default 120)60 seconds

Per-key overrides are stored in api_keys.rate_limit_per_minute.

Self-serve keys are issued with a fixed default of 30 requests per minute and a 60 day expiry. The request workflow has separate abuse limits: initial submissions are throttled by salted IP hash (5/hour) and private email hash (3/day), verification attempts are throttled by salted IP hash (20/10 minutes) and token hash (5/10 minutes), and successful issuance allows one self-serve key creation per salted IP hash per 24 hours.

When the per-key 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 the feedback limiter's D1 dependency is unavailable, the endpoint returns 503 Service Unavailable with { "error": "Feedback service temporarily unavailable. Please try again." } and Retry-After: 60.

API-key authentication and per-key limiter storage normally rely on D1. For protected cacheable GET edge-cache hits, the worker can serve a recently verified non-self-serve key through a bounded isolate-local auth/limiter path. It can also continue serving a recently verified non-self-serve key during a brief D1 outage by reusing its bounded verified-key cache and isolate-local limiter. Self-serve keys are refused when their D1 lookup is unavailable because revocation and claim state cannot be rechecked from stale isolate cache. Unknown or not-yet-verified keys still fail closed with 503 Service Unavailable, { "error": "Public API temporarily unavailable" }, and Retry-After: 60. Best-effort API-key usage timestamp updates do not fail otherwise successful reads.

Retry Guidance

  • Respect the Retry-After header 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/* and HEAD /api/og/* return image/png on success for known image routes; unknown OG route patterns return the normal JSON error body, while OG data/render failures inside known image routes can return text/plain.

StatusMeaningWhen
400Bad RequestMissing 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.
401UnauthorizedPublic /api/* 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)
403ForbiddenDisallowed 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
404Not FoundUnknown stablecoin ID or missing resource
429Too Many RequestsRate limit exceeded (per-key public API limiter or feedback-specific limiter; feedback uses its own message body)
500Internal Server ErrorUnhandled exception (caught by withErrorHandler)
502Bad GatewayUpstream fetch failed (external data provider or Pages proxy upstream), or the ops proxy received a Cloudflare Access login redirect from ops-api
503Service UnavailableCache-passthrough endpoint where cache has never been populated, cached payload is corrupt / rejected by validation, a protected public API request cannot be authenticated from D1 or the recent verified-key cache, the feedback limiter/storage dependency fails, or MAINTENANCE_MODE=true (global kill switch via wrangler secret put)
504Gateway TimeoutPages /_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; 45 s for ops /api/audit-depeg-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).

  • GET is accepted for read endpoints (plus admin debug/status endpoints, GET /api/backfill-dews, and dry-run repair previews for GET /api/backfill-dews?repair=...&dry-run=true).
  • POST is accepted for mutating admin endpoints, POST /api/feedback, POST /api/api-key-requests, POST /api/api-key-requests/verify, POST /api/telegram-webhook, POST /api/telegram-mini-app/session, and POST /api/telegram-mini-app/mutate.
  • GET, POST is accepted on /api/api-keys so operators can list keys and create a new key through the same route.
  • POST is accepted on /api/api-keys/:id/update, /api/api-keys/:id/deactivate, and /api/api-keys/:id/rotate.
  • /api/audit-depeg-history allows GET only with ?dry-run=true; otherwise it is POST-only.
  • /api/backfill-dews allows GET for the historical backtest and for repair=...&dry-run=true previews; mutating repair runs are POST-only.
  • Unknown public /api/* requests can return 401 first when the API key is missing or invalid. After lane auth succeeds, unregistered paths return 404 because no route dependencies can be hydrated. Once a static or dynamic route family is registered, known paths with disallowed methods return 405 with Allow; unsupported verbs on known endpoint families return 405 with Allow: 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.

<!-- GENERATED-START: public-endpoints-quick-reference --> <!-- This block is generated by scripts/maintenance/generate-api-reference.mjs from public/openapi.json. --> <!-- Do not edit by hand. Run `node scripts/maintenance/generate-api-reference.mjs` to refresh. -->

Public Endpoints Quick Reference

Generated from public/openapi.json (Pharos API v1.0.0). The OpenAPI artifact intentionally excludes Cloudflare-Access-gated admin routes, self-serve key issuance POST endpoints, feedback submission, Telegram webhook ingestion, Telegram Mini App endpoints, and dynamic OG image routes. Those endpoints are documented in the hand-written sections below.

Total documented public operations: 38.

MethodPathSummaryTagsAuthParametersStatus codes
GET/api/blacklistBlacklist eventsBlacklistX-API-Keystablecoin?, chain?, chainId?, eventType?, q?, sortBy?, sortDirection?, limit?, offset?, includeTotal?200, 400, 401, 429, 503
GET/api/blacklist-summaryBlacklist summaryBlacklistX-API-Key200, 400, 401, 429, 503
GET/api/bluechip-ratingsBluechip ratingsRiskX-API-Key200, 400, 401, 429, 503
GET/api/chainsChainsChainsX-API-Key200, 400, 401, 429, 503
GET/api/daily-digestDaily digestDigestX-API-Key200, 400, 401, 429, 503
GET/api/depeg-eventsDepeg eventsPeg MonitoringX-API-Keystablecoin?, limit?, offset?, cursor?, active?, includeTotal?, includePending?200, 400, 401, 429, 503
GET/api/depeg-resolverDepeg Duration ResolverRisk, Peg MonitoringX-API-Key200, 400, 401, 429, 503
GET/api/depeg-resolver-reviewDepeg Duration Resolver ReviewerRisk, Peg MonitoringX-API-Key200, 400, 401, 429, 503
GET/api/dex-liquidityDEX liquidityLiquidityX-API-Key200, 400, 401, 429, 503
GET/api/dex-liquidity-historyDEX liquidity historyLiquidity, HistoryX-API-Keystablecoin, days?200, 400, 401, 429, 503
GET/api/digest-archiveDigest archiveDigestX-API-Key200, 400, 401, 429, 503
GET/api/digest-snapshotDigest snapshotDigestX-API-Keydate200, 400, 401, 429, 503
GET/api/eventsTape eventsRiskX-API-Keytype?, class?, coin?, pegCurrency?, chain?, q?, severityFloor?, since?, until?, cursor?, limit?, includeTotal?200, 400, 401, 429, 503
GET/api/healthHealth checkHealthnone200, 400, 503
GET/api/mint-burn-eventsMint and burn eventsFlowsX-API-Keystablecoin, direction?, chain?, burnType?, scope?, minAmount?, limit?, offset?, cursor?, includeTotal?200, 400, 401, 429, 503
GET/api/mint-burn-flowsMint and burn flowsFlowsX-API-Keystablecoin?, hours?200, 400, 401, 429, 503
GET/api/non-usd-shareNon-USD shareMarket Structure, HistoryX-API-Keydays?200, 400, 401, 429, 503
GET/api/peg-summaryPeg summaryPeg MonitoringX-API-Key200, 400, 401, 429, 503
GET/api/public-status-historyPublic status historyStatusX-API-Keylimit?, window?200, 400, 401, 429, 503
GET/api/redemption-backstopsRedemption backstopsRisk, ReservesX-API-Key200, 400, 401, 429, 503
GET/api/report-cardsReport cardsRiskX-API-Key200, 400, 401, 429, 503
GET/api/safety-score-historySafety score historyRisk, HistoryX-API-Keystablecoin, days?200, 400, 401, 429, 503
GET/api/snapshot/{date}/stablecoin/{stablecoinId}Public snapshot projection for a single coinDigest, Stablecoins, HistoryX-API-Keydate, stablecoinId200, 400, 401, 429, 503
GET/api/snapshots/{date}.jsonPublic snapshot for a single dayDigest, HistoryX-API-Keydate200, 400, 401, 429, 503
GET/api/snapshots/indexPublic snapshot indexDigestX-API-Key200, 400, 401, 429, 503
GET/api/stability-indexPharos Stability IndexRiskX-API-Keydetail?200, 400, 401, 429, 503
GET/api/stablecoin-chartsStablecoin chartsStablecoins, HistoryX-API-Key200, 400, 401, 429, 503
GET/api/stablecoin-reserves/{stablecoinId}Stablecoin reservesStablecoins, ReservesX-API-KeystablecoinId200, 400, 401, 429, 503
GET/api/stablecoin-summary/{stablecoinId}Stablecoin summaryStablecoinsX-API-KeystablecoinId200, 400, 401, 429, 503
GET/api/stablecoin/{stablecoinId}Stablecoin detailStablecoinsX-API-KeystablecoinId200, 400, 401, 429, 503
GET/api/stablecoinsList stablecoinsStablecoinsX-API-Key200, 400, 401, 429, 503
GET/api/stress-signalsStress signalsRisk, Peg MonitoringX-API-Keystablecoin?, days?200, 400, 401, 429, 503
GET/api/supply-historySupply historyHistoryX-API-Keystablecoin, days?200, 400, 401, 429, 503
GET/api/telegram-pulseTelegram pulseStatusX-API-Key200, 400, 401, 429, 503
GET/api/usds-statusUSDS freeze statusRiskX-API-Key200, 400, 401, 429, 503
GET/api/yield-adapter-manifestYield adapter manifestYieldX-API-Key200, 400, 401, 429, 503
GET/api/yield-historyYield historyYield, HistoryX-API-Keystablecoin, days?, mode?, sourceKey?200, 400, 401, 429, 503
GET/api/yield-rankingsYield rankingsYieldX-API-Key200, 400, 401, 429, 503
<!-- GENERATED-END: public-endpoints-quick-reference -->

GET /api/events

Tape events surface, backed by worker/src/api/events.ts. The handler already accepts type, class, coin (multi-value), pegCurrency, chain, q (case-insensitive free-text search), severityFloor, since / until (epoch ms), cursor, limit, and includeTotal per the quick-reference table above. The response envelope is { events[], nextCursor, total, totalExact, _meta }: nextCursor is a keyset cursor string (null when there are no more rows), total is the exact count only when includeTotal=true (otherwise null), and totalExact mirrors that boolean.

As of the May 2026 detail-page pass, the frontend hook useChartAnnotations (src/hooks/use-chart-annotations.ts) consumes this endpoint to drive per-coin chart annotations on the stablecoin detail route. The hook is gated by NEXT_PUBLIC_PHAROS_CHART_ANNOTATIONS — see process/feature-flags.md.

Phase 1 (shipped May 2026): the hook is wired through the consumer surface (<ChartAnnotationDots> + screen-reader-only legend) but returns an empty array. Charts render byte-identically to the pre-flag baseline; the flag-off path never fetches.

Phase 2 (planned): align the hook's URL params to the handler's existing shape — coin=<id> + since / until in epoch ms — or extend the worker to accept chart-friendly aliases (stablecoin, from, to). Phase 2 will also wire useApiQueryWithMeta, map tape-event rows into ChartAnnotation, and clamp results to the rendered chart's [fromMs, toMs] window inside the memo so out-of-range markers cannot push the data domain.

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.

The canonical stablecoins cache is written only after StablecoinListResponseSchema validation. Worker consumers that require the published public contract can opt into the same schema on cache read and return 503 for schema-invalid cached objects. Compatibility readers that only need critical fields may still salvage valid entries from older or partially malformed payloads, but they surface that state as degraded with a filtered-entry count instead of treating the filtered payload as fully healthy.

Response

{
  "peggedAssets": [StablecoinData, ...],
  "fxFallbackRates": { "peggedEUR": 1.082, "peggedGBP": 1.26 },
  "_meta": { "updatedAt": 1710500000, "ageSeconds": 42, "status": "fresh" }
}

fxFallbackRates is present when the FX-rate state loaded by sync-stablecoins has usable fresh or static references; inputs can come from Frankfurter/ECB, secondary or tertiary FX mirrors, commodity references, or cached/static fallback rates. Keys are pegType strings (e.g. "peggedEUR"), values are rates in USD.

StablecoinData fields

FieldTypeDescription
idstringPharos stablecoin ID
namestringFull name (e.g. "Tether")
symbolstringTicker (e.g. "USDT")
geckoIdstring | nullCoinGecko ID (normalized output key; upstream DefiLlama uses gecko_id)
pegTypestringDefiLlama peg type (e.g. "peggedUSD", "peggedEUR")
pegMechanismstring"fiat-backed", "crypto-backed-algorithmic", etc.
priceSourcestringSource 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.
priceConfidencestring | nullPrice confidence level: "high" (cross-validated agreement), "single-source", "low" (sources diverge), "fallback" (enrichment pipeline)
priceUpdatedAtnumber | nullCompatibility timestamp for the current price; mirrors the effective observation time when available
priceObservedAtnumber | nullUnix seconds for the effective observation time attached to the selected source price; interpret alongside priceObservedAtMode
priceObservedAtMode"upstream" | "local_fetch" | "unknown" | nullWhether priceObservedAt came from source-native freshness metadata, local fetch time, or legacy/unknown provenance
priceSyncedAtnumber | nullUnix seconds when Pharos selected and wrote the current price during the sync
supplySourcestring | undefinedSupply 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", "onchain-total-supply" (used when a supplemental asset is normalized from on-chain total supply instead of an upstream market-cap field), or "onchain-circulating-supply" (used when the same live on-chain fallback subtracts configured non-circulating protocol inventory balances before normalization)
pricenumber | nullCurrent 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.
circulatingRecord<string, number>Current supply in USD, keyed by pegType (e.g. { "peggedUSD": 138000000 })
circulatingPrevDayRecord<string, number>Supply 24 h ago
circulatingPrevWeekRecord<string, number>Supply 7 days ago
circulatingPrevMonthRecord<string, number>Supply ~30 days ago
chainCirculatingRecord<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.
chainsstring[]List of chain names where the token is deployed
contractsContractDeployment[] | undefinedCurated on-chain deployments for tracked stablecoins (active and frozen). Omitted when curated metadata has no contracts on file. Use this to map a Pharos id to its on-chain token contracts when joining with /api/report-cards or other endpoints keyed by id.
consensusSourcesstring[]Source names that returned a valid price for this coin during the sync cycle. Defaults to [] when absent.
priceSourceConfidenceProfilePriceSourceConfidenceProfile | undefinedPresent for DEX-inclusive primary prices. Summarizes active protocol DEX lanes, the freshest DEX lane age, and whether the price relies only on the aggregate dex-promoted lane.
agreeSourcesstring[] | undefinedCompatibility alias for agreeing/current price sources when present

ContractDeployment

FieldTypeDescription
chainstringPharos chain identifier (e.g. "ethereum", "arbitrum", "solana").
addressstringToken contract address as published by the issuer.
decimalsnumberToken decimals.

PriceSourceConfidenceProfile

FieldTypeDescription
activeDexLanesnumberCount of accepted protocol-specific DEX lanes such as balancer-dex or raydium-dex.
freshestDexLaneAgeSecnumber | nullAge in seconds of the freshest accepted DEX lane when the source carried observation metadata.
aggregateLaneOnlybooleantrue when the only DEX contribution is the legacy aggregate dex-promoted source.

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.

When a D1 detail cache row exists but is older than the 5-minute TTL and younger than 24 hours, the Worker serves that stale row immediately with Warning: 110, X-Data-Age, and Cache-Control: no-store, then refreshes the coin in the background. Refresh work is best-effort single-flight per coin within a Worker isolate, so bursts of stale reads do not all fan out to upstream providers. Rows older than 24 hours are not served as stale fallback; they force the same synchronous refresh path used by cold misses, sharing an in-flight refresh where one already exists in the same isolate.

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

FieldTypeDescription
datenumberUnix timestamp (seconds)
totalCirculatingUSDRecord<string, number>Supply in USD per pegType key
totalCirculatingRecord<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
}
FieldTypeDescription
idstringPharos stablecoin ID
namestringAsset name
symbolstringTicker symbol
pegTypestringPeg type key (peggedUSD, peggedEUR, etc.)
pegMechanismstringBacking/mechanism classification
priceUsdnumber | nullCurrent price in USD
priceSourcestringPrice source identifier. When priceUsd is null, this may be "missing" to indicate that no usable current price survived validation.
priceConfidencestring | nullPrice confidence label
supplySourcestring | nullSupply source identifier
supplyByPegUsdRecord<string, number>Current supply by peg bucket (USD)
supplyUsdobjectAggregate USD supply values and deltas (current, prevDay, prevWeek, prevMonth, change1d, change7d, change30d)
chainCountnumberNumber of chains where the asset is deployed
updatedAtnumberUnix 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 non-commodity non-USD buckets. The response keeps the legacy fiatNonUsd* field names for wire compatibility, but those fields include currency-linked plus other non-commodity non-USD pegs. 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

Freshness headers are emitted from the latest completed snapshot-supply run when available. Stale responses include X-Data-Age and can downgrade to Cache-Control: no-store with Warning: 110 once the daily history runway is exceeded. Rows newer than the completed daily snapshot marker are hidden so a failed chunked write cannot expose a partial latest day.

ParamTypeDefaultConstraintsDescription
daysnumber5000min 30, max 5000Lookback window in days

Unlike most numeric-query handlers, this endpoint defaults missing or malformed days values to 5000 and clamps most out-of-range values into 30..5000 instead of returning 400. Current parser quirk: days=0 is treated like a missing value and returns the default 5000 rather than the minimum 30.

Response: Array<{ date, commodityShare, fiatNonUsdShare, commodity, fiatNonUsd, total }>

FieldTypeDescription
datenumberUnix seconds (snapshot date)
commoditySharenumberCommodity-pegged share as % of total supply
fiatNonUsdSharenumberNon-commodity non-USD share as % of total supply, using the legacy field name
commoditynumberCommodity-pegged circulating USD
fiatNonUsdnumberNon-commodity non-USD circulating USD, using the legacy field name
totalnumberTotal 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:

StatusMeaning
200Chain aggregates computed successfully; freshness may still be degraded in _meta
503Stablecoins 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,
  "chainAttributedTotalUsd": 218000000000,
  "unattributedTotalUsd": 12000000000,
  "globalChange24hPct": 0.0012,
  "globalChange7dPct": 0.0045,
  "globalChange30dPct": 0.018,
  "updatedAt": 1710500000,
  "healthMethodologyVersion": "1.2"
}
FieldTypeDescription
chainsChainSummary[]Chains sorted by totalUsd descending
globalTotalUsdnumberTotal tracked stablecoin supply in USD, matching GET /api/stablecoins aggregate supply
chainAttributedTotalUsdnumberSupply that the source data attributes to concrete chains in USD
unattributedTotalUsdnumberPositive residual between tracked supply and chain-attributed supply in USD
globalChange24hPctnumber24h change for total tracked stablecoin supply as a decimal share
globalChange7dPctnumber7d change for total tracked stablecoin supply as a decimal share
globalChange30dPctnumber30d change for total tracked stablecoin supply as a decimal share
updatedAtnumberUnix epoch seconds of the underlying stablecoins snapshot
healthMethodologyVersionstringChain 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:

FieldTypeDescription
idstringCanonical chain identifier (DefiLlama chain name)
namestringHuman-readable chain name
logoPathstring | nullPath to chain logo asset
type"evm" | "tron" | "other"Chain runtime family from CHAIN_META
totalUsdnumberTotal stablecoin supply on this chain in USD
change24hnumberAbsolute 24h supply change in USD
change24hPctnumber24h supply change as a percentage
change7dnumberAbsolute 7d supply change in USD
change7dPctnumber7d supply change as a percentage
change30dnumberAbsolute 30d supply change in USD
change30dPctnumber30d supply change as a percentage
stablecoinCountnumberNumber of distinct stablecoins on this chain
dominantStablecoin{ id, symbol, share }Largest stablecoin by supply on the chain
topStablecoins{ id, symbol, share, supplyUsd }[]Up to five largest stablecoins by supply on the chain; share is chain-local (0–1) and supplyUsd is USD-denominated
dominanceSharenumberChain share of globalTotalUsd (0–1); chain rows may sum below 1 when source data has unattributed supply
healthScorenumber | nullChain Health Score 0–100, or null if insufficient data
healthBandstring | nullHealth band label: "robust" (80–100), "healthy" (60–79), "mixed" (40–59), "fragile" (20–39), "concentrated" (0–19)
healthFactorsChainHealthFactorsRaw sub-factor scores (0–100 each; quality may still be null)

ChainHealthFactors fields:

FieldTypeDescription
concentrationnumberHHI-based supply concentration score (higher = more diverse)
qualitynumber | nullSupply-weighted average stablecoin quality from report-card grades; null when rated supply coverage is below 50% by value
chainEnvironmentnumberResilience-tier score for the chain itself (100 tier 1, 60 tier 2, 20 tier 3)
pegStabilitynumberSupply-weighted average peg deviation score
backingDiversitynumberShannon 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 200 even 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 live only 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.
  • Successful responses are covered by the shared StablecoinReservesResponseSchema; frontend API clients validate 200 payloads strictly while preserving 404 as the not-live-enabled/null path.

Cache: dynamic

  • Live snapshots: slow (public, s-maxage=3600, max-age=300)
  • live-stale snapshots: 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):

FieldTypeDescription
stablecoinIdstringPharos coin ID
modestringOne of live, live-stale, curated-fallback, template-fallback, unavailable. This is snapshot transport/freshness state, not the user-facing reserve badge semantics.
reservesReserveSlice[]Reserve slices currently being shown to the user
estimatedbooleantrue only when using the classification template fallback
liveAtnumber?Unix seconds of the last successful live snapshot. Present only when live data exists
sourcestring?Adapter key (for example "infinifi", "m0", "openeden-usdo", or "accountable"). Present only when live data exists
displayUrlstring?Curated click-through page shown as Source in the UI. Present only when configured
evidenceUrlsstring[]?Adapter-emitted evidence URLs for the authoritative live snapshot, shown separately as Evidence links when available
displayBadgeobject?User-facing reserve badge semantics for authoritative live snapshots (live, curated-validated, or proof)
metadataobject?Adapter snapshot metadata for authoritative live snapshots. This can include feed-specific context such as yieldBasisCollateralPct for crvusd; adapter-specific metadata and nested details remain passthrough
provenanceobject?Evidence-quality envelope for authoritative live snapshots (evidenceClass, sourceModel, optional freshnessMode, scoringEligible)
syncobject?Live sync state (status, bootstrap, stale, lastAttemptedAt, lastSuccessAt, warnings, lastError, optional failureCategory, optional uncertainWrite). 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. sync.failureCategory is copied from reserve_sync_state.metadata.failureCategory when available. sync.uncertainWrite=true means the latest attempt hit the D1 write-timeout / finalize-rejection path, so the endpoint may be serving the last consistent snapshot or fallback while the attempted write remains ambiguous until the next clean run.

displayUrl and evidenceUrls are intentionally different:

  • displayUrl is the curated reserve-card destination
  • evidenceUrls are adapter-emitted URLs tied to the authoritative live snapshot metadata
  • some live feeds expose only displayUrl, while others expose both

When present, displayBadge has:

FieldTypeDescription
kind"live" | "curated-validated" | "proof"User-facing reserve badge classification
labelstringBadge label rendered by the frontend (Live, Curated-Validated, or Proof)

When present, provenance has:

FieldTypeDescription
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" | undefinedExplicit freshness policy when the adapter emits one
scoringEligiblebooleanWhether this exact snapshot is currently eligible for collateral-quality passthrough

Response (404): unknown or non-canonical IDs, known active coins without live reserve support, and live-enabled coins with no resolved reserve result return { "error": "Not found" }.


GET /api/stablecoin-charts

Aggregate historical supply chart data across the live homepage market-cap universe, broken down by peg type. The hourly stablecoin-charts cache still starts from DefiLlama's aggregate chart history, but the worker now reconciles structurally supplemental tracked assets (for example wrapper NAV tokens and commodity tokens that are not present in DefiLlama's aggregate chart feed) from D1 supply_history before publishing the cache. At read time the handler also appends or replaces the latest point with a live snapshot derived from the current stablecoins cache so the endpoint's trailing point matches the homepage KPI card. 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
    }
  }
]
FieldTypeDescription
datenumberUnix timestamp (seconds). Historical points are downsampled cache entries; the trailing point may be a fresher live stablecoins snapshot timestamp
totalCirculatingUSDRecord<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

ParamTypeDefaultDescription
stablecoinstringFilter by uppercase blacklist-tracker symbol from the full BLACKLIST_STABLECOINS set in shared/types/market.ts (for example USDT, not usdt-tether)
chainstringFilter by exact chain display name (e.g. Ethereum, Tron)
chainIdstringFilter by canonical chain-registry ID (e.g. ethereum, tron). When both chain and chainId are supplied, they must identify the same chain or the endpoint returns 400
eventTypestringFilter by type: blacklist, unblacklist, destroy
qstringCase-insensitive address substring search
sortBystringdateSort field: date, stablecoin, chain, event
sortDirectionstringdescSort direction: asc, desc
limitinteger1000Max results (0–1000; 0 maps to default 1000)
offsetinteger0Pagination offset
includeTotalbooleantrueWhen false, skips the exact COUNT(*); total becomes a page lower bound and totalExact is false

Response

{
  "events": [BlacklistEvent, ...],
  "total": 13422,
  "methodology": {
    "version": "3.99",
    "versionLabel": "v3.99",
    "currentVersion": "3.993",
    "currentVersionLabel": "v3.993",
    "changelogPath": "/methodology/blacklist-tracker-changelog/",
    "asOf": 1776729600,
    "isCurrent": false
  }
}

BlacklistEvent

FieldTypeDescription
idstringComposite ID: {chainId}-{txHash}-{logIndex}
stablecoinstringToken symbol (USDC, USDT, etc.)
chainIdstringStable chain identifier from the shared chain registry (e.g. "ethereum", "tron"). This is the preferred join key for integrations
chainNamestringHuman-readable chain name (e.g. "Ethereum")
eventTypestring"blacklist", "unblacklist", or "destroy"
addressstringAffected address (EVM 0x… or Tron T…)
amountNativenumber | nullCanonical token-native amount recovered from event data or historical balance lookup
amountUsdAtEventnumber | nullEvent-time USD value when Pharos can justify one
amountSourcestringevent, historical_balance, current_balance_snapshot, derived, legacy_migration, or unavailable
amountStatusstringresolved, recoverable_pending, permanently_unavailable, provider_failed, ambiguous
txHashstringTransaction hash
blockNumbernumberBlock number
timestampnumberUnix seconds
methodologyVersionstringMethodology version attributed to this event row
contractAddressstring | nullEmitting token contract when known
configKeystring | nullInternal tracker config identity ({chainId}-{contract})
eventSignaturestring | nullHuman-readable event signature/name when known
eventTopic0string | nullRaw EVM topic0 when applicable
suppressionReasonstring | nullAlways 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
explorerTxUrlstringBlock explorer URL for the transaction
explorerAddressUrlstringBlock explorer URL for the address

methodology

FieldTypeDescription
versionstringMethodology version of the latest returned event in this response
versionLabelstringDisplay label (e.g. "v3.2")
currentVersionstringLatest methodology version
currentVersionLabelstringDisplay label for latest methodology version
changelogPathstringRelative URL to the methodology changelog page
asOfnumberUnix timestamp of latest event used for freshness
isCurrentbooleanWhether 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.activeAddressCount, stats.activeFrozenTotal, and stats.activeAmountGapCount are legacy wire-compatible fields for Pharos' local net-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. New consumers should prefer trackedAddressCount, trackedFrozenTotal, and trackedAmountGapCount for public freeze-ledger exposure, and use the active fields only when they specifically need the current local net-frozen state. These current-balance totals are last-known successful snapshots, not a live guarantee; provider refresh failures preserve the last successful amount and should be interpreted through freshness, status, and provenance metadata when present. New snapshot rows are contract/config-scoped; older rows can still fall back to the legacy symbol/chain/address identity until remediated. 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 last-known successful blacklist_current_balances.balance_usd snapshots 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" }
  ],
  "coverage": {
    "supported": [
      {
        "symbol": "USDT",
        "stablecoinId": "usdt-tether",
        "chainId": "ethereum",
        "chainName": "Ethereum",
        "contractAddress": "0xdac17f958d2ee523a2206206994597c13d831ec7",
        "configKey": "ethereum-0xdac17f958d2ee523a2206206994597c13d831ec7",
        "providerSource": "evm-logs",
        "eventFamilies": ["USDT legacy"],
        "eventTypes": ["blacklist", "unblacklist", "destroy"]
      }
    ],
    "unsupportedDeferred": [
      { "symbol": "TUSD", "chainId": "bsc", "reason": "deferred_contract_creation_verification" }
    ],
    "counts": {
      "supportedConfigs": 71,
      "unsupportedDeferredConfigs": 10,
      "bySymbol": { "USDT": 8 },
      "byChain": { "ethereum": 35 },
      "byProviderSource": { "evm-logs": 70, "trongrid": 1 }
    }
  },
  "freezeLedgerMeta": {
    "totalRows": 9466,
    "scopedRows": 240,
    "legacyRows": 9226,
    "oldestObservedAt": 1710000000,
    "newestObservedAt": 1776729600,
    "oldestAgeSec": 66600000,
    "newestAgeSec": 1200,
    "statusDistribution": { "resolved": 9466 },
    "sourceDistribution": { "current_balance": 240, "bootstrap_kyc_rip": 9226 },
    "freshnessDistribution": { "fresh": 9450, "degraded": 10, "stale": 6 },
    "currentFreshnessDistribution": { "fresh": 240, "degraded": 0, "stale": 0 },
    "providerFailedCount": 0,
    "lastErrorClassDistribution": {},
    "sourceCategoryCounts": { "bootstrap": 9226, "current": 240, "destroy": 0, "other": 0 },
    "gaps": {
      "tracked": 0,
      "recoverable": 17,
      "unrecoverable": 0,
      "recentRecoverable": 0,
      "neverAttempted": 0,
      "repeatedFailures": 0,
      "oldestRecoverableAgeSec": null,
      "amountStatusDistribution": { "resolved": 13405, "recoverable_pending": 17 },
      "amountSourceDistribution": { "historical_balance": 9000, "event": 4405, "unavailable": 17 }
    }
  },
  "dataQuality": {
    "status": "ok",
    "warnings": [],
    "amountGaps": {
      "totalEvents": 13422,
      "recoverable": 17,
      "unrecoverable": 0,
      "recentRecoverable": 0,
      "missingRatio": 0.0013,
      "recentWindowSec": 86400
    },
    "freezeLedger": {
      "providerFailedCount": 0,
      "staleSnapshotCount": 0,
      "trackedGapCount": 0,
      "scopedRows": 240,
      "legacyRows": 9226
    },
    "coverage": { "supportedConfigs": 71, "unsupportedDeferredConfigs": 10 }
  },
  "totalEvents": 13422,
  "methodology": {
    "version": "3.993",
    "versionLabel": "v3.993",
    "currentVersion": "3.993",
    "currentVersionLabel": "v3.993",
    "changelogPath": "/methodology/blacklist-tracker-changelog/",
    "asOf": 1776729600,
    "isCurrent": true
  }
}

coverage is the machine-readable tracker coverage inventory. supported entries are contract/config-level rows; each row includes the required tracked fields symbol, stablecoinId, chainId, chainName, contractAddress, configKey, providerSource, eventFamilies, and eventTypes. unsupportedDeferred identifies known deferred or explicitly de-scoped deployments from the runtime manifest and the reason they are not live; current examples use chainId values from the same shared chain registry as event rows. freezeLedgerMeta describes the last-known snapshot ledger used by trackedFrozenTotal, including scoped-vs-legacy row counts, observed-age bounds, source/status distributions, provider failures, and amount-gap distributions. freshnessDistribution covers every historical ledger row; currentFreshnessDistribution isolates rows produced by the current-balance provider when present and is what current API versions use for dataQuality.freezeLedger.staleSnapshotCount. dataQuality.status summarizes those coverage, gap, and current-provider snapshot signals into ok, degraded, or stale; clients should display or alert on warnings rather than inferring quality from null amount fields alone.


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

ParamTypeDefaultDescription
stablecoinstringFilter by Pharos stablecoin ID
active"true"When "true", return only ongoing (unresolved) depeg events
limitinteger100Max results (1–1000)
offsetinteger0Pagination offset (0–50,000); cannot be combined with cursor
cursorstringOpaque keyset cursor from nextCursor
includeTotalbooleantrueWhen false, skips the exact COUNT(*); total becomes a page lower bound and totalExact is false
includePendingbooleanfalseWhen true, includes pending incidents awaiting confirmation in pending

Response

{
  "events": [DepegEvent, ...],
  "pending": [DepegPendingIncident, ...],
  "total": 4080,
  "totalExact": true,
  "nextCursor": "eyJ2IjoxLCJ2YWx1ZXMiOlsxNzcyNjA2NDAwLDQwODBdfQ",
  "methodology": {
    "version": "6.0",
    "versionLabel": "v6.0",
    "currentVersion": "6.0",
    "currentVersionLabel": "v6.0",
    "changelogPath": "/methodology/depeg-changelog/",
    "asOf": 1772606400,
    "isCurrent": true
  }
}

Results are ordered by startedAt DESC, id DESC. Prefer cursor/nextCursor for deep pagination; offset pagination is retained for shallow compatibility only.

Results are ordered by startedAt descending (most recent first).

DepegEvent

FieldTypeDescription
idnumberAuto-increment DB ID
stablecoinIdstringPharos stablecoin ID
symbolstringToken symbol
pegTypestringDefiLlama peg type (e.g. "peggedUSD")
direction"above" | "below"Whether the price was above or below the peg
peakDeviationBpsnumberLargest deviation observed (basis points, signed; negative = below peg, positive = above peg)
startedAtnumberUnix seconds when depeg was first detected
endedAtnumber | nullUnix seconds when price returned to peg; null if still active
startPricenumberPrice at event start (USD)
peakPricenumber | nullPrice at worst deviation
recoveryPricenumber | nullPrice at recovery
pegReferencenumberReference peg value used (USD)
source"live" | "backfill"Detection method
confirmationSourcesstring | nullComposite 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).
pendingReasonstring | nullComposite 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.
provenanceobject | nullPublic replay/audit metadata when available: sourceKind, replayRunId, replayVersion, sourcePriceProviders, quoteMode, pegReferenceSource, supplySource, confirmationPolicy, confirmationPointCount, confidenceTier, auditVerdict, pegScoreEligible, and updatedAt. Legacy rows return null.

DepegPendingIncident — returned only when includePending=true

FieldTypeDescription
stablecoinIdstringPharos stablecoin ID
symbolstringToken symbol
direction"above" | "below"Pending deviation direction
firstSeenAtnumberUnix seconds when the pending incident was first observed
lastSeenAtnumberUnix seconds when the pending incident was last refreshed
firstSeenBpsnumberFirst observed deviation in basis points
lastSeenBpsnumberMost recent observed deviation in basis points
peakSeenBpsnumberWorst observed pending deviation in basis points
reasonstringPending lane reason, e.g. "large-cap" or "large-cap+low-confidence"
ageSecnumberSeconds elapsed since firstSeenAt
expiresAtnumberUnix seconds when the pending row expires if it is not confirmed
availableConfirmationCategoriesstring[]Confirmation categories currently derivable from public metadata / D1 snapshots
missingConfirmationCategoriesstring[]Expected confirmation categories not currently derivable from public metadata / D1 snapshots

methodology

FieldTypeDescription
versionstringMethodology version attributed from the latest returned event timestamp
versionLabelstringDisplay label (e.g. "v5.94")
currentVersionstringLatest methodology version
currentVersionLabelstringDisplay label for latest methodology version
changelogPathstringRelative URL to the methodology changelog page
asOfnumberUnix timestamp used for methodology attribution
isCurrentbooleanWhether version matches currentVersion

GET /api/depeg-resolver

Cache-backed Depeg Duration Resolver readouts for active/current confirmed depeg incidents. DDRv2 emits one row per canonical incident projection, keyed by incidentKey, and separates live facts from the official public lock outcome.

Cache: standard — X-Data-Age and Warning headers included. Freshness threshold: 900 s. Missing or invalid snapshots return 200 with _meta.degraded=true and rows: []; stale snapshots mark _meta.degraded=true, include the read overlay when available, and keep pre-publication rows free of verdict/duration details.

Row states

StateMeaning
pending_lockIncident is active and younger than the 24h public lock point. Live facts and lock metadata may render; no verdict or duration is exposed.
lock_deferredThe lock point arrived, but a deterministic system-health predicate failed. Shows deferral/retry status only; no no-call, verdict, or duration is created.
publication_retry_pendingA lock outcome sealed, but first-publication manifest finalization has not succeeded. The sealed outcome stays hidden until publication.
frozenFirst-published official prediction. Shows frozen verdict/duration, lock timestamp, lock timing, anchored duration, and live overlay facts separately.
no_callHealthy lock run had insufficient row-level signal. Shows missing inputs and lock metadata, not a recovery/terminal verdict.
invalidatedAppend-only erratum invalidated the original first-published prediction or no-call; original exposure remains visible with correction history.

Response

{
  "_meta": {
    "schemaVersion": 2,
    "dataAsOf": 1779700000,
    "modelAsOf": 1779700000,
    "computedAt": 1779700000,
    "expiresAt": 1779701800,
    "snapshotToken": "ddrpub_...",
    "snapshotGeneration": 2,
    "publicPredictionIds": [101, 102],
    "publicPredictionRowHashes": { "101": "..." },
    "basePayloadHash": "...",
    "readOverlay": {
      "degradedLockDeferralIncidentKeys": [],
      "closedPendingReviewIncidentKeys": [],
      "suppressedIncidentKeys": []
    },
    "degraded": false,
    "degradedReason": null,
    "publicWarning": "Probabilistic estimate from Pharos historical data. Not investment advice or a credit rating.",
    "resolutionRubricVersion": "resolution-rubric-v1",
    "durationModelVersion": "duration-landmark-v1",
    "incidentGroupingVersion": "incident-group-v1",
    "supportRulesVersion": "support-rules-v1",
    "lineage": { "eventCount": 34129, "incidentCount": 1820, "coinCount": 142, "quarantinedCoins": 7 }
  },
  "rows": [DdrV2ResponseRow, ...],
  "methodology": { "version": "2.0", "versionLabel": "v2.0", "changelogPath": "/methodology/depeg-resolver-changelog/" }
}

DdrV2ResponseRow.kind is one of pending, prediction, no_call, or invalidated_prediction. prediction.state carries the public state above. Prediction rows include frozen.resolution and frozen.duration; no-call rows include noCall.missingReasons; invalidated rows include originalOutcome, latestErratum, and errata history. All rows include a live overlay with current event age, peak/current deviation, event state, freshness, and degraded reason.


GET /api/depeg-resolver-review

Cache-backed Depeg Duration Resolver Reviewer snapshot. DDRR v2 reviews frozen public predictions and no-calls that reached first publication, then reports the full incident-scoped policy universe so missing predictions are visible coverage debt rather than silently excluded.

Cache: standard — X-Data-Age and Warning headers included. Freshness threshold: 900 s. Missing or invalid snapshots return 200 with _meta.degraded=true, an empty summary, and rows: []; stale snapshots keep review rows but set _meta.degraded=true and degradedReason="stale-cache".

Headline fields

FieldTypeMeaning
summary.headline.recoveryLikelihoodAccuracyPctnumber | nullStrict scored DDR recovery-verdict accuracy over first-published frozen predictions.
summary.headline.meanSignedDurationErrorSecnumber | nullMean observed-minus-DDR duration error for recovered rows with a DDR median remaining-time estimate. Positive means observed recovery was slower than DDR predicted; negative means faster.
summary.headline.meanAbsoluteDurationErrorSecnumber | nullMean absolute duration miss for the same scored recovered rows.
summary.headline.durationScoredCountnumberNumber of recovered, duration-scored rows included in duration-error averages.
summary.headline.predictionRatePctnumber | nullShare of eligible finalized incidents that received a published prediction/no-call decision.
summary.headline.finalizedCoveragePctnumber | nullShare of the policy universe assigned to a finalized/public coverage state.
summary.headline.noCallRatePctnumber | nullShare of finalized lock outcomes that became no-calls.
summary.headline.invalidatedPctnumber | nullShare of first-published predictions invalidated by errata.

Response

{
  "_meta": {
    "computedAt": 1779700000,
    "expiresAt": 1779701800,
    "degraded": false,
    "degradedReason": null,
    "reviewerVersion": "ddr-reviewer-v2",
    "assessedEventCount": 12,
    "reviewedEventCount": 12,
    "pendingEventCount": 8,
    "durationScoredCount": 6,
    "verdictScoredCount": 10,
    "methodologyVersions": ["2.0"]
  },
  "summary": {
    "headlineScope": "current_policy",
    "headlineLabel": "DDR v2 public predictions",
    "headline": {
      "policyUniverseIncidentCount": 20,
      "predictionRatePct": 0.65,
      "finalizedCoveragePct": 0.9,
      "noCallRatePct": 0.1,
      "invalidatedPct": 0.05,
      "recoveryLikelihoodAccuracyPct": 0.7,
      "meanSignedDurationErrorSec": 3600,
      "meanAbsoluteDurationErrorSec": 7200,
      "horizonHitRates": [{ "horizon": "6h", "scored": 5, "hits": 3, "misses": 2, "hitRate": 0.6 }]
    },
    "byPredictionPolicy": []
  },
  "rows": [DdrrRow, ...],
  "methodology": { "version": "2.0", "versionLabel": "v2.0", "changelogPath": "/methodology/depeg-resolver-changelog/" }
}

DdrrRow.kind is one of prediction_review, no_call_review, coverage, or invalidated_prediction. Only prediction_review rows enter recovery-likelihood and duration accuracy. no_call_review rows are deliberate lock outcomes but unscored. coverage rows include states such as resolved_before_prediction, terminal_before_prediction, missed_lock_recovered, missed_lock_terminal, publication_retry_pending, and publication_failed. invalidated_prediction rows retain original exposure and attach errata history.


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": "6.0",
    "versionLabel": "v6.0",
    "currentVersion": "6.0",
    "currentVersionLabel": "v6.0",
    "changelogPath": "/methodology/depeg-changelog/",
    "asOf": 1772606400,
    "isCurrent": true
  }
}

PegSummaryCoin

FieldTypeDescription
idstringPharos stablecoin ID
symbolstringToken symbol
namestringFull name
pegTypestringDefiLlama peg type
pegCurrencystringPeg currency code (USD, EUR, GOLD, etc.)
governancestring"centralized", "centralized-dependent", "decentralized"
currentDeviationBpsnumber | nullLive 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.
depegEventCoverageLimitedbooleanPresent 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.
priceSourcestringPrimary 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" | nullConfidence tier attached to the primary price input
priceUpdatedAtnumber | nullCompatibility timestamp for the primary price; now mirrors the effective observation time rather than the cache-write time
priceObservedAtnumber | nullUnix seconds for the effective observation time attached to the selected primary price; interpret alongside priceObservedAtMode
priceObservedAtMode"upstream" | "local_fetch" | "unknown" | nullWhether priceObservedAt came from source-native freshness metadata, local fetch time, or legacy/unknown provenance
priceSyncedAtnumber | nullUnix 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
pegScorenumber | nullComposite peg score 0–100 (higher = more stable)
pegPctnumber% of tracked time within ±100 bps
severityScorenumberSeverity sub-score (0–100)
spreadPenaltynumberSpread/liquidity penalty applied to score
eventCountnumberNumber of depeg events in the 4-year window
worstDeviationBpsnumber | nullWorst single deviation seen (basis points)
activeDepegbooleanWhether a depeg event is currently open
lastEventAtnumber | nullUnix seconds of most recent depeg event
trackingSpanDaysnumberDays of history used for score computation
methodologyVersionstringMethodology version attributed to this coin snapshot
dexPriceCheckDexPriceCheck | nullOptional 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)
consensusSourcesstring[]Source names that returned a valid price for this coin. Defaults to [] when absent.
agreeSourcesstring[] | undefinedCompatibility alias for agreeing/current price sources when present

DexPriceCheck

FieldTypeDescription
dexPricenumberDEX-derived price (USD)
dexDeviationBpsnumberDEX price deviation from peg (basis points, signed)
agreesbooleanWhether primary and DEX prices are within 50 bps
sourcePoolsnumberNumber of DEX pools contributing to the price
sourceTvlnumberCombined TVL of those pools (USD)

PegSummaryStats

FieldTypeDescription
activeDepegCountnumberCoins with an open depeg event
medianDeviationBpsnumberMedian absolute deviation across rows with a live current deviation
worstCurrent{ id, symbol, bps } | nullCoin with the largest current deviation among rows with a live current deviation
coinsAtPegnumberRows with a live current deviation that are below their live depeg threshold (100 bps for USD pegs, 150 bps for non-USD pegs)
totalTrackednumberRows included in the live peg-status aggregate (currentDeviationBps !== null)
depegEventsTodaynumberNumber of depeg events whose startedAt is in the current UTC day
depegEventsYesterdaynumberNumber of depeg events whose startedAt is in the previous UTC day
fallbackPegRatesstring[](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" }
}
FieldTypeDescription
freezeActivebooleanWhether the USDS freeze module is currently enabled
implementationAddressstringAddress of the current USDS implementation contract
lastCheckednumberUnix 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

FieldTypeDescription
gradestringLetter grade: "A+", "A", "A-", "B+""F"
slugstringBluechip report slug (e.g. "usdt")
collateralizationnumberCollateralization percentage
smartContractAuditbooleanWhether an audit exists
dateOfRatingstringISO 8601 date of rating
dateLastChangestring | nullISO 8601 date of last grade change
smidgeBluechipSmidgePlain-text evaluation summaries (HTML stripped)

BluechipSmidge — each field is string | null:

FieldDescription
stabilityReserves management and stabilization mechanisms
managementPersonnel restrictions and track records
implementationSmart contract implementation assessment
decentralizationDecentralization posture
governanceGovernance and redemption terms
externalsExternal 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

FieldTypeDescription
totalTvlUsdnumberTotal DEX TVL (USD)
totalVolume24hUsdnumber24 h trading volume (USD)
totalVolume7dUsdnumber7-day trading volume (USD)
poolCountnumberNumber of liquidity pools
pairCountnumberNumber of unique trading pairs
chainCountnumberNumber of chains with active pools
protocolTvlRecord<string, number>TVL per DEX protocol (e.g. { "uniswap-v3": 100000 })
chainTvlRecord<string, number>TVL per chain (e.g. { "Ethereum": 500000 })
topPoolsDexLiquidityPool[]Top 10 retained pools sorted by 24h volume, then TVL
liquidityScorenumber | nullComposite liquidity score 0–100
concentrationHhinumber | nullHerfindahl–Hirschman Index for pool concentration (0–1; lower = more distributed), computed from the full retained pool set before top-10 truncation
depthStabilitynumber | nullPool depth stability metric
tvlChange24hnumber | null% TVL change vs. 24 h ago
tvlChange7dnumber | null% TVL change vs. 7 days ago
updatedAtnumberUnix seconds of last cron update
dexPriceUsdnumber | nullDEX-derived price (USD)
dexDeviationBpsnumber | nullDEX price deviation from peg (basis points, signed)
priceSourceCountnumber | nullNumber of pools used for DEX price (all must meet the shared $50K observation floor)
priceSourceTvlnumber | nullCombined TVL of price-source pools (USD)
priceSourcesDexPriceSource[] | nullAggregated price sources by protocol (for example one balancer or raydium entry per asset)
effectiveTvlUsdnumberTVL after applying quality multipliers
avgPoolStressnumber | nullAverage pool stress index on a 0–100 scale (0 = balanced, 100 = maximally stressed / imbalanced)
weightedBalanceRationumber | nullTVL-weighted balance ratio across pools
organicFractionnumber | nullFraction of TVL from organic (non-incentivized) pools
durabilityScorenumber | nullScore for pool maturity and reliability
coverageClass"primary" | "mixed" | "fallback" | "legacy" | "unobserved" | nullCoverage-confidence classification for the retained pool set; primary includes pure dl and pure direct_api rows. The __global__ aggregate sentinel uses null.
coverageConfidencenumberEvidence-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
hasMeasuredLiquidityEvidencebooleanWhether any retained liquidity evidence for the row includes measured pool balances
trendworthybooleanWhether this row is suitable for trend baselines (coverageConfidence >= 0.75, positive TVL, and primary/mixed coverage)
sourceMixRecord<string, { poolCount: number; tvlUsd: number }>TVL/pool-count mix across source families (dl, direct_api, cg_onchain, gecko_terminal, dexscreener, cg_tickers)
balanceMeasuredTvlUsdnumberTVL denominator actually used for weightedBalanceRatio
organicMeasuredTvlUsdnumberTVL denominator actually used for organicFraction
scoreComponentsScoreComponents | nullBreakdown of the composite liquidity score
lockedLiquidityPctnumber | nullTVL-weighted fraction of liquidity reported as locked by source pools
methodologyVersionstringMethodology version attributed to this row

ScoreComponents

FieldTypeDescription
tvlDepthnumberTVL depth sub-score
volumeActivitynumberVolume activity sub-score
poolQualitynumberPool quality sub-score
durabilitynumberDurability sub-score
pairDiversitynumberPair diversity sub-score

DexLiquidityPool

FieldTypeDescription
projectstringProtocol slug (e.g. "curve-dex", "uniswap-v3")
chainstringChain name
tvlUsdnumberPool TVL (USD)
symbolstringPool pair name (e.g. "USDC-USDT"), normalized to tracked tickers when direct-API sources only provide token addresses
volumeUsd1dnumber24 h volume (USD)
poolTypestringPool type (e.g. "curve-stableswap", "uniswap-v3-5bp")
sourcestring | undefinedCanonical source family for this retained pool
extraobject | undefinedOptional detailed pool metrics (A-factor, balance ratio, measurement flags, etc.)

extra may include:

FieldTypeDescription
amplificationCoefficientnumber | undefinedCurve amplification coefficient (A)
balanceRationumber | undefinedMeasured pool balance ratio from 0 to 1; Balancer weighted pools normalize against weights and Fluid uses official DexReservesResolver balances where deployed
feeTiernumber | undefinedNormalized fee tier in basis points
balanceDetailsArray<{ symbol: string; balancePct: number; isTracked: boolean }> | undefinedPer-token USD composition shares used for balance tooltips/detail
measurementobject | undefinedPer-pool provenance flags such as tvlMeasured, volumeMeasured, balanceMeasured, maturityMeasured, priceMeasured, synthetic, decayed, and capped

DexPriceSource

FieldTypeDescription
protocolstringDEX protocol name
chainstringChain name
pricenumberPrice from this source
tvlnumberTVL 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

ParamTypeDescription
stablecoinstringPharos stablecoin ID (required)

Optional query parameters

ParamTypeDefaultBoundsDescription
daysinteger901–365Lookback 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"
  }
]
FieldTypeDescription
tvlnumberTotal DEX TVL snapshot (USD)
volume24hnumber24 h volume at time of snapshot (USD)
scorenumber | nullLiquidity score at time of snapshot
datenumberUnix seconds
coverageClassstringSnapshot confidence class (primary, mixed, fallback, legacy, unobserved)
coverageConfidencenumberSnapshot confidence score
liquidityEvidenceClassstringSnapshot evidence class (measured, partial_measured, observed_unmeasured, unobserved)
hasMeasuredLiquidityEvidencebooleanWhether the snapshot qualifies as balance-measured liquidity evidence
trendworthybooleanWhether the snapshot is suitable for trend baselines rather than informational use
methodologyVersionstringMethodology version attributed to this snapshot

GET /api/supply-history

Per-coin circulating supply and price history. The snapshot-supply cron writes one snapshot per UTC day; it runs on the quarter-hourly trigger once the previous day's snapshot is at least 20 hours old, with an 08:00 UTC daily trigger as a safety-net fallback.

Cache: slow — public, s-maxage=3600, max-age=300

Freshness headers are emitted from the latest completed snapshot-supply run when available. Rows newer than the completed daily snapshot marker are hidden so a failed chunked write cannot expose a partial latest day.

Required query parameter

ParamTypeDescription
stablecoinstringPharos stablecoin ID (required)

Optional query parameters

ParamTypeDefaultBoundsDescription
daysinteger3651–5000Lookback window in days

Response: Array sorted by date ascending.

[
  {
    "date": 1771500000,
    "circulatingUsd": 138000000000,
    "price": 1.0001
  }
]
FieldTypeDescription
datenumberUnix seconds
circulatingUsdnumberCirculating supply in USD
pricenumber | nullPrice 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 keyed to a 24 h endpoint budget. The bootstrap { "digest": null } response carries only Cache-Control.

Response

{
  "digest": "USDC absorbed $812M of the market's $1.36B weekly inflow…",
  "editionNumber": 214,
  "riskSignal": {
    "kind": "depeg",
    "symbol": "PMUSD",
    "bps": -5284,
    "mcapUsd": 65610000,
    "severity": "critical",
    "activeCount": 7,
    "date": null
  },
  "riskTape": [{ "id": "risk-tape:depegs", "label": "Depegs", "value": "PMUSD 5284bps", "tone": "critical" }],
  "nextTriggers": [
    {
      "id": "trigger:depeg:pmusd",
      "label": "PMUSD depeg widening",
      "metric": "depeg-bps",
      "comparator": "abs-gte",
      "thresholdLabel": "5500 bps off peg",
      "thresholdValue": 5500,
      "symbol": "PMUSD",
      "rationale": "A wider deviation raises severity.",
      "detail": "If PMUSD reaches 5500 bps off peg, severity rises."
    }
  ]
}

If no digest exists yet, the endpoint returns only { "digest": null }.

FieldTypeDescription
digeststring | nullTweet-ready summary (≤ 240 characters). null if no digest has been generated yet.
digestTitlestring | nullShort headline for the digest
digestExtendedstring | nullExtended commentary for the website view
generatedAtnumberUnix seconds when this digest was generated (present only when digest is non-null)
editionNumbernumber | nullSequential daily digest number (present only when digest is non-null)
riskSignalDigestRiskSignal | nullCompact active-depeg risk summary parsed from stored digest input data
changeSummaryDigestChangeSummary | nullDeterministic "what changed since yesterday" summary parsed from stored digest input data
nextTriggersDigestNextTrigger[] | nullStructured forward-looking threshold checks for tomorrow's digest
forwardLookOutcomesDigestForwardLookOutcome[] | nullEvaluation of the previous digest's next triggers against the latest input
riskTapeDigestRiskTapeItem[] | nullCompact reader-facing risk state chips parsed from stored digest input data

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,
      "riskSignal": {
        "kind": "depeg",
        "symbol": "PMUSD",
        "bps": -5284,
        "mcapUsd": 65610000,
        "severity": "critical",
        "activeCount": 7,
        "date": "2026-05-05"
      },
      "riskTape": [{ "id": "risk-tape:depegs", "label": "Depegs", "value": "PMUSD 5284bps", "tone": "critical" }],
      "nextTriggers": [
        {
          "id": "trigger:depeg:pmusd",
          "label": "PMUSD depeg widening",
          "metric": "depeg-bps",
          "comparator": "abs-gte",
          "thresholdLabel": "5500 bps off peg",
          "thresholdValue": 5500,
          "symbol": "PMUSD",
          "rationale": "A wider deviation raises severity.",
          "detail": "If PMUSD reaches 5500 bps off peg, severity rises."
        }
      ],
      "digestType": "daily",
      "editionNumber": 214
    }
  ]
}

Each element uses digestText (note: differs from the singular /api/daily-digest which uses digest).

FieldTypeDescription
digestTextstringTweet-ready summary
digestTitlestring | nullShort headline
digestExtendedstring | nullExtended commentary
generatedAtnumberUnix seconds of generation time
psiScorenumber | nullPSI score parsed from archived digest input data
psiBandstring | nullPSI condition band parsed from archived digest input data
totalMcapUsdnumber | nullEcosystem market cap parsed from archived digest input data
riskSignalDigestRiskSignal | nullCompact active-depeg risk summary parsed from archived digest input data
nextTriggersDigestNextTrigger[] | nullStructured forward-looking threshold checks parsed from archived digest input data
forwardLookOutcomesDigestForwardLookOutcome[] | nullEvaluation of the previous digest's next triggers parsed from archived digest input data
riskTapeDigestRiskTapeItem[] | nullCompact risk-state chips parsed from archived digest input data
digestType"daily" | "weekly"Digest cadence for this archived entry
editionNumbernumberSequential edition number within that digest cadence

DigestRiskSignal

FieldTypeDescription
kind"depeg"Risk signal family; currently active-depeg context
symbolstringStablecoin symbol
bpsnumberSigned basis-point deviation where available; archive badges display absolute value
mcapUsdnumber | nullMarket cap associated with the stored digest signal
severity"critical" | "watch""critical" at ≥2,500 bps and ≥$50M mcap, or ≥5,000 bps and ≥$10M mcap
activeCountnumberActive depeg count from the stored digest input, when available
datestring | nullDaily input date for weekly archive entries; null for latest daily responses

Digest intelligence fields

FieldTypeDescription
DigestRiskTapeItem.idstringStable identifier for the displayed tape item
DigestRiskTapeItem.labelstringShort label such as PSI, Depegs, Gauge, DEWS, or Supply
DigestRiskTapeItem.valuestringAlready formatted compact value for display
DigestRiskTapeItem.tone"critical" | "warning" | "neutral" | "positive"Presentation severity
DigestRiskTapeItem.detailstringOptional supporting detail
DigestNextTrigger.metric"depeg-bps" | "supply-1d-usd" | "supply-7d-usd" | "bank-run-gauge" | "dews-band" | "psi-score"Metric that the next daily input can evaluate
DigestNextTrigger.comparator"abs-gte" | "gte" | "lte" | "band-gte"Comparison operator for thresholdValue
DigestNextTrigger.thresholdLabelstringDisplay string for the threshold
DigestForwardLookOutcome.status"hit" | "missed" | "pending"Whether the prior trigger fired, failed, or still needs more data
DigestChangeSummary.*SignalsarrayBuckets of signal changes, each with id, label, kind, symbols, and detail

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

ParamTypeDescription
datestringDate 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", ... }]
}
FieldTypeDescription
datestringThe requested date
inputDataobject | nullDigest input data (mcap, depegs, supply changes, PSI, digest intelligence fields) for this date
prevInputDataobject | nullPrevious day's input data for delta computation
depegEventsarrayUp to 20 depeg events active on that date, ordered by severity
blacklistEventsarrayUp to 50 blacklist events on that date

Error responses: 400 for missing/invalid date, 404 if no digest exists for that date.


GET /api/snapshots/index

Lists immutable public daily dataset snapshots written by the snapshot-public-dataset cron. Each row points to a dated payload that can be fetched through GET /api/snapshots/:date.json.

Cache: archive

Response

{
  "snapshots": [
    {
      "snapshotDate": "2026-05-17",
      "methodologyVersions": { "safetyScore": "5.9", "psi": "3.0" },
      "contentHash": "sha256-hex",
      "byteSize": 1234567,
      "createdAt": 1778976000
    }
  ]
}
FieldTypeDescription
snapshotsarraySnapshot index sorted newest first
snapshots[].snapshotDatestringUTC snapshot date in YYYY-MM-DD format
snapshots[].methodologyVersionsRecord<string, string>?Methodology versions embedded in the snapshot
snapshots[].contentHashstringSnapshot payload hash used by dated ETags
snapshots[].byteSizenumberUncompressed JSON payload size in bytes
snapshots[].createdAtnumberSnapshot creation timestamp in Unix seconds

GET /api/snapshots/:date.json

Returns the full immutable public dataset snapshot for a UTC date. The worker reads the gzipped payload from D1, decompresses it, and returns the original JSON envelope.

Cache: immutable-snapshot

Path parameters

ParamTypeDescription
datestringUTC snapshot date in YYYY-MM-DD form

Response

PublicSnapshotEnvelope {
  snapshotDate,
  generatedAt,
  methodologyVersions,
  stablecoinRows,
  fxFallbackRates,
  reportCards,
  psi,
  dewsRows,
  liquidityRows
}

Headers: ETag: "<contentHash>".

Error responses: 400 for invalid date format, 404 if no snapshot exists for that date, 500 if the stored snapshot payload is unreadable or corrupted.


GET /api/snapshot/:date/stablecoin/:id

Returns a per-stablecoin projection from a dated public dataset snapshot. The projection includes the stablecoin row plus the matching report-card, DEWS, and liquidity rows when those datasets were present in the snapshot.

Cache: immutable-snapshot

Path parameters

ParamTypeDescription
datestringUTC snapshot date in YYYY-MM-DD form
idstringCanonical Pharos stablecoin ID

Response

{
  snapshotDate: "2026-05-17",
  stablecoinId: "usdt-tether",
  generatedAt: 1778976000,
  methodologyVersions: { safetyScore: "5.9", psi: "3.0" },
  stablecoin: { id: "usdt-tether", symbol: "USDT" },
  scores: {
    reportCard,
    psi,
    dews,
    liquidity
  }
}
FieldTypeDescription
snapshotDatestringServed snapshot date
stablecoinIdstringRequested stablecoin ID
generatedAtnumberSnapshot generation timestamp
methodologyVersionsobject?Methodology versions embedded in the snapshot
stablecoinobjectStablecoin row from the dated public dataset
scores.reportCardobject?Matching report-card score, or null
scores.psiobject?Snapshot-level PSI object, or null
scores.dewsobject?Matching DEWS stress-signal row, or null
scores.liquidityobject?Matching DEX-liquidity row, or null

Headers: ETag: "<contentHash>-<stablecoinId>".

Error responses: 400 for invalid date format, 404 if no snapshot exists or the stablecoin is absent from that snapshot, 500 if the stored snapshot payload is unreadable or corrupted.


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": "ok",
    "safetyAlertSourceState": "ok",
    "safetyAlertSourceAgeSeconds": 120,
    "safetyAlertsSuppressed": false,
    "safetyAlertSourceGeneration": "safety-7.12-alert-source-v1"
  }
}
FieldTypeDescription
statusstring"healthy" / "degraded" / "stale"
timestampnumberUnix seconds at time of response
warningsstring[]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.
cachesRecord<string, CacheStatus>Per-cache freshness status
caches["fx-rates"]CacheStatusFX cache freshness plus source-cadence diagnostics (mode, sourceUpdatedAt, sourceAgeSeconds, sourceStatus, warning, consecutiveFallbackRuns)
blacklist.totalEventsnumberTotal events in blacklist table
blacklist.missingAmountsnumberEvents where amount is null (should be 0)
blacklist.recentMissingAmountsnumberMissing-amount events inside the recent monitoring window used by status logic
blacklist.recentWindowSecnumberSize of the recent monitoring window in seconds
blacklist.missingRationumbermissingAmounts / totalEvents (0 when no blacklist rows exist yet)
telegramSummaryTelegramHealthSummary | nullLightweight Telegram delivery health summary. null when the Telegram tables are unavailable or not yet migrated
telegramSummary.totalChatsnumberTotal subscribed Telegram chats currently stored
telegramSummary.pendingDeliveriesnumberPending overflow alert deliveries waiting in telegram_pending_alerts
telegramSummary.lastDispatchAtnumber | nullUnix seconds of the most recent dispatch-telegram-alerts cron run, if available
telegramSummary.lastDispatchStatusstring | nullStatus of the most recent dispatch-telegram-alerts cron run, if available
telegramSummary.safetyAlertSourceState"ok" | "missing" | "corrupt" | "stale" | "wrong-generation" | nullLive safety-alert source-cache state from the most recent Telegram dispatch run
telegramSummary.safetyAlertSourceAgeSecondsnumber | nullAge of the current live safety-alert source snapshot when available
telegramSummary.safetyAlertsSuppressedbooleantrue when safety alerts are paused because the live source snapshot is missing, corrupt, stale, or from the wrong generation
telegramSummary.safetyAlertSourceGenerationstring | nullGeneration marker of the current live safety-alert source snapshot
mintBurn.totalEventsnumber | nullLegacy 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.latestEventTsnumber | nullLegacy 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.latestHourlyTsnumber | nullLegacy advisory timestamp. null on the budget-capped health path because /api/health no longer scans mint_burn_hourly.
mintBurn.freshnessAgeSecnumber | nullLegacy advisory age. null on the budget-capped health path; derive critical-lane age from mintBurn.sync.lastSuccessfulSyncAt.
mintBurn.majorStaleCountnumberLegacy advisory count. Always 0 on the budget-capped health path because per-symbol stale checks are intentionally not scanned from D1.
mintBurn.staleMajorSymbolsstring[]Legacy advisory list. Always empty on the budget-capped health path because per-symbol stale checks are intentionally not scanned from D1.
mintBurn.syncobjectCritical-lane sync freshness summary used for public health evaluation
mintBurn.sync.lastSuccessfulSyncAtnumber | nullUnix 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.warningstring | nullHuman-readable warning when the critical lane is stale, degraded, or errored
mintBurn.sync.criticalLaneHealthybooleantrue when the latest critical-lane run is ok, degraded, or skipped_locked
circuitsRecord<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

FieldTypeDescription
ageSecondsnumber | nullSeconds since last cron update; null if never populated
maxAgenumberAvailability budget in seconds for this cache key; same value as availabilityMaxAge for current workers
healthybooleantrue when ageSeconds / maxAge ≤ 12.0; status-page bands use >8.0x for degraded and >12.0x for stale
producerJobstring | null | undefinedCron job that produces the cache freshness signal
producerIntervalSecnumber | null | undefinedExpected producer cadence in seconds
endpointMaxAgenumber | null | undefinedEndpoint freshness basis used by _meta, X-Data-Age, and generic freshness warning runway when available
availabilityMaxAgenumber | null | undefinedAvailability budget used by /api/health, /api/status, and status-page cache ratios
endpointBudgetReasonstring | null | undefinedShort explanation when endpoint freshness differs from producer cadence or availability budget
availabilityBudgetReasonstring | null | undefinedShort explanation for the availability budget
freshnessSource"freshness-sentinel" | "table-fallback" | "cron-fallback" | undefinedSource used to derive freshness for sentinel-backed cache lanes
sentinelValidationReasonstring | null | undefinedPresent when a malformed, stale, wrong-source, future-dated, or non-ok freshness sentinel was ignored
mode"live" | "cached-fallback" | undefinedFX cache only: whether the latest usable sync came from a live fetch or cached fallback
sourceUpdatedAtnumber | null | undefinedFX cache only: Unix seconds for the source currently driving sourceStatus
sourceAgeSecondsnumber | null | undefinedFX cache only: age of the source currently driving sourceStatus
sourceStatus"fresh" | "degraded" | "stale" | "none"FX cache only: cadence-aware source freshness status
warningstring | null | undefinedHuman-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)
consecutiveFallbackRunsnumber | undefinedFX 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 compute freshness from producer-owned cache sentinels first (freshness:dex-liquidity, freshness:yield-data, freshness:dews). A sentinel is trusted only when its JSON payload has updatedAt, the expected producer source, and publishStatus: "ok"; optional rowsWritten and coverageRatio fields may also be present. If the sentinel is missing or fails validation, the worker falls back to the legacy table query. If that freshness diagnostic also fails, it can fall back again to the latest successful producer cron timestamp. Invalid sentinels surface freshnessSource, sentinelValidationReason, and a cache warning instead of making the sentinel row authoritative.

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 cleanly
  • degraded — 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 open
  • stale — 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

ParamTypeDefaultDescription
window"24h" | "7d" | "30d""30d"Transition time window applied server-side before rows are returned
limitinteger50Max raw status transitions loaded within the time window before public-impact filtering (1–200); returned public transitions may be fewer

Response

{
  "timestamp": 1771856453,
  "currentStatus": "healthy",
  "lastChangedAt": 1771770000,
  "transitions": [
    {
      "id": 418,
      "from": "degraded",
      "to": "healthy",
      "transitionType": "recover",
      "reason": "raw-healthy-recovery-threshold",
      "at": 1771770000
    }
  ]
}
FieldTypeDescription
timestampnumberUnix seconds at time of response
currentStatus"healthy" | "degraded" | "stale"Current public status, sourced from assessPublicHealth (matches the status field from GET /api/health)
lastChangedAtnumber | nullUnix seconds for the latest admin status-machine change, if known
transitionsPublicStatusTransition[]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, cache_warning, fx_source_*, fx_cached_fallback, mint_burn_public_*, mint_burn_health_query_failed, 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 PharosWatchBot page. The canonical page route is /pharoswatchbot/; the legacy /telegram alias redirects there. Returns aggregate watcher/subscription counts, explicit vs preset-implied alert follows, the most subscribed coin symbols, and snapshot-backed watcher history when available. When active chats predate the first daily snapshot row, the history is prefixed with live active-chat cohort points so the public lifecycle chart shows the full available bot lifecycle. The common path serves the five-minute telegram:pulse:snapshot cache written by the Telegram cron sidecar; live aggregation is a stale/missing snapshot fallback.

Direct https://api.pharos.watch/api/telegram-pulse access is protected and requires X-API-Key. Public browser access on pharos.watch and ops.pharos.watch uses same-origin /_site-data/telegram-pulse, which proxies to the internal site-api lane with X-Pharos-Site-Proxy-Secret.

Cache: public, max-age=300, s-maxage=300

Response

{
  "activeWatchers": 1842,
  "coinSubscriptions": 5621,
  "explicitCoinSubscriptions": 5000,
  "presetImpliedCoinSubscriptions": 621,
  "activePresetFollowers": 81,
  "newWatchersToday": 12,
  "churnedWatchersToday": 3,
  "reactivatedWatchersToday": 5,
  "historySource": "snapshot",
  "pendingDeliveries": 3,
  "quietHoursEnabledChats": 42,
  "miniAppSessionsToday": 88,
  "miniAppMutationsToday": 31,
  "miniAppDeniedToday": 2,
  "miniAppReplayClaimsToday": 1,
  "miniAppOpenToFirstMutationP50Sec": null,
  "alertTypeChats": {
    "dews": 1701,
    "depeg": 1644,
    "safety": 1512,
    "launch": 1208,
    "allTypes": 1191
  },
  "currentSnapshotAt": 1771856400,
  "lifecycleHistoryUpdatedAt": 1775088900,
  "lifecycleHistoryEverySeconds": 900,
  "quality": {
    "status": "complete",
    "unavailableFields": []
  },
  "privacy": {
    "exactActiveWatchers": true,
    "lowCardinalityThreshold": 5,
    "suppressedFields": []
  },
  "updatedAt": 1771856400,
  "updatedEverySeconds": 300,
  "topCoins": ["USDT", "USDC", "USDe"],
  "watcherHistory": [
    {
      "date": "2026-04-01",
      "timestamp": 1775001600000,
      "snapshotAt": 1775002500,
      "newWatchers": 12,
      "activeWatchers": 12
    },
    {
      "date": "2026-04-02",
      "timestamp": 1775088000000,
      "newWatchers": 9,
      "activeWatchers": 21,
      "churnedWatchers": 1,
      "reactivatedWatchers": 2
    }
  ]
}
FieldTypeDescription
activeWatchersnumberSubscribers with at least one active global, explicit coin, or preset alert follow
coinSubscriptionsnumberTotal active alert follows, including explicit coin follows plus preset-implied follows
explicitCoinSubscriptionsnumberActive explicit per-coin subscription rows
presetImpliedCoinSubscriptionsnumberDynamic preset follower count multiplied by each preset's currently resolved coin set
activePresetFollowersnumberChats with at least one active preset follow
newWatchersTodaynumber | nullActive watchers created in the current UTC day snapshot; null when suppressed by low-cardinality privacy filtering
churnedWatchersTodaynumber | nullSnapshot-estimated active watcher churn for the current UTC day; null when suppressed by low-cardinality privacy filtering
reactivatedWatchersTodaynumber | nullSnapshot-estimated active watcher reactivation for the current UTC day; null when suppressed by low-cardinality privacy filtering
historySource"snapshot" | "live-fallback"snapshot when the response is only fixed telegram_watcher_lifecycle_daily rows, or when no live fallback rows are available; live-fallback when subscriber-created-at aggregation supplies older active-chat cohort points ahead of the fixed snapshot rows
pendingDeliveriesnumber | nullCount of queued Telegram alert deliveries; null when unavailable or suppressed by low-cardinality privacy filtering
quietHoursEnabledChatsnumber | nullAggregate count of chats with quiet hours enabled; null when suppressed by low-cardinality privacy filtering
miniAppSessionsTodaynumber | nullValid Mini App session launches today; null when unavailable or suppressed by low-cardinality privacy filtering
miniAppMutationsTodaynumber | nullSuccessful Mini App mutations today; null when unavailable or suppressed by low-cardinality privacy filtering
miniAppDeniedTodaynumber | nullMini App denial count for abuse/health monitoring. This field is not suppressed by the low-cardinality privacy rule because it is an operational counter, not an adoption signal.
miniAppReplayClaimsTodaynumber | nullMini App replay-protection claim count for abuse/health monitoring. This field is not suppressed by the low-cardinality privacy rule because it is an operational counter, not an adoption signal.
miniAppOpenToFirstMutationP50Secnumber | nullReserved Mini App session-to-first-mutation latency metric; currently null until bucketed latency is wired through telegram_usage_daily
alertTypeChatsobjectAggregate chat counts with DEWS, depeg, safety, launch, and all-four alert coverage
currentSnapshotAtnumberUnix seconds when the current aggregate pulse snapshot was measured
lifecycleHistoryUpdatedAtnumber | nullUnix seconds of the latest daily lifecycle snapshot when any snapshot row exists; can be non-null while historySource="live-fallback" when subscriber-created-at aggregation yields older chart points; null when no snapshot history exists
lifecycleHistoryEverySecondsnumberExpected lifecycle-history snapshot cadence, currently 900 seconds
qualityobjectPublic telemetry quality marker. partial means one or more non-critical fields were unavailable; raw errors are omitted from public pulse responses.
privacyobjectPublic privacy stance and suppressed field list. Exact active watcher totals are public; nonzero supporting metrics below lowCardinalityThreshold are suppressed.
updatedAtnumberUnix seconds when the pulse payload was produced
updatedEverySecondsnumberCache cadence for consumers that display freshness
topCoinsstring[]Up to five most subscribed coin tickers, ordered by subscription count
watcherHistoryarrayUTC day buckets. Snapshot-backed points preserve historical active counts and include snapshotAt; daily delta fields can be null when suppressed. During bootstrap, fallback prefix points use current active watcher created-at aggregation and cumulative active watchers so the public chart keeps all available historical points before fixed snapshots take over.

Low-cardinality privacy rule: nonzero values below privacy.lowCardinalityThreshold are hidden for public daily deltas, pending deliveries, quiet-hours chats, Mini App session/mutation adoption counts, and lifecycle-history delta fields. Consumers should treat null as "not publicly shown", not as zero. Mini App denied/replay counters are an explicit exception because they are abuse/health counters; they remain visible when available and are not listed in privacy.suppressedFields.


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 after at least one PSI sample/history row exists. Before bootstrap, the empty response carries Cache-Control only and contains { current: null, history: [], methodology: ... } without malformedRows.

Error responses: 503 when the canonical current PSI components or input_snapshot payload is missing or malformed.

Optional query parameters

ParamTypeDefaultDescription
detail"true"When "true", returns full history with persisted 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
  }
}
FieldTypeDescription
currentobject | nullLatest PSI score and components. null if cron has not yet run
current.scorenumberPSI score 0–100
current.bandstringCondition band: "BEDROCK", "STEADY", "TREMOR", "FRACTURE", "CRISIS", "MELTDOWN"
current.avg24hnumber | undefinedRolling 24 h average PSI score
current.avg24hBandstring | undefinedCondition band for avg24h
current.componentsobjectComponent breakdown: severity, breadth, stressBreadth, trend
current.contributorsarrayTop per-coin contributors from input_snapshot.contributors (empty when unavailable)
current.inputDegradationobject | undefinedDependency-loss metadata carried by the served sample when the stored input snapshot recorded degraded upstream inputs
current.totalMcapUsdnumberTotal ecosystem market cap from the latest input snapshot (0 when unavailable)
current.computedAtnumberUnix seconds of computation
current.methodologyVersionstringMethodology version used to compute the current score
historyarrayHistorical scores, newest first. With detail=true, persisted rows include components; the synthesized current-day running-average point may omit them
malformedRowsnumberCount of historical rows dropped from detail=true because persisted components JSON was malformed
history[].methodologyVersionstringMethodology version used for that history point
methodologyobjectVersion metadata for current PSI methodology context
methodology.versionstringMethodology version used by current score
methodology.changelogPathstringRelative path to full methodology changelog

GET /api/og/* / HEAD /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

  • 404 with text/plain for unknown coin IDs inside /api/og/stablecoin/:id; unknown OG route patterns return the standard JSON { "error": "Unknown OG route" }
  • 503 when required cached data is not yet available
  • 400 for malformed URI encoding in /api/og/stablecoin/:id
  • 500 with text/plain body 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": "usdc-circle", "to": "usde-ethena", "weight": 0.9, "type": "collateral" }, ...]
  },
  "methodology": {
    "version": "7.13",
    "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 }
  },
  "collateralDriftCoins": [{ "id": "jupusd-jupiter", "liveScore": 80, "curatedScore": 65, "delta": 15 }],
  "liveToFallbackCoins": ["usdaf-asymmetry"],
  "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.

When present, collateralDriftCoins lists live-reserve scoring deltas that exceed the reserve-drift threshold, and liveToFallbackCoins lists live-reserve-enabled assets whose report card used curated fallback reserves because no scoring-eligible live snapshot was available.

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 normally serves the full report-card payload from the private report-cards:snapshot cache envelope published by publish-report-card-cache. That envelope pins the expected cache generation and Safety Score methodology version; compute-on-read is used when the published snapshot is missing, malformed, generation-mismatched, or methodology-mismatched. The published envelope is also the preferred Safety Score source for yield hydration, while the smaller report_card_cache score map remains available for lightweight Chain Health/OG consumers.

Report-card generation treats the stablecoins cache and readable redemption-backstop table as hard dependencies. The stablecoins cache is read in published-contract mode, so malformed cached objects that fail StablecoinListResponseSchema validation fail closed instead of being partially filtered for scoring. 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, generation continues with 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

FieldTypeDescription
idstringCanonical Pharos stablecoin ID for live cards; stable cemetery ID for defunct cards
namestringFull name
symbolstringTicker
overallGradestringLetter grade: "A+" through "F", or "NR"
overallScorenumber | nullWeighted score 0–100. null for unrated coins
baseScorenumber | nullPre-peg-multiplier/no-liquidity/active-depeg-cap score after base dimension blending
overallCappedbooleantrue when the card is capped at a tracked parent stablecoin's overall score
uncappedOverallScorenumber | nullPost-dimension/post-peg score before the tracked-parent overall cap, when applicable
dimensionsRecord<DimensionKey, DimensionScore>Per-dimension grade, score, and detail text
ratedDimensionsnumberNumber of dimensions with data (max 5)
rawInputsRawDimensionInputsRaw scoring inputs for client-side grade recomputation (stress testing)
isDefunctbooleantrue 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. When total dependency weight is ≤ 1.0, the remainder represents non-stablecoin collateral; when declared dependency weight exceeds 1.0, dependency scoring normalizes by raw total and uses no self-backed remainder.

RawDimensionInputs

FieldType
pegScorenumber | null
activeDepegboolean
activeDepegBpsnumber | null
depegEventCountnumber
lastEventAtnumber | null
liquidityScorenumber | null
effectiveExitScorenumber | null
redemptionBackstopScorenumber | null
redemptionRouteFamilyRedemptionRouteFamily | null
redemptionModelConfidence"high" | "medium" | "low" | null
redemptionUsedForLiquidityboolean
redemptionImmediateCapacityUsdnumber | null
redemptionImmediateCapacityRationumber | null
concentrationHhinumber | null
bluechipGradeBluechipGrade | null
canBeBlacklistedboolean | "possible" | "inherited"
chainTierChainTier
deploymentModelDeploymentModel
collateralQualityCollateralQuality
custodyModelCustodyModel
governanceTierGovernanceType
governanceQualityGovernanceQuality
dependenciesDependencyWeight[]
variantParentIdstring | null
variantKind"savings-passthrough" | "strategy-vault" | "risk-absorption" | "bond-maturity" | null
navTokenboolean
collateralFromLiveboolean

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. Product labels map the wire values to the four-status model: true -> Yes, "inherited" -> Upstream, "possible" -> Possible, and false -> No. Admin mint authority is reviewed separately in Mint Authority and is not a FreezeWatch status. "inherited" / Upstream can be produced by any reserve, backing, custody, parent-asset, or CEX/custody-rail exposure; it does not require a majority reserve weight.

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 valid completed run when one exists, which prevents a partially written hourly sync from being treated as a fresh complete dataset. If the newest completed manifest is incomplete or its rows are unreadable, the reader tries recent earlier completed runs before returning 503. If no completed manifest exists but the manifest table has run records and the current table contains rows with a non-null snapshot_run_id, the reader returns 503 instead of treating those partial manifested rows as legacy data. Legacy rows without a completed run remain readable during bootstrap and migration fallback only when the current table has no manifested rows.

Response

{
  "coins": {
    "cusd-cap": {
      "stablecoinId": "cusd-cap",
      "score": 88,
      "effectiveExitScore": 56,
      "dexLiquidityScore": 29,
      "accessScore": 100,
      "settlementScore": 100,
      "executionCertaintyScore": 80,
      "capacityScore": 100,
      "outputAssetQualityScore": 80,
      "costScore": 40,
      "routeFamily": "basket-redeem",
      "accessModel": "permissionless-onchain",
      "settlementModel": "atomic",
      "executionModel": "deterministic-basket",
      "outputAssetType": "stable-basket",
      "provider": "supply-full-model",
      "immediateCapacityUsd": null,
      "immediateCapacityRatio": null,
      "sourceMode": "estimated",
      "resolutionState": "resolved",
      "routeStatus": "open",
      "routeStatusSource": "static-config",
      "holderEligibility": "any-holder",
      "capacityConfidence": "heuristic",
      "capacitySemantics": "eventual-only",
      "capacityKind": "documented-eventual",
      "freshnessKind": "reviewed-static",
      "sourceTimestamp": 1773350300,
      "sourceUrls": ["https://example.com/redemption-source"],
      "settlementDelaySec": 86400,
      "queueDepthUsd": 12000000,
      "dailyLimitUsd": 5000000,
      "minRedeemUsd": 100000,
      "liveHolderEligibility": "any-holder",
      "feeConfidence": "undisclosed-reviewed",
      "feeModelKind": "undisclosed-reviewed",
      "modelConfidence": "low",
      "feeBps": null,
      "queueEnabled": false,
      "updatedAt": 1773350400,
      "methodologyVersion": "4.06"
    }
  },
  "methodology": {
    "version": "4.06",
    "versionLabel": "v4.06",
    "currentVersion": "4.06",
    "currentVersionLabel": "v4.06",
    "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,
      "modeledExitSize": {
        "supplyRatio": 0.05,
        "floorUsd": 100000,
        "capUsd": 25000000
      },
      "capacityFactor": {
        "formula": "min(1, currentExecutableCapacityUsd / modeledExitSizeUsd)",
        "missingCapacityBehavior": "unbounded"
      },
      "confidenceFactors": {
        "high": 1,
        "medium": 0.75,
        "low": 0.35
      },
      "diversificationPolicy": "Only independent issuer rails receive the secondary-path diversification bonus in v4 snapshots."
    },
    "routeFamilyCaps": {
      "queueRedeem": 70,
      "offchainIssuer": 65
    }
  },
  "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. In v4, redemption contribution is adjusted by capacityFactor = min(1, currentExecutableCapacityUsd / modeledExitSizeUsd) and by route confidence (high = 1, medium = 0.75, low = 0.35) before the best-path blend. modeledExitSizeUsd is min(max(supplyUsd × 0.05, 100000), 25000000). When both DEX liquidity and adjusted redemption exist, only independent issuer rails receive the 10% secondary-path diversification bonus. 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 telemetry
  • estimated = modelled from current supply and conservative route assumptions
  • static = route remains configured, but current runtime inputs did not resolve a usable score

resolutionState:

  • resolved = the route produced a usable score
  • missing-cache = the stablecoins snapshot did not include the asset or its current supply
  • missing-capacity = the route is configured, but the snapshot could not resolve enough capacity to score it
  • failed = a route-specific resolver failed
  • impaired = 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.

For v4-compatible snapshots, route-status and capacity telemetry remain part of the four-hour sync-redemption-backstops snapshot. The worker does not fetch a separate real-time route-status feed during this sync; route status comes from live-reserve adapter metadata, static reviewed policy, and market-implied severe-depeg overlays.

Top-level fields:

FieldTypeDescription
coinsRecord<string, RedemptionBackstopEntry>Current snapshot keyed by Pharos stablecoin ID
methodologyobjectVersion metadata plus component weights, effective-exit blend weights, and route-family caps
updatedAtnumberFreshest updated_at timestamp for the served completed run, or freshest current row for legacy snapshots

RedemptionBackstopEntry highlights:

FieldTypeDescription
scorenumber | nullDirect redemption-quality score after route-family/config caps
effectiveExitScorenumber | nullBlended exit score used by report cards
dexLiquidityScorenumber | nullDEX liquidity input used in the blend
routeFamilystringstablecoin-redeem, basket-redeem, collateral-redeem, psm-swap, queue-redeem, or offchain-issuer
accessModelstringpermissionless-onchain, whitelisted-onchain, issuer-api, or manual
settlementModelstringatomic, immediate, same-day, days, or queued
outputAssetTypestringstable-single, stable-basket, bluechip-collateral, mixed-collateral, or nav
sourceModestringdynamic, estimated, or static capacity provenance
resolutionStatestringresolved, missing-cache, missing-capacity, failed, or impaired
routeStatusstringCurrent route availability: open, degraded, paused, cohort-limited, or unknown
routeStatusSourcestringSource for current route availability: static-config, market-implied, operator-notice, protocol-api, or onchain
routeStatusReasonstring | undefinedHuman-readable explanation when current availability impairs scoring
routeStatusReviewedAtstring | undefinedUTC date (YYYY-MM-DD) for the current route-status assessment
holderEligibilitystringModeled holder cohort: any-holder, verified-customer, whitelisted-primary, pre-incident-holder, issuer-discretionary, or unknown
capacityConfidencestringlive-direct, live-proxy, documented-bound, heuristic, or legacy dynamic fidelity tag for the capacity model
capacityBasisstring | undefinedTyped 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
capacitySemanticsstringimmediate-bounded or eventual-only, distinguishing current redeemable buffer from eventual redeemability
capacityProfileobject | undefinedOptional v4 capacity profile separating immediate, daily, queued, eventual, and scoring capacity with a scoringHorizon and capacityProfileConfidence
capacityKindstring | undefinedOptional adapter-declared live evidence shape, such as live-direct-bounded, live-queue, live-proxy-validated, documented-bound, documented-eventual, or heuristic. Context only; not Safety eligibility by itself
freshnessKindstring | undefinedOptional adapter-declared redemption freshness evidence, such as verified-source-timestamp, same-run-onchain, same-run-api, reviewed-static, or unverified
confidenceDetailsobject | undefinedOptional v4 confidence rollup dimensions for capacity evidence, fee evidence, route-status freshness, holder-cohort breadth, and source quality
sourceTimestampnumber | undefinedOptional source timestamp emitted by a live reserve adapter for the redemption telemetry
sourceUrlsstring[] | undefinedOptional source URLs emitted by a live reserve adapter for the redemption telemetry
settlementDelaySecnumber | undefinedOptional adapter-emitted settlement delay constraint in seconds
queueDepthUsdnumber | undefinedOptional adapter-emitted queued redemption depth in USD
dailyLimitUsdnumber | undefinedOptional adapter-emitted daily redemption limit in USD
minRedeemUsdnumber | undefinedOptional adapter-emitted minimum redemption size in USD
liveHolderEligibilitystring | undefinedOptional adapter-emitted holder eligibility context when it differs from or sharpens the static model
eventualRedeemabilityScorenumber | null | undefinedOptional v4 long-tail legal/protocol redeemability score, separate from current executable exit capacity
feeConfidencestringfixed, formula, or undisclosed-reviewed fidelity tag for the fee model
feeModelKindstringfixed-bps, formula, documented-variable, or undisclosed-reviewed
modelConfidencestringOverall route-fidelity rollup: high, medium, or low
immediateCapacityUsdnumber | nullImmediate redeemable capacity in USD. null when the model is eventual-only or currently unrated
immediateCapacityRationumber | nullImmediate redeemable capacity as a share of supply. null when not separately quantified
feeBpsnumber | nullExplicit bounded fee when configured
feeDescriptionstring | undefinedDocs-backed fee description for variable, conditional, flat-minimum, or undisclosed redemption schedules
costScenarioScoresobject | undefinedOptional v4 cost scores for retail, active-user, and institutional route-size scenarios
routeExitCorrelationstring | undefinedOptional v4 correlation tag for DEX-vs-redemption independence, such as independent-issuer-rail, same-stablecoin-pool-backing, same-protocol-liquidity, wrapper-to-parent-dependency, or unknown
queueEnabledbooleanWhether the modeled route is explicitly queued/serial
docs{ label?: string, url?: string, reviewedAt?: string, provenance?: string, sources?: { label: string, url: string, supports?: string[] }[] } | undefinedOptional documentation / transparency metadata. reviewedAt is the route-review date, while provenance is config-reviewed, live-reserve-display, proof-of-reserves, or preferred-link
notesstring[] | undefinedRuntime notes such as stale reserve metadata fallback
capsAppliedstring[] | undefinedApplied 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

ParamTypeDescription
stablecoinstringPharos stablecoin ID (required)

Optional query parameters

ParamTypeDefaultBoundsDescription
daysinteger3651–3650Lookback window in days

Response: Array sorted by date ascending.

[
  {
    "date": 1771977600,
    "grade": "B+",
    "score": 78,
    "prevGrade": "B",
    "prevScore": 74,
    "methodologyVersion": "5.5"
  }
]
FieldTypeDescription
datenumberUTC day bucket (Unix seconds) when the event was recorded
gradestringCurrent Safety Score letter grade at date
scorenumber | nullCurrent numeric score (0–100); null when grade is NR
prevGradestring | nullPrevious grade before this event; null for the seed row
prevScorenumber | nullPrevious score before this event; null for the seed row
methodologyVersionstringSafety 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 cron-published report-card snapshot so Yield Intelligence stays aligned with /api/report-cards without rebuilding the full Safety Score envelope on every read. Compute-on-read is used only when the published snapshot is unavailable. Royco Dawn structured-tranche rows use the attached underlying asset's report-card snapshot as input, then expose an opportunity-level tranche Safety Score in the yield row. PYS v8 is benchmark-aware and source-risk-aware: it starts from cached APY inputs, adds a weighted slice of the row's benchmark spread, divides by the nested sourceRisk.sourceRiskPenalty populated from measured source evidence, then applies the current Safety Score and volatility multiplier. Missing source-risk evidence is neutral. 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, unparseable JSON, or parseable JSON that fails the YieldRankingsResponseSchema cache-read validation. Schema-invalid cached objects are not served because the endpoint cannot safely hydrate or trust their row shape.

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 }
  },
  "warnings": [],
  "publication": {
    "generationId": "yield-1772000000",
    "updatedAt": 1772000000,
    "cutoffAt": 1772000000,
    "schemaVersion": 1,
    "status": "published"
  },
  "methodology": {
    "version": "8.19",
    "currentVersion": "8.19",
    "changelogPath": "/methodology/yield-changelog/"
  },
  "_meta": { "updatedAt": 1710500000, "ageSeconds": 42, "status": "fresh" }
}
FieldTypeDescription
rankingsYieldRanking[]All ranked stablecoins, sorted by Pharos Yield Score descending
riskFreeRatenumberDefault USD benchmark rate (%) retained for backward compatibility and mixed-view fallback
benchmarksobject | nullBenchmark registry keyed by currency (USD, EUR, CHF, plus v8.13: GBP, JPY, MXN, BRL, AUD, CAD; SGD is reserved without a fetcher) with label, rate, freshness, fallback, and proxy metadata
scalingFactornumberScaling factor applied in yield score computation
medianApynumberTVL-weighted median APY (30d) across best-source rows, used as a peer reference in warning heuristics
updatedAtnumberUnix seconds when the rankings were last computed
provenanceobject | nullSnapshot-level provenance for default benchmark freshness, full benchmark registry, DeFiLlama pool input freshness, safety coverage, and selection method
warningsYieldResponseWarning[]Optional body-level warnings for degraded but still served payloads, such as live safety hydration gaps. Clients should surface these separately from row-level warningSignals
publicationobject | nullOptional publication metadata for generation-aware payloads; omitted on legacy payloads
methodologyobject | undefinedOptional Yield Intelligence methodology envelope for rankings payloads when emitted by the publisher

Optional v8 fields are nullable and omittable. Publication-generation fields are populated by the generation-aware publisher when available; legacy rows and old payloads may still omit them. Public source-risk values are nested under the sourceRisk object; flattened top-level fields such as sourceRiskPenalty or rewardShare are not part of the public API contract.

FieldSurfaceTypePopulation status
publication.generationIdrankings/history rootstring | null | undefinedPublisher generation identifier, e.g. yield-1774526400; omitted on legacy payloads
publication.updatedAtrankings/history rootnumber | null | undefinedUnix seconds when the generation was computed
publication.cutoffAtrankings/history rootnumber | null | undefinedLatest history timestamp approved for public reads
publication.schemaVersionrankings/history rootnumber | null | undefinedPayload-generation schema version
publication.statusrankings/history root"staged" | "published" | "failed" | null | undefinedPublic cache payloads should expose published; failed or CAS-skipped generations do not replace current yield_data rows
warnings[]rankings rootYieldResponseWarning[] | undefinedBody-level degraded-response advisories; row source warnings remain in warningSignals
publicationGenerationIdranking/history rowsstring | null | undefinedRow-to-generation join identifier; null on legacy rows
publishedRankranking rowsinteger >= 1 | null | undefinedStable rank from the published cache order before live Safety Score hydration
liveRankranking rowsinteger >= 1 | null | undefinedPost-hydration rank assigned after live Safety Score recomputation
sourceRisk.sourceRiskScoreranking/history/source rows0..100 number | null | undefinedOptional source-risk score when populated by the source-risk worker
sourceRisk.sourceRiskPenaltyranking/history/source rowsnumber >= 1 | null | undefinedActive PYS v8 source-risk multiplier derived from reliable source evidence. Missing/invalid values are neutral (1); runtime clamps values to 1..2.5
sourceRisk.sourceDepthRatioranking/history/source rowsnumber >= 0 | null | undefinedOptional venue-depth ratio
sourceRisk.rewardShareranking/history/source rows0..1 number | null | undefinedOptional reward APY share
sourceRisk.sourceAgeSecondsranking/history/source rowsinteger seconds >= 0 | null | undefinedOptional source-observation age
sourceRisk.observationCount30dranking/history/source rowsinteger >= 0 | null | undefinedOptional 30-day observation count for the source
sourceRisk.sourceSwitchCount30dranking/history/source rowsinteger >= 0 | null | undefinedOptional 30-day selected-source switch count
sourceRisk.deploymentPlaceranking/history/source rows"native-wrapper" | "issuer-savings" | "lending-market" | "strategy-vault" | "structured-tranche" | "lp-or-dex" | "rwa-fund" | "reward-program" | "rate-derived" | "price-derived" | null | undefinedOptional sourced deployment-place label
sourceRisk.venueProtocolranking/history/source rowsstring | null | undefinedOptional venue protocol label
sourceRisk.venueChainranking/history/source rowsstring | null | undefinedOptional venue chain label
sourceRisk.venueRiskTierranking/history/source rows"low" | "medium" | "high" | "unknown" | null | undefinedOptional sourced venue tier; unknown remains neutral
sourceRisk.investabilityFlagsranking/history/source rowsstring[] | undefinedOptional investability caveats
sourceRisk.trancheSideranking/history/source rows"senior" | "junior" | null | undefinedRoyco Dawn tranche side for structured-tranche rows
sourceRisk.trancheSafetyScoreranking/history/source rows0..100 number | null | undefinedOpportunity-level Safety Score used by Royco Dawn tranche rows after underlying-score and tranche-risk adjustments
sourceRisk.trancheSafetyPenaltyranking/history/source rowsnumber >= 0 | null | undefinedDifference between the underlying report-card Safety Score and the final tranche Safety Score
sourceRisk.underlyingSafetyScoreranking/history/source rows0..100 number | null | undefinedCurrent underlying report-card Safety Score input used for opportunity-level tranche scoring
sourceRisk.marketCoverageRatioranking/history/source rowsnumber >= 0 | null | undefinedCurrent Royco market coverage ratio
sourceRisk.marketMinCoverageRatioranking/history/source rowsnumber >= 0 | null | undefinedRoyco market minimum required coverage ratio
sourceRisk.marketUtilizationRatioranking/history/source rowsnumber >= 0 | null | undefinedCurrent Royco market utilization ratio
sourceRisk.marketUtilizationLimitRatioranking/history/source rowsnumber >= 0 | null | undefinedRoyco market utilization target or limit ratio when supplied
sourceRisk.marketDrawdownRatioranking/history/source rowsnumber >= 0 | null | undefinedCurrent Royco market drawdown ratio
sourceRisk.marketTotalDrawdownsranking/history/source rowsinteger >= 0 | null | undefinedRoyco market drawdown count when supplied
sourceRisk.marketStatusranking/history/source rows"normal" | "protected" | "unhealthy" | "critical" | null | undefinedNormalized Royco market status used by tranche scoring
sourceRisk.marketTvlUsdranking/history/source rowsnumber >= 0 | null | undefinedRoyco market-level TVL in USD
sourceRisk.trancheTvlUsdranking/history/source rowsnumber >= 0 | null | undefinedRoyco vault/tranche TVL in USD
sourceRisk.trancheShareTokenAddressranking/history/source rowsstring | null | undefinedShare-token address for the Royco tranche vault
sourceRisk.trancheDepositTokenAddressranking/history/source rowsstring | null | undefinedDeposit-token address used to attach the tranche row to a tracked underlying stablecoin
sourceRisk.withdrawalDelaySecondsranking/history/source rowsinteger seconds >= 0 | null | undefinedWithdrawal/redemption delay for the tranche when supplied
sourceRisk.kycRequiredranking/history/source rowsboolean | null | undefinedWhether the source marks KYC as required
sourceRisk.accessRestrictedranking/history/source rowsboolean | null | undefinedWhether the source marks jurisdictional or other access restrictions
rankChangeAttributionranking rowsobject | null | undefinedOptional previous-rank/PYS delta attribution with primary driver and contribution hints

Current v8.19 scoring treats missing source-risk evidence as neutral: omitted or null sourceRisk, sourceRisk.sourceRiskPenalty, or sourceRisk.venueRiskTier values resolve to a neutral source-risk penalty and do not change PYS or report-card scoring. sourceRisk.sourceRiskScore is now derived from the resolved source-risk penalty when no upstream value is provided. Royco Dawn structured-tranche rows additionally carry opportunity-level Safety Score evidence under sourceRisk.tranche*; this changes only the yield row's safety input and PYS, not the underlying stablecoin's report-card Safety Score. DEWS methodology v5.99 consumes only populated structured yield stress evidence inside its Yield Anomaly sub-signal; neutral, malformed, or missing structured rows remain no-ops. Saved payloads used by calibration tooling should normalize from the nested sourceRisk.* fields before analysis rather than assuming flattened row properties.

YieldRanking

FieldTypeDescription
idstringPharos stablecoin ID
symbolstringToken symbol
namestringFull name
currentApynumberCurrent APY (%)
apy7dnumber7-day average APY (%)
apy30dnumber30-day average APY (%)
apyBasenumber | nullBase APY component (%)
apyRewardnumber | nullReward APY component (%), null if none
yieldSourcestringHuman-readable yield source description
yieldSourceUrlstring | nullOfficial URL for the selected source when Pharos has a curated or metadata-derived link
yieldTypestringYield type classification (e.g. "lending-vault", "staking")
dataSourcestringData source identifier (e.g. "defillama")
sourceTvlUsdnumber | nullTVL of the yield source pool (USD)
pharosYieldScorenumber | nullComposite Pharos Yield Score (0–100), recomputed at read time from cached APY + benchmark inputs plus the current Safety Score
safetyScorenumber | nullCurrent Safety Score input used by Yield Intelligence. Rated coins match /api/report-cards; Royco structured-tranche rows expose the row-level tranche score; unrated coins use the default NR penalty input (40)
safetyGradestring | nullCurrent Safety Score letter grade ("A+" through "F", or "NR"); Royco structured-tranche rows derive the grade from the opportunity-level tranche score
yieldToRisknumber | nullYield-to-risk ratio recomputed at read time from cached APY inputs plus the current Safety Score
excessYieldnumber | null30-day average APY above the row benchmark (percentage points)
benchmarkKey"USD" | "EUR" | "CHF" | "GBP" | "JPY" | "MXN" | "BRL" | "AUD" | "CAD" | "SGD" | undefinedBenchmark selected for this row's excessYield and any rate-derived APY logic (v8.13 expanded the set; SGD is reserved without a fetcher and routes to USD fallback)
benchmarkLabelstring | undefinedHuman-readable benchmark label for the row
benchmarkCurrencystring | undefinedBenchmark currency code used for the row
benchmarkRatenumber | undefinedBenchmark rate (%) applied to this row
benchmarkRecordDatestring | null | undefinedMarket or policy record date for the selected benchmark
benchmarkIsFallbackboolean | undefinedWhether the row benchmark is currently on a fallback path
benchmarkFallbackModestring | null | undefinedFallback reason for the row benchmark when applicable
benchmarkSelectionMode"native" | "fallback-usd" | "manual-override" | undefinedHow the row benchmark was selected
benchmarkIsProxyboolean | undefinedTrue when the selected benchmark is an explicit proxy rather than the exact reference rate
yieldStabilitynumber | nullYield stability metric (0–1; higher = more stable)
apyVariance30dnumber | null30-day APY variance
apyMin30dnumber | nullMinimum APY in last 30 days (%)
apyMax30dnumber | nullMaximum APY in last 30 days (%)
warningSignalsstring[]Active warning-signal flags for the selected best source
altSourcesAltYieldSource[]Additional non-selected source rows for the same coin
provenanceobject | nullSource-level provenance: confidence tier, selection reason, benchmark state, source-switch metadata, source freshness, and optional anchor timing
publicationGenerationIdstring | null | undefinedPublication-generation identifier, or null/omitted for legacy rows
publishedRanknumber | null | undefinedStable publication-order rank from the cached generation
liveRanknumber | null | undefinedPost-hydration rank from the response order after live Safety Score recomputation
sourceRiskobject | null | undefinedOptional nested source-risk payload. Runtime rows derive or resolve sourceRisk.sourceRiskPenalty before PYS v8 scoring; missing or unknown values remain neutral
rankChangeAttributionobject | null | undefinedOptional rank-change attribution with previous rank/PYS, delta, primary driver, and driver contribution hints

When present, YieldRanking.provenance includes:

  • sourceObservedAt / sourceAgeSeconds: the timestamp and age of the latest observation actually backing the row
  • comparisonAnchorObservedAt / comparisonAnchorAgeSeconds: optional prior-anchor timing for APYs derived from two observations, such as price-derived and on-chain exchange-rate rows
  • benchmarkKey, benchmarkLabel, benchmarkRate, benchmarkIsFallback, benchmarkSelectionMode, and related fields for the exact benchmark applied to that row

GET /api/yield-adapter-manifest

Yield adapter manifest for every yield-bearing asset. The route is public-read, uses the standard cache profile (s-maxage=300), and requires X-API-Key on the public API lane.

sourceKey is an exact runtime key only when it can join to /api/yield-history?sourceKey=..., rankings provenance, or decision-ledger rows. Runtime-resolved DeFiLlama variant strategies and disabled/quarantined readers return sourceKey: null with sourceKeyPattern set to the runtime pattern or would-be disabled key instead of a synthetic non-runtime value.

Response

{
  "methodologyVersion": "v8.16",
  "updatedAt": 1779210000,
  "entries": [
    {
      "stablecoinId": "susde-ethena",
      "coinSymbol": "sUSDe",
      "family": "defillama",
      "sourceKey": "66985a81-9c51-46ca-9977-42b4fe7bc6df",
      "sourceKeyPattern": null,
      "label": "Curated DeFiLlama pool UUID",
      "chain": null,
      "project": null,
      "lifecycle": "active",
      "quarantineReason": null,
      "methodologyVersion": "v8.16",
      "updatedAt": 1779210000
    }
  ]
}

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. Generation-aware rows are returned only after their publication generation is marked published; legacy rows remain readable through the existing cutoff fallback. 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.

For tracked savings-wrapper handoffs (USDe, USDS, DAI, frxUSD, crvUSD, avUSD), legacy parent-owned wrapper rows are filtered immediately at read time and are also purged by the hourly publisher plus the operator cleanup tool. The discontinuity is intentional: those old child-owned series no longer remain queryable through the parent id or through mode=source&sourceKey=.... New linked parent rows use linked-variant:<variantId>:<sourceKey> source keys when a tracked variant's eligible native/wrapper source is intentionally exposed on the active parent for comparison and coverage context.

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

ParamTypeDescription
stablecoinstringPharos stablecoin ID (required)

Optional query parameters

ParamTypeDefaultBoundsDescription
daysinteger901–365Lookback window in days
modestringbestbest, sourcebest for historically selected best-source rows; source requires sourceKey
sourceKeystringRequired with mode=source; returns source-specific history for that source key

Response

{
  "current": {
    "date": 1772000000,
    "apy": 4.21,
    "sourceKey": "onchain:susde-ethena",
    "yieldSource": "Ethena staking (sUSDe)"
  },
  "history": [YieldHistoryPoint, "..."],
  "publication": {
    "generationId": "yield-1772000000",
    "updatedAt": 1772000000,
    "cutoffAt": 1772000000,
    "schemaVersion": 1,
    "status": "published"
  },
  "methodology": {
    "version": "8.13",
    "currentVersion": "8.13",
    "changelogPath": "/methodology/yield-changelog/"
  }
}
FieldTypeDescription
currentYieldHistoryPoint|nullLatest row in the returned history window, or null when no history exists
historyYieldHistoryPoint[]History rows sorted by date ASC
methodologyobjectYield methodology envelope for the response
warningstring | undefinedPresent when freshness lookup fails and the handler falls back to cache metadata
publicationobject | nullOptional published-generation metadata; omitted for legacy rankings payloads

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,
  "publicationGenerationId": "yield-1771500000"
}
FieldTypeDescription
datenumberUnix seconds
apynumberTotal APY at snapshot time (%)
apyBasenumber | nullBase APY component (%)
apyRewardnumber | nullReward APY component (%); null if none
exchangeRatenumber | nullExchange rate at snapshot time (e.g. sUSDe/USDe); null if not applicable
sourceTvlUsdnumber | nullTVL of the yield source pool at snapshot time (USD)
warningSignalsstring[]Active warning-signal flags at that snapshot
sourceKeystring | nullStable source identifier for this history row (for example a DL pool UUID, onchain:<stablecoinId>, or linked-variant:<variantId>:<sourceKey>)
yieldSourcestring | nullHuman-readable source label at that snapshot
yieldSourceUrlstring | nullOfficial URL for that source when Pharos has a curated or metadata-derived link
yieldTypestring | nullYield type classification at that snapshot
dataSourcestring | nullUnderlying data-source family
isBestbooleanWhether this row was the selected best source at that timestamp
sourceSwitchbooleanTrue when the historically selected best source changed at this row
publicationGenerationIdstring | null | undefinedPublished generation identifier for generation-aware rows; null/omitted on legacy rows
sourceRiskobject | null | undefinedOptional nested source-risk payload for historical rows; missing or unknown values are neutral

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. Aggregate responses without stablecoin are served cache-first; the critical sync lane pre-publishes the default 24h and 168h windows after successful runs so public reads avoid rescanning hourly mint/burn aggregates.

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

ParamTypeDefaultBoundsDescription
stablecoinstringFilter to a single stablecoin ID. Changes response shape to per-coin mode
hoursinteger241–720Lookback 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

FieldTypeDescription
scorenumber | nullMarket-cap-weighted pressure-shift composite (-100 to +100). null when insufficient data
bandstring | nullGauge band: "CRISIS", "STRESS", "CAUTIOUS", "NEUTRAL", "HEALTHY", "CONFIDENT", "SURGE"
flightToQualitybooleanWhether flight-to-quality conditions are active
flightIntensitynumberFlight-to-quality intensity (0–100). 0 when not active
trackedCoinsnumberNumber of stablecoins tracked for mint/burn flows
trackedMcapUsdnumberCombined market cap of tracked coins (USD)
intensitySemanticsstringScoring semantics version identifier (currently "signed-v2")
classificationSourcestringSource of flight-to-quality classification ("report-card-cache" or "unavailable")

Top-level metadata

FieldTypeDescription
windowHoursnumberRequested chart window for hourly[]
scopeobjectCurrent ingestion scope, for example { chainIds: ["ethereum", "arbitrum"], label: "Configured issuance chains" }
syncobjectLatest critical-lane freshness metadata, warning state, and optional classificationWarning

CoinFlow

FieldTypeDescription
stablecoinIdstringPharos stablecoin ID
symbolstringToken symbol
flowIntensitynumber | nullDeprecated alias for pressureShiftScore; retained for compatibility
pressureShiftScorenumber | nullCanonical 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
has24hActivitybooleanWhether any 24h mint/burn events were recorded for the coin
baselineDailyNetUsdnumber | nullAverage daily net flow over the baseline window used for scoring
baselineDailyAbsUsdnumber | nullAverage daily absolute flow over the baseline window used for scoring
baselineDataDaysnumber | nullNumber of tracked days contributing to the baseline window
netFlow24hUsdnumberRaw 24h net flow (USD, positive = net minting, negative = net burning). Fixed to the canonical 24h window even when hours changes
mintVolume24hUsdnumberTotal mint volume in the canonical 24h window (USD)
burnVolume24hUsdnumberTotal burn volume in the canonical 24h window (USD)
mintCount24hnumberNumber of mint events in the canonical 24h window
burnCount24hnumberNumber of burn events in the canonical 24h window
netFlow7dUsdnumber7-day net flow (USD)
netFlow30dUsdnumber30-day net flow (USD)
netFlow90dUsdnumber90-day net flow (USD)
largestEvent24hobject | nullLargest event in the last 24h: { direction, amountUsd, txHash, timestamp }
coverageobject | undefinedCoverage metadata: startBlock, lastSyncedBlock, lagBlocks, historyStartAt, window booleans, adapter provenance (adapterKinds, startBlockSource, startBlockConfidence), and status

HourlyFlow

FieldTypeDescription
hourTsnumberUnix seconds (start of hour)
netFlowUsdnumberNet flow for this hour (USD)
mintVolumeUsdnumberMint volume for this hour (USD)
burnVolumeUsdnumberBurn 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

ParamTypeDescription
stablecoinstringPharos stablecoin ID (required)

Optional query parameters

ParamTypeDefaultBoundsDescription
directionstring"mint" or "burn"Filter by direction
chainstringtracked chain IDs for the requested stablecoinFilter by chain ID within the stablecoin's configured issuance scope
burnTypestring"effective_burn", "bridge_burn", "review_required"Filter burn rows by classification
scopestring"all""all" or "counted"counted returns only rows used in economic-flow aggregates (flow_type='standard' and mint/effective-burn semantics)
minAmountnumberMinimum USD amount; unpriced rows are excluded when this filter is used
limitinteger501–500Max results
offsetinteger00–25,000Pagination offset; cannot be combined with cursor
cursorstringopaqueKeyset cursor from nextCursor
includeTotalbooleantruetrue or falseWhen false, skips the exact COUNT(*); total becomes a page lower bound and totalExact is false

Response

{
  "events": [MintBurnEvent, ...],
  "total": 1234,
  "totalExact": true,
  "nextCursor": "eyJ2IjoxLCJ2YWx1ZXMiOlsxNzcyMDAwMDAwLDE5MDAwMDAwLCJtYi0xIl19"
}

Results are ordered by timestamp DESC, blockNumber DESC, id DESC. Prefer cursor/nextCursor for deep pagination; offset pagination is capped for D1 safety.

MintBurnEvent

FieldTypeDescription
idstringComposite ID: {chainId}-{txHash}-{logIndex}
stablecoinIdstringPharos stablecoin ID
symbolstringToken symbol
chainIdstringChain 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
amountnumberAmount in native token units
amountUsdnumber | nullUSD value at time of event
burnType"effective_burn" | "bridge_burn" | "review_required" | nullBurn classification; null for mint rows
burnReviewReasonstring | nullReason emitted when a burn requires manual review classification
counterpartystring | nullNon-zero address (recipient for mint, sender for burn)
txHashstringTransaction hash
blockNumbernumberBlock number
timestampnumberUnix seconds
explorerTxUrlstringBlock explorer URL for the transaction
priceUsednumber | nullPrice used to derive amountUsd
priceTimestampnumber | nullUnix seconds of the price snapshot used
priceSourcestring | nullValuation 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

ParamTypeDefaultDescription
stablecoinstringSingle coin mode: return latest + daily history
daysinteger30History 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": "6.0"
    }
  },
  "updatedAt": 1740000000,
  "oldestComputedAt": 1740000000,
  "malformedRows": 0,
  "methodology": {
    "version": "6.0",
    "versionLabel": "v6.0",
    "currentVersion": "6.0",
    "currentVersionLabel": "v6.0",
    "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": "6.0"
  },
  "history": [
    {
      "date": 1739900000,
      "score": 3,
      "band": "CALM",
      "signals": {
        "supply": { "value": 1, "available": true },
        "price": { "value": 1, "available": true }
      },
      "amplifiers": { "psi": 1, "contagion": 1 },
      "methodologyVersion": "6.0"
    }
  ],
  "malformedRows": 0,
  "methodology": {
    "version": "6.0",
    "versionLabel": "v6.0",
    "currentVersion": "6.0",
    "currentVersionLabel": "v6.0",
    "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, exposed as a body-only lag diagnostic

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/api-key-requests

Public self-serve API key request endpoint used by https://pharos.watch/api/. It records the request, reserves the normalized email claim, and sends an email verification link through Resend. It does not issue a key until the email verification endpoint succeeds.

Authentication: exempt

Cache: no-store

Default key policy after verification

  • tier: "self-serve"
  • trafficClass: "external"
  • rateLimitPerMinute: 30
  • expiresAt: 60 days after issuance
  • one active or pending self-serve key per normalized email

Request body

{
  "email": "dev@example.com",
  "requesterName": "Optional name",
  "organization": "Optional organization",
  "projectUrl": "https://example.com",
  "useCase": "Required, 10-1200 characters",
  "intendedEndpoints": ["/api/stablecoins", "/api/stablecoin/:id"],
  "expectedCadence": "hourly",
  "expectedVolume": "Optional free-form estimate",
  "acceptedTerms": true,
  "website": ""
}
FieldTypeRequiredNotes
emailstringYesNormalized to lowercase for the one-key-per-email claim
requesterNamestringNoPrivate operator context only
organizationstringNoPrivate operator context only
projectUrlstringNoMust start with https:// when provided
useCasestringYes10-1200 characters
intendedEndpointsstring[]NoPublic API paths, known dynamic patterns such as /api/stablecoin/:id, or unknown; admin/API-key/backfill paths are rejected
expectedCadence"hourly" | "every_5_min" | "every_1_min" | "manual" | "other"YesUsed for review context
expectedVolumestringNoPrivate operator context only
acceptedTermstrueYesFair-use acknowledgement
websitestringNoHoneypot field; non-empty submissions are silently accepted without issuing work

Success response: 202 Accepted

{
  "status": "pending_verification",
  "message": "If this address can receive verification email, check your inbox to continue."
}

Error responses

  • 400 invalid body, invalid email, invalid project URL, unknown/admin intended endpoint, or missing fair-use acknowledgement
  • duplicate active or pending self-serve key claims receive the same 202 response shape as a new pending request and do not send a second verification link
  • 429 request throttle exceeded; responses include Retry-After
  • 503 self-serve env/email dependency unavailable (Retry-After: 60)

POST /api/api-key-requests/verify

Public self-serve verification endpoint used by the email link. Links carry the token only in the URL fragment as raw /api/#akv_..., which is not sent to the server in the page request, logged by intermediaries, or leaked via Referer. The browser strips the fragment before posting and exchanges the token here. Query-string and legacy fragment-parameter verification forms are not accepted. A successful response creates the key, marks the request issued, and returns the plaintext API token exactly once.

Authentication: exempt

Cache: no-store

Request body

{ "token": "akv_..." }

Success response: 201 Created

{
  "status": "issued",
  "key": {
    "keyPrefix": "0123456789abcdef",
    "maskedToken": "ph_live_0123456789abcdef_...",
    "tier": "self-serve",
    "trafficClass": "external",
    "rateLimitPerMinute": 30,
    "expiresAt": 1715686400
  },
  "token": "ph_live_...",
  "usage": {
    "baseUrl": "https://api.pharos.watch",
    "headerName": "X-API-Key",
    "retryGuidance": "Respect Retry-After on 429 responses and add jitter to polling intervals."
  }
}

Error responses

  • 400 invalid, expired, used, or no-longer-pending verification token
  • 429 verification attempt throttle exceeded or daily issuance limit for the salted IP hash reached; responses include Retry-After
  • 503 self-serve dependency unavailable or issuance consistency compensation triggered (Retry-After: 60)

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

  • Feedback endpoint limiter: 3 submissions / 10 minutes per salted IP hash in D1.
  • Feedback limiter dependency failure: 503 with Retry-After: 60 and { "error": "Feedback service temporarily unavailable. Please try again." }.

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": ""
}
FieldTypeRequiredNotes
type"bug" | "data-correction" | "feature-request"YesSubmission category
titlestringConditionalRequired for bug and feature-request (3–100 chars); optional for data-correction
descriptionstringYes10–2000 chars
pageUrlstringYesRelative app path (must be a single-slash internal path such as /stablecoin/usdc-circle/; protocol-relative //... values are rejected)
websitestringNoHoneypot field; non-empty is silently accepted/dropped
expectedValue, stablecoinId, stablecoinName, pegValuestringNoOptional metadata
contactHandlestringNoOptional Telegram/X handle that appears publicly on GitHub

Response

{ "ok": true }

Error responses

  • 400 invalid payload
  • 429 rate limited (3 submissions / 10 minutes per salted IP hash)
  • 500 forwarding/processing failure
  • 503 service misconfigured (missing FEEDBACK_IP_SALT or GITHUB_PAT) or feedback limiter/storage dependency failure (Retry-After: 60)

POST /api/telegram-mini-app/session

Returns the current private-chat Mini App control-panel state for a Telegram user.

Authentication: exempt from X-API-Key; requires Telegram Mini App initData signed with the bot token. The worker excludes Telegram's transport hash field from the HMAC data-check string, includes every other signed field, rejects missing/invalid hashes, and accepts sessions up to 24 hours old for read-only state loading.

Site-data lane: denied. The frontend calls the public API host through src/lib/api.ts; /_site-data/* never proxies this route.

Rate limiting: cache-backed cooldown per Telegram user (mini-app:session, 2 seconds). Invalid-auth requests are rejected before cooldown or analytics writes.

Cache: no-store.

Request body:

{
  "initData": "query-string-from-Telegram.WebApp.initData"
}

The request schema is strict. Launch context such as start_param must come from signed Telegram initData.

Response: JSON Mini App state. Private fresh sessions return viewer.canMutate=true, including Telegram direct-link launches where chat_type="sender" identifies the user's private context. Private sessions older than the 5-minute mutation window but younger than 24 hours return state with viewer.canMutate=false and viewer.mutationBlockReason="stale-auth". Group, supergroup, and channel launches return read-only state with viewer.mutationBlockReason="not-private".

Key fields:

  • viewer — Telegram user, optional chatId, chat type, startParam, and mutation eligibility.
  • subscriber — global alert flags, quiet-hours settings, and chat-level snooze.
  • subscriptions — explicit per-coin follows with alert flags and per-coin thresholds.
  • presets — followed preset watchlists.
  • catalog — recommended presets and searchable tracked stablecoins for the UI.
  • health — last successful delivery/reply, recent failure class, and queued alert count.

Errors: 400 invalid request shape, 401 invalid or stale Telegram session, 429 cooldown, 503 missing bot-token configuration.

POST /api/telegram-mini-app/mutate

Applies one private-chat Mini App setting mutation, then returns the refreshed Mini App state.

Authentication: exempt from X-API-Key; requires signed Telegram Mini App initData no older than 5 minutes. Mutations are private-user-context only (chat_type absent, private, or Telegram direct-link sender). The same fresh launch can perform multiple mutations inside that 5-minute window; stale auth returns 401.

Site-data lane: denied.

Rate limiting: cache-backed cooldown per Telegram user across all mutation kinds (mini-app:mutation:any, 5 seconds).

Cache: no-store.

Request body:

{
  "initData": "query-string-from-Telegram.WebApp.initData",
  "operation": {
    "kind": "set-global",
    "alertType": "safety",
    "enabled": true
  }
}

Supported operation.kind values:

  • recommended-setup — canonical first-run setup only: presetId="usd-top25" and alertTypes=["dews","depeg"].
  • follow-preset — follow any supported preset with selected alert types.
  • set-global — toggle one global alert family (dews, depeg, safety, launch).
  • set-global-depeg-step — set or clear the global depeg severity and worsening-step threshold (100, 250, 500, or null).
  • set-quiet-hours — enable or disable UTC quiet hours.
  • clear-snooze — clear chat-level snooze.
  • set-snooze — set chat-level snooze for 1h, 4h, or 24h.
  • set-coin-snooze — set or clear one explicit coin subscription's snooze.
  • set-timezone — set the chat timezone used for quiet-hours display.
  • unsubscribe-all — clear all global, per-coin, and preset alert settings.
  • forget-me — delete the private subscriber row and mutable alert settings.
  • set-coin — add or tune one explicit coin subscription.
  • remove-coin — remove one explicit coin subscription.
  • follow-preset / unfollow-preset — add or remove a dynamic preset watchlist.

Errors: 400 invalid operation, unknown coin/preset, or empty alert type selection; 401 invalid or stale Telegram session; 403 group mutation attempt; 429 cooldown; 503 preset cache unavailable or missing bot-token configuration.

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 for processing. Missing or invalid webhook secrets are acknowledged with 200 ok and ignored to prevent Telegram retry storms. The webhook never uses the operator Cloudflare Access lane.

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: Normal authenticated, unauthenticated/ignored, and command-processing paths return 200 OK with plain-text body ok so Telegram does not retry routine bot decisions. Uncaught handler errors still use the standard API error wrapper and can return 500.

Commands handled:

  • /start — Welcome/setup wizard with onboarding examples plus @pharoswatch and @pharoswatchers links
  • /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, disable every current alert flag including launch, and clear the global depeg worsening step
  • /set <ticker> <setting> <value> — Tune per-coin thresholds and modes
  • /set all <setting> <value> — Toggle global all-stablecoin alert types
  • /settings — Open the inline-keyboard settings menu
  • /mute <start>-<end> — Enable quiet hours (in the subscriber's configured timezone, defaulting to UTC)
  • /unmutehours — Disable quiet hours
  • /timezone [<IANA zone>] — Show or set the subscriber's timezone for quiet hours; without an argument it offers a quick-pick keyboard
  • /status <ticker> — Read-only per-coin status summary
  • /brief (alias: /market) — Market brief built from current cached datasets
  • /top <view> — Top movers/leaders for depeg, dews, yield, liquidity, chains, or safety
  • /why <ticker> — Explain the current Safety Score for a coin
  • /coverage <ticker> — Per-coin coverage diagnostics for the subscriber surface
  • /health — Chat self-diagnostics: last successful delivery/reply, queued alerts, recent failure class, quiet-hours/snooze state, and alert readiness
  • /unsnooze — Clear an active alert snooze without waiting for it to expire
  • /cancel — Cancel a pending disambiguation flow
  • /list — Show current subscriptions, per-coin settings, and quiet hours
  • /help — Command reference

Preset watchlists are stored in telegram_preset_subscriptions and resolved dynamically at dispatch/list/status time. Supported aliases are usd-top10, usd-top25, usd-top50, non-usd-top10, non-usd-top25, non-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.


Pages Function endpoints

These endpoints are served by Cloudflare Pages Functions from the website host (pharos.watch), not by the Worker API host (api.pharos.watch). They are out of scope for the public X-API-Key regime: no API key is required, they do not appear in the OpenAPI artifact, and they do not honor Idempotency-Key.

Same-origin only. Browser CORS blocks cross-origin POST before the function executes; foreign-origin requests receive 404. Documented for completeness and for external tooling that reads share URLs. These endpoints are snapshot storage for the website UI, not a public integration API.

GET /selector-snapshot/:sid

Returns a previously stored Stablecoin Picker output JSON identified by content-addressed sid (32 hex chars). The returned artifact is frozen; clients that offer "Compare to today's data" must compute a separate live Picker run and keep the stored artifact unchanged.

Authentication: exempt — same-origin gated via Origin / Referer allowlist.

Path parameter: sid — 32 lowercase hex chars, content-addressed SHA-256 truncation. The server recomputes the identifier from the canonicalized selector output before storing or reading a snapshot.

Response (200):

{
  "profile": "treasury",
  "engineVersion": "selector-v1.3",
  "datasetHash": "<content hash>",
  "timestamp": 1715000000,
  "input": {
    "profile": "treasury",
    "pegCurrency": "EUR",
    "horizon": "6mplus",
    "depegTolerance": "zero",
    "composability": "none",
    "venuePreferences": ["custody"],
    "exitSpeed": "any",
    "minApy": null,
    "yieldNativeOnly": false,
    "decentralization": "any",
    "custodyOk": "any"
  },
  "universe": { "active": 392, "surviving": 12 },
  "recommended": [ /* ranked shortlist entries */ ],
  "lowerRanked": [ /* lower-ranked entries */ ],
  "usedRelaxedFallback": false,
  "relaxedReasons": [],
  "coverageWarnings": {
    "skippedForCoverageCount": 0,
    "sparse": false,
    "uneven": false,
    "skippedForCoverage": [],
    "newListingCount": 0,
    "redistributionCount": 0
  },
  "exclusionSummary": [],
  "closestSurvivors": [],
  "relaxableConstraints": [],
  "lowConfidence": false,
  "methodologyVersions": { "safetyScore": "v7.25" }
}

The full SelectorOutput shape is owned by shared/lib/selector/types.ts. The Pages Function rejects snapshots missing the frontend replay fields (input.pegCurrency, universe, lowConfidence, usedRelaxedFallback, relaxedReasons, exclusionSummary, closestSurvivors, relaxableConstraints, coverage warning counts, and authored recommendation/lower-ranked prose). Semantic validation rejects impossible component/score ranges, unknown enum values, wrong-profile venuePreferences, unknown whyKeys, malformed confidence/rank diagnostics, unknown lower-ranked reason keys, malformed recommendedSource objects, malformed perInputStaleness, and impossible rank/slot values. Readers should treat unknown fields permissively; datasetHash is scoped to the selected-peg decision universe and must change when any exclusion, scoring, tie-break, explanation, source-selection, or version-affecting field changes. Freshness-only metadata is excluded unless it affects output semantics. engineVersion / selector version carries the bump on deterministic behavior, weight, exclusion-rule, missing-data, tie-break, or explanation changes.

On GET, the Pages Function parses the stored payload, recomputes the canonical sid, and verifies it matches the requested sid. A mismatch is treated as corrupt storage and returns 502.

Cache: private, no-store — reads are same-origin gated with Origin / Referer, so stored snapshots are intentionally not served from a public shared cache.

Failure modes:

StatusWhen
404Origin disallowed, sid not 32 hex chars, or KV miss.
500SELECTOR_SNAPSHOTS KV binding missing on the Pages project.
502Stored KV value is corrupt, fails semantic validation, or recomputes to a different sid.
503KV read throws transiently (Cloudflare KV outage).

POST /selector-snapshot

Stores a Stablecoin Picker output JSON under a server-recomputed sid. Idempotent — re-POSTing the same canonical payload returns the same sid. Documented here for completeness; external integrations should not call this endpoint (it is bound to the Picker wizard at https://pharos.watch/screener/picker/).

Authentication: exempt — same-origin gated.

Body: application/json, a complete SelectorOutput. Max 100 KB defensive cap. Debug traces are stripped before canonical sid computation and storage.

Response (200): { "sid": "<32 hex chars>" }. The sid is content-addressed: SHA-256 over a canonicalized JSON payload with debug/freshness-derived fields stripped (timestamp, debug, perInputStaleness, plus fields matching the suffixes ageSeconds / capturedAt / stalenessMs / updatedAt / fetchedAt), with keys lexicographically sorted at every depth. coverageWarnings.newListingCount is not stripped because the implemented engine derives it from content-level recent-listing flags. Engine and integration agree on the same strip-list, so a sid computed client-side matches the server's authoritative value.

Share-link privacy property: the stored payload contains the Picker answers and output rows, not IP addresses, browser fingerprints, wallet addresses, or account identifiers. The website UI must disclose that anyone with the resulting link can view the frozen artifact and that the KV entry is retained for 5 years.

Validation matrix:

CaseStatus / client contract
Invalid sid path syntax404; clients should surface invalid-link/not-found state without replaying unrelated live output.
Missing required replay fields400 on POST, 502 on GET.
Unknown enum, reason key, source shape, or impossible score/rank400 on POST, 502 on GET.
Stored payload canonical sid differs from requested sid502.
Clipboard denied after a successful POSTEndpoint still returns 200; UI shows a selectable URL fallback.
Trading profile has stale share-blocking inputsUI should not POST until refreshed; endpoint remains shape-focused and does not recompute live staleness.

Failure modes:

StatusWhen
400Body parse error, unsupported JSON, missing required replay fields, malformed recommendation / coverage-warning basics, or semantic validation failure.
404Origin disallowed.
405Method on the wrong path — POST is accepted only at /selector-snapshot without a path segment.
413Payload exceeds 100 KB defensive cap.
500SELECTOR_SNAPSHOTS KV binding missing.
503KV write throws transiently.