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.watchis the external integration API. Protected public routes requireX-API-Key.https://site-api.pharos.watchis the website-internal Worker host. It accepts only allowlistedGETreads plusX-Pharos-Site-Proxy-Secret./_site-data/*is the same-origin Pages Functions proxy used by browsers onpharos.watch,ops.pharos.watch,stablecoin-dashboard.pages.dev, and subdomains ofstablecoin-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/healthGET /api/og/*HEAD /api/og/*POST /api/feedbackPOST /api/api-key-requestsPOST /api/api-key-requests/verifyPOST /api/telegram-webhookPOST /api/telegram-mini-app/sessionPOST /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:
| Example | Asset |
|---|---|
"usdt-tether" | Tether (USDT) |
"usdc-circle" | USD Coin (USDC) |
"paxg-paxos" | PAX Gold (PAXG) |
"ustb-superstate" | Superstate USTB |
"gyen-gyen" | GYEN |
The full list is exported from shared/lib/stablecoins/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:
| Header | Description |
|---|---|
X-Data-Age | Seconds elapsed since the cron last wrote this data to D1 |
Warning | Freshness warning (110) when cached data is older than the generic freshness runway, plus endpoint-specific advisory warnings (199) on a few compute-on-read routes |
Generic freshness status is fresh through 8x maxAge, degraded through 12x maxAge, then stale. Generic freshness headers emit Warning and downgrade Cache-Control to no-store after age > 8x maxAge so edge/browser caches do not keep serving an old payload after the underlying cron data recovers. Some routes also use Warning for dependency or quality advisories even when the age is still inside that runway; clients should treat body _meta.status as authoritative when it exists.
Response Body Freshness (_meta)
Endpoints that emit _meta into plain-object (non-array) response bodies do so through createCacheHandler() or route-specific manual injection, alongside the HTTP freshness headers above. This provides inline freshness metadata for consumers that prefer not to parse response headers.
Shape:
{
"_meta": {
"updatedAt": 1710500000,
"ageSeconds": 42,
"status": "fresh"
}
}
| Field | Type | Description |
|---|---|---|
updatedAt | number | Unix epoch seconds when the cron last wrote this data to D1 |
ageSeconds | number | floor(now / 1000) - updatedAt |
status | string | "fresh" (age/max <= 8.0), "degraded" (8.0 < ratio <= 12.0), or "stale" (ratio > 12.0) |
Route-specific manual _meta injectors can be stricter. GET /api/chains uses its 1800-second budget directly (fresh <= 1x, degraded <= 2x, then stale) and switches its response to no-store whenever the chain snapshot is not fresh.
Endpoints with _meta:
| Endpoint | Max Age (sec) | Source |
|---|---|---|
GET /api/stablecoins | 600 | createCacheHandler |
GET /api/chains | 1800 | worker/src/api/chains.ts |
GET /api/bluechip-ratings | 43200 | createCacheHandler |
GET /api/usds-status | 86400 | createCacheHandler |
GET /api/yield-rankings | 3600 | Manual injection after live safety hydration |
GET /api/depeg-resolver | 900 | worker/src/api/depeg-resolver.ts |
GET /api/depeg-resolver-review | 900 | worker/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.
| Profile | Cache-Control | Used by |
|---|---|---|
| realtime | public, s-maxage=60, max-age=10 | stablecoins, stablecoin-summary, blacklist, blacklist-summary, depeg-events, peg-summary, mint-burn-events, chains |
| standard | public, s-maxage=300, max-age=60 | stablecoin-charts, depeg-resolver, depeg-resolver-review, redemption-backstops, usds-status, daily-digest, digest-archive, report-cards, stability-index, yield-rankings, mint-burn-flows, stress-signals |
| custom | public, s-maxage=300, max-age=300 | dex-liquidity (browser-side max-age extended to match CDN TTL) |
| per-coin | public, s-maxage=300, max-age=10 | stablecoin/:id (cache-aside with 5-min per-coin TTL in D1) |
| slow | public, s-maxage=3600, max-age=300 | supply-history, dex-liquidity-history, bluechip-ratings, yield-history, safety-score-history, non-usd-share |
| archive | public, s-maxage=86400, max-age=3600 | digest-snapshot, snapshots-index |
| immutable-snapshot | public, s-maxage=31536000, max-age=31536000, immutable | snapshots/:date.json, snapshot/:date/stablecoin/:id |
| public-status | public, max-age=60 | public-status-history |
| og-image | public, max-age=900, s-maxage=900 | dynamic Open Graph images |
| reserve-live | public, s-maxage=3600, max-age=300 | stablecoin-reserves live mode |
| reserve-live-stale | public, s-maxage=1800, max-age=120 | stablecoin-reserves live-stale mode |
| reserve-fallback | public, s-maxage=300, max-age=60 | stablecoin-reserves curated/template/unavailable fallback modes |
| no-store | no-store | health 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 profile | Minimum poll interval | Notes |
|---|---|---|
| realtime | 60 seconds | Polling faster usually re-fetches the same edge-cached payload |
| standard | 300 seconds | Preferred baseline for most dashboards |
| per-coin | 300 seconds | GET /api/stablecoin/:id is history-heavy; avoid short loops |
| slow | 3600 seconds | Historical/timeline endpoints should generally be polled hourly |
| archive | 86400 seconds | Historical digest snapshots and public snapshot index listings |
| immutable-snapshot | On-demand only | Dated public dataset snapshots are content-addressed and immutable |
| no-store | On-demand only | Health/admin diagnostics; avoid high-frequency polling |
Client best practices:
- Add interval jitter (
±10%) to avoid synchronized bursts. - Read
X-Data-Age+Warningfor freshness/stale decisions when those optional headers are present. - Back off exponentially on
429and5xxresponses.
Rate Limits
Public API traffic enforces 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
| Scope | Limit | Window |
|---|---|---|
| Per API key | Varies (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-Afterheader when present - Add random jitter (0–2 seconds) to avoid thundering-herd retries
- Use exponential backoff for sustained 429 responses
- Combine with the polling cadences in the section above to stay well under limits
Error Response Conventions
JSON API handlers use { "error": "message" } JSON format. GET /api/og/* 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.
| Status | Meaning | When |
|---|---|---|
| 400 | Bad Request | Missing required parameters, invalid enum values, malformed numeric input, or out-of-range numeric/filter values on handlers that opt into rejection (rangePolicy: "reject"). Some endpoints intentionally clamp or default selected numeric params; endpoint sections call this out where it is part of the contract. |
| 401 | Unauthorized | Public /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) |
| 403 | Forbidden | Disallowed CORS preflight from a foreign Origin, Pages ops proxy mutating request without a matching same-origin Origin, or mutating admin request missing X-Pharos-Admin: 1 |
| 404 | Not Found | Unknown stablecoin ID or missing resource |
| 429 | Too Many Requests | Rate limit exceeded (per-key public API limiter or feedback-specific limiter; feedback uses its own message body) |
| 500 | Internal Server Error | Unhandled exception (caught by withErrorHandler) |
| 502 | Bad Gateway | Upstream fetch failed (external data provider or Pages proxy upstream), or the ops proxy received a Cloudflare Access login redirect from ops-api |
| 503 | Service Unavailable | Cache-passthrough endpoint where cache has never been populated, cached payload is corrupt / rejected by validation, 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) |
| 504 | Gateway Timeout | Pages /_site-data/* or /api/admin/* proxy timed out waiting for its Worker upstream (10 s default; 20 s for ops /api/status and /api/status-history; 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).
GETis accepted for read endpoints (plus admin debug/status endpoints,GET /api/backfill-dews, and dry-run repair previews forGET /api/backfill-dews?repair=...&dry-run=true).POSTis accepted for mutating admin endpoints,POST /api/feedback,POST /api/api-key-requests,POST /api/api-key-requests/verify,POST /api/telegram-webhook,POST /api/telegram-mini-app/session, andPOST /api/telegram-mini-app/mutate.GET, POSTis accepted on/api/api-keysso operators can list keys and create a new key through the same route.POSTis accepted on/api/api-keys/:id/update,/api/api-keys/:id/deactivate, and/api/api-keys/:id/rotate./api/audit-depeg-historyallowsGETonly with?dry-run=true; otherwise it isPOST-only./api/backfill-dewsallowsGETfor the historical backtest and forrepair=...&dry-run=truepreviews; mutating repair runs arePOST-only.- Unknown public
/api/*requests can return401first when the API key is missing or invalid. After lane auth succeeds, unregistered paths return404because no route dependencies can be hydrated. Once a static or dynamic route family is registered, known paths with disallowed methods return405withAllow; unsupported verbs on known endpoint families return405withAllow: GET, POST.
The same shared endpoint descriptors now also carry static worker dependency-hydration hints consumed by worker/src/routes/registry.ts, where the worker binds shared endpoint keys directly to handlers through a single static route-definition list. That keeps endpoint metadata, router behavior, method guards, admin status-page actions, and worker-side static route wiring aligned from one source of truth plus one worker binding table.
Public Endpoints
Unless an endpoint section explicitly says Authentication: exempt, routes in this section require X-API-Key when called on https://api.pharos.watch.
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.
| Method | Path | Summary | Tags | Auth | Parameters | Status codes |
|---|---|---|---|---|---|---|
| GET | /api/blacklist | Blacklist events | Blacklist | X-API-Key | stablecoin?, chain?, chainId?, eventType?, q?, sortBy?, sortDirection?, limit?, offset?, includeTotal? | 200, 400, 401, 429, 503 |
| GET | /api/blacklist-summary | Blacklist summary | Blacklist | X-API-Key | — | 200, 400, 401, 429, 503 |
| GET | /api/bluechip-ratings | Bluechip ratings | Risk | X-API-Key | — | 200, 400, 401, 429, 503 |
| GET | /api/chains | Chains | Chains | X-API-Key | — | 200, 400, 401, 429, 503 |
| GET | /api/daily-digest | Daily digest | Digest | X-API-Key | — | 200, 400, 401, 429, 503 |
| GET | /api/depeg-events | Depeg events | Peg Monitoring | X-API-Key | stablecoin?, limit?, offset?, cursor?, active?, includeTotal?, includePending? | 200, 400, 401, 429, 503 |
| GET | /api/depeg-resolver | Depeg Duration Resolver | Risk, Peg Monitoring | X-API-Key | — | 200, 400, 401, 429, 503 |
| GET | /api/depeg-resolver-review | Depeg Duration Resolver Reviewer | Risk, Peg Monitoring | X-API-Key | — | 200, 400, 401, 429, 503 |
| GET | /api/dex-liquidity | DEX liquidity | Liquidity | X-API-Key | — | 200, 400, 401, 429, 503 |
| GET | /api/dex-liquidity-history | DEX liquidity history | Liquidity, History | X-API-Key | stablecoin, days? | 200, 400, 401, 429, 503 |
| GET | /api/digest-archive | Digest archive | Digest | X-API-Key | — | 200, 400, 401, 429, 503 |
| GET | /api/digest-snapshot | Digest snapshot | Digest | X-API-Key | date | 200, 400, 401, 429, 503 |
| GET | /api/events | Tape events | Risk | X-API-Key | type?, class?, coin?, pegCurrency?, chain?, q?, severityFloor?, since?, until?, cursor?, limit?, includeTotal? | 200, 400, 401, 429, 503 |
| GET | /api/health | Health check | Health | none | — | 200, 400, 503 |
| GET | /api/mint-burn-events | Mint and burn events | Flows | X-API-Key | stablecoin, direction?, chain?, burnType?, scope?, minAmount?, limit?, offset?, cursor?, includeTotal? | 200, 400, 401, 429, 503 |
| GET | /api/mint-burn-flows | Mint and burn flows | Flows | X-API-Key | stablecoin?, hours? | 200, 400, 401, 429, 503 |
| GET | /api/non-usd-share | Non-USD share | Market Structure, History | X-API-Key | days? | 200, 400, 401, 429, 503 |
| GET | /api/peg-summary | Peg summary | Peg Monitoring | X-API-Key | — | 200, 400, 401, 429, 503 |
| GET | /api/public-status-history | Public status history | Status | X-API-Key | limit?, window? | 200, 400, 401, 429, 503 |
| GET | /api/redemption-backstops | Redemption backstops | Risk, Reserves | X-API-Key | — | 200, 400, 401, 429, 503 |
| GET | /api/report-cards | Report cards | Risk | X-API-Key | — | 200, 400, 401, 429, 503 |
| GET | /api/safety-score-history | Safety score history | Risk, History | X-API-Key | stablecoin, days? | 200, 400, 401, 429, 503 |
| GET | /api/snapshot/{date}/stablecoin/{stablecoinId} | Public snapshot projection for a single coin | Digest, Stablecoins, History | X-API-Key | date, stablecoinId | 200, 400, 401, 429, 503 |
| GET | /api/snapshots/{date}.json | Public snapshot for a single day | Digest, History | X-API-Key | date | 200, 400, 401, 429, 503 |
| GET | /api/snapshots/index | Public snapshot index | Digest | X-API-Key | — | 200, 400, 401, 429, 503 |
| GET | /api/stability-index | Pharos Stability Index | Risk | X-API-Key | detail? | 200, 400, 401, 429, 503 |
| GET | /api/stablecoin-charts | Stablecoin charts | Stablecoins, History | X-API-Key | — | 200, 400, 401, 429, 503 |
| GET | /api/stablecoin-reserves/{stablecoinId} | Stablecoin reserves | Stablecoins, Reserves | X-API-Key | stablecoinId | 200, 400, 401, 429, 503 |
| GET | /api/stablecoin-summary/{stablecoinId} | Stablecoin summary | Stablecoins | X-API-Key | stablecoinId | 200, 400, 401, 429, 503 |
| GET | /api/stablecoin/{stablecoinId} | Stablecoin detail | Stablecoins | X-API-Key | stablecoinId | 200, 400, 401, 429, 503 |
| GET | /api/stablecoins | List stablecoins | Stablecoins | X-API-Key | — | 200, 400, 401, 429, 503 |
| GET | /api/stress-signals | Stress signals | Risk, Peg Monitoring | X-API-Key | stablecoin?, days? | 200, 400, 401, 429, 503 |
| GET | /api/supply-history | Supply history | History | X-API-Key | stablecoin, days? | 200, 400, 401, 429, 503 |
| GET | /api/telegram-pulse | Telegram pulse | Status | X-API-Key | — | 200, 400, 401, 429, 503 |
| GET | /api/usds-status | USDS freeze status | Risk | X-API-Key | — | 200, 400, 401, 429, 503 |
| GET | /api/yield-adapter-manifest | Yield adapter manifest | Yield | X-API-Key | — | 200, 400, 401, 429, 503 |
| GET | /api/yield-history | Yield history | Yield, History | X-API-Key | stablecoin, days?, mode?, sourceKey? | 200, 400, 401, 429, 503 |
| GET | /api/yield-rankings | Yield rankings | Yield | X-API-Key | — | 200, 400, 401, 429, 503 |
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
| Field | Type | Description |
|---|---|---|
id | string | Pharos stablecoin ID |
name | string | Full name (e.g. "Tether") |
symbol | string | Ticker (e.g. "USDT") |
geckoId | string | null | CoinGecko ID (normalized output key; upstream DefiLlama uses gecko_id) |
pegType | string | DefiLlama peg type (e.g. "peggedUSD", "peggedEUR") |
pegMechanism | string | "fiat-backed", "crypto-backed-algorithmic", etc. |
priceSource | string | Source label for the current price ("defillama-list", "coingecko", composite agreement labels such as "binance+coingecko+kraken", "geckoterminal", "protocol-redeem", "dexscreener", etc.). For high-confidence consensus this label can describe the full agreeing cluster even when the published price is the cluster median rather than one member's raw mark. When no usable current price survives validation, the cache keeps price = null and serializes priceSource = "missing" for contract stability. |
priceConfidence | string | null | Price confidence level: "high" (cross-validated agreement), "single-source", "low" (sources diverge), "fallback" (enrichment pipeline) |
priceUpdatedAt | number | null | Compatibility timestamp for the current price; mirrors the effective observation time when available |
priceObservedAt | number | null | Unix seconds for the effective observation time attached to the selected source price; interpret alongside priceObservedAtMode |
priceObservedAtMode | "upstream" | "local_fetch" | "unknown" | null | Whether priceObservedAt came from source-native freshness metadata, local fetch time, or legacy/unknown provenance |
priceSyncedAt | number | null | Unix seconds when Pharos selected and wrote the current price during the sync |
supplySource | string | undefined | Supply data source: "defillama", "defillama-history-gap-fill" (used when a tracked DefiLlama live row collapses to zero supply but recent DefiLlama chart history still has a fresh non-zero total), "coingecko-gap-fill" (used when tracked deployments are missing from DefiLlama chain coverage and CoinGecko repairs the total/history buckets), "coingecko-fallback", "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) |
price | number | null | Current price in USD. For high-confidence consensus this is the median of the winning agreeing cluster; for single-source, low-confidence, or fallback outcomes it is the selected source price. |
circulating | Record<string, number> | Current supply in USD, keyed by pegType (e.g. { "peggedUSD": 138000000 }) |
circulatingPrevDay | Record<string, number> | Supply 24 h ago |
circulatingPrevWeek | Record<string, number> | Supply 7 days ago |
circulatingPrevMonth | Record<string, number> | Supply ~30 days ago |
chainCirculating | Record<string, ChainCirculating> | Per-chain breakdown. For "coingecko-gap-fill" and "defillama-history-gap-fill" assets this remains DefiLlama-led unless the missing total can be allocated safely to one tracked chain, so the per-chain sum may be a lower bound on total supply. |
chains | string[] | List of chain names where the token is deployed |
contracts | ContractDeployment[] | undefined | Curated 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. |
consensusSources | string[] | Source names that returned a valid price for this coin during the sync cycle. Defaults to [] when absent. |
priceSourceConfidenceProfile | PriceSourceConfidenceProfile | undefined | Present 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. |
agreeSources | string[] | undefined | Compatibility alias for agreeing/current price sources when present |
ContractDeployment
| Field | Type | Description |
|---|---|---|
chain | string | Pharos chain identifier (e.g. "ethereum", "arbitrum", "solana"). |
address | string | Token contract address as published by the issuer. |
decimals | number | Token decimals. |
PriceSourceConfidenceProfile
| Field | Type | Description |
|---|---|---|
activeDexLanes | number | Count of accepted protocol-specific DEX lanes such as balancer-dex or raydium-dex. |
freshestDexLaneAgeSec | number | null | Age in seconds of the freshest accepted DEX lane when the source carried observation metadata. |
aggregateLaneOnly | boolean | true 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
| Field | Type | Description |
|---|---|---|
date | number | Unix timestamp (seconds) |
totalCirculatingUSD | Record<string, number> | Supply in USD per pegType key |
totalCirculating | Record<string, number> | Supply in native units per pegType key |
For regular stablecoins the response still includes the raw DefiLlama detail fields, but the worker now also materializes totalCirculatingUSD and totalCirculating on each token row for contract consistency. Commodity and CG-only tokens are returned directly in the normalized shape above.
For non-USD pegs, totalCirculating remains in native units while totalCirculatingUSD is converted to USD using the current token price before caching, so the USD field always reflects market cap regardless of peg type.
GET /api/stablecoin-summary/:id
Lightweight per-coin snapshot sourced from cached stablecoins data. Designed for integrators that need current price/supply context without transferring full /api/stablecoin/:id history payloads.
Browser surfaces on pharos.watch and ops.pharos.watch should reach this route through same-origin /_site-data/stablecoin-summary/:id, which proxies onto the internal site-api lane instead of the external API-key lane.
Path parameter: :id — Pharos stablecoin ID.
Cache: realtime — X-Data-Age and Warning headers included.
Error responses: 503 when the shared stablecoins cache is missing or structurally corrupt; 404 when the requested coin ID is absent from an otherwise valid cache snapshot.
Response
{
"id": "usdt-tether",
"name": "Tether",
"symbol": "USDT",
"pegType": "peggedUSD",
"pegMechanism": "fiat-backed",
"priceUsd": 1.0001,
"priceSource": "coingecko+defillama-list",
"priceConfidence": "high",
"supplySource": "defillama",
"supplyByPegUsd": { "peggedUSD": 183883564940.52 },
"supplyUsd": {
"current": 183883564940.52,
"prevDay": 183697699496.48,
"prevWeek": 183673067145.19,
"prevMonth": 185316486043.16,
"change1d": 185865444.03,
"change7d": 210497795.33,
"change30d": -1432921102.64
},
"chainCount": 17,
"updatedAt": 1772718367
}
| Field | Type | Description |
|---|---|---|
id | string | Pharos stablecoin ID |
name | string | Asset name |
symbol | string | Ticker symbol |
pegType | string | Peg type key (peggedUSD, peggedEUR, etc.) |
pegMechanism | string | Backing/mechanism classification |
priceUsd | number | null | Current price in USD |
priceSource | string | Price source identifier. When priceUsd is null, this may be "missing" to indicate that no usable current price survived validation. |
priceConfidence | string | null | Price confidence label |
supplySource | string | null | Supply source identifier |
supplyByPegUsd | Record<string, number> | Current supply by peg bucket (USD) |
supplyUsd | object | Aggregate USD supply values and deltas (current, prevDay, prevWeek, prevMonth, change1d, change7d, change30d) |
chainCount | number | Number of chains where the asset is deployed |
updatedAt | number | Unix seconds of the stablecoins snapshot used for this response |
GET /api/non-usd-share
Returns historical non-USD stablecoin market share data from supply_history, split into commodity-pegged (gold/silver) and 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.
| Param | Type | Default | Constraints | Description |
|---|---|---|---|---|
days | number | 5000 | min 30, max 5000 | Lookback 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 }>
| Field | Type | Description |
|---|---|---|
date | number | Unix seconds (snapshot date) |
commodityShare | number | Commodity-pegged share as % of total supply |
fiatNonUsdShare | number | Non-commodity non-USD share as % of total supply, using the legacy field name |
commodity | number | Commodity-pegged circulating USD |
fiatNonUsd | number | Non-commodity non-USD circulating USD, using the legacy field name |
total | number | Total circulating USD across all tracked coins |
GET /api/chains
Returns chain-level stablecoin aggregates with Chain Health Scores. Computed on-the-fly from the stablecoins cache and report-card cache (two D1 reads) — no dedicated chain table is required for the live leaderboard. The response body also carries _meta, so the frontend can distinguish fresh, degraded, and missing-dependency states without inferring freshness from fetch timing alone.
Cache: realtime — public, s-maxage=60, max-age=10
Freshness threshold: 1800 seconds. Returns 503 when the stablecoins cache is unavailable or structurally corrupt. When dependent snapshots lag, the endpoint stays readable but the body _meta.status degrades and the frontend surfaces stale-data warnings.
Status codes:
| Status | Meaning |
|---|---|
| 200 | Chain aggregates computed successfully; freshness may still be degraded in _meta |
| 503 | Stablecoins cache unavailable (missing or structurally corrupt) |
Response (ChainsResponse):
{
"_meta": {
"updatedAt": 1710500000,
"ageSeconds": 42,
"status": "fresh",
"dependencies": {
"reportCards": {
"updatedAt": 1710499800,
"ageSeconds": 242,
"status": "fresh"
}
}
},
"chains": [ChainSummary, ...],
"globalTotalUsd": 230000000000,
"chainAttributedTotalUsd": 218000000000,
"unattributedTotalUsd": 12000000000,
"globalChange24hPct": 0.0012,
"globalChange7dPct": 0.0045,
"globalChange30dPct": 0.018,
"updatedAt": 1710500000,
"healthMethodologyVersion": "1.2"
}
| Field | Type | Description |
|---|---|---|
chains | ChainSummary[] | Chains sorted by totalUsd descending |
globalTotalUsd | number | Total tracked stablecoin supply in USD, matching GET /api/stablecoins aggregate supply |
chainAttributedTotalUsd | number | Supply that the source data attributes to concrete chains in USD |
unattributedTotalUsd | number | Positive residual between tracked supply and chain-attributed supply in USD |
globalChange24hPct | number | 24h change for total tracked stablecoin supply as a decimal share |
globalChange7dPct | number | 7d change for total tracked stablecoin supply as a decimal share |
globalChange30dPct | number | 30d change for total tracked stablecoin supply as a decimal share |
updatedAt | number | Unix epoch seconds of the underlying stablecoins snapshot |
healthMethodologyVersion | string | Chain Health Score methodology version (currently "1.2") |
_meta.dependencies.reportCards is present when the endpoint can determine report-card freshness. When that dependency is stale or unavailable, healthScore degrades to null and the route UI surfaces the dependency reason instead of pretending the chain is fully fresh.
ChainSummary fields:
| Field | Type | Description |
|---|---|---|
id | string | Canonical chain identifier (DefiLlama chain name) |
name | string | Human-readable chain name |
logoPath | string | null | Path to chain logo asset |
type | "evm" | "tron" | "other" | Chain runtime family from CHAIN_META |
totalUsd | number | Total stablecoin supply on this chain in USD |
change24h | number | Absolute 24h supply change in USD |
change24hPct | number | 24h supply change as a percentage |
change7d | number | Absolute 7d supply change in USD |
change7dPct | number | 7d supply change as a percentage |
change30d | number | Absolute 30d supply change in USD |
change30dPct | number | 30d supply change as a percentage |
stablecoinCount | number | Number of distinct stablecoins on this chain |
dominantStablecoin | { id, symbol, share } | Largest stablecoin by supply on the chain |
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 |
dominanceShare | number | Chain share of globalTotalUsd (0–1); chain rows may sum below 1 when source data has unattributed supply |
healthScore | number | null | Chain Health Score 0–100, or null if insufficient data |
healthBand | string | null | Health band label: "robust" (80–100), "healthy" (60–79), "mixed" (40–59), "fragile" (20–39), "concentrated" (0–19) |
healthFactors | ChainHealthFactors | Raw sub-factor scores (0–100 each; quality may still be null) |
ChainHealthFactors fields:
| Field | Type | Description |
|---|---|---|
concentration | number | HHI-based supply concentration score (higher = more diverse) |
quality | number | null | Supply-weighted average stablecoin quality from report-card grades; null when rated supply coverage is below 50% by value |
chainEnvironment | number | Resilience-tier score for the chain itself (100 tier 1, 60 tier 2, 20 tier 3) |
pegStability | number | Supply-weighted average peg deviation score |
backingDiversity | number | Shannon entropy of the active backing split across the chain (rwa-backed vs crypto-backed) |
GET /api/stablecoin-reserves/:id
Returns the resolved reserve presentation for a stablecoin with liveReservesConfig.
- Unknown IDs or coins without live reserve support return
404. - Live-enabled coins return
200even before the first successful sync; the payload includes fallback mode + sync state. - This endpoint powers the stablecoin detail-page reserve card. The same underlying live-reserve dataset also feeds report-card collateral quality, reserve-drift monitoring, and
/status, but those surfaces read D1-backed reserve snapshots directly rather than calling this endpoint. - A response is treated as
liveonly when the stored reserve snapshot matches the latest successful sync state and passes strict integrity validation; orphaned partial writes or corrupt stored snapshots fall back to the curated/template presentation instead of presenting malformed live data as authoritative. - Successful responses are covered by the shared
StablecoinReservesResponseSchema; frontend API clients validate200payloads strictly while preserving404as the not-live-enabled/null path.
Cache: dynamic
- Live snapshots: slow (
public, s-maxage=3600, max-age=300) live-stalesnapshots:public, s-maxage=1800, max-age=120- Bootstrap / fallback / unavailable presentations: shorter (
public, s-maxage=300, max-age=60) so pre-sync fallback responses do not stay pinned at the edge after the first successful live sync
Response (200):
| Field | Type | Description |
|---|---|---|
stablecoinId | string | Pharos coin ID |
mode | string | One of live, live-stale, curated-fallback, template-fallback, unavailable. This is snapshot transport/freshness state, not the user-facing reserve badge semantics. |
reserves | ReserveSlice[] | Reserve slices currently being shown to the user |
estimated | boolean | true only when using the classification template fallback |
liveAt | number? | Unix seconds of the last successful live snapshot. Present only when live data exists |
source | string? | Adapter key (for example "infinifi", "m0", "openeden-usdo", or "accountable"). Present only when live data exists |
displayUrl | string? | Curated click-through page shown as Source in the UI. Present only when configured |
evidenceUrls | string[]? | Adapter-emitted evidence URLs for the authoritative live snapshot, shown separately as Evidence links when available |
displayBadge | object? | User-facing reserve badge semantics for authoritative live snapshots (live, curated-validated, or proof) |
metadata | object? | Adapter snapshot metadata for authoritative live snapshots. This can include feed-specific context such as yieldBasisCollateralPct for crvusd; adapter-specific metadata and nested details remain passthrough |
provenance | object? | Evidence-quality envelope for authoritative live snapshots (evidenceClass, sourceModel, optional freshnessMode, scoringEligible) |
sync | object? | Live sync state (status, bootstrap, stale, lastAttemptedAt, lastSuccessAt, warnings, lastError, 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:
displayUrlis the curated reserve-card destinationevidenceUrlsare adapter-emitted URLs tied to the authoritative live snapshot metadata- some live feeds expose only
displayUrl, while others expose both
When present, displayBadge has:
| Field | Type | Description |
|---|---|---|
kind | "live" | "curated-validated" | "proof" | User-facing reserve badge classification |
label | string | Badge label rendered by the frontend (Live, Curated-Validated, or Proof) |
When present, provenance has:
| Field | Type | Description |
|---|---|---|
evidenceClass | "independent" | "static-validated" | "weak-live-probe" | Evidence class used for scoring and provenance. This is related to, but not identical to, the UI badge semantics. |
sourceModel | "dynamic-mix" | "validated-static" | "single-bucket" | Structural shape of the reserve feed |
freshnessMode | "verified" | "unverified" | "not-applicable" | undefined | Explicit freshness policy when the adapter emits one |
scoringEligible | boolean | Whether this exact snapshot is currently eligible for collateral-quality passthrough |
Response (404): 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
}
}
]
| Field | Type | Description |
|---|---|---|
date | number | Unix timestamp (seconds). Historical points are downsampled cache entries; the trailing point may be a fresher live stablecoins snapshot timestamp |
totalCirculatingUSD | Record<string, number> | Aggregate supply in USD per peg type |
GET /api/blacklist
Freeze, blacklist, block/unblock, account-pause, and token-destruction events for symbols in the shared BLACKLIST_STABLECOINS set. EURC mirror-zero rows are preserved with suppression metadata and excluded from public aggregates. Data is sourced from on-chain logs via Etherscan, Tron, and EVM RPCs.
Cache: realtime
Freshness note: X-Data-Age / Warning track the latest successful 6-hourly sync-blacklist writer timestamp. Public freshness stays fresh through that 6-hour budget and only degrades once the scheduled blacklist sync is actually late.
Query parameters
| Param | Type | Default | Description |
|---|---|---|---|
stablecoin | string | — | Filter by uppercase blacklist-tracker symbol from the full BLACKLIST_STABLECOINS set in shared/types/market.ts (for example USDT, not usdt-tether) |
chain | string | — | Filter by exact chain display name (e.g. Ethereum, Tron) |
chainId | string | — | Filter 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 |
eventType | string | — | Filter by type: blacklist, unblacklist, destroy |
q | string | — | Case-insensitive address substring search |
sortBy | string | date | Sort field: date, stablecoin, chain, event |
sortDirection | string | desc | Sort direction: asc, desc |
limit | integer | 1000 | Max results (0–1000; 0 maps to default 1000) |
offset | integer | 0 | Pagination offset |
includeTotal | boolean | true | When 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
| Field | Type | Description |
|---|---|---|
id | string | Composite ID: {chainId}-{txHash}-{logIndex} |
stablecoin | string | Token symbol (USDC, USDT, etc.) |
chainId | string | Stable chain identifier from the shared chain registry (e.g. "ethereum", "tron"). This is the preferred join key for integrations |
chainName | string | Human-readable chain name (e.g. "Ethereum") |
eventType | string | "blacklist", "unblacklist", or "destroy" |
address | string | Affected address (EVM 0x… or Tron T…) |
amountNative | number | null | Canonical token-native amount recovered from event data or historical balance lookup |
amountUsdAtEvent | number | null | Event-time USD value when Pharos can justify one |
amountSource | string | event, historical_balance, current_balance_snapshot, derived, legacy_migration, or unavailable |
amountStatus | string | resolved, recoverable_pending, permanently_unavailable, provider_failed, ambiguous |
txHash | string | Transaction hash |
blockNumber | number | Block number |
timestamp | number | Unix seconds |
methodologyVersion | string | Methodology version attributed to this event row |
contractAddress | string | null | Emitting token contract when known |
configKey | string | null | Internal tracker config identity ({chainId}-{contract}) |
eventSignature | string | null | Human-readable event signature/name when known |
eventTopic0 | string | null | Raw EVM topic0 when applicable |
suppressionReason | string | null | Always null or absent on public rows because /api/blacklist filters suppression_reason IS NULL; non-null reasons are retained only on internal/audit DB rows excluded from public aggregates/events |
explorerTxUrl | string | Block explorer URL for the transaction |
explorerAddressUrl | string | Block explorer URL for the address |
methodology
| Field | Type | Description |
|---|---|---|
version | string | Methodology version of the latest returned event in this response |
versionLabel | string | Display label (e.g. "v3.2") |
currentVersion | string | Latest methodology version |
currentVersionLabel | string | Display label for latest methodology version |
changelogPath | string | Relative URL to the methodology changelog page |
asOf | number | Unix timestamp of latest event used for freshness |
isCurrent | boolean | Whether version matches currentVersion |
GET /api/blacklist-summary
Server-side aggregates for the Blacklist Tracker overview cards, chart, and filter options. This lets the frontend render summary state without hydrating the full blacklist history first.
stats.destroyedTotal remains an event-history total. stats.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
| Param | Type | Default | Description |
|---|---|---|---|
stablecoin | string | — | Filter by Pharos stablecoin ID |
active | "true" | — | When "true", return only ongoing (unresolved) depeg events |
limit | integer | 100 | Max results (1–1000) |
offset | integer | 0 | Pagination offset (0–50,000); cannot be combined with cursor |
cursor | string | — | Opaque keyset cursor from nextCursor |
includeTotal | boolean | true | When false, skips the exact COUNT(*); total becomes a page lower bound and totalExact is false |
includePending | boolean | false | When 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
| Field | Type | Description |
|---|---|---|
id | number | Auto-increment DB ID |
stablecoinId | string | Pharos stablecoin ID |
symbol | string | Token symbol |
pegType | string | DefiLlama peg type (e.g. "peggedUSD") |
direction | "above" | "below" | Whether the price was above or below the peg |
peakDeviationBps | number | Largest deviation observed (basis points, signed; negative = below peg, positive = above peg) |
startedAt | number | Unix seconds when depeg was first detected |
endedAt | number | null | Unix seconds when price returned to peg; null if still active |
startPrice | number | Price at event start (USD) |
peakPrice | number | null | Price at worst deviation |
recoveryPrice | number | null | Price at recovery |
pegReference | number | Reference peg value used (USD) |
source | "live" | "backfill" | Detection method |
confirmationSources | string | null | Composite provenance tag recorded when a pending depeg was promoted. Components (joined with +): the off-chain source label (CoinGecko, DefiLlama, or NativePeg(<currency>)), DEX, CEX, Pool. Example: "DEX+CEX" or "CoinGecko+Pool". null for events that bypassed the pending lane (small-cap authoritative direct-insert and historical backfill rows). |
pendingReason | string | null | Composite reason the incident entered the pending lane, e.g. "large-cap", "low-confidence", "large-cap+low-confidence", "extreme-move". null when the event did not enter pending. |
provenance | object | null | Public 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
| Field | Type | Description |
|---|---|---|
stablecoinId | string | Pharos stablecoin ID |
symbol | string | Token symbol |
direction | "above" | "below" | Pending deviation direction |
firstSeenAt | number | Unix seconds when the pending incident was first observed |
lastSeenAt | number | Unix seconds when the pending incident was last refreshed |
firstSeenBps | number | First observed deviation in basis points |
lastSeenBps | number | Most recent observed deviation in basis points |
peakSeenBps | number | Worst observed pending deviation in basis points |
reason | string | Pending lane reason, e.g. "large-cap" or "large-cap+low-confidence" |
ageSec | number | Seconds elapsed since firstSeenAt |
expiresAt | number | Unix seconds when the pending row expires if it is not confirmed |
availableConfirmationCategories | string[] | Confirmation categories currently derivable from public metadata / D1 snapshots |
missingConfirmationCategories | string[] | Expected confirmation categories not currently derivable from public metadata / D1 snapshots |
methodology
| Field | Type | Description |
|---|---|---|
version | string | Methodology version attributed from the latest returned event timestamp |
versionLabel | string | Display label (e.g. "v5.94") |
currentVersion | string | Latest methodology version |
currentVersionLabel | string | Display label for latest methodology version |
changelogPath | string | Relative URL to the methodology changelog page |
asOf | number | Unix timestamp used for methodology attribution |
isCurrent | boolean | Whether version matches currentVersion |
GET /api/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
| State | Meaning |
|---|---|
pending_lock | Incident is active and younger than the 24h public lock point. Live facts and lock metadata may render; no verdict or duration is exposed. |
lock_deferred | The 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_pending | A lock outcome sealed, but first-publication manifest finalization has not succeeded. The sealed outcome stays hidden until publication. |
frozen | First-published official prediction. Shows frozen verdict/duration, lock timestamp, lock timing, anchored duration, and live overlay facts separately. |
no_call | Healthy lock run had insufficient row-level signal. Shows missing inputs and lock metadata, not a recovery/terminal verdict. |
invalidated | Append-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
| Field | Type | Meaning |
|---|---|---|
summary.headline.recoveryLikelihoodAccuracyPct | number | null | Strict scored DDR recovery-verdict accuracy over first-published frozen predictions. |
summary.headline.meanSignedDurationErrorSec | number | null | Mean 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.meanAbsoluteDurationErrorSec | number | null | Mean absolute duration miss for the same scored recovered rows. |
summary.headline.durationScoredCount | number | Number of recovered, duration-scored rows included in duration-error averages. |
summary.headline.predictionRatePct | number | null | Share of eligible finalized incidents that received a published prediction/no-call decision. |
summary.headline.finalizedCoveragePct | number | null | Share of the policy universe assigned to a finalized/public coverage state. |
summary.headline.noCallRatePct | number | null | Share of finalized lock outcomes that became no-calls. |
summary.headline.invalidatedPct | number | null | Share 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
| Field | Type | Description |
|---|---|---|
id | string | Pharos stablecoin ID |
symbol | string | Token symbol |
name | string | Full name |
pegType | string | DefiLlama peg type |
pegCurrency | string | Peg currency code (USD, EUR, GOLD, etc.) |
governance | string | "centralized", "centralized-dependent", "decentralized" |
currentDeviationBps | number | null | Live price deviation from peg (basis points, signed). null for NAV / non-fixed-peg rows, for coins with current supply below the live depeg-event floor, or when price / peg-reference inputs are missing. |
depegEventCoverageLimited | boolean | Present when the coin's current supply is below the live depeg-event floor ($1M). Use this to distinguish "below coverage floor" from generic missing-price cases when currentDeviationBps is null. |
priceSource | string | Primary price source label used for current deviation (defillama-list, coingecko, composite agreement labels such as binance+coingecko+kraken, protocol-redeem, defillama-contract, coinmarketcap, dexscreener, cached, etc.). High-confidence consensus can expose the agreeing cluster label even when the published price is the cluster median. |
priceConfidence | "high" | "single-source" | "low" | "fallback" | null | Confidence tier attached to the primary price input |
priceUpdatedAt | number | null | Compatibility timestamp for the primary price; now mirrors the effective observation time rather than the cache-write time |
priceObservedAt | number | null | Unix seconds for the effective observation time attached to the selected primary price; interpret alongside priceObservedAtMode |
priceObservedAtMode | "upstream" | "local_fetch" | "unknown" | null | Whether priceObservedAt came from source-native freshness metadata, local fetch time, or legacy/unknown provenance |
priceSyncedAt | number | null | Unix seconds when Pharos selected and wrote the primary price during the sync |
primaryTrust | "authoritative" | "confirm_required" | "unusable" | Whether the current primary price is trusted to mutate live depeg state directly |
pegScore | number | null | Composite peg score 0–100 (higher = more stable) |
pegPct | number | % of tracked time within ±100 bps |
severityScore | number | Severity sub-score (0–100) |
spreadPenalty | number | Spread/liquidity penalty applied to score |
eventCount | number | Number of depeg events in the 4-year window |
worstDeviationBps | number | null | Worst single deviation seen (basis points) |
activeDepeg | boolean | Whether a depeg event is currently open |
lastEventAt | number | null | Unix seconds of most recent depeg event |
trackingSpanDays | number | Days of history used for score computation |
methodologyVersion | string | Methodology version attributed to this coin snapshot |
dexPriceCheck | DexPriceCheck | null | Optional cross-validation against DEX price (shown when coin supply is at or above the live depeg-event floor, DEX data is ≤ 60 minutes old, and aggregate source TVL is ≥ $250K) |
consensusSources | string[] | Source names that returned a valid price for this coin. Defaults to [] when absent. |
agreeSources | string[] | undefined | Compatibility alias for agreeing/current price sources when present |
DexPriceCheck
| Field | Type | Description |
|---|---|---|
dexPrice | number | DEX-derived price (USD) |
dexDeviationBps | number | DEX price deviation from peg (basis points, signed) |
agrees | boolean | Whether primary and DEX prices are within 50 bps |
sourcePools | number | Number of DEX pools contributing to the price |
sourceTvl | number | Combined TVL of those pools (USD) |
PegSummaryStats
| Field | Type | Description |
|---|---|---|
activeDepegCount | number | Coins with an open depeg event |
medianDeviationBps | number | Median absolute deviation across rows with a live current deviation |
worstCurrent | { id, symbol, bps } | null | Coin with the largest current deviation among rows with a live current deviation |
coinsAtPeg | number | Rows with a live current deviation that are below their live depeg threshold (100 bps for USD pegs, 150 bps for non-USD pegs) |
totalTracked | number | Rows included in the live peg-status aggregate (currentDeviationBps !== null) |
depegEventsToday | number | Number of depeg events whose startedAt is in the current UTC day |
depegEventsYesterday | number | Number of depeg events whose startedAt is in the previous UTC day |
fallbackPegRates | string[] | (optional) pegType keys using stale FX fallback rates |
methodology — same fields and semantics as /api/depeg-events
GET /api/usds-status
Sky/USDS protocol status — whether the freeze module is currently active.
Cache: standard — X-Data-Age and Warning headers included.
Response
{
"freezeActive": false,
"implementationAddress": "0x1923dfee706a8e78157416c29cbccfde7cdf4102",
"lastChecked": 1771809338,
"_meta": { "updatedAt": 1710500000, "ageSeconds": 42, "status": "fresh" }
}
| Field | Type | Description |
|---|---|---|
freezeActive | boolean | Whether the USDS freeze module is currently enabled |
implementationAddress | string | Address of the current USDS implementation contract |
lastChecked | number | Unix seconds when this was last fetched on-chain |
GET /api/bluechip-ratings
Safety ratings from bluechip.org for covered stablecoins. Updated daily at 08:05 UTC.
Cache: slow — X-Data-Age and Warning headers included.
Response: Object keyed by Pharos stablecoin ID, plus top-level _meta freshness metadata.
{
"usdt-tether": BluechipRating,
"usdc-circle": BluechipRating,
"_meta": { "updatedAt": 1710500000, "ageSeconds": 42, "status": "fresh" }
}
BluechipRating
| Field | Type | Description |
|---|---|---|
grade | string | Letter grade: "A+", "A", "A-", "B+" … "F" |
slug | string | Bluechip report slug (e.g. "usdt") |
collateralization | number | Collateralization percentage |
smartContractAudit | boolean | Whether an audit exists |
dateOfRating | string | ISO 8601 date of rating |
dateLastChange | string | null | ISO 8601 date of last grade change |
smidge | BluechipSmidge | Plain-text evaluation summaries (HTML stripped) |
BluechipSmidge — each field is string | null:
| Field | Description |
|---|---|
stability | Reserves management and stabilization mechanisms |
management | Personnel restrictions and track records |
implementation | Smart contract implementation assessment |
decentralization | Decentralization posture |
governance | Governance and redemption terms |
externals | External risk factors |
GET /api/dex-liquidity
DEX liquidity scores, pool breakdowns, source-confidence metadata, and on-chain DEX price data for all tracked stablecoins. Updated every 30 minutes. Trend data is only returned when a trusted historical baseline exists.
Cache: custom — public, s-maxage=300, max-age=300
Freshness note: In addition to stale-data warnings, this endpoint can also emit a Warning header when the latest sync-dex-liquidity run finished in degraded or error state and the API is serving the last successful dataset.
Response: Object keyed by Pharos stablecoin ID plus a __global__ aggregate sentinel row.
{
"usdt-tether": DexLiquidityData,
"usdc-circle": DexLiquidityData,
"__global__": DexLiquidityData
}
DexLiquidityData
| Field | Type | Description |
|---|---|---|
totalTvlUsd | number | Total DEX TVL (USD) |
totalVolume24hUsd | number | 24 h trading volume (USD) |
totalVolume7dUsd | number | 7-day trading volume (USD) |
poolCount | number | Number of liquidity pools |
pairCount | number | Number of unique trading pairs |
chainCount | number | Number of chains with active pools |
protocolTvl | Record<string, number> | TVL per DEX protocol (e.g. { "uniswap-v3": 100000 }) |
chainTvl | Record<string, number> | TVL per chain (e.g. { "Ethereum": 500000 }) |
topPools | DexLiquidityPool[] | Top 10 retained pools sorted by 24h volume, then TVL |
liquidityScore | number | null | Composite liquidity score 0–100 |
concentrationHhi | number | null | Herfindahl–Hirschman Index for pool concentration (0–1; lower = more distributed), computed from the full retained pool set before top-10 truncation |
depthStability | number | null | Pool depth stability metric |
tvlChange24h | number | null | % TVL change vs. 24 h ago |
tvlChange7d | number | null | % TVL change vs. 7 days ago |
updatedAt | number | Unix seconds of last cron update |
dexPriceUsd | number | null | DEX-derived price (USD) |
dexDeviationBps | number | null | DEX price deviation from peg (basis points, signed) |
priceSourceCount | number | null | Number of pools used for DEX price (all must meet the shared $50K observation floor) |
priceSourceTvl | number | null | Combined TVL of price-source pools (USD) |
priceSources | DexPriceSource[] | null | Aggregated price sources by protocol (for example one balancer or raydium entry per asset) |
effectiveTvlUsd | number | TVL after applying quality multipliers |
avgPoolStress | number | null | Average pool stress index on a 0–100 scale (0 = balanced, 100 = maximally stressed / imbalanced) |
weightedBalanceRatio | number | null | TVL-weighted balance ratio across pools |
organicFraction | number | null | Fraction of TVL from organic (non-incentivized) pools |
durabilityScore | number | null | Score for pool maturity and reliability |
coverageClass | "primary" | "mixed" | "fallback" | "legacy" | "unobserved" | null | Coverage-confidence classification for the retained pool set; primary includes pure dl and pure direct_api rows. The __global__ aggregate sentinel uses null. |
coverageConfidence | number | Evidence-weighted confidence (0-1) derived from retained-pool breadth, measured TVL share, and synthetic/decayed dependence |
liquidityEvidenceClass | "unobserved" | "measured" | "partial_measured" | "observed_unmeasured" | Explicit classification of whether liquidity is balance-measured versus only observed from TVL / price evidence |
hasMeasuredLiquidityEvidence | boolean | Whether any retained liquidity evidence for the row includes measured pool balances |
trendworthy | boolean | Whether this row is suitable for trend baselines (coverageConfidence >= 0.75, positive TVL, and primary/mixed coverage) |
sourceMix | Record<string, { poolCount: number; tvlUsd: number }> | TVL/pool-count mix across source families (dl, direct_api, cg_onchain, gecko_terminal, dexscreener, cg_tickers) |
balanceMeasuredTvlUsd | number | TVL denominator actually used for weightedBalanceRatio |
organicMeasuredTvlUsd | number | TVL denominator actually used for organicFraction |
scoreComponents | ScoreComponents | null | Breakdown of the composite liquidity score |
lockedLiquidityPct | number | null | TVL-weighted fraction of liquidity reported as locked by source pools |
methodologyVersion | string | Methodology version attributed to this row |
ScoreComponents
| Field | Type | Description |
|---|---|---|
tvlDepth | number | TVL depth sub-score |
volumeActivity | number | Volume activity sub-score |
poolQuality | number | Pool quality sub-score |
durability | number | Durability sub-score |
pairDiversity | number | Pair diversity sub-score |
DexLiquidityPool
| Field | Type | Description |
|---|---|---|
project | string | Protocol slug (e.g. "curve-dex", "uniswap-v3") |
chain | string | Chain name |
tvlUsd | number | Pool TVL (USD) |
symbol | string | Pool pair name (e.g. "USDC-USDT"), normalized to tracked tickers when direct-API sources only provide token addresses |
volumeUsd1d | number | 24 h volume (USD) |
poolType | string | Pool type (e.g. "curve-stableswap", "uniswap-v3-5bp") |
source | string | undefined | Canonical source family for this retained pool |
extra | object | undefined | Optional detailed pool metrics (A-factor, balance ratio, measurement flags, etc.) |
extra may include:
| Field | Type | Description |
|---|---|---|
amplificationCoefficient | number | undefined | Curve amplification coefficient (A) |
balanceRatio | number | undefined | Measured pool balance ratio from 0 to 1; Balancer weighted pools normalize against weights and Fluid uses official DexReservesResolver balances where deployed |
feeTier | number | undefined | Normalized fee tier in basis points |
balanceDetails | Array<{ symbol: string; balancePct: number; isTracked: boolean }> | undefined | Per-token USD composition shares used for balance tooltips/detail |
measurement | object | undefined | Per-pool provenance flags such as tvlMeasured, volumeMeasured, balanceMeasured, maturityMeasured, priceMeasured, synthetic, decayed, and capped |
DexPriceSource
| Field | Type | Description |
|---|---|---|
protocol | string | DEX protocol name |
chain | string | Chain name |
price | number | Price from this source |
tvl | number | TVL of this pool (USD) |
GET /api/dex-liquidity-history
Per-coin historical DEX liquidity snapshots. Snapshots are recorded daily (UTC midnight, first sync after day rollover). Baseline consumers should use coverageClass / coverageConfidence before treating a history point as trend-worthy.
Cache: slow — public, s-maxage=3600, max-age=300
Required query parameter
| Param | Type | Description |
|---|---|---|
stablecoin | string | Pharos stablecoin ID (required) |
Optional query parameters
| Param | Type | Default | Bounds | Description |
|---|---|---|---|---|
days | integer | 90 | 1–365 | Lookback window in days |
Response: Array sorted by date ascending.
[
{
"tvl": 1658000000,
"volume24h": 1700000000,
"score": 93,
"date": 1771500000,
"coverageClass": "mixed",
"coverageConfidence": 0.85,
"liquidityEvidenceClass": "partial_measured",
"hasMeasuredLiquidityEvidence": true,
"trendworthy": true,
"methodologyVersion": "3.1"
}
]
| Field | Type | Description |
|---|---|---|
tvl | number | Total DEX TVL snapshot (USD) |
volume24h | number | 24 h volume at time of snapshot (USD) |
score | number | null | Liquidity score at time of snapshot |
date | number | Unix seconds |
coverageClass | string | Snapshot confidence class (primary, mixed, fallback, legacy, unobserved) |
coverageConfidence | number | Snapshot confidence score |
liquidityEvidenceClass | string | Snapshot evidence class (measured, partial_measured, observed_unmeasured, unobserved) |
hasMeasuredLiquidityEvidence | boolean | Whether the snapshot qualifies as balance-measured liquidity evidence |
trendworthy | boolean | Whether the snapshot is suitable for trend baselines rather than informational use |
methodologyVersion | string | Methodology version attributed to this snapshot |
GET /api/supply-history
Per-coin circulating supply and price history. 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
| Param | Type | Description |
|---|---|---|
stablecoin | string | Pharos stablecoin ID (required) |
Optional query parameters
| Param | Type | Default | Bounds | Description |
|---|---|---|---|---|
days | integer | 365 | 1–5000 | Lookback window in days |
Response: Array sorted by date ascending.
[
{
"date": 1771500000,
"circulatingUsd": 138000000000,
"price": 1.0001
}
]
| Field | Type | Description |
|---|---|---|
date | number | Unix seconds |
circulatingUsd | number | Circulating supply in USD |
price | number | null | Price at snapshot time (USD); may be null for older records |
GET /api/daily-digest
Latest AI-generated market summary, produced daily at 08:05 UTC via the Claude API.
Cache: standard. When a digest exists, the response includes X-Data-Age and Warning freshness headers 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 }.
| Field | Type | Description |
|---|---|---|
digest | string | null | Tweet-ready summary (≤ 240 characters). null if no digest has been generated yet. |
digestTitle | string | null | Short headline for the digest |
digestExtended | string | null | Extended commentary for the website view |
generatedAt | number | Unix seconds when this digest was generated (present only when digest is non-null) |
editionNumber | number | null | Sequential daily digest number (present only when digest is non-null) |
riskSignal | DigestRiskSignal | null | Compact active-depeg risk summary parsed from stored digest input data |
changeSummary | DigestChangeSummary | null | Deterministic "what changed since yesterday" summary parsed from stored digest input data |
nextTriggers | DigestNextTrigger[] | null | Structured forward-looking threshold checks for tomorrow's digest |
forwardLookOutcomes | DigestForwardLookOutcome[] | null | Evaluation of the previous digest's next triggers against the latest input |
riskTape | DigestRiskTapeItem[] | null | Compact 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).
| Field | Type | Description |
|---|---|---|
digestText | string | Tweet-ready summary |
digestTitle | string | null | Short headline |
digestExtended | string | null | Extended commentary |
generatedAt | number | Unix seconds of generation time |
psiScore | number | null | PSI score parsed from archived digest input data |
psiBand | string | null | PSI condition band parsed from archived digest input data |
totalMcapUsd | number | null | Ecosystem market cap parsed from archived digest input data |
riskSignal | DigestRiskSignal | null | Compact active-depeg risk summary parsed from archived digest input data |
nextTriggers | DigestNextTrigger[] | null | Structured forward-looking threshold checks parsed from archived digest input data |
forwardLookOutcomes | DigestForwardLookOutcome[] | null | Evaluation of the previous digest's next triggers parsed from archived digest input data |
riskTape | DigestRiskTapeItem[] | null | Compact risk-state chips parsed from archived digest input data |
digestType | "daily" | "weekly" | Digest cadence for this archived entry |
editionNumber | number | Sequential edition number within that digest cadence |
DigestRiskSignal
| Field | Type | Description |
|---|---|---|
kind | "depeg" | Risk signal family; currently active-depeg context |
symbol | string | Stablecoin symbol |
bps | number | Signed basis-point deviation where available; archive badges display absolute value |
mcapUsd | number | null | Market 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 |
activeCount | number | Active depeg count from the stored digest input, when available |
date | string | null | Daily input date for weekly archive entries; null for latest daily responses |
Digest intelligence fields
| Field | Type | Description |
|---|---|---|
DigestRiskTapeItem.id | string | Stable identifier for the displayed tape item |
DigestRiskTapeItem.label | string | Short label such as PSI, Depegs, Gauge, DEWS, or Supply |
DigestRiskTapeItem.value | string | Already formatted compact value for display |
DigestRiskTapeItem.tone | "critical" | "warning" | "neutral" | "positive" | Presentation severity |
DigestRiskTapeItem.detail | string | Optional 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.thresholdLabel | string | Display string for the threshold |
DigestForwardLookOutcome.status | "hit" | "missed" | "pending" | Whether the prior trigger fired, failed, or still needs more data |
DigestChangeSummary.*Signals | array | Buckets 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
| Param | Type | Description |
|---|---|---|
date | string | Date in YYYY-MM-DD format, or YYYY-MM-DD-weekly for weekly recap pages (required) |
Response
{
"date": "2026-02-27",
"inputData": { "totalMcapUsd": 230000000000, "mcap7dDelta": 0.012, ... },
"prevInputData": { ... },
"depegEvents": [{ "stablecoinId": "usdt-tether", "symbol": "USDT", "direction": "below", "peakDeviationBps": -150, ... }],
"blacklistEvents": [{ "stablecoin": "USDT", "chainName": "Ethereum", "eventType": "blacklist", ... }]
}
| Field | Type | Description |
|---|---|---|
date | string | The requested date |
inputData | object | null | Digest input data (mcap, depegs, supply changes, PSI, digest intelligence fields) for this date |
prevInputData | object | null | Previous day's input data for delta computation |
depegEvents | array | Up to 20 depeg events active on that date, ordered by severity |
blacklistEvents | array | Up to 50 blacklist events on that date |
Error responses: 400 for missing/invalid date, 404 if no digest exists for that date.
GET /api/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
}
]
}
| Field | Type | Description |
|---|---|---|
snapshots | array | Snapshot index sorted newest first |
snapshots[].snapshotDate | string | UTC snapshot date in YYYY-MM-DD format |
snapshots[].methodologyVersions | Record<string, string>? | Methodology versions embedded in the snapshot |
snapshots[].contentHash | string | Snapshot payload hash used by dated ETags |
snapshots[].byteSize | number | Uncompressed JSON payload size in bytes |
snapshots[].createdAt | number | Snapshot 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
| Param | Type | Description |
|---|---|---|
date | string | UTC 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
| Param | Type | Description |
|---|---|---|
date | string | UTC snapshot date in YYYY-MM-DD form |
id | string | Canonical 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
}
}
| Field | Type | Description |
|---|---|---|
snapshotDate | string | Served snapshot date |
stablecoinId | string | Requested stablecoin ID |
generatedAt | number | Snapshot generation timestamp |
methodologyVersions | object? | Methodology versions embedded in the snapshot |
stablecoin | object | Stablecoin row from the dated public dataset |
scores.reportCard | object? | Matching report-card score, or null |
scores.psi | object? | Snapshot-level PSI object, or null |
scores.dews | object? | Matching DEWS stress-signal row, or null |
scores.liquidity | object? | 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"
}
}
| Field | Type | Description |
|---|---|---|
status | string | "healthy" / "degraded" / "stale" |
timestamp | number | Unix seconds at time of response |
warnings | string[] | Best-effort machine-readable warnings when health subqueries fail but the endpoint can still return a non-500 payload. Messages are sanitized for public output and do not include raw exception text, SQL fragments, or table names. |
caches | Record<string, CacheStatus> | Per-cache freshness status |
caches["fx-rates"] | CacheStatus | FX cache freshness plus source-cadence diagnostics (mode, sourceUpdatedAt, sourceAgeSeconds, sourceStatus, warning, consecutiveFallbackRuns) |
blacklist.totalEvents | number | Total events in blacklist table |
blacklist.missingAmounts | number | Events where amount is null (should be 0) |
blacklist.recentMissingAmounts | number | Missing-amount events inside the recent monitoring window used by status logic |
blacklist.recentWindowSec | number | Size of the recent monitoring window in seconds |
blacklist.missingRatio | number | missingAmounts / totalEvents (0 when no blacklist rows exist yet) |
telegramSummary | TelegramHealthSummary | null | Lightweight Telegram delivery health summary. null when the Telegram tables are unavailable or not yet migrated |
telegramSummary.totalChats | number | Total subscribed Telegram chats currently stored |
telegramSummary.pendingDeliveries | number | Pending overflow alert deliveries waiting in telegram_pending_alerts |
telegramSummary.lastDispatchAt | number | null | Unix seconds of the most recent dispatch-telegram-alerts cron run, if available |
telegramSummary.lastDispatchStatus | string | null | Status of the most recent dispatch-telegram-alerts cron run, if available |
telegramSummary.safetyAlertSourceState | "ok" | "missing" | "corrupt" | "stale" | "wrong-generation" | null | Live safety-alert source-cache state from the most recent Telegram dispatch run |
telegramSummary.safetyAlertSourceAgeSeconds | number | null | Age of the current live safety-alert source snapshot when available |
telegramSummary.safetyAlertsSuppressed | boolean | true when safety alerts are paused because the live source snapshot is missing, corrupt, stale, or from the wrong generation |
telegramSummary.safetyAlertSourceGeneration | string | null | Generation marker of the current live safety-alert source snapshot |
mintBurn.totalEvents | number | null | Legacy advisory total. null on the budget-capped health path because /api/health no longer scans mint_burn_hourly; use /api/mint-burn-flows or /api/mint-burn-events for mint/burn data views. |
mintBurn.latestEventTs | number | null | Legacy advisory timestamp. null on the budget-capped health path because /api/health no longer scans mint_burn_events; freshness is represented by mintBurn.sync.lastSuccessfulSyncAt. |
mintBurn.latestHourlyTs | number | null | Legacy advisory timestamp. null on the budget-capped health path because /api/health no longer scans mint_burn_hourly. |
mintBurn.freshnessAgeSec | number | null | Legacy advisory age. null on the budget-capped health path; derive critical-lane age from mintBurn.sync.lastSuccessfulSyncAt. |
mintBurn.majorStaleCount | number | Legacy advisory count. Always 0 on the budget-capped health path because per-symbol stale checks are intentionally not scanned from D1. |
mintBurn.staleMajorSymbols | string[] | Legacy advisory list. Always empty on the budget-capped health path because per-symbol stale checks are intentionally not scanned from D1. |
mintBurn.sync | object | Critical-lane sync freshness summary used for public health evaluation |
mintBurn.sync.lastSuccessfulSyncAt | number | null | Unix seconds of the latest successful sync-mint-burn run |
mintBurn.sync.freshnessStatus | "fresh" | "degraded" | "stale" | Public freshness state keyed to the 30-minute critical-lane cadence (fresh <= 60m, degraded <= 90m, stale > 90m) |
mintBurn.sync.warning | string | null | Human-readable warning when the critical lane is stale, degraded, or errored |
mintBurn.sync.criticalLaneHealthy | boolean | true when the latest critical-lane run is ok, degraded, or skipped_locked |
circuits | Record<string, CircuitRecord> | Per-source circuit breaker states. Keys include defillama-stablecoins, defillama-stablecoin-detail, defillama-coins, defillama-yields, defillama-protocols, coingecko-prices, coingecko-detail-platforms, coingecko-mcap, coingecko-discovery, coinmarketcap-prices, dexscreener-prices, dexscreener-search, treasury-rates, etherscan, alchemy, twitter-api, telegram-api, pyth-prices, binance-prices, coinbase-prices, redstone-prices, curve-onchain, curve-liquidity-api, fx-realtime |
CacheStatus
| Field | Type | Description |
|---|---|---|
ageSeconds | number | null | Seconds since last cron update; null if never populated |
maxAge | number | Availability budget in seconds for this cache key; same value as availabilityMaxAge for current workers |
healthy | boolean | true when ageSeconds / maxAge ≤ 12.0; status-page bands use >8.0x for degraded and >12.0x for stale |
producerJob | string | null | undefined | Cron job that produces the cache freshness signal |
producerIntervalSec | number | null | undefined | Expected producer cadence in seconds |
endpointMaxAge | number | null | undefined | Endpoint freshness basis used by _meta, X-Data-Age, and generic freshness warning runway when available |
availabilityMaxAge | number | null | undefined | Availability budget used by /api/health, /api/status, and status-page cache ratios |
endpointBudgetReason | string | null | undefined | Short explanation when endpoint freshness differs from producer cadence or availability budget |
availabilityBudgetReason | string | null | undefined | Short explanation for the availability budget |
freshnessSource | "freshness-sentinel" | "table-fallback" | "cron-fallback" | undefined | Source used to derive freshness for sentinel-backed cache lanes |
sentinelValidationReason | string | null | undefined | Present when a malformed, stale, wrong-source, future-dated, or non-ok freshness sentinel was ignored |
mode | "live" | "cached-fallback" | undefined | FX cache only: whether the latest usable sync came from a live fetch or cached fallback |
sourceUpdatedAt | number | null | undefined | FX cache only: Unix seconds for the source currently driving sourceStatus |
sourceAgeSeconds | number | null | undefined | FX cache only: age of the source currently driving sourceStatus |
sourceStatus | "fresh" | "degraded" | "stale" | "none" | FX cache only: cadence-aware source freshness status |
warning | string | null | undefined | Human-readable warning when a lane is running on degraded freshness evidence (FX fallback/source cadence, or cache freshness fallback from sentinel to table/cron evidence) |
consecutiveFallbackRuns | number | undefined | FX cache only: number of back-to-back cached-fallback runs |
The /status/ page consumes the richer blacklist fields directly so it can distinguish long-tail historical cleanup from fresh incoming amount gaps.
Blacklist amount-gap severity is intentionally tolerant of isolated parser/provider misses: data-quality degrades when the missing-amount ratio reaches 1% or when at least 5 recent events are missing amounts, and becomes stale at 2% or at 25 recent missing events.
dex-liquidity, yield-data, and dews 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 cleanlydegraded— any cache impact is degraded (including FX cached-fallback or source-cadence lag), any of the blacklist/mint-burn/circuit health subqueries failed, the public mint/burn lane is warning-only, or 3+ public-impact circuit groups are openstale— any cache impact is stale, or the public mint/burn lane is stale versus its critical-lane cadence
/api/health still emits every circuit record under circuits, including dynamic per-coin live-reserves:* scopes, but those reserve-specific breakers do not change the top-level public status on their own; reserve sync health is evaluated on the dedicated reserve/data-quality lanes instead.
Blacklist ratio fields are still emitted here for the public surface, but threshold-based blacklist severity lives under /api/status data-quality; /api/health only escalates its top-level status when the blacklist health loader itself fails.
GET /api/public-status-history
Public transition history for the read-only /status/ page. Returns the current public status plus recent state transitions within a requested time window. Not edge-cached beyond the standard 60-second response cache.
Query parameters
| Param | Type | Default | Description |
|---|---|---|---|
window | "24h" | "7d" | "30d" | "30d" | Transition time window applied server-side before rows are returned |
limit | integer | 50 | Max 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
}
]
}
| Field | Type | Description |
|---|---|---|
timestamp | number | Unix seconds at time of response |
currentStatus | "healthy" | "degraded" | "stale" | Current public status, sourced from assessPublicHealth (matches the status field from GET /api/health) |
lastChangedAt | number | null | Unix seconds for the latest admin status-machine change, if known |
transitions | PublicStatusTransition[] | Recent public-impact incident transitions, inside the requested window, newest first |
This endpoint powers two separate public /status/ views: the hero Status runway always uses window=30d, while the transition table owns its own user-selected 24h / 7d / 30d filter.
Browser consumers on pharos.watch and ops.pharos.watch should use same-origin /_site-data/public-status-history, which proxies onto the internal website lane instead of calling the external API host directly.
Public-impact filtering (2026-04-13): The endpoint filters the admin state-machine transitions down to incidents opened by at least one public-facing impact code (cache_ratio_*, cache_freshness_query_failed, 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
}
]
}
| Field | Type | Description |
|---|---|---|
activeWatchers | number | Subscribers with at least one active global, explicit coin, or preset alert follow |
coinSubscriptions | number | Total active alert follows, including explicit coin follows plus preset-implied follows |
explicitCoinSubscriptions | number | Active explicit per-coin subscription rows |
presetImpliedCoinSubscriptions | number | Dynamic preset follower count multiplied by each preset's currently resolved coin set |
activePresetFollowers | number | Chats with at least one active preset follow |
newWatchersToday | number | null | Active watchers created in the current UTC day snapshot; null when suppressed by low-cardinality privacy filtering |
churnedWatchersToday | number | null | Snapshot-estimated active watcher churn for the current UTC day; null when suppressed by low-cardinality privacy filtering |
reactivatedWatchersToday | number | null | Snapshot-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 |
pendingDeliveries | number | null | Count of queued Telegram alert deliveries; null when unavailable or suppressed by low-cardinality privacy filtering |
quietHoursEnabledChats | number | null | Aggregate count of chats with quiet hours enabled; null when suppressed by low-cardinality privacy filtering |
miniAppSessionsToday | number | null | Valid Mini App session launches today; null when unavailable or suppressed by low-cardinality privacy filtering |
miniAppMutationsToday | number | null | Successful Mini App mutations today; null when unavailable or suppressed by low-cardinality privacy filtering |
miniAppDeniedToday | number | null | Mini 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. |
miniAppReplayClaimsToday | number | null | Mini 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. |
miniAppOpenToFirstMutationP50Sec | number | null | Reserved Mini App session-to-first-mutation latency metric; currently null until bucketed latency is wired through telegram_usage_daily |
alertTypeChats | object | Aggregate chat counts with DEWS, depeg, safety, launch, and all-four alert coverage |
currentSnapshotAt | number | Unix seconds when the current aggregate pulse snapshot was measured |
lifecycleHistoryUpdatedAt | number | null | Unix 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 |
lifecycleHistoryEverySeconds | number | Expected lifecycle-history snapshot cadence, currently 900 seconds |
quality | object | Public telemetry quality marker. partial means one or more non-critical fields were unavailable; raw errors are omitted from public pulse responses. |
privacy | object | Public privacy stance and suppressed field list. Exact active watcher totals are public; nonzero supporting metrics below lowCardinalityThreshold are suppressed. |
updatedAt | number | Unix seconds when the pulse payload was produced |
updatedEverySeconds | number | Cache cadence for consumers that display freshness |
topCoins | string[] | Up to five most subscribed coin tickers, ordered by subscription count |
watcherHistory | array | UTC 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
| Param | Type | Default | Description |
|---|---|---|---|
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
}
}
| Field | Type | Description |
|---|---|---|
current | object | null | Latest PSI score and components. null if cron has not yet run |
current.score | number | PSI score 0–100 |
current.band | string | Condition band: "BEDROCK", "STEADY", "TREMOR", "FRACTURE", "CRISIS", "MELTDOWN" |
current.avg24h | number | undefined | Rolling 24 h average PSI score |
current.avg24hBand | string | undefined | Condition band for avg24h |
current.components | object | Component breakdown: severity, breadth, stressBreadth, trend |
current.contributors | array | Top per-coin contributors from input_snapshot.contributors (empty when unavailable) |
current.inputDegradation | object | undefined | Dependency-loss metadata carried by the served sample when the stored input snapshot recorded degraded upstream inputs |
current.totalMcapUsd | number | Total ecosystem market cap from the latest input snapshot (0 when unavailable) |
current.computedAt | number | Unix seconds of computation |
current.methodologyVersion | string | Methodology version used to compute the current score |
history | array | Historical scores, newest first. With detail=true, persisted rows include components; the synthesized current-day running-average point may omit them |
malformedRows | number | Count of historical rows dropped from detail=true because persisted components JSON was malformed |
history[].methodologyVersion | string | Methodology version used for that history point |
methodology | object | Version metadata for current PSI methodology context |
methodology.version | string | Methodology version used by current score |
methodology.changelogPath | string | Relative path to full methodology changelog |
GET /api/og/* / 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
404withtext/plainfor unknown coin IDs inside/api/og/stablecoin/:id; unknown OG route patterns return the standard JSON{ "error": "Unknown OG route" }503when required cached data is not yet available400for malformed URI encoding in/api/og/stablecoin/:id500withtext/plainbody when OG image rendering fails
/api/og/stablecoin/:id accepts tracked public stablecoin IDs only. The renderer assembles each card from cached stablecoin, DEWS, PSI, report-card, depeg, liquidity, and mint/burn data on the worker.
GET /api/report-cards
Stablecoin risk grade cards with dimension-level scores. Output includes 5 dimensions; overall score is the weighted base (exit-liquidity/resilience/decentralization/dependency) plus peg-multiplier adjustment.
Cache: standard
Response
{
"cards": [ReportCard, ...],
"dependencyGraph": {
"edges": [{ "from": "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
| Field | Type | Description |
|---|---|---|
id | string | Canonical Pharos stablecoin ID for live cards; stable cemetery ID for defunct cards |
name | string | Full name |
symbol | string | Ticker |
overallGrade | string | Letter grade: "A+" through "F", or "NR" |
overallScore | number | null | Weighted score 0–100. null for unrated coins |
baseScore | number | null | Pre-peg-multiplier/no-liquidity/active-depeg-cap score after base dimension blending |
overallCapped | boolean | true when the card is capped at a tracked parent stablecoin's overall score |
uncappedOverallScore | number | null | Post-dimension/post-peg score before the tracked-parent overall cap, when applicable |
dimensions | Record<DimensionKey, DimensionScore> | Per-dimension grade, score, and detail text |
ratedDimensions | number | Number of dimensions with data (max 5) |
rawInputs | RawDimensionInputs | Raw scoring inputs for client-side grade recomputation (stress testing) |
isDefunct | boolean | true for cemetery coins (permanent F grade) |
DependencyWeight: { id: string, weight: number, type?: DependencyType } — upstream stablecoin ID + fraction of collateral from that source (0–1), with optional dependency category. 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
| Field | Type |
|---|---|
pegScore | number | null |
activeDepeg | boolean |
activeDepegBps | number | null |
depegEventCount | number |
lastEventAt | number | null |
liquidityScore | number | null |
effectiveExitScore | number | null |
redemptionBackstopScore | number | null |
redemptionRouteFamily | RedemptionRouteFamily | null |
redemptionModelConfidence | "high" | "medium" | "low" | null |
redemptionUsedForLiquidity | boolean |
redemptionImmediateCapacityUsd | number | null |
redemptionImmediateCapacityRatio | number | null |
concentrationHhi | number | null |
bluechipGrade | BluechipGrade | null |
canBeBlacklisted | boolean | "possible" | "inherited" |
chainTier | ChainTier |
deploymentModel | DeploymentModel |
collateralQuality | CollateralQuality |
custodyModel | CustodyModel |
governanceTier | GovernanceType |
governanceQuality | GovernanceQuality |
dependencies | DependencyWeight[] |
variantParentId | string | null |
variantKind | "savings-passthrough" | "strategy-vault" | "risk-absorption" | "bond-maturity" | null |
navToken | boolean |
collateralFromLive | boolean |
rawInputs.canBeBlacklisted is the canonical resolved blacklist status used by report-card-backed product surfaces. It can therefore differ from the raw StablecoinMeta.canBeBlacklisted override field, which only carries manual metadata and never stores computed "inherited" values. 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 telemetryestimated= modelled from current supply and conservative route assumptionsstatic= route remains configured, but current runtime inputs did not resolve a usable score
resolutionState:
resolved= the route produced a usable scoremissing-cache= the stablecoins snapshot did not include the asset or its current supplymissing-capacity= the route is configured, but the snapshot could not resolve enough capacity to score itfailed= a route-specific resolver failedimpaired= the route shape is known but current market or route-availability evidence contradicts broad par redemption
routeStatus / routeStatusSource describe current route availability separately from the static route shape. Normal rows use routeStatus: "open" and routeStatusSource: "static-config". A severe active depeg (>=2500 bps) can publish routeStatus: "degraded" and routeStatusSource: "market-implied" for static or non-live-direct routes; those impaired rows have score = null, effectiveExitScore = null, and modelConfidence = "low". holderEligibility describes the modeled holder cohort, such as any-holder, verified-customer, whitelisted-primary, pre-incident-holder, issuer-discretionary, or unknown.
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:
| Field | Type | Description |
|---|---|---|
coins | Record<string, RedemptionBackstopEntry> | Current snapshot keyed by Pharos stablecoin ID |
methodology | object | Version metadata plus component weights, effective-exit blend weights, and route-family caps |
updatedAt | number | Freshest updated_at timestamp for the served completed run, or freshest current row for legacy snapshots |
RedemptionBackstopEntry highlights:
| Field | Type | Description |
|---|---|---|
score | number | null | Direct redemption-quality score after route-family/config caps |
effectiveExitScore | number | null | Blended exit score used by report cards |
dexLiquidityScore | number | null | DEX liquidity input used in the blend |
routeFamily | string | stablecoin-redeem, basket-redeem, collateral-redeem, psm-swap, queue-redeem, or offchain-issuer |
accessModel | string | permissionless-onchain, whitelisted-onchain, issuer-api, or manual |
settlementModel | string | atomic, immediate, same-day, days, or queued |
outputAssetType | string | stable-single, stable-basket, bluechip-collateral, mixed-collateral, or nav |
sourceMode | string | dynamic, estimated, or static capacity provenance |
resolutionState | string | resolved, missing-cache, missing-capacity, failed, or impaired |
routeStatus | string | Current route availability: open, degraded, paused, cohort-limited, or unknown |
routeStatusSource | string | Source for current route availability: static-config, market-implied, operator-notice, protocol-api, or onchain |
routeStatusReason | string | undefined | Human-readable explanation when current availability impairs scoring |
routeStatusReviewedAt | string | undefined | UTC date (YYYY-MM-DD) for the current route-status assessment |
holderEligibility | string | Modeled holder cohort: any-holder, verified-customer, whitelisted-primary, pre-incident-holder, issuer-discretionary, or unknown |
capacityConfidence | string | live-direct, live-proxy, documented-bound, heuristic, or legacy dynamic fidelity tag for the capacity model |
capacityBasis | string | undefined | Typed basis for the modeled capacity, such as issuer-term-redemption, full-system-eventual, psm-balance-share, strategy-buffer, hot-buffer, daily-limit, live-direct-telemetry, or live-proxy-buffer |
capacitySemantics | string | immediate-bounded or eventual-only, distinguishing current redeemable buffer from eventual redeemability |
capacityProfile | object | undefined | Optional v4 capacity profile separating immediate, daily, queued, eventual, and scoring capacity with a scoringHorizon and capacityProfileConfidence |
capacityKind | string | undefined | Optional 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 |
freshnessKind | string | undefined | Optional adapter-declared redemption freshness evidence, such as verified-source-timestamp, same-run-onchain, same-run-api, reviewed-static, or unverified |
confidenceDetails | object | undefined | Optional v4 confidence rollup dimensions for capacity evidence, fee evidence, route-status freshness, holder-cohort breadth, and source quality |
sourceTimestamp | number | undefined | Optional source timestamp emitted by a live reserve adapter for the redemption telemetry |
sourceUrls | string[] | undefined | Optional source URLs emitted by a live reserve adapter for the redemption telemetry |
settlementDelaySec | number | undefined | Optional adapter-emitted settlement delay constraint in seconds |
queueDepthUsd | number | undefined | Optional adapter-emitted queued redemption depth in USD |
dailyLimitUsd | number | undefined | Optional adapter-emitted daily redemption limit in USD |
minRedeemUsd | number | undefined | Optional adapter-emitted minimum redemption size in USD |
liveHolderEligibility | string | undefined | Optional adapter-emitted holder eligibility context when it differs from or sharpens the static model |
eventualRedeemabilityScore | number | null | undefined | Optional v4 long-tail legal/protocol redeemability score, separate from current executable exit capacity |
feeConfidence | string | fixed, formula, or undisclosed-reviewed fidelity tag for the fee model |
feeModelKind | string | fixed-bps, formula, documented-variable, or undisclosed-reviewed |
modelConfidence | string | Overall route-fidelity rollup: high, medium, or low |
immediateCapacityUsd | number | null | Immediate redeemable capacity in USD. null when the model is eventual-only or currently unrated |
immediateCapacityRatio | number | null | Immediate redeemable capacity as a share of supply. null when not separately quantified |
feeBps | number | null | Explicit bounded fee when configured |
feeDescription | string | undefined | Docs-backed fee description for variable, conditional, flat-minimum, or undisclosed redemption schedules |
costScenarioScores | object | undefined | Optional v4 cost scores for retail, active-user, and institutional route-size scenarios |
routeExitCorrelation | string | undefined | Optional 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 |
queueEnabled | boolean | Whether the modeled route is explicitly queued/serial |
docs | { label?: string, url?: string, reviewedAt?: string, provenance?: string, sources?: { label: string, url: string, supports?: string[] }[] } | undefined | Optional documentation / transparency metadata. reviewedAt is the route-review date, while provenance is config-reviewed, live-reserve-display, proof-of-reserves, or preferred-link |
notes | string[] | undefined | Runtime notes such as stale reserve metadata fallback |
capsApplied | string[] | undefined | Applied score caps (queue-route-cap, offchain-route-cap, config-cap) |
Response (503): { "error": "Data not yet available" } or { "error": "Redemption backstop snapshot unavailable" }
GET /api/safety-score-history
Per-coin Safety Score grade transition history (seed row + grade changes only). Rows are written by the daily snapshot-safety-grade-history cron and returned in ascending date order.
Cache: slow
Required query parameter
| Param | Type | Description |
|---|---|---|
stablecoin | string | Pharos stablecoin ID (required) |
Optional query parameters
| Param | Type | Default | Bounds | Description |
|---|---|---|---|---|
days | integer | 365 | 1–3650 | Lookback window in days |
Response: Array sorted by date ascending.
[
{
"date": 1771977600,
"grade": "B+",
"score": 78,
"prevGrade": "B",
"prevScore": 74,
"methodologyVersion": "5.5"
}
]
| Field | Type | Description |
|---|---|---|
date | number | UTC day bucket (Unix seconds) when the event was recorded |
grade | string | Current Safety Score letter grade at date |
score | number | null | Current numeric score (0–100); null when grade is NR |
prevGrade | string | null | Previous grade before this event; null for the seed row |
prevScore | number | null | Previous score before this event; null for the seed row |
methodologyVersion | string | Safety Score methodology version used for this event row |
GET /api/yield-rankings
Cache-backed yield rankings written by the sync-yield-data cron. The endpoint rehydrates safetyScore, safetyGrade, yieldToRisk, and pharosYieldScore from the 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" }
}
| Field | Type | Description |
|---|---|---|
rankings | YieldRanking[] | All ranked stablecoins, sorted by Pharos Yield Score descending |
riskFreeRate | number | Default USD benchmark rate (%) retained for backward compatibility and mixed-view fallback |
benchmarks | object | null | Benchmark registry keyed by currency (USD, EUR, CHF, plus v8.13: GBP, JPY, MXN, BRL, AUD, CAD; SGD is reserved without a fetcher) with label, rate, freshness, fallback, and proxy metadata |
scalingFactor | number | Scaling factor applied in yield score computation |
medianApy | number | TVL-weighted median APY (30d) across best-source rows, used as a peer reference in warning heuristics |
updatedAt | number | Unix seconds when the rankings were last computed |
provenance | object | null | Snapshot-level provenance for default benchmark freshness, full benchmark registry, DeFiLlama pool input freshness, safety coverage, and selection method |
warnings | YieldResponseWarning[] | 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 |
publication | object | null | Optional publication metadata for generation-aware payloads; omitted on legacy payloads |
methodology | object | undefined | Optional 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.
| Field | Surface | Type | Population status |
|---|---|---|---|
publication.generationId | rankings/history root | string | null | undefined | Publisher generation identifier, e.g. yield-1774526400; omitted on legacy payloads |
publication.updatedAt | rankings/history root | number | null | undefined | Unix seconds when the generation was computed |
publication.cutoffAt | rankings/history root | number | null | undefined | Latest history timestamp approved for public reads |
publication.schemaVersion | rankings/history root | number | null | undefined | Payload-generation schema version |
publication.status | rankings/history root | "staged" | "published" | "failed" | null | undefined | Public cache payloads should expose published; failed or CAS-skipped generations do not replace current yield_data rows |
warnings[] | rankings root | YieldResponseWarning[] | undefined | Body-level degraded-response advisories; row source warnings remain in warningSignals |
publicationGenerationId | ranking/history rows | string | null | undefined | Row-to-generation join identifier; null on legacy rows |
publishedRank | ranking rows | integer >= 1 | null | undefined | Stable rank from the published cache order before live Safety Score hydration |
liveRank | ranking rows | integer >= 1 | null | undefined | Post-hydration rank assigned after live Safety Score recomputation |
sourceRisk.sourceRiskScore | ranking/history/source rows | 0..100 number | null | undefined | Optional source-risk score when populated by the source-risk worker |
sourceRisk.sourceRiskPenalty | ranking/history/source rows | number >= 1 | null | undefined | Active PYS v8 source-risk multiplier derived from reliable source evidence. Missing/invalid values are neutral (1); runtime clamps values to 1..2.5 |
sourceRisk.sourceDepthRatio | ranking/history/source rows | number >= 0 | null | undefined | Optional venue-depth ratio |
sourceRisk.rewardShare | ranking/history/source rows | 0..1 number | null | undefined | Optional reward APY share |
sourceRisk.sourceAgeSeconds | ranking/history/source rows | integer seconds >= 0 | null | undefined | Optional source-observation age |
sourceRisk.observationCount30d | ranking/history/source rows | integer >= 0 | null | undefined | Optional 30-day observation count for the source |
sourceRisk.sourceSwitchCount30d | ranking/history/source rows | integer >= 0 | null | undefined | Optional 30-day selected-source switch count |
sourceRisk.deploymentPlace | ranking/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 | undefined | Optional sourced deployment-place label |
sourceRisk.venueProtocol | ranking/history/source rows | string | null | undefined | Optional venue protocol label |
sourceRisk.venueChain | ranking/history/source rows | string | null | undefined | Optional venue chain label |
sourceRisk.venueRiskTier | ranking/history/source rows | "low" | "medium" | "high" | "unknown" | null | undefined | Optional sourced venue tier; unknown remains neutral |
sourceRisk.investabilityFlags | ranking/history/source rows | string[] | undefined | Optional investability caveats |
sourceRisk.trancheSide | ranking/history/source rows | "senior" | "junior" | null | undefined | Royco Dawn tranche side for structured-tranche rows |
sourceRisk.trancheSafetyScore | ranking/history/source rows | 0..100 number | null | undefined | Opportunity-level Safety Score used by Royco Dawn tranche rows after underlying-score and tranche-risk adjustments |
sourceRisk.trancheSafetyPenalty | ranking/history/source rows | number >= 0 | null | undefined | Difference between the underlying report-card Safety Score and the final tranche Safety Score |
sourceRisk.underlyingSafetyScore | ranking/history/source rows | 0..100 number | null | undefined | Current underlying report-card Safety Score input used for opportunity-level tranche scoring |
sourceRisk.marketCoverageRatio | ranking/history/source rows | number >= 0 | null | undefined | Current Royco market coverage ratio |
sourceRisk.marketMinCoverageRatio | ranking/history/source rows | number >= 0 | null | undefined | Royco market minimum required coverage ratio |
sourceRisk.marketUtilizationRatio | ranking/history/source rows | number >= 0 | null | undefined | Current Royco market utilization ratio |
sourceRisk.marketUtilizationLimitRatio | ranking/history/source rows | number >= 0 | null | undefined | Royco market utilization target or limit ratio when supplied |
sourceRisk.marketDrawdownRatio | ranking/history/source rows | number >= 0 | null | undefined | Current Royco market drawdown ratio |
sourceRisk.marketTotalDrawdowns | ranking/history/source rows | integer >= 0 | null | undefined | Royco market drawdown count when supplied |
sourceRisk.marketStatus | ranking/history/source rows | "normal" | "protected" | "unhealthy" | "critical" | null | undefined | Normalized Royco market status used by tranche scoring |
sourceRisk.marketTvlUsd | ranking/history/source rows | number >= 0 | null | undefined | Royco market-level TVL in USD |
sourceRisk.trancheTvlUsd | ranking/history/source rows | number >= 0 | null | undefined | Royco vault/tranche TVL in USD |
sourceRisk.trancheShareTokenAddress | ranking/history/source rows | string | null | undefined | Share-token address for the Royco tranche vault |
sourceRisk.trancheDepositTokenAddress | ranking/history/source rows | string | null | undefined | Deposit-token address used to attach the tranche row to a tracked underlying stablecoin |
sourceRisk.withdrawalDelaySeconds | ranking/history/source rows | integer seconds >= 0 | null | undefined | Withdrawal/redemption delay for the tranche when supplied |
sourceRisk.kycRequired | ranking/history/source rows | boolean | null | undefined | Whether the source marks KYC as required |
sourceRisk.accessRestricted | ranking/history/source rows | boolean | null | undefined | Whether the source marks jurisdictional or other access restrictions |
rankChangeAttribution | ranking rows | object | null | undefined | Optional 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
| Field | Type | Description |
|---|---|---|
id | string | Pharos stablecoin ID |
symbol | string | Token symbol |
name | string | Full name |
currentApy | number | Current APY (%) |
apy7d | number | 7-day average APY (%) |
apy30d | number | 30-day average APY (%) |
apyBase | number | null | Base APY component (%) |
apyReward | number | null | Reward APY component (%), null if none |
yieldSource | string | Human-readable yield source description |
yieldSourceUrl | string | null | Official URL for the selected source when Pharos has a curated or metadata-derived link |
yieldType | string | Yield type classification (e.g. "lending-vault", "staking") |
dataSource | string | Data source identifier (e.g. "defillama") |
sourceTvlUsd | number | null | TVL of the yield source pool (USD) |
pharosYieldScore | number | null | Composite Pharos Yield Score (0–100), recomputed at read time from cached APY + benchmark inputs plus the current Safety Score |
safetyScore | number | null | Current Safety Score input used by Yield Intelligence. Rated coins match /api/report-cards; Royco structured-tranche rows expose the row-level tranche score; unrated coins use the default NR penalty input (40) |
safetyGrade | string | null | Current Safety Score letter grade ("A+" through "F", or "NR"); Royco structured-tranche rows derive the grade from the opportunity-level tranche score |
yieldToRisk | number | null | Yield-to-risk ratio recomputed at read time from cached APY inputs plus the current Safety Score |
excessYield | number | null | 30-day average APY above the row benchmark (percentage points) |
benchmarkKey | "USD" | "EUR" | "CHF" | "GBP" | "JPY" | "MXN" | "BRL" | "AUD" | "CAD" | "SGD" | undefined | Benchmark 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) |
benchmarkLabel | string | undefined | Human-readable benchmark label for the row |
benchmarkCurrency | string | undefined | Benchmark currency code used for the row |
benchmarkRate | number | undefined | Benchmark rate (%) applied to this row |
benchmarkRecordDate | string | null | undefined | Market or policy record date for the selected benchmark |
benchmarkIsFallback | boolean | undefined | Whether the row benchmark is currently on a fallback path |
benchmarkFallbackMode | string | null | undefined | Fallback reason for the row benchmark when applicable |
benchmarkSelectionMode | "native" | "fallback-usd" | "manual-override" | undefined | How the row benchmark was selected |
benchmarkIsProxy | boolean | undefined | True when the selected benchmark is an explicit proxy rather than the exact reference rate |
yieldStability | number | null | Yield stability metric (0–1; higher = more stable) |
apyVariance30d | number | null | 30-day APY variance |
apyMin30d | number | null | Minimum APY in last 30 days (%) |
apyMax30d | number | null | Maximum APY in last 30 days (%) |
warningSignals | string[] | Active warning-signal flags for the selected best source |
altSources | AltYieldSource[] | Additional non-selected source rows for the same coin |
provenance | object | null | Source-level provenance: confidence tier, selection reason, benchmark state, source-switch metadata, source freshness, and optional anchor timing |
publicationGenerationId | string | null | undefined | Publication-generation identifier, or null/omitted for legacy rows |
publishedRank | number | null | undefined | Stable publication-order rank from the cached generation |
liveRank | number | null | undefined | Post-hydration rank from the response order after live Safety Score recomputation |
sourceRisk | object | null | undefined | Optional nested source-risk payload. Runtime rows derive or resolve sourceRisk.sourceRiskPenalty before PYS v8 scoring; missing or unknown values remain neutral |
rankChangeAttribution | object | null | undefined | Optional 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 rowcomparisonAnchorObservedAt/comparisonAnchorAgeSeconds: optional prior-anchor timing for APYs derived from two observations, such as price-derived and on-chain exchange-rate rowsbenchmarkKey,benchmarkLabel,benchmarkRate,benchmarkIsFallback,benchmarkSelectionMode, and related fields for the exact benchmark applied to that row
GET /api/yield-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
| Param | Type | Description |
|---|---|---|
stablecoin | string | Pharos stablecoin ID (required) |
Optional query parameters
| Param | Type | Default | Bounds | Description |
|---|---|---|---|---|
days | integer | 90 | 1–365 | Lookback window in days |
mode | string | best | best, source | best for historically selected best-source rows; source requires sourceKey |
sourceKey | string | — | — | Required 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/"
}
}
| Field | Type | Description |
|---|---|---|
current | YieldHistoryPoint|null | Latest row in the returned history window, or null when no history exists |
history | YieldHistoryPoint[] | History rows sorted by date ASC |
methodology | object | Yield methodology envelope for the response |
warning | string | undefined | Present when freshness lookup fails and the handler falls back to cache metadata |
publication | object | null | Optional 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"
}
| Field | Type | Description |
|---|---|---|
date | number | Unix seconds |
apy | number | Total APY at snapshot time (%) |
apyBase | number | null | Base APY component (%) |
apyReward | number | null | Reward APY component (%); null if none |
exchangeRate | number | null | Exchange rate at snapshot time (e.g. sUSDe/USDe); null if not applicable |
sourceTvlUsd | number | null | TVL of the yield source pool at snapshot time (USD) |
warningSignals | string[] | Active warning-signal flags at that snapshot |
sourceKey | string | null | Stable source identifier for this history row (for example a DL pool UUID, onchain:<stablecoinId>, or linked-variant:<variantId>:<sourceKey>) |
yieldSource | string | null | Human-readable source label at that snapshot |
yieldSourceUrl | string | null | Official URL for that source when Pharos has a curated or metadata-derived link |
yieldType | string | null | Yield type classification at that snapshot |
dataSource | string | null | Underlying data-source family |
isBest | boolean | Whether this row was the selected best source at that timestamp |
sourceSwitch | boolean | True when the historically selected best source changed at this row |
publicationGenerationId | string | null | undefined | Published generation identifier for generation-aware rows; null/omitted on legacy rows |
sourceRisk | object | null | undefined | Optional 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
| Param | Type | Default | Bounds | Description |
|---|---|---|---|---|
stablecoin | string | — | — | Filter to a single stablecoin ID. Changes response shape to per-coin mode |
hours | integer | 24 | 1–720 | Lookback window for the returned hourly[] series |
Response (aggregate mode — no stablecoin param)
{
"gauge": {
"score": 2.3,
"band": "NEUTRAL",
"flightToQuality": false,
"flightIntensity": 0,
"trackedCoins": 8,
"trackedMcapUsd": 215000000000,
"intensitySemantics": "signed-v2",
"classificationSource": "report-card-cache"
},
"coins": [CoinFlow, ...],
"hourly": [HourlyFlow, ...],
"updatedAt": 1772000000,
"windowHours": 24,
"scope": { "chainIds": ["ethereum", "arbitrum"], "label": "Configured issuance chains" },
"sync": { "lastSuccessfulSyncAt": 1772000200, "freshnessStatus": "fresh", "warning": null, "criticalLaneHealthy": true }
}
gauge
| Field | Type | Description |
|---|---|---|
score | number | null | Market-cap-weighted pressure-shift composite (-100 to +100). null when insufficient data |
band | string | null | Gauge band: "CRISIS", "STRESS", "CAUTIOUS", "NEUTRAL", "HEALTHY", "CONFIDENT", "SURGE" |
flightToQuality | boolean | Whether flight-to-quality conditions are active |
flightIntensity | number | Flight-to-quality intensity (0–100). 0 when not active |
trackedCoins | number | Number of stablecoins tracked for mint/burn flows |
trackedMcapUsd | number | Combined market cap of tracked coins (USD) |
intensitySemantics | string | Scoring semantics version identifier (currently "signed-v2") |
classificationSource | string | Source of flight-to-quality classification ("report-card-cache" or "unavailable") |
Top-level metadata
| Field | Type | Description |
|---|---|---|
windowHours | number | Requested chart window for hourly[] |
scope | object | Current ingestion scope, for example { chainIds: ["ethereum", "arbitrum"], label: "Configured issuance chains" } |
sync | object | Latest critical-lane freshness metadata, warning state, and optional classificationWarning |
CoinFlow
| Field | Type | Description |
|---|---|---|
stablecoinId | string | Pharos stablecoin ID |
symbol | string | Token symbol |
flowIntensity | number | null | Deprecated alias for pressureShiftScore; retained for compatibility |
pressureShiftScore | number | null | Canonical baseline-relative pressure score (-100 to +100). null if < 7 days of data or no current activity |
pressureShiftState | "improving" | "stable" | "worsening" | "nr" | Interpreted pressure state from pressureShiftScore |
netFlowDirection24h | "minting" | "burning" | "flat" | "inactive" | Current 24h direction derived from raw net flow + activity |
has24hActivity | boolean | Whether any 24h mint/burn events were recorded for the coin |
baselineDailyNetUsd | number | null | Average daily net flow over the baseline window used for scoring |
baselineDailyAbsUsd | number | null | Average daily absolute flow over the baseline window used for scoring |
baselineDataDays | number | null | Number of tracked days contributing to the baseline window |
netFlow24hUsd | number | Raw 24h net flow (USD, positive = net minting, negative = net burning). Fixed to the canonical 24h window even when hours changes |
mintVolume24hUsd | number | Total mint volume in the canonical 24h window (USD) |
burnVolume24hUsd | number | Total burn volume in the canonical 24h window (USD) |
mintCount24h | number | Number of mint events in the canonical 24h window |
burnCount24h | number | Number of burn events in the canonical 24h window |
netFlow7dUsd | number | 7-day net flow (USD) |
netFlow30dUsd | number | 30-day net flow (USD) |
netFlow90dUsd | number | 90-day net flow (USD) |
largestEvent24h | object | null | Largest event in the last 24h: { direction, amountUsd, txHash, timestamp } |
coverage | object | undefined | Coverage metadata: startBlock, lastSyncedBlock, lagBlocks, historyStartAt, window booleans, adapter provenance (adapterKinds, startBlockSource, startBlockConfidence), and status |
HourlyFlow
| Field | Type | Description |
|---|---|---|
hourTs | number | Unix seconds (start of hour) |
netFlowUsd | number | Net flow for this hour (USD) |
mintVolumeUsd | number | Mint volume for this hour (USD) |
burnVolumeUsd | number | Burn volume for this hour (USD) |
Response (per-coin mode — with stablecoin param)
Returns per-chain breakdown and hourly timeseries for a single coin. Returns 404 if the stablecoin is not tracked for mint/burn flows.
{
"stablecoinId": "usdt-tether",
"symbol": "USDT",
"mintVolumeUsd": 50000000,
"burnVolumeUsd": 30000000,
"netFlowUsd": 20000000,
"mintCount": 12,
"burnCount": 8,
"chains": [{ "chainId": "ethereum", "mintVolumeUsd": 40000000, ... }],
"hourly": [HourlyFlow, ...],
"updatedAt": 1772000000,
"windowHours": 24,
"scope": { "chainIds": ["ethereum"], "label": "Ethereum-only" },
"sync": { "lastSuccessfulSyncAt": 1772000200, "freshnessStatus": "fresh", "warning": null, "criticalLaneHealthy": true }
}
GET /api/mint-burn-events
Paginated list of individual mint/burn events for a specific stablecoin. Events are sourced from on-chain logs via Alchemy JSON-RPC.
Cache: realtime
Required query parameter
| Param | Type | Description |
|---|---|---|
stablecoin | string | Pharos stablecoin ID (required) |
Optional query parameters
| Param | Type | Default | Bounds | Description |
|---|---|---|---|---|
direction | string | — | "mint" or "burn" | Filter by direction |
chain | string | — | tracked chain IDs for the requested stablecoin | Filter by chain ID within the stablecoin's configured issuance scope |
burnType | string | — | "effective_burn", "bridge_burn", "review_required" | Filter burn rows by classification |
scope | string | "all" | "all" or "counted" | counted returns only rows used in economic-flow aggregates (flow_type='standard' and mint/effective-burn semantics) |
minAmount | number | — | — | Minimum USD amount; unpriced rows are excluded when this filter is used |
limit | integer | 50 | 1–500 | Max results |
offset | integer | 0 | 0–25,000 | Pagination offset; cannot be combined with cursor |
cursor | string | — | opaque | Keyset cursor from nextCursor |
includeTotal | boolean | true | true or false | When 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
| Field | Type | Description |
|---|---|---|
id | string | Composite ID: {chainId}-{txHash}-{logIndex} |
stablecoinId | string | Pharos stablecoin ID |
symbol | string | Token symbol |
chainId | string | Chain identifier (e.g. "ethereum") |
direction | "mint" | "burn" | Whether tokens were minted or burned |
flowType | "standard" | "bridge_transfer" | "atomic_roundtrip" | Flow-noise classification; bridge_transfer and atomic_roundtrip rows are excluded from aggregate flow metrics |
amount | number | Amount in native token units |
amountUsd | number | null | USD value at time of event |
burnType | "effective_burn" | "bridge_burn" | "review_required" | null | Burn classification; null for mint rows |
burnReviewReason | string | null | Reason emitted when a burn requires manual review classification |
counterparty | string | null | Non-zero address (recipient for mint, sender for burn) |
txHash | string | Transaction hash |
blockNumber | number | Block number |
timestamp | number | Unix seconds |
explorerTxUrl | string | Block explorer URL for the transaction |
priceUsed | number | null | Price used to derive amountUsd |
priceTimestamp | number | null | Unix seconds of the price snapshot used |
priceSource | string | null | Valuation provenance (supply_history, price_cache, price_cache_heal, etc.) |
GET /api/stress-signals
Returns Depeg Early Warning Score (DEWS) data for active tracked stablecoins.
All coins (no params): Latest DEWS score + signal breakdown per coin.
Single coin: Add ?stablecoin=ID&days=30 for latest + daily history.
stablecoin must be an active tracked Pharos stablecoin ID. Unknown IDs return 404 with { "error": "Unknown stablecoin" }; tracked-but-non-active IDs return 404 with { "error": "Stablecoin not tracked" }.
Cache: standard (public, s-maxage=300, max-age=60). Freshness threshold: 1800 s (30 minutes, aligned to compute-dews).
Query parameters
| Param | Type | Default | Description |
|---|---|---|---|
stablecoin | string | — | Single coin mode: return latest + daily history |
days | integer | 30 | History lookback (max 365) |
Aggregate responses are filtered to active tracked stablecoin IDs only, even if stale rows for non-active or de-tracked IDs still exist in storage. The aggregate response keeps updatedAt as the newest returned current row, but X-Data-Age / Warning freshness headers are based on oldestComputedAt so a stale per-coin row cannot be hidden by fresher rows for other coins.
Response (all coins)
{
"signals": {
"usdt-tether": {
"score": 5,
"band": "CALM",
"signals": {
"supply": { "value": 2, "available": true },
"price": { "value": 1, "available": true }
},
"amplifiers": { "psi": 1, "contagion": 1 },
"computedAt": 1740000000,
"methodologyVersion": "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:30expiresAt: 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": ""
}
| Field | Type | Required | Notes |
|---|---|---|---|
email | string | Yes | Normalized to lowercase for the one-key-per-email claim |
requesterName | string | No | Private operator context only |
organization | string | No | Private operator context only |
projectUrl | string | No | Must start with https:// when provided |
useCase | string | Yes | 10-1200 characters |
intendedEndpoints | string[] | No | Public 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" | Yes | Used for review context |
expectedVolume | string | No | Private operator context only |
acceptedTerms | true | Yes | Fair-use acknowledgement |
website | string | No | Honeypot 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
400invalid 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
202response shape as a new pending request and do not send a second verification link 429request throttle exceeded; responses includeRetry-After503self-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
400invalid, expired, used, or no-longer-pending verification token429verification attempt throttle exceeded or daily issuance limit for the salted IP hash reached; responses includeRetry-After503self-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 minutesper salted IP hash in D1. - Feedback limiter dependency failure:
503withRetry-After: 60and{ "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": ""
}
| Field | Type | Required | Notes |
|---|---|---|---|
type | "bug" | "data-correction" | "feature-request" | Yes | Submission category |
title | string | Conditional | Required for bug and feature-request (3–100 chars); optional for data-correction |
description | string | Yes | 10–2000 chars |
pageUrl | string | Yes | Relative app path (must be a single-slash internal path such as /stablecoin/usdc-circle/; protocol-relative //... values are rejected) |
website | string | No | Honeypot field; non-empty is silently accepted/dropped |
expectedValue, stablecoinId, stablecoinName, pegValue | string | No | Optional metadata |
contactHandle | string | No | Optional Telegram/X handle that appears publicly on GitHub |
Response
{ "ok": true }
Error responses
400invalid payload429rate limited (3 submissions / 10 minutes per salted IP hash)500forwarding/processing failure503service misconfigured (missingFEEDBACK_IP_SALTorGITHUB_PAT) 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, optionalchatId, 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"andalertTypes=["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, ornull).set-quiet-hours— enable or disable UTC quiet hours.clear-snooze— clear chat-level snooze.set-snooze— set chat-level snooze for1h,4h, or24h.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@pharoswatchand@pharoswatcherslinks/presets— List the preset watchlist catalog and example commands/subscribe <types> <targets>— Subscribe to alerts for explicit coins or preset watchlists (types: dews, depeg, safety, launch)/subscribe <types> all— Enable one or more alert types across all tracked stablecoins/unsubscribe <targets>— Remove explicit coin subscriptions or the concrete coin rows covered by a preset watchlist/unsubscribe all— Remove all per-coin subscriptions, 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 fordepeg,dews,yield,liquidity,chains, orsafety/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:
| Status | When |
|---|---|
| 404 | Origin disallowed, sid not 32 hex chars, or KV miss. |
| 500 | SELECTOR_SNAPSHOTS KV binding missing on the Pages project. |
| 502 | Stored KV value is corrupt, fails semantic validation, or recomputes to a different sid. |
| 503 | KV 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:
| Case | Status / client contract |
|---|---|
Invalid sid path syntax | 404; clients should surface invalid-link/not-found state without replaying unrelated live output. |
| Missing required replay fields | 400 on POST, 502 on GET. |
| Unknown enum, reason key, source shape, or impossible score/rank | 400 on POST, 502 on GET. |
Stored payload canonical sid differs from requested sid | 502. |
| Clipboard denied after a successful POST | Endpoint still returns 200; UI shows a selectable URL fallback. |
| Trading profile has stale share-blocking inputs | UI should not POST until refreshed; endpoint remains shape-focused and does not recompute live staleness. |
Failure modes:
| Status | When |
|---|---|
| 400 | Body parse error, unsupported JSON, missing required replay fields, malformed recommendation / coverage-warning basics, or semantic validation failure. |
| 404 | Origin disallowed. |
| 405 | Method on the wrong path — POST is accepted only at /selector-snapshot without a path segment. |
| 413 | Payload exceeds 100 KB defensive cap. |
| 500 | SELECTOR_SNAPSHOTS KV binding missing. |
| 503 | KV write throws transiently. |