Skip to main content
Pharos
PHAROSlive stablecoin signals

API Reference

HTTP contracts, authentication notes, and response conventions.

The Pharos API is a REST API served by a Cloudflare Worker backed by a D1 database. It powers the pharos.watch stablecoin analytics dashboard through a split website-data lane plus an external integration API. On https://api.pharos.watch, all public routes are API-key protected unless this reference explicitly marks them as exempt.

Base URL: https://api.pharos.watch

Unless noted otherwise, responses are Content-Type: application/json. Exceptions: GET /api/og/* returns image/png, and POST /api/telegram-webhook returns a plain-text ok body. CORS headers are added to every response, but Access-Control-Allow-Origin is restricted by the Worker CORS_ORIGIN allowlist (production repo config: https://pharos.watch,https://ops.pharos.watch). When the request Origin matches an allowlisted entry, the Worker echoes that origin and sets Vary: Origin; when a request includes a foreign Origin, the worker omits Access-Control-Allow-Origin, and OPTIONS preflights from foreign origins receive 403. Requests without an Origin header keep the existing first-allowlisted-origin fallback. Protected public /api/* traffic on api.pharos.watch is API-key-gated via X-API-Key; production runs with PUBLIC_API_AUTH_MODE=enforce, so missing or invalid keys normally receive 401 Unauthorized after public-limiter checks. Rate-limit overages return 429, and limiter/auth dependency failures can return 503.

Surface Split

The runtime now uses three HTTP lanes:

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

Browser consumers should use same-origin /_site-data/* via the frontend helpers in src/lib/api.ts. In production, that Pages proxy targets https://site-api.pharos.watch through SITE_API_ORIGIN. Direct integrations, CI smoke, and build-time sync scripts should target https://api.pharos.watch.

The direct Worker cache profiles below describe responses from api.pharos.watch / site-api.pharos.watch. The Pages /_site-data/* proxy adds a separate same-origin Cache API layer for successful responses without Set-Cookie; this can cache even a direct Worker route whose own profile is no-store, such as /api/health.

Public API Auth

Unless a route is explicitly called out below as exempt, requests to https://api.pharos.watch must send:

  • header: X-API-Key: ph_live_<16 hex prefix>_<32 char base64url secret>
  • example shape: ph_live_0123456789abcdef_abcdefghijklmnopqrstuvwxyzABCDEF

Public, non-admin routes on https://api.pharos.watch that do not require X-API-Key are limited to:

  • GET /api/health
  • GET /api/og/*
  • POST /api/feedback
  • POST /api/telegram-webhook

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

Admin/operator routes are also outside the public API-key gate, but they remain Cloudflare-Access-gated and are supported only through ops-api.pharos.watch or the ops.pharos.watch/api/admin/* Pages proxy.

The worker stores only the key prefix plus a peppered HMAC of the secret portion. Admin callers create, rotate, and deactivate keys through the operator lane (ops.pharos.watch / ops-api.pharos.watch); plaintext tokens are returned only once at creation/rotation time.


Stablecoin IDs

Most endpoints use the Pharos stablecoin ID in ticker-issuer format (e.g. usdt-tether). IDs are checked through the shared stablecoin-ID registry (shared/lib/stablecoin-id-registry.ts). Unknown or non-canonical IDs return 404.

Canonical IDs use ticker-issuer format — lowercase ticker symbol hyphenated with the issuer/protocol name:

ExampleAsset
"usdt-tether"Tether (USDT)
"usdc-circle"USD Coin (USDC)
"paxg-paxos"PAX Gold (PAXG)
"ustb-superstate"Superstate USTB
"gyen-gyen"GYEN

The full list is exported from shared/lib/stablecoins/index.ts, with the raw metadata stored in shared/data/stablecoins/*.json and validated by shared/lib/stablecoins/schema.ts. The API currently accepts canonical IDs only; legacy route redirects are handled at the frontend/static-export layer, not by the worker ID resolver.


Response Headers

Endpoints backed by the cron cache include these additional headers:

HeaderDescription
X-Data-AgeSeconds elapsed since the cron last wrote this data to D1
WarningFreshness warning (110) when cached data is older than the generic freshness runway, plus endpoint-specific advisory warnings (199) on a few compute-on-read routes

Generic freshness status is fresh through 8x maxAge, degraded through 12x maxAge, then stale. Generic freshness headers emit Warning and downgrade Cache-Control to no-store after age > 8x maxAge so edge/browser caches do not keep serving an old payload after the underlying cron data recovers. Some routes also use Warning for dependency or quality advisories even when the age is still inside that runway; clients should treat body _meta.status as authoritative when it exists.


Response Body Freshness (_meta)

Endpoints that emit _meta into plain-object (non-array) response bodies do so through createCacheHandler() or route-specific manual injection, alongside the HTTP freshness headers above. This provides inline freshness metadata for consumers that prefer not to parse response headers.

Shape:

{
  "_meta": {
    "updatedAt": 1710500000,
    "ageSeconds": 42,
    "status": "fresh"
  }
}
FieldTypeDescription
updatedAtnumberUnix epoch seconds when the cron last wrote this data to D1
ageSecondsnumberfloor(now / 1000) - updatedAt
statusstring"fresh" (age/max <= 8.0), "degraded" (8.0 < ratio <= 12.0), or "stale" (ratio > 12.0)

Route-specific manual _meta injectors can be stricter. GET /api/chains uses its 1800-second budget directly (fresh <= 1x, degraded <= 2x, then stale) and switches its response to no-store whenever the chain snapshot is not fresh.

Endpoints with _meta:

EndpointMax Age (sec)Source
GET /api/stablecoins600createCacheHandler
GET /api/chains1800worker/src/api/chains.ts
GET /api/bluechip-ratings43200Custom cache reader in worker/src/api/cache-handlers.ts
GET /api/usds-status86400createCacheHandler
GET /api/yield-rankings3600Manual injection after live safety hydration

Array-typed responses (e.g., endpoints returning a JSON array at the top level) do not include _meta. They receive X-Data-Age / Warning only when their handler wires freshness metadata explicitly; history endpoints such as supply history, DEX liquidity history, and non-USD share currently expose cache headers but no freshness headers.

The frontend apiFetchWithMeta() helper (in src/lib/api.ts) reads _meta from the response body when present, falling back to the X-Data-Age header for endpoints that do not include it.


Cache-Control Profiles

These profiles apply while the dataset is within its generic freshness runway. Once a cache-backed response exceeds 8x its endpoint max age, the worker overrides that response to Cache-Control: no-store until a fresh response is generated.

ProfileCache-ControlUsed by
realtimepublic, s-maxage=60, max-age=10stablecoins, stablecoin-summary, blacklist, blacklist-summary, depeg-events, peg-summary, mint-burn-events, chains
standardpublic, s-maxage=300, max-age=60stablecoin-charts, redemption-backstops, usds-status, daily-digest, digest-archive, report-cards, stability-index, yield-rankings, mint-burn-flows, stress-signals
custompublic, s-maxage=300, max-age=300dex-liquidity (browser-side max-age extended to match CDN TTL)
per-coinpublic, s-maxage=300, max-age=10stablecoin/:id (cache-aside with 5-min per-coin TTL in D1)
slowpublic, s-maxage=3600, max-age=300supply-history, dex-liquidity-history, bluechip-ratings, yield-history, safety-score-history, non-usd-share
archivepublic, s-maxage=86400, max-age=3600digest-snapshot
no-storeno-storehealth plus all admin GET routes after router override (status, status-history, request-source-stats, API key inventory/audit routes, admin-action-log, debug-sync-state, backfill-dews, backfill-dews?repair=...&dry-run=true, audit-depeg-history?dry-run=true, discovery-candidates, status-probe-history)

POST /api/feedback, POST /api/telegram-webhook, and admin POST endpoints bypass edge caching because they are non-GET request paths. They are not part of a cacheable Cache-Control profile and do not currently rely on an emitted Cache-Control: no-store header.


Polling Guidance

Recommended minimum polling cadence for external integrations:

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

Client best practices:

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

Rate Limits

Public API traffic enforces rate limiting to ensure fair usage. The Telegram webhook is exempt from the public IP limiter because Telegram sends from fixed infrastructure and is authenticated separately with X-Telegram-Bot-Api-Secret-Token; valid API-key traffic uses the per-key limiter instead of the IP limiter.

Global Limit

ScopeLimitWindow
Per IP (unauthenticated)300 requests60 seconds
Per API keyVaries (default 120)60 seconds

When the public IP limiter or per-key public API limiter is exceeded, the API returns 429 Too Many Requests:

{
  "error": "Rate limit exceeded"
}

Rate-limited responses include the retry delay in the HTTP Retry-After header when the worker can compute one.

POST /api/feedback also has a form-specific limiter. Its 429 body is { "error": "Too many submissions. Please wait a few minutes." }, and it should be handled as a local submission throttle rather than as a public API quota response.

If global public-IP limiter bookkeeping fails repeatedly, the worker fails closed after 3 consecutive limiter errors and returns 503 Service Unavailable with { "error": "Public API temporarily unavailable" } plus Retry-After: 60. Treat this as an emergency limiter-health condition, not as successful quota exhaustion. API-key traffic uses a separate per-key limiter; quota overages return 429 with Retry-After when available, and dependency failures do not use the same 3-strike public-IP emergency state unless the implementation changes.

Retry Guidance

  • Respect the Retry-After header when present
  • Add random jitter (0–2 seconds) to avoid thundering-herd retries
  • Use exponential backoff for sustained 429 responses
  • Combine with the polling cadences in the section above to stay well under limits

Error Response Conventions

JSON API handlers use { "error": "message" } JSON format. GET /api/og/* returns image/png on success; unknown OG route patterns return the normal JSON error body, while OG data/render failures inside known image routes can return text/plain.

StatusMeaningWhen
400Bad RequestMissing required parameters, invalid enum values, malformed numeric input, or out-of-range numeric/filter values on handlers that opt into rejection (rangePolicy: "reject"). Some endpoints intentionally clamp or default selected numeric params; endpoint sections call this out where it is part of the contract.
401UnauthorizedProtected public endpoint called without a valid X-API-Key, or admin endpoint called without a valid ops-api Access JWT (typically obtained through Cloudflare Access user login or service-token auth)
403ForbiddenDisallowed CORS preflight from a foreign Origin, Pages ops proxy mutating request without a matching same-origin Origin, or mutating admin request missing X-Pharos-Admin: 1
404Not FoundUnknown stablecoin ID or missing resource
429Too Many RequestsRate limit exceeded (global public IP limiter, per-key public API limiter, or feedback-specific limiter; feedback uses its own message body)
500Internal Server ErrorUnhandled exception (caught by withErrorHandler)
502Bad GatewayUpstream fetch failed (external data provider or Pages proxy upstream), or the ops proxy received a Cloudflare Access login redirect from ops-api
503Service UnavailableCache-passthrough endpoint where cache has never been populated, cached payload is corrupt / rejected by validation, limiter storage fails closed after repeated D1 errors, or MAINTENANCE_MODE=true (global kill switch via wrangler secret put)
504Gateway TimeoutPages /_site-data/* or /api/admin/* proxy timed out waiting for its Worker upstream (10 s default; 20 s for ops /api/status and /api/status-history)

Rule: Cache-passthrough handlers return 503 when data hasn't been populated yet or when the stored cache payload is malformed and rejected at read time. Query handlers that find no matching rows return 200 with empty results (e.g., { events: [], total: 0 }). When MAINTENANCE_MODE is set to "true", all non-OPTIONS requests immediately return 503 with { "error": "maintenance", "message": "..." } — used during DB migrations. OPTIONS CORS preflights are handled before the maintenance gate.


Method Gating Policy

HTTP method allowance is defined centrally in shared/lib/api-endpoints/ and enforced by worker/src/router.ts (validateEndpointMethod).

  • GET is accepted for read endpoints (plus admin debug/status endpoints, GET /api/backfill-dews, and dry-run repair previews for GET /api/backfill-dews?repair=...&dry-run=true).
  • POST is accepted for mutating admin endpoints, POST /api/feedback, and POST /api/telegram-webhook.
  • GET, POST is accepted on /api/api-keys so operators can list keys and create a new key through the same route.
  • POST is accepted on /api/api-keys/:id/update, /api/api-keys/:id/deactivate, and /api/api-keys/:id/rotate.
  • /api/audit-depeg-history allows GET only with ?dry-run=true; otherwise it is POST-only.
  • /api/backfill-dews allows GET for the historical backtest and for repair=...&dry-run=true previews; mutating repair runs are POST-only.
  • Unmatched unknown paths return 404 before method validation. Once a static or dynamic route family is registered, known paths with disallowed methods return 405 with Allow; unsupported verbs on known endpoint families return 405 with Allow: GET, POST.

The same shared endpoint descriptors now also carry static worker dependency-hydration hints consumed by worker/src/routes/registry.ts, where the worker binds shared endpoint keys directly to handlers through a single static route-definition list. That keeps endpoint metadata, router behavior, method guards, admin status-page actions, and worker-side static route wiring aligned from one source of truth plus one worker binding table.

Public Endpoints

Unless an endpoint section explicitly says Authentication: exempt, routes in this section require X-API-Key when called on https://api.pharos.watch.

GET /api/stablecoins

Full stablecoin list with current supply, price, chain breakdown, and FX rates. Data is refreshed by cron every 15 minutes; the cache entry has a 10-minute max-age.

Cache: realtime — X-Data-Age and Warning headers included.

Response

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

fxFallbackRates is present when the ECB FX-rate cron has run; keys are pegType strings (e.g. "peggedEUR"), values are rates in USD.

StablecoinData fields

FieldTypeDescription
idstringPharos stablecoin ID
namestringFull name (e.g. "Tether")
symbolstringTicker (e.g. "USDT")
geckoIdstring | nullCoinGecko ID (normalized output key; upstream DefiLlama uses gecko_id)
pegTypestringDefiLlama peg type (e.g. "peggedUSD", "peggedEUR")
pegMechanismstring"fiat-backed", "crypto-backed-algorithmic", etc.
priceSourcestringSource label for the current price ("defillama-list", "coingecko", composite agreement labels such as "binance+coingecko+kraken", "geckoterminal", "protocol-redeem", "dexscreener", etc.). For high-confidence consensus this label can describe the full agreeing cluster even when the published price is the cluster median rather than one member's raw mark. When no usable current price survives validation, the cache keeps price = null and serializes priceSource = "missing" for contract stability.
priceConfidencestring | nullPrice confidence level: "high" (cross-validated agreement), "single-source", "low" (sources diverge), "fallback" (enrichment pipeline)
priceUpdatedAtnumber | nullCompatibility timestamp for the current price; mirrors the effective observation time when available
priceObservedAtnumber | nullUnix seconds for the effective observation time attached to the selected source price; interpret alongside priceObservedAtMode
priceObservedAtMode"upstream" | "local_fetch" | "unknown" | nullWhether priceObservedAt came from source-native freshness metadata, local fetch time, or legacy/unknown provenance
priceSyncedAtnumber | nullUnix seconds when Pharos selected and wrote the current price during the sync
supplySourcestring | undefinedSupply data source: "defillama", "defillama-history-gap-fill" (used when a tracked DefiLlama live row collapses to zero supply but recent DefiLlama chart history still has a fresh non-zero total), "coingecko-gap-fill" (used when tracked deployments are missing from DefiLlama chain coverage and CoinGecko repairs the total/history buckets), "coingecko-fallback", or "onchain-total-supply" (used when a supplemental asset is normalized from on-chain total supply instead of an upstream market-cap field)
pricenumber | nullCurrent price in USD. For high-confidence consensus this is the median of the winning agreeing cluster; for single-source, low-confidence, or fallback outcomes it is the selected source price.
circulatingRecord<string, number>Current supply in USD, keyed by pegType (e.g. { "peggedUSD": 138000000 })
circulatingPrevDayRecord<string, number>Supply 24 h ago
circulatingPrevWeekRecord<string, number>Supply 7 days ago
circulatingPrevMonthRecord<string, number>Supply ~30 days ago
chainCirculatingRecord<string, ChainCirculating>Per-chain breakdown. For "coingecko-gap-fill" and "defillama-history-gap-fill" assets this remains DefiLlama-led unless the missing total can be allocated safely to one tracked chain, so the per-chain sum may be a lower bound on total supply.
chainsstring[]List of chain names where the token is deployed
consensusSourcesstring[]Source names that returned a valid price for this coin during the sync cycle. Defaults to [] when absent.
agreeSourcesstring[] | undefinedCompatibility alias for agreeing/current price sources when present

ChainCirculating

{
  "current": 50000000,
  "circulatingPrevDay": 49000000,
  "circulatingPrevWeek": 47000000,
  "circulatingPrevMonth": 44000000
}

All circulating values are already in USD (the list endpoint does not return native-currency values for non-USD pegs). Do not multiply by price.


GET /api/stablecoin/:id

Historical price and supply chart data for a single stablecoin. Proxies DefiLlama (or CoinGecko for commodity/CG-only tokens) with a 5-minute server-side cache. All upstream calls use fetchWithRetry with explicit per-request timeouts; on upstream/parse failures, or when CoinGecko-derived history is empty/stale, logs include source tags and stablecoin ID before stale-cache fallback or supply_history reconstruction. CoinGecko history is treated as stale when its newest point is more than 72 hours old.

Path parameter: :id — Pharos stablecoin ID.

Cache: per-coin — custom Cache-Control with a 5-minute server-side D1 TTL (public, s-maxage<=300, max-age=10)

Response

{
  "tokens": [TokenPoint, ...]
}

TokenPoint

FieldTypeDescription
datenumberUnix timestamp (seconds)
totalCirculatingUSDRecord<string, number>Supply in USD per pegType key
totalCirculatingRecord<string, number>Supply in native units per pegType key

For regular stablecoins the response still includes the raw DefiLlama detail fields, but the worker now also materializes totalCirculatingUSD and totalCirculating on each token row for contract consistency. Commodity and CG-only tokens are returned directly in the normalized shape above.

For non-USD pegs, totalCirculating remains in native units while totalCirculatingUSD is converted to USD using the current token price before caching, so the USD field always reflects market cap regardless of peg type.


GET /api/stablecoin-summary/:id

Lightweight per-coin snapshot sourced from cached stablecoins data. Designed for integrators that need current price/supply context without transferring full /api/stablecoin/:id history payloads. Browser surfaces on pharos.watch and ops.pharos.watch should reach this route through same-origin /_site-data/stablecoin-summary/:id, which proxies onto the internal site-api lane instead of the external API-key lane.

Path parameter: :id — Pharos stablecoin ID.

Cache: realtime — X-Data-Age and Warning headers included.

Error responses: 503 when the shared stablecoins cache is missing or structurally corrupt; 404 when the requested coin ID is absent from an otherwise valid cache snapshot.

Response

{
  "id": "usdt-tether",
  "name": "Tether",
  "symbol": "USDT",
  "pegType": "peggedUSD",
  "pegMechanism": "fiat-backed",
  "priceUsd": 1.0001,
  "priceSource": "coingecko+defillama-list",
  "priceConfidence": "high",
  "supplySource": "defillama",
  "supplyByPegUsd": { "peggedUSD": 183883564940.52 },
  "supplyUsd": {
    "current": 183883564940.52,
    "prevDay": 183697699496.48,
    "prevWeek": 183673067145.19,
    "prevMonth": 185316486043.16,
    "change1d": 185865444.03,
    "change7d": 210497795.33,
    "change30d": -1432921102.64
  },
  "chainCount": 17,
  "updatedAt": 1772718367
}
FieldTypeDescription
idstringPharos stablecoin ID
namestringAsset name
symbolstringTicker symbol
pegTypestringPeg type key (peggedUSD, peggedEUR, etc.)
pegMechanismstringBacking/mechanism classification
priceUsdnumber | nullCurrent price in USD
priceSourcestringPrice source identifier. When priceUsd is null, this may be "missing" to indicate that no usable current price survived validation.
priceConfidencestring | nullPrice confidence label
supplySourcestring | nullSupply source identifier
supplyByPegUsdRecord<string, number>Current supply by peg bucket (USD)
supplyUsdobjectAggregate USD supply values and deltas (current, prevDay, prevWeek, prevMonth, change1d, change7d, change30d)
chainCountnumberNumber of chains where the asset is deployed
updatedAtnumberUnix seconds of the stablecoins snapshot used for this response

GET /api/non-usd-share

Returns historical non-USD stablecoin market share data from supply_history, split into commodity-pegged (gold/silver) and fiat non-USD buckets. Data is downsampled: daily for the last 90 days, weekly for the last 2 years, monthly beyond that.

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

ParamTypeDefaultConstraintsDescription
daysnumber1825min 30, max 1825Lookback window in days

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

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

FieldTypeDescription
datenumberUnix seconds (snapshot date)
commoditySharenumberCommodity-pegged share as % of total supply
fiatNonUsdSharenumberFiat non-USD share as % of total supply
commoditynumberCommodity-pegged circulating USD
fiatNonUsdnumberFiat non-USD circulating USD
totalnumberTotal circulating USD across all tracked coins

GET /api/chains

Returns chain-level stablecoin aggregates with Chain Health Scores. Computed on-the-fly from the stablecoins cache and report-card cache (two D1 reads) — no dedicated chain table is required for the live leaderboard. The response body also carries _meta, so the frontend can distinguish fresh, degraded, and missing-dependency states without inferring freshness from fetch timing alone.

Cache: realtime — public, s-maxage=60, max-age=10

Freshness threshold: 1800 seconds. Returns 503 when the stablecoins cache is unavailable or structurally corrupt. When dependent snapshots lag, the endpoint stays readable but the body _meta.status degrades and the frontend surfaces stale-data warnings.

Status codes:

StatusMeaning
200Chain aggregates computed successfully; freshness may still be degraded in _meta
503Stablecoins cache unavailable (missing or structurally corrupt)

Response (ChainsResponse):

{
  "_meta": {
    "updatedAt": 1710500000,
    "ageSeconds": 42,
    "status": "fresh",
    "dependencies": {
      "reportCards": {
        "updatedAt": 1710499800,
        "ageSeconds": 242,
        "status": "fresh"
      }
    }
  },
  "chains": [ChainSummary, ...],
  "globalTotalUsd": 230000000000,
  "updatedAt": 1710500000,
  "healthMethodologyVersion": "1.2"
}
FieldTypeDescription
chainsChainSummary[]Chains sorted by totalUsd descending
globalTotalUsdnumberSum of all chain supply in USD
updatedAtnumberUnix epoch seconds of the underlying stablecoins snapshot
healthMethodologyVersionstringChain Health Score methodology version (currently "1.2")

_meta.dependencies.reportCards is present when the endpoint can determine report-card freshness. When that dependency is stale or unavailable, healthScore degrades to null and the route UI surfaces the dependency reason instead of pretending the chain is fully fresh.

ChainSummary fields:

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

ChainHealthFactors fields:

FieldTypeDescription
concentrationnumberHHI-based supply concentration score (higher = more diverse)
qualitynumber | nullSupply-weighted average stablecoin quality from report-card grades; null when rated supply coverage is below 50% by value
chainEnvironmentnumberResilience-tier score for the chain itself (100 tier 1, 60 tier 2, 20 tier 3)
pegStabilitynumberSupply-weighted average peg deviation score
backingDiversitynumberShannon entropy of the active backing split across the chain (rwa-backed vs crypto-backed)

GET /api/stablecoin-reserves/:id

Returns the resolved reserve presentation for a stablecoin with liveReservesConfig.

  • Unknown IDs or coins without live reserve support return 404.
  • Live-enabled coins return 200 even before the first successful sync; the payload includes fallback mode + sync state.
  • This endpoint powers the stablecoin detail-page reserve card. The same underlying live-reserve dataset also feeds report-card collateral quality, reserve-drift monitoring, and /status, but those surfaces read D1-backed reserve snapshots directly rather than calling this endpoint.
  • A response is treated as live only when the stored reserve snapshot matches the latest successful sync state and passes strict integrity validation; orphaned partial writes or corrupt stored snapshots fall back to the curated/template presentation instead of presenting malformed live data as authoritative.

Cache: dynamic

  • Live snapshots: slow (public, s-maxage=3600, max-age=300)
  • live-stale snapshots: public, s-maxage=1800, max-age=120
  • Bootstrap / fallback / unavailable presentations: shorter (public, s-maxage=300, max-age=60) so pre-sync fallback responses do not stay pinned at the edge after the first successful live sync

Response (200):

FieldTypeDescription
stablecoinIdstringPharos coin ID
modestringOne of live, live-stale, curated-fallback, template-fallback, unavailable. This is snapshot transport/freshness state, not the user-facing reserve badge semantics.
reservesReserveSlice[]Reserve slices currently being shown to the user
estimatedbooleantrue only when using the classification template fallback
liveAtnumber?Unix seconds of the last successful live snapshot. Present only when live data exists
sourcestring?Adapter key (for example "infinifi", "m0", "openeden-usdo", or "accountable"). Present only when live data exists
displayUrlstring?Human-readable source link shown in the UI. Present only when configured
displayBadgeobject?User-facing reserve badge semantics for authoritative live snapshots (live, curated-validated, or proof)
metadataobject?Adapter snapshot metadata for authoritative live snapshots. This can include feed-specific context such as yieldBasisCollateralPct for crvusd
provenanceobject?Evidence-quality envelope for authoritative live snapshots (evidenceClass, sourceModel, optional freshnessMode, scoringEligible)
syncobject?Live sync state (status, bootstrap, stale, lastAttemptedAt, lastSuccessAt, warnings, lastError). Present only when live-enabled

sync.warnings can include both adapter-emitted warnings from the latest attempt and storage-integrity warnings when a stored live snapshot is rejected and the endpoint fails closed to a fallback presentation.

When present, displayBadge has:

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

When present, provenance has:

FieldTypeDescription
evidenceClass"independent" | "static-validated" | "weak-live-probe"Evidence class used for scoring and provenance. This is related to, but not identical to, the UI badge semantics.
sourceModel"dynamic-mix" | "validated-static" | "single-bucket"Structural shape of the reserve feed
freshnessMode"verified" | "unverified" | "not-applicable" | undefinedExplicit freshness policy when the adapter emits one
scoringEligiblebooleanWhether this exact snapshot is currently eligible for collateral-quality passthrough

Response (404): { "error": "Not found" }


GET /api/stablecoin-charts

Aggregate historical supply chart data across all stablecoins, broken down by peg type. sync-stablecoin-charts is triggered every 30 minutes, but a stablecoin-charts:last-write cooldown caps successful refreshes at once per hour; /api/health treats the cache as healthy for up to 1 hour.

Cache: standard — X-Data-Age and Warning headers included. This array response gets freshness headers only; it does not receive a response-body _meta envelope.

Response: A top-level array.

[
  {
    "date": 1511913600,
    "totalCirculatingUSD": {
      "peggedUSD": 110105,
      "peggedEUR": 14967600
    }
  }
]
FieldTypeDescription
datenumberUnix timestamp (seconds)
totalCirculatingUSDRecord<string, number>Aggregate supply in USD per peg type

GET /api/blacklist

Freeze, blacklist, block/unblock, account-pause, and token-destruction events for symbols in the shared BLACKLIST_STABLECOINS set. EURC mirror-zero rows are preserved with suppression metadata and excluded from public aggregates. Data is sourced from on-chain logs via Etherscan, Tron, and EVM RPCs.

Cache: realtime

Freshness note: X-Data-Age / Warning track the latest successful 6-hourly sync-blacklist writer timestamp. Public freshness stays fresh through that 6-hour budget and only degrades once the scheduled blacklist sync is actually late.

Query parameters

ParamTypeDefaultDescription
stablecoinstringFilter by token symbol from the full BLACKLIST_STABLECOINS set in shared/types/market.ts
chainstringFilter by chain name (e.g. Ethereum, Tron)
eventTypestringFilter by type: blacklist, unblacklist, destroy
qstringCase-insensitive address substring search
sortBystringdateSort field: date, stablecoin, chain, event
sortDirectionstringdescSort direction: asc, desc
limitinteger1000Max results (1–1000; 0 maps to default 1000)
offsetinteger0Pagination offset

Response

{
  "events": [BlacklistEvent, ...],
  "total": 13422,
  "methodology": {
    "version": "3.9",
    "versionLabel": "v3.9",
    "currentVersion": "3.97",
    "currentVersionLabel": "v3.97",
    "changelogPath": "/methodology/blacklist-tracker-changelog/",
    "asOf": 1772606400,
    "isCurrent": true
  }
}

BlacklistEvent

FieldTypeDescription
idstringComposite ID: {chainId}-{txHash}-{logIndex}
stablecoinstringToken symbol (USDC, USDT, etc.)
chainIdstringChain identifier (e.g. "ethereum", "tron")
chainNamestringHuman-readable chain name (e.g. "Ethereum")
eventTypestring"blacklist", "unblacklist", or "destroy"
addressstringAffected address (EVM 0x… or Tron T…)
amountNativenumber | nullCanonical token-native amount recovered from event data or historical balance lookup
amountUsdAtEventnumber | nullEvent-time USD value when Pharos can justify one
amountSourcestringevent, historical_balance, current_balance_snapshot, derived, legacy_migration, or unavailable
amountStatusstringresolved, recoverable_pending, permanently_unavailable, provider_failed, ambiguous
txHashstringTransaction hash
blockNumbernumberBlock number
timestampnumberUnix seconds
methodologyVersionstringMethodology version attributed to this event row
contractAddressstring | nullEmitting token contract when known
configKeystring | nullInternal tracker config identity ({chainId}-{contract})
eventSignaturestring | nullHuman-readable event signature/name when known
eventTopic0string | nullRaw EVM topic0 when applicable
suppressionReasonstring | nullAlways null or absent on public rows because /api/blacklist filters suppression_reason IS NULL; non-null reasons are retained only on internal/audit DB rows excluded from public aggregates/events
explorerTxUrlstringBlock explorer URL for the transaction
explorerAddressUrlstringBlock explorer URL for the address

methodology

FieldTypeDescription
versionstringMethodology version of the latest returned event in this response
versionLabelstringDisplay label (e.g. "v3.2")
currentVersionstringLatest methodology version
currentVersionLabelstringDisplay label for latest methodology version
changelogPathstringRelative URL to the methodology changelog page
asOfnumberUnix timestamp of latest event used for freshness
isCurrentbooleanWhether version matches currentVersion

GET /api/blacklist-summary

Server-side aggregates for the Blacklist Tracker overview cards, chart, and filter options. This lets the frontend render summary state without hydrating the full blacklist history first.

stats.destroyedTotal remains an event-history total. stats.activeFrozenTotal reflects Pharos' local active blacklist state machine. stats.trackedFrozenTotal is the persistent freeze-ledger total sourced from blacklist_current_balances, including reconciled historical bootstrap rows where later seizures or unblacklists would otherwise hide the frozen amount. stats.recentCount covers the last 30 days, while stats.recentCount24h is the last-24-hours subset used by chrome-level monitoring surfaces. The chart now uses that same freeze ledger and attributes each tracked balance back to its latest recorded blacklist quarter, so the quarterly buckets explain the trackedFrozenTotal headline rather than raw event-time intake.

The four perCoin* maps power the per-coin "Blacklist Activity" block on stablecoin detail pages. perCoinFrozenAddressCount counts addresses whose latest event is blacklist (net-frozen). perCoinFrozenTotal sums blacklist_current_balances.balance_usd per coin. perCoinDestroyedTotal sums amount_usd_at_event over destroy events per coin. perCoinQuarterlyEventTypes contains each coin's quarterly breakdown of event-type counts, zero-filled between the coin's first and last event quarters so bars render contiguously. All per-coin aggregations exclude rows where suppression_reason is set (e.g. EURC mirror zero-balance entries).

Cache: realtime

Freshness note: Shares the same 6-hourly freshness headers as GET /api/blacklist, keyed to the latest successful sync-blacklist write rather than the request time of the summary endpoint itself.

Response

{
  "stats": {
    "usdcBlacklisted": 1204,
    "usdtBlacklisted": 3881,
    "goldBlacklisted": 19,
    "frozenAddresses": 5071,
    "destroyedTotal": 158938221.19,
    "activeAddressCount": 5071,
    "activeFrozenTotal": 2120456789.42,
    "activeAmountGapCount": 17,
    "trackedAddressCount": 9466,
    "trackedFrozenTotal": 3235360796.7,
    "trackedAmountGapCount": 0,
    "recentCount": 42,
    "recentCount24h": 3,
    "recoverableGapCount": 17,
    "perCoinBlacklistCounts": { "USDC": 1204, "USDT": 3881 },
    "perCoinTotalEvents": { "USDC": 1210, "USDT": 3945 },
    "perCoinFrozenAddressCount": { "USDC": 1151, "USDT": 3794 },
    "perCoinFrozenTotal": { "USDC": 143000000, "USDT": 1800000000 },
    "perCoinDestroyedTotal": { "USDC": 0, "USDT": 158900000 },
    "perCoinQuarterlyEventTypes": {
      "USDC": [{ "quarter": "Q1 '26", "blacklist": 42, "unblacklist": 0, "destroy": 1 }]
    }
  },
  "chart": [
    { "quarter": "Q1 '24", "USDT": 1200000, "USDC": 850000, "PAXG": 0, "XAUT": 0, "total": 2050000 }
  ],
  "chains": [
    { "id": "ethereum", "name": "Ethereum" },
    { "id": "tron", "name": "Tron" }
  ],
  "totalEvents": 13422,
  "methodology": {
    "version": "3.9",
    "versionLabel": "v3.9",
    "currentVersion": "3.97",
    "currentVersionLabel": "v3.97",
    "changelogPath": "/methodology/blacklist-tracker-changelog/",
    "asOf": 1772606400,
    "isCurrent": true
  }
}

GET /api/depeg-events

Peg deviation events (≥ 100 bps for USD-pegged, ≥ 150 bps for non-USD pegs). Events are detected every 15 minutes by the cron.

Cache: realtime

Query parameters

ParamTypeDefaultDescription
stablecoinstringFilter by Pharos stablecoin ID
active"true"When "true", return only ongoing (unresolved) depeg events
limitinteger100Max results (1–1000)
offsetinteger0Pagination offset

Response

{
  "events": [DepegEvent, ...],
  "total": 4080,
  "methodology": {
    "version": "5.95",
    "versionLabel": "v5.95",
    "currentVersion": "5.95",
    "currentVersionLabel": "v5.95",
    "changelogPath": "/methodology/depeg-changelog/",
    "asOf": 1772606400,
    "isCurrent": true
  }
}

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

DepegEvent

FieldTypeDescription
idnumberAuto-increment DB ID
stablecoinIdstringPharos stablecoin ID
symbolstringToken symbol
pegTypestringDefiLlama peg type (e.g. "peggedUSD")
direction"above" | "below"Whether the price was above or below the peg
peakDeviationBpsnumberLargest deviation observed (basis points, signed; negative = below peg, positive = above peg)
startedAtnumberUnix seconds when depeg was first detected
endedAtnumber | nullUnix seconds when price returned to peg; null if still active
startPricenumberPrice at event start (USD)
peakPricenumber | nullPrice at worst deviation
recoveryPricenumber | nullPrice at recovery
pegReferencenumberReference peg value used (USD)
source"live" | "backfill"Detection method
confirmationSourcesstring | nullComposite provenance tag recorded when a pending depeg was promoted. Components (joined with +): the off-chain source label (CoinGecko, DefiLlama, or NativePeg(<currency>)), DEX, CEX, Pool. Example: "DEX+CEX" or "CoinGecko+Pool". null for events that bypassed the pending lane (small-cap authoritative direct-insert and historical backfill rows).
pendingReasonstring | nullComposite reason the incident entered the pending lane, e.g. "large-cap", "low-confidence", "large-cap+low-confidence", "extreme-move". null when the event did not enter pending.

methodology

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

GET /api/peg-summary

Composite peg scores and aggregate statistics for tracked stablecoins. Scores are computed over a 4-year window from live depeg events, DEX prices, and current prices. The coins array can still include NAV / other non-peg rows with currentDeviationBps = null, while the summary counters only cover rows with a live peg-status deviation.

Cache: realtime

Response

{
  "coins": [PegSummaryCoin, ...],
  "summary": PegSummaryStats,
  "methodology": {
    "version": "5.95",
    "versionLabel": "v5.95",
    "currentVersion": "5.95",
    "currentVersionLabel": "v5.95",
    "changelogPath": "/methodology/depeg-changelog/",
    "asOf": 1772606400,
    "isCurrent": true
  }
}

PegSummaryCoin

FieldTypeDescription
idstringPharos stablecoin ID
symbolstringToken symbol
namestringFull name
pegTypestringDefiLlama peg type
pegCurrencystringPeg currency code (USD, EUR, GOLD, etc.)
governancestring"centralized", "centralized-dependent", "decentralized"
currentDeviationBpsnumber | nullLive price deviation from peg (basis points, signed). null for NAV / non-fixed-peg rows, for coins with current supply below the live depeg-event floor, or when price / peg-reference inputs are missing.
depegEventCoverageLimitedbooleanPresent when the coin's current supply is below the live depeg-event floor ($1M). Use this to distinguish "below coverage floor" from generic missing-price cases when currentDeviationBps is null.
priceSourcestringPrimary price source label used for current deviation (defillama-list, coingecko, composite agreement labels such as binance+coingecko+kraken, protocol-redeem, defillama-contract, coinmarketcap, dexscreener, cached, etc.). High-confidence consensus can expose the agreeing cluster label even when the published price is the cluster median.
priceConfidence"high" | "single-source" | "low" | "fallback" | nullConfidence tier attached to the primary price input
priceUpdatedAtnumber | nullCompatibility timestamp for the primary price; now mirrors the effective observation time rather than the cache-write time
priceObservedAtnumber | nullUnix seconds for the effective observation time attached to the selected primary price; interpret alongside priceObservedAtMode
priceObservedAtMode"upstream" | "local_fetch" | "unknown" | nullWhether priceObservedAt came from source-native freshness metadata, local fetch time, or legacy/unknown provenance
priceSyncedAtnumber | nullUnix seconds when Pharos selected and wrote the primary price during the sync
primaryTrust"authoritative" | "confirm_required" | "unusable"Whether the current primary price is trusted to mutate live depeg state directly
pegScorenumber | nullComposite peg score 0–100 (higher = more stable)
pegPctnumber% of tracked time within ±100 bps
severityScorenumberSeverity sub-score (0–100)
spreadPenaltynumberSpread/liquidity penalty applied to score
eventCountnumberNumber of depeg events in the 4-year window
worstDeviationBpsnumber | nullWorst single deviation seen (basis points)
activeDepegbooleanWhether a depeg event is currently open
lastEventAtnumber | nullUnix seconds of most recent depeg event
trackingSpanDaysnumberDays of history used for score computation
methodologyVersionstringMethodology version attributed to this coin snapshot
dexPriceCheckDexPriceCheck | nullOptional cross-validation against DEX price (shown when coin supply is at or above the live depeg-event floor, DEX data is ≤ 60 minutes old, and aggregate source TVL is ≥ $250K)
consensusSourcesstring[]Source names that returned a valid price for this coin. Defaults to [] when absent.
agreeSourcesstring[] | undefinedCompatibility alias for agreeing/current price sources when present

DexPriceCheck

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

PegSummaryStats

FieldTypeDescription
activeDepegCountnumberCoins with an open depeg event
medianDeviationBpsnumberMedian absolute deviation across rows with a live current deviation
worstCurrent{ id, symbol, bps } | nullCoin with the largest current deviation among rows with a live current deviation
coinsAtPegnumberRows with a live current deviation that are below their live depeg threshold (100 bps for USD pegs, 150 bps for non-USD pegs)
totalTrackednumberRows included in the live peg-status aggregate (currentDeviationBps !== null)
depegEventsTodaynumberNumber of depeg events whose startedAt is in the current UTC day
depegEventsYesterdaynumberNumber of depeg events whose startedAt is in the previous UTC day
fallbackPegRatesstring[](optional) pegType keys using stale FX fallback rates

methodology — same fields and semantics as /api/depeg-events


GET /api/usds-status

Sky/USDS protocol status — whether the freeze module is currently active.

Cache: standard — X-Data-Age and Warning headers included.

Response

{
  "freezeActive": false,
  "implementationAddress": "0x1923dfee706a8e78157416c29cbccfde7cdf4102",
  "lastChecked": 1771809338,
  "_meta": { "updatedAt": 1710500000, "ageSeconds": 42, "status": "fresh" }
}
FieldTypeDescription
freezeActivebooleanWhether the USDS freeze module is currently enabled
implementationAddressstringAddress of the current USDS implementation contract
lastCheckednumberUnix seconds when this was last fetched on-chain

GET /api/bluechip-ratings

Safety ratings from bluechip.org for covered stablecoins. Updated daily at 08:05 UTC.

Cache: slow — X-Data-Age and Warning headers included.

Response: Object keyed by Pharos stablecoin ID, plus top-level _meta freshness metadata.

{
  "usdt-tether": BluechipRating,
  "usdc-circle": BluechipRating,
  "_meta": { "updatedAt": 1710500000, "ageSeconds": 42, "status": "fresh" }
}

BluechipRating

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

BluechipSmidge — each field is string | null:

FieldDescription
stabilityReserves management and stabilization mechanisms
managementPersonnel restrictions and track records
implementationSmart contract implementation assessment
decentralizationDecentralization posture
governanceGovernance and redemption terms
externalsExternal risk factors

GET /api/dex-liquidity

DEX liquidity scores, pool breakdowns, source-confidence metadata, and on-chain DEX price data for all tracked stablecoins. Updated every 30 minutes. Trend data is only returned when a trusted historical baseline exists.

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

Freshness note: In addition to stale-data warnings, this endpoint can also emit a Warning header when the latest sync-dex-liquidity run finished in degraded or error state and the API is serving the last successful dataset.

Response: Object keyed by Pharos stablecoin ID plus a __global__ aggregate sentinel row.

{
  "usdt-tether": DexLiquidityData,
  "usdc-circle": DexLiquidityData,
  "__global__": DexLiquidityData
}

DexLiquidityData

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

ScoreComponents

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

DexLiquidityPool

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

extra may include:

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

DexPriceSource

FieldTypeDescription
protocolstringDEX protocol name
chainstringChain name
pricenumberPrice from this source
tvlnumberTVL of this pool (USD)

GET /api/dex-liquidity-history

Per-coin historical DEX liquidity snapshots. Snapshots are recorded daily (UTC midnight, first sync after day rollover). Baseline consumers should use coverageClass / coverageConfidence before treating a history point as trend-worthy.

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

Required query parameter

ParamTypeDescription
stablecoinstringPharos stablecoin ID (required)

Optional query parameters

ParamTypeDefaultBoundsDescription
daysinteger901–365Lookback window in days

Response: Array sorted by date ascending.

[
  {
    "tvl": 1658000000,
    "volume24h": 1700000000,
    "score": 93,
    "date": 1771500000,
    "coverageClass": "mixed",
    "coverageConfidence": 0.85,
    "liquidityEvidenceClass": "partial_measured",
    "hasMeasuredLiquidityEvidence": true,
    "trendworthy": true,
    "methodologyVersion": "3.1"
  }
]
FieldTypeDescription
tvlnumberTotal DEX TVL snapshot (USD)
volume24hnumber24 h volume at time of snapshot (USD)
scorenumber | nullLiquidity score at time of snapshot
datenumberUnix seconds
coverageClassstringSnapshot confidence class (primary, mixed, fallback, legacy, unobserved)
coverageConfidencenumberSnapshot confidence score
liquidityEvidenceClassstringSnapshot evidence class (measured, partial_measured, observed_unmeasured, unobserved)
hasMeasuredLiquidityEvidencebooleanWhether the snapshot qualifies as balance-measured liquidity evidence
trendworthybooleanWhether the snapshot is suitable for trend baselines rather than informational use
methodologyVersionstringMethodology version attributed to this snapshot

GET /api/supply-history

Per-coin circulating supply and price history, snapshotted once daily at 08:00 UTC.

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

Required query parameter

ParamTypeDescription
stablecoinstringPharos stablecoin ID (required)

Optional query parameters

ParamTypeDefaultBoundsDescription
daysinteger3651–1825Lookback window in days

Response: Array sorted by date ascending.

[
  {
    "date": 1771500000,
    "circulatingUsd": 138000000000,
    "price": 1.0001
  }
]
FieldTypeDescription
datenumberUnix seconds
circulatingUsdnumberCirculating supply in USD
pricenumber | nullPrice at snapshot time (USD); may be null for older records

GET /api/daily-digest

Latest AI-generated market summary, produced daily at 08:05 UTC via the Claude API.

Cache: standard. When a digest exists, the response includes X-Data-Age and Warning freshness headers (max 2 h). The bootstrap { "digest": null } response carries only Cache-Control.

Response

{
  "digest": "USDC absorbed $812M of the market's $1.36B weekly inflow…",
  "editionNumber": 214
}

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

FieldTypeDescription
digeststring | nullTweet-ready summary (≤ 240 characters). null if no digest has been generated yet.
digestTitlestring | nullShort headline for the digest
digestExtendedstring | nullExtended commentary for the website view
generatedAtnumberUnix seconds when this digest was generated (present only when digest is non-null)
editionNumbernumber | nullSequential daily digest number (present only when digest is non-null)

GET /api/digest-archive

Newest-first archive of up to 365 daily and weekly digests.

Cache: standard

Response

{
  "digests": [
    {
      "digestText": "USDC absorbed $812M…",
      "digestTitle": "USDC Eats the Week",
      "digestExtended": "Longer editorial…",
      "generatedAt": 1771839719,
      "psiScore": 81.1,
      "psiBand": "STEADY",
      "totalMcapUsd": 234500000000,
      "digestType": "daily",
      "editionNumber": 214
    }
  ]
}

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

FieldTypeDescription
digestTextstringTweet-ready summary
digestTitlestring | nullShort headline
digestExtendedstring | nullExtended commentary
generatedAtnumberUnix seconds of generation time
psiScorenumber | nullPSI score parsed from archived digest input data
psiBandstring | nullPSI condition band parsed from archived digest input data
totalMcapUsdnumber | nullEcosystem market cap parsed from archived digest input data
digestType"daily" | "weekly"Digest cadence for this archived entry
editionNumbernumberSequential edition number within that digest cadence

GET /api/digest-snapshot

Contextual data snapshot for a specific digest date — includes the digest's input data, active depeg events, and blacklist events for that day. Used by SSG builds for individual digest pages.

Cache: archive

Required query parameter

ParamTypeDescription
datestringDate in YYYY-MM-DD format, or YYYY-MM-DD-weekly for weekly recap pages (required)

Response

{
  "date": "2026-02-27",
  "inputData": { "totalMcapUsd": 230000000000, "mcap7dDelta": 0.012, ... },
  "prevInputData": { ... },
  "depegEvents": [{ "stablecoinId": "usdt-tether", "symbol": "USDT", "direction": "below", "peakDeviationBps": -150, ... }],
  "blacklistEvents": [{ "stablecoin": "USDT", "chainName": "Ethereum", "eventType": "blacklist", ... }]
}
FieldTypeDescription
datestringThe requested date
inputDataobject | nullDigest input data (mcap, depegs, supply changes, PSI) for this date
prevInputDataobject | nullPrevious day's input data for delta computation
depegEventsarrayUp to 20 depeg events active on that date, ordered by severity
blacklistEventsarrayUp to 50 blacklist events on that date

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


GET /api/health

Worker health check. Reports cache freshness, blacklist integrity, mint/burn freshness, and circuit-breaker states. Not served from Cloudflare edge cache (no-store).

Cache freshness in /api/health separates producer cadence, endpoint freshness, and availability impact. caches[*].maxAge is the availability budget used by /api/health, /api/status, and the public/admin status pages. endpointMaxAge is the endpoint freshness basis used for _meta, X-Data-Age, and the generic freshness warning runway when it differs. producerIntervalSec is the expected writer cadence.

Authentication: exempt

Response

{
  "status": "healthy",
  "timestamp": 1771856453,
  "warnings": [],
  "caches": {
    "stablecoins": { "ageSeconds": 323, "maxAge": 600, "healthy": true, "producerJob": "sync-stablecoins", "producerIntervalSec": 900, "endpointMaxAge": 600, "availabilityMaxAge": 600 },
    "stablecoin-charts": { "ageSeconds": 323, "maxAge": 3600, "healthy": true, "producerJob": "sync-stablecoin-charts", "producerIntervalSec": 3600, "endpointMaxAge": 3600, "availabilityMaxAge": 3600 },
    "usds-status": { "ageSeconds": 47118, "maxAge": 86400, "healthy": true, "producerJob": "sync-usds-status", "producerIntervalSec": 86400, "endpointMaxAge": 86400, "availabilityMaxAge": 86400 },
    "fx-rates": {
      "ageSeconds": 1223,
      "maxAge": 1800,
      "healthy": true,
      "producerJob": "sync-fx-rates",
      "producerIntervalSec": 1800,
      "endpointMaxAge": 1800,
      "availabilityMaxAge": 1800,
      "mode": "live",
      "sourceUpdatedAt": 1771855200,
      "sourceAgeSeconds": 323,
      "sourceStatus": "fresh",
      "warning": null,
      "consecutiveFallbackRuns": 0
    },
    "bluechip-ratings": { "ageSeconds": 22815, "maxAge": 86400, "healthy": true, "producerJob": "sync-bluechip", "producerIntervalSec": 86400, "endpointMaxAge": 43200, "availabilityMaxAge": 86400 },
    "dex-liquidity": { "ageSeconds": 290, "maxAge": 43200, "healthy": true, "producerJob": "sync-dex-liquidity", "producerIntervalSec": 1800, "endpointMaxAge": 3600, "availabilityMaxAge": 43200 },
    "yield-data": { "ageSeconds": 820, "maxAge": 3600, "healthy": true, "producerJob": "sync-yield-data", "producerIntervalSec": 3600, "endpointMaxAge": 3600, "availabilityMaxAge": 3600 },
    "dews": { "ageSeconds": 240, "maxAge": 1800, "healthy": true, "producerJob": "compute-dews", "producerIntervalSec": 1800, "endpointMaxAge": 1800, "availabilityMaxAge": 1800 }
  },
  "blacklist": {
    "totalEvents": 13422,
    "missingAmounts": 0,
    "recentMissingAmounts": 0,
    "recentWindowSec": 86400,
    "missingRatio": 0
  },
  "mintBurn": {
    "totalEvents": null,
    "latestEventTs": null,
    "latestHourlyTs": null,
    "freshnessAgeSec": null,
    "majorStaleCount": 0,
    "staleMajorSymbols": [],
    "sync": {
      "lastSuccessfulSyncAt": 1771856400,
      "freshnessStatus": "fresh",
      "warning": null,
      "criticalLaneHealthy": true
    }
  },
  "circuits": {
    "defillama-stablecoins": { "state": "closed", "consecutiveFailures": 0, "lastSuccessAt": 1772190029 },
    "coingecko-prices": { "state": "closed", "consecutiveFailures": 0, "lastSuccessAt": 1772190030 }
  },
  "telegramSummary": {
    "totalChats": 142,
    "pendingDeliveries": 0,
    "lastDispatchAt": 1771856400,
    "lastDispatchStatus": "success"
  }
}
FieldTypeDescription
statusstring"healthy" / "degraded" / "stale"
timestampnumberUnix seconds at time of response
warningsstring[]Best-effort machine-readable warnings when health subqueries fail but the endpoint can still return a non-500 payload. Messages are sanitized for public output and do not include raw exception text, SQL fragments, or table names.
cachesRecord<string, CacheStatus>Per-cache freshness status
caches["fx-rates"]CacheStatusFX cache freshness plus source-cadence diagnostics (mode, sourceUpdatedAt, sourceAgeSeconds, sourceStatus, warning, consecutiveFallbackRuns)
blacklist.totalEventsnumberTotal events in blacklist table
blacklist.missingAmountsnumberEvents where amount is null (should be 0)
blacklist.recentMissingAmountsnumberMissing-amount events inside the recent monitoring window used by status logic
blacklist.recentWindowSecnumberSize of the recent monitoring window in seconds
blacklist.missingRationumbermissingAmounts / totalEvents (0 when no blacklist rows exist yet)
telegramSummaryTelegramHealthSummary | nullLightweight Telegram delivery health summary. null when the Telegram tables are unavailable or not yet migrated
telegramSummary.totalChatsnumberTotal subscribed Telegram chats currently stored
telegramSummary.pendingDeliveriesnumberPending overflow alert deliveries waiting in telegram_pending_alerts
telegramSummary.lastDispatchAtnumber | nullUnix seconds of the most recent dispatch-telegram-alerts cron run, if available
telegramSummary.lastDispatchStatusstring | nullStatus of the most recent dispatch-telegram-alerts cron run, if available
mintBurn.totalEventsnumber | nullLegacy advisory total. null on the budget-capped health path because /api/health no longer scans mint_burn_hourly; use /api/mint-burn-flows or /api/mint-burn-events for mint/burn data views.
mintBurn.latestEventTsnumber | nullLegacy advisory timestamp. null on the budget-capped health path because /api/health no longer scans mint_burn_events; freshness is represented by mintBurn.sync.lastSuccessfulSyncAt.
mintBurn.latestHourlyTsnumber | nullLegacy advisory timestamp. null on the budget-capped health path because /api/health no longer scans mint_burn_hourly.
mintBurn.freshnessAgeSecnumber | nullLegacy advisory age. null on the budget-capped health path; derive critical-lane age from mintBurn.sync.lastSuccessfulSyncAt.
mintBurn.majorStaleCountnumberLegacy advisory count. Always 0 on the budget-capped health path because per-symbol stale checks are intentionally not scanned from D1.
mintBurn.staleMajorSymbolsstring[]Legacy advisory list. Always empty on the budget-capped health path because per-symbol stale checks are intentionally not scanned from D1.
mintBurn.syncobjectCritical-lane sync freshness summary used for public health evaluation
mintBurn.sync.lastSuccessfulSyncAtnumber | nullUnix seconds of the latest successful sync-mint-burn run
mintBurn.sync.freshnessStatus"fresh" | "degraded" | "stale"Public freshness state keyed to the 30-minute critical-lane cadence (fresh <= 60m, degraded <= 90m, stale > 90m)
mintBurn.sync.warningstring | nullHuman-readable warning when the critical lane is stale, degraded, or errored
mintBurn.sync.criticalLaneHealthybooleantrue when the latest critical-lane run is ok, degraded, or skipped_locked
circuitsRecord<string, CircuitRecord>Per-source circuit breaker states. Keys include defillama-stablecoins, defillama-stablecoin-detail, defillama-coins, defillama-yields, defillama-protocols, coingecko-prices, coingecko-detail-platforms, coingecko-mcap, coingecko-discovery, coinmarketcap-prices, dexscreener-prices, dexscreener-search, treasury-rates, etherscan, alchemy, twitter-api, telegram-api, pyth-prices, binance-prices, coinbase-prices, redstone-prices, curve-onchain, curve-liquidity-api, fx-realtime

CacheStatus

FieldTypeDescription
ageSecondsnumber | nullSeconds since last cron update; null if never populated
maxAgenumberAvailability budget in seconds for this cache key; same value as availabilityMaxAge for current workers
healthybooleantrue when ageSeconds / maxAge ≤ 12.0; status-page bands use >8.0x for degraded and >12.0x for stale
producerJobstring | null | undefinedCron job that produces the cache freshness signal
producerIntervalSecnumber | null | undefinedExpected producer cadence in seconds
endpointMaxAgenumber | null | undefinedEndpoint freshness basis used by _meta, X-Data-Age, and generic freshness warning runway when available
availabilityMaxAgenumber | null | undefinedAvailability budget used by /api/health, /api/status, and status-page cache ratios
endpointBudgetReasonstring | null | undefinedShort explanation when endpoint freshness differs from producer cadence or availability budget
availabilityBudgetReasonstring | null | undefinedShort explanation for the availability budget
mode"live" | "cached-fallback" | undefinedFX cache only: whether the latest usable sync came from a live fetch or cached fallback
sourceUpdatedAtnumber | null | undefinedFX cache only: Unix seconds for the source currently driving sourceStatus
sourceAgeSecondsnumber | null | undefinedFX cache only: age of the source currently driving sourceStatus
sourceStatus"fresh" | "degraded" | "stale" | "none"FX cache only: cadence-aware source freshness status
warningstring | null | undefinedHuman-readable warning when a lane is running on degraded freshness evidence (FX fallback/source cadence, or cache freshness fallback from sentinel to table/cron evidence)
consecutiveFallbackRunsnumber | undefinedFX cache only: number of back-to-back cached-fallback runs

The /status/ page consumes the richer blacklist fields directly so it can distinguish long-tail historical cleanup from fresh incoming amount gaps. Blacklist amount-gap severity is intentionally tolerant of isolated parser/provider misses: data-quality degrades when the missing-amount ratio reaches 1% or when at least 5 recent events are missing amounts, and becomes stale at 2% or at 25 recent missing events.

dex-liquidity, yield-data, and dews now compute freshness from producer-owned cache sentinels first (freshness:dex-liquidity, freshness:yield-data, freshness:dews). If a sentinel is missing, the worker temporarily falls back to the legacy table query; if the freshness diagnostic still fails, it can fall back again to the latest successful producer cron timestamp and emits a warning/info cause instead of treating the diagnostic miss itself as public stale.

Overall status logic:

  • healthy — every cache impact is healthy, the public mint/burn lane is healthy, fewer than 3 public-impact circuit groups are open, and the health subqueries all resolved cleanly
  • degraded — any cache impact is degraded (including FX cached-fallback or source-cadence lag), any of the blacklist/mint-burn/circuit health subqueries failed, the public mint/burn lane is warning-only, or 3+ public-impact circuit groups are open
  • stale — any cache impact is stale, or the public mint/burn lane is stale versus its critical-lane cadence

/api/health still emits every circuit record under circuits, including dynamic per-coin live-reserves:* scopes, but those reserve-specific breakers do not change the top-level public status on their own; reserve sync health is evaluated on the dedicated reserve/data-quality lanes instead.

Blacklist ratio fields are still emitted here for the public surface, but threshold-based blacklist severity lives under /api/status data-quality; /api/health only escalates its top-level status when the blacklist health loader itself fails.


GET /api/public-status-history

Public transition history for the read-only /status/ page. Returns the current public status plus recent state transitions within a requested time window. Not edge-cached beyond the standard 60-second response cache.

Query parameters

ParamTypeDefaultDescription
window"24h" | "7d" | "30d""30d"Transition time window applied server-side before rows are returned
limitinteger50Max transitions returned after the time-window filter (1–200)

Response

{
  "timestamp": 1771856453,
  "currentStatus": "healthy",
  "lastChangedAt": 1771770000,
  "transitions": [
    {
      "id": 418,
      "from": "degraded",
      "to": "healthy",
      "transitionType": "recover",
      "reason": "raw-healthy-recovery-threshold",
      "at": 1771770000
    }
  ]
}
FieldTypeDescription
timestampnumberUnix seconds at time of response
currentStatus"healthy" | "degraded" | "stale"Current public status, sourced from assessPublicHealth (matches /api/health.status)
lastChangedAtnumber | nullUnix seconds for the latest admin status-machine change, if known
transitionsPublicStatusTransition[]Recent public-impact incident transitions, inside the requested window, newest first

This endpoint powers two separate public /status/ views: the hero Status runway always uses window=30d, while the transition table owns its own user-selected 24h / 7d / 30d filter.

Browser consumers on pharos.watch and ops.pharos.watch should use same-origin /_site-data/public-status-history, which proxies onto the internal website lane instead of calling the external API host directly.

Public-impact filtering (2026-04-13): The endpoint filters the admin state-machine transitions down to incidents opened by at least one public-facing impact code (cache_ratio_*, cache_freshness_query_failed, fx_source_*, fx_cached_fallback, mint_burn_public_*, open_circuit_groups, circuit_query_failed, cron_error_runs, multiple_unhealthy_crons, unhealthy_crons_present, db_unhealthy). Admin-only data-quality causes (missing_prices_*, blacklist_gaps_*, reserve_sync_*, onchain_*, watch_*) are excluded, and info-severity causes cannot open a public incident. Once a public-impact incident is retained, the endpoint also retains the recovery path needed to return that incident to healthy, even when those recovery rows only carry info-level causes. This ensures the public /status/ hero (driven by /api/health) and the uptime bar / transition timeline (driven by this endpoint) always agree. The unfiltered admin view is still available via the admin /api/status endpoint.


GET /api/telegram-pulse

Lightweight Telegram adoption metrics for the public /telegram/ landing page. Returns only aggregate watcher/subscription counts plus the most subscribed coin symbols.

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

Response

{
  "activeWatchers": 1842,
  "coinSubscriptions": 5621,
  "topCoins": ["USDT", "USDC", "USDe"]
}
FieldTypeDescription
activeWatchersnumberSubscribers with at least one active alert type or per-coin alert
coinSubscriptionsnumberTotal active per-coin subscription rows
topCoinsstring[]Up to five most subscribed coin tickers, ordered by subscription count

Browser consumers on pharos.watch and ops.pharos.watch should use same-origin /_site-data/telegram-pulse, which proxies onto the internal website lane instead of calling the external API host directly.


GET /api/stability-index

Latest Pharos Stability Index (PSI) sample plus daily history. The PSI is a composite ecosystem health score (0–100) computed from active depeg severity, affected-market breadth, DEWS stress breadth, and 7-day ecosystem trend across the PSI-eligible universe (tracked coins plus shadow assets used for historical continuity). If a dependency failure prevents a safe fresh sample, the endpoint continues serving the last healthy stored PSI sample instead of publishing a degraded substitute.

Cache: standard — X-Data-Age and Warning headers included.

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

Optional query parameters

ParamTypeDefaultDescription
detail"true"When "true", returns full history with per-day component breakdowns instead of last 91 days

Response

{
  "current": {
    "score": 81.1,
    "band": "STEADY",
    "components": { "severity": 4.59, "breadth": 15, "stressBreadth": 1.8, "trend": 0.65 },
    "computedAt": 1771977600,
    "methodologyVersion": "3.2"
  },
  "history": [{ "date": 1771891200, "score": 81.0, "band": "STEADY", "methodologyVersion": "2.1" }],
  "methodology": {
    "version": "3.2",
    "versionLabel": "v3.2",
    "currentVersion": "3.2",
    "currentVersionLabel": "v3.2",
    "changelogPath": "/methodology/stability-index-changelog/",
    "asOf": 1771977600,
    "isCurrent": true
  }
}
FieldTypeDescription
currentobject | nullLatest PSI score and components. null if cron has not yet run
current.scorenumberPSI score 0–100
current.bandstringCondition band: "BEDROCK", "STEADY", "TREMOR", "FRACTURE", "CRISIS", "MELTDOWN"
current.avg24hnumber | undefinedRolling 24 h average PSI score
current.avg24hBandstring | undefinedCondition band for avg24h
current.componentsobjectComponent breakdown: severity, breadth, stressBreadth, trend
current.contributorsarrayTop per-coin contributors from input_snapshot.contributors (empty when unavailable)
current.inputDegradationobject | undefinedDependency-loss metadata carried by the served sample when the stored input snapshot recorded degraded upstream inputs
current.totalMcapUsdnumberTotal ecosystem market cap from the latest input snapshot (0 when unavailable)
current.computedAtnumberUnix seconds of computation
current.methodologyVersionstringMethodology version used to compute the current score
historyarrayHistorical scores, newest first. With detail=true, each entry includes components
malformedRowsnumberCount of historical rows dropped from detail=true because persisted components JSON was malformed
history[].methodologyVersionstringMethodology version used for that history point
methodologyobjectVersion metadata for current PSI methodology context
methodology.versionstringMethodology version used by current score
methodology.changelogPathstringRelative path to full methodology changelog

GET /api/og/*

Dynamic Open Graph PNG images used by share buttons and page metadata.

Authentication: exempt

Supported routes

  • /api/og/stablecoin/:id
  • /api/og/safety-scores
  • /api/og/depeg
  • /api/og/stability-index

Content-Type: image/png

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

Error cases

  • 404 with text/plain for unknown coin IDs inside /api/og/stablecoin/:id; unknown OG route patterns return the standard JSON { "error": "Unknown OG route" }
  • 503 when required cached data is not yet available
  • 400 for malformed URI encoding in /api/og/stablecoin/:id
  • 500 with text/plain body when OG image rendering fails

/api/og/stablecoin/:id accepts tracked public stablecoin IDs only. The renderer assembles each card from cached stablecoin, DEWS, PSI, report-card, depeg, liquidity, and mint/burn data on the worker.


GET /api/report-cards

Stablecoin risk grade cards with dimension-level scores. Output includes 5 dimensions; overall score is the weighted base (exit-liquidity/resilience/decentralization/dependency) plus peg-multiplier adjustment.

Cache: standard

Response

{
  "cards": [ReportCard, ...],
  "dependencyGraph": {
    "edges": [{ "from": "usde-ethena", "to": "usdc-circle", "weight": 0.9, "type": "collateral" }, ...]
  },
  "methodology": {
    "version": "7.07",
    "weights": { "pegStability": 0, "liquidity": 0.30, "resilience": 0.20, "decentralization": 0.15, "dependencyRisk": 0.25 },
    "pegMultiplierExponent": 0.4,
    "activeDepegSeveritySource": "open-event-peak",
    "activeDepegCaps": {
      "d": { "thresholdBps": 1000, "score": 49 },
      "f": { "thresholdBps": 2500, "score": 39 }
    },
    "thresholds": [{ "grade": "A+", "min": 87 }, { "grade": "A", "min": 83 }, ...]
  },
  "liquidityStale": false,
  "redemptionStale": false,
  "inputFreshness": {
    "dexLiquidity": { "updatedAt": 1771977600, "ageSeconds": 120, "stale": false },
    "redemptionBackstops": { "updatedAt": 1771977600, "ageSeconds": 300, "stale": false }
  },
  "updatedAt": 1771977600
}

The Liquidity dimension now represents effectiveExitScore: the public DEX liquidity score remains the floor, while redeemable assets can receive uplift from redemptionBackstopScore when a meaningful direct exit path exists. Documented offchain issuer exits with eventual-only capacity can add only a DEX-gated primary-market bonus; they do not replace missing DEX liquidity. Last-known DEX liquidity remains usable even after its freshness runway, with staleness surfaced through liquidityStale and inputFreshness.dexLiquidity.stale. Low-confidence redemption routes stay visible but do not uplift the score, and materially stale redemption inputs are not blended. Report-card redemption inputs are treated as materially stale after more than twice the 4-hourly redemption sync cadence, so normal cron lag does not globally remove medium- or high-confidence redemption uplift.

For peg handling, rawInputs.pegScore is the effective peg input used by report-card scoring. Most coins use their direct peg-summary value. Configured NAV wrappers can inherit peg stability from a referenced base stablecoin when the wrapper share price is not the right peg-tracking surface; pure NAV tokens without a configured reference remain null and keep neutral handling. rawInputs.activeDepegBps is the open active depeg event's absolute peak deviation used for final Safety Score caps; it is not the latest spot deviation.

GET /api/report-cards treats the stablecoins cache and readable redemption-backstop table as hard dependencies. DEX liquidity, bluechip ratings, live-reserve inputs, and materially stale redemption rows are soft dependencies: if one of those loaders is temporarily unavailable or stale beyond its scoring freshness runway, the endpoint continues serving a degraded snapshot instead of failing closed, with stale inputs suppressed from scoring.

dependencyGraph.edges: Pre-computed forward edges. from = upstream stablecoin ID, to = dependent stablecoin ID. weight and type carry the worker's canonical dependency metadata, so frontend graph consumers can use the snapshot directly instead of re-deriving edge semantics from static stablecoin metadata.

ReportCard

FieldTypeDescription
idstringPharos stablecoin ID
namestringFull name
symbolstringTicker
overallGradestringLetter grade: "A+" through "F", or "NR"
overallScorenumber | nullWeighted score 0–100. null for unrated coins
baseScorenumber | nullPre-peg-multiplier/no-liquidity/active-depeg-cap score after base dimension blending
dimensionsRecord<DimensionKey, DimensionScore>Per-dimension grade, score, and detail text
ratedDimensionsnumberNumber of dimensions with data (max 5)
rawInputsRawDimensionInputsRaw scoring inputs for client-side grade recomputation (stress testing)
isDefunctbooleantrue for cemetery coins (permanent F grade)

DependencyWeight: { id: string, weight: number, type?: DependencyType } — upstream stablecoin ID + fraction of collateral from that source (0–1), with optional dependency category. Weights sum to ≤ 1.0; the remainder represents non-stablecoin collateral.

RawDimensionInputs

FieldType
pegScorenumber | null
activeDepegboolean
activeDepegBpsnumber | null
depegEventCountnumber
lastEventAtnumber | null
liquidityScorenumber | null
effectiveExitScorenumber | null
redemptionBackstopScorenumber | null
redemptionRouteFamilyRedemptionRouteFamily | null
redemptionModelConfidence"high" | "medium" | "low" | null
redemptionUsedForLiquidityboolean
redemptionImmediateCapacityUsdnumber | null
redemptionImmediateCapacityRationumber | null
concentrationHhinumber | null
bluechipGradeBluechipGrade | null
canBeBlacklistedboolean | "possible" | "inherited"
chainTierChainTier
deploymentModelDeploymentModel
collateralQualityCollateralQuality
custodyModelCustodyModel
governanceTierGovernanceType
governanceQualityGovernanceQuality
dependenciesDependencyWeight[]
navTokenboolean
collateralFromLiveboolean

rawInputs.canBeBlacklisted is the canonical resolved blacklist status used by report-card-backed product surfaces. It can therefore differ from the raw StablecoinMeta.canBeBlacklisted override field, which only carries manual metadata and never stores computed "inherited" values.

rawInputs.collateralFromLive is true when score-grade live reserve data drove collateral scoring for the card.

Dimensions: pegStability, liquidity, resilience, decentralization, dependencyRisk


GET /api/redemption-backstops

Current redemption-backstop dataset for redeemable assets.

Cache: standard

Error responses: 503 when redemption_backstop has no rows yet, or when the current snapshot cannot be read cleanly.

Rows written by the current worker are grouped by a completed snapshot run manifest. The API serves the latest completed run when one exists, which prevents a partially written hourly sync from being treated as a fresh complete dataset. Legacy rows without a completed run remain readable during bootstrap and migration fallback.

Response

{
  "coins": {
    "cusd-cap": {
      "stablecoinId": "cusd-cap",
      "score": 88,
      "effectiveExitScore": 56,
      "dexLiquidityScore": 29,
      "routeFamily": "basket-redeem",
      "accessModel": "permissionless-onchain",
      "settlementModel": "atomic",
      "outputAssetType": "stable-basket",
      "immediateCapacityUsd": null,
      "immediateCapacityRatio": null,
      "sourceMode": "estimated",
      "resolutionState": "resolved",
      "routeStatus": "open",
      "routeStatusSource": "static-config",
      "holderEligibility": "any-holder",
      "capacityConfidence": "heuristic",
      "capacitySemantics": "eventual-only",
      "feeConfidence": "undisclosed-reviewed",
      "feeModelKind": "undisclosed-reviewed",
      "modelConfidence": "low",
      "updatedAt": 1773350400,
      "methodologyVersion": "3.98"
    }
  },
  "methodology": {
    "version": "3.98",
    "versionLabel": "v3.98",
    "currentVersion": "3.98",
    "currentVersionLabel": "v3.98",
    "changelogPath": "/methodology/#safety-scores-methodology",
    "asOf": 1773350400,
    "isCurrent": true,
    "componentWeights": {
      "access": 0.2,
      "settlement": 0.15,
      "executionCertainty": 0.15,
      "capacity": 0.25,
      "outputAssetQuality": 0.15,
      "cost": 0.1
    },
    "effectiveExitModel": {
      "model": "best-path",
      "diversificationFactor": 0.1
    }
  },
  "updatedAt": 1773350400
}

score is the direct redemption-quality score.

effectiveExitScore is the raw best-path exit score written into the redemption snapshot when the route resolved cleanly and is not currently impaired. It reuses last-known DEX liquidity even when the DEX input is stale; the cron records that operational state through metadata.liquidityStale. When both DEX liquidity and redemption exist, the model uses min(100, max(dex, redemption) + min(dex, redemption) × 0.10). Report cards may still recompute liquidity from the same underlying redemption score with additional confidence, eligibility, and active-depeg gating, so this raw endpoint value can differ numerically from dimensions.liquidity.score.

methodology.version is attributed from the latest completed redemption snapshot run, falling back to the latest stored row for legacy snapshots. methodology.currentVersion remains the live code version when the API is serving an older snapshot that has not yet been recomputed.

sourceMode:

  • dynamic = live reserve/protocol telemetry
  • estimated = modelled from current supply and conservative route assumptions
  • static = route remains configured, but current runtime inputs did not resolve a usable score

resolutionState:

  • resolved = the route produced a usable score
  • missing-cache = the stablecoins snapshot did not include the asset or its current supply
  • missing-capacity = the route is configured, but the snapshot could not resolve enough capacity to score it
  • failed = a route-specific resolver failed
  • impaired = the route shape is known but current market or route-availability evidence contradicts broad par redemption

routeStatus / routeStatusSource describe current route availability separately from the static route shape. Normal rows use routeStatus: "open" and routeStatusSource: "static-config". A severe active depeg (>=2500 bps) can publish routeStatus: "degraded" and routeStatusSource: "market-implied" for static or non-live-direct routes; those impaired rows have score = null, effectiveExitScore = null, and modelConfidence = "low". holderEligibility describes the modeled holder cohort, such as any-holder, verified-customer, whitelisted-primary, pre-incident-holder, issuer-discretionary, or unknown.

Top-level fields:

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

RedemptionBackstopEntry highlights:

FieldTypeDescription
scorenumber | nullDirect redemption-quality score after route-family/config caps
effectiveExitScorenumber | nullBlended exit score used by report cards
dexLiquidityScorenumber | nullDEX liquidity input used in the blend
routeFamilystringstablecoin-redeem, basket-redeem, collateral-redeem, psm-swap, queue-redeem, or offchain-issuer
accessModelstringpermissionless-onchain, whitelisted-onchain, issuer-api, or manual
settlementModelstringatomic, immediate, same-day, days, or queued
outputAssetTypestringstable-single, stable-basket, bluechip-collateral, mixed-collateral, or nav
sourceModestringdynamic, estimated, or static capacity provenance
resolutionStatestringresolved, missing-cache, missing-capacity, failed, or impaired
routeStatusstringCurrent route availability: open, degraded, paused, cohort-limited, or unknown
routeStatusSourcestringSource for current route availability: static-config, market-implied, operator-notice, protocol-api, or onchain
routeStatusReasonstring | undefinedHuman-readable explanation when current availability impairs scoring
routeStatusReviewedAtstring | undefinedUTC date (YYYY-MM-DD) for the current route-status assessment
holderEligibilitystringModeled holder cohort: any-holder, verified-customer, whitelisted-primary, pre-incident-holder, issuer-discretionary, or unknown
capacityConfidencestringlive-direct, live-proxy, documented-bound, heuristic, or legacy dynamic fidelity tag for the capacity model
capacityBasisstring | undefinedTyped basis for the modeled capacity, such as issuer-term-redemption, full-system-eventual, psm-balance-share, strategy-buffer, hot-buffer, daily-limit, live-direct-telemetry, or live-proxy-buffer
capacitySemanticsstringimmediate-bounded or eventual-only, distinguishing current redeemable buffer from eventual redeemability
feeConfidencestringfixed, formula, or undisclosed-reviewed fidelity tag for the fee model
feeModelKindstringfixed-bps, formula, documented-variable, or undisclosed-reviewed
modelConfidencestringOverall route-fidelity rollup: high, medium, or low
immediateCapacityUsdnumber | nullImmediate redeemable capacity in USD. null when the model is eventual-only or currently unrated
immediateCapacityRationumber | nullImmediate redeemable capacity as a share of supply. null when not separately quantified
feeBpsnumber | nullExplicit bounded fee when configured
feeDescriptionstring | undefinedDocs-backed fee description for variable, conditional, flat-minimum, or undisclosed redemption schedules
queueEnabledbooleanWhether the modeled route is explicitly queued/serial
docs{ label?: string, url?: string, reviewedAt?: string, provenance?: string, sources?: { label: string, url: string, supports?: string[] }[] } | undefinedOptional documentation / transparency metadata. reviewedAt is the route-review date, while provenance is config-reviewed, live-reserve-display, proof-of-reserves, or preferred-link
notesstring[] | undefinedRuntime notes such as stale reserve metadata fallback
capsAppliedstring[] | undefinedApplied score caps (queue-route-cap, offchain-route-cap, config-cap)

Response (503): { "error": "Data not yet available" } or { "error": "Redemption backstop snapshot unavailable" }


GET /api/safety-score-history

Per-coin Safety Score grade transition history (seed row + grade changes only). Rows are written by the daily snapshot-safety-grade-history cron and returned in ascending date order.

Cache: slow

Required query parameter

ParamTypeDescription
stablecoinstringPharos stablecoin ID (required)

Optional query parameters

ParamTypeDefaultBoundsDescription
daysinteger3651–3650Lookback window in days

Response: Array sorted by date ascending.

[
  {
    "date": 1771977600,
    "grade": "B+",
    "score": 78,
    "prevGrade": "B",
    "prevScore": 74,
    "methodologyVersion": "5.5"
  }
]
FieldTypeDescription
datenumberUTC day bucket (Unix seconds) when the event was recorded
gradestringCurrent Safety Score letter grade at date
scorenumber | nullCurrent numeric score (0–100); null when grade is NR
prevGradestring | nullPrevious grade before this event; null for the seed row
prevScorenumber | nullPrevious score before this event; null for the seed row
methodologyVersionstringSafety Score methodology version used for this event row

GET /api/yield-rankings

Cache-backed yield rankings written by the sync-yield-data cron. The endpoint rehydrates safetyScore, safetyGrade, yieldToRisk, and pharosYieldScore from the current report-card snapshot at read time so Yield Intelligence stays aligned with /api/report-cards. PYS is benchmark-aware: it starts from cached APY inputs, adds a weighted slice of the row's benchmark spread, and then applies the current Safety Score. The response also includes source-selection provenance, the default USD benchmark (riskFreeRate), and the structured benchmark registry used for row-level excess-yield selection. If a ranking row has no matching live report-card snapshot, the API now retains the row and falls back to DEFAULT_SAFETY_SCORE (40) and grade NR instead of dropping coverage.

Cache: standard — X-Data-Age and Warning headers included. Freshness threshold: 3600 s (1 hour, aligned to the hourly sync-yield-data publisher).

Error responses: 503 when the cached rankings payload is missing or unparseable JSON. If a parseable payload fails the live schema validation path, the handler logs the validation issue and still serves the cached object with _meta, without live safety hydration.

Response

{
  "rankings": [YieldRanking, ...],
  "riskFreeRate": 4.25,
  "benchmarks": {
    "USD": { "key": "USD", "label": "USD 3M T-Bill", "currency": "USD", "rate": 4.25, "recordDate": "2026-03-25", "fetchedAt": 1774425600, "ageSeconds": 0, "source": "fred-dgs3mo", "isFallback": false, "fallbackMode": null, "isProxy": false },
    "EUR": { "key": "EUR", "label": "EUR 3M compounded €STR", "currency": "EUR", "rate": 1.9358, "recordDate": "2026-03-26", "fetchedAt": 1774425600, "ageSeconds": 0, "source": "ecb-estr-3m", "isFallback": false, "fallbackMode": null, "isProxy": false },
    "CHF": { "key": "CHF", "label": "CHF 3M compounded SARON", "currency": "CHF", "rate": -0.0539, "recordDate": "2026-03-25", "fetchedAt": 1774425600, "ageSeconds": 0, "source": "six-sar3mc", "isFallback": false, "fallbackMode": null, "isProxy": false }
  },
  "scalingFactor": 8,
  "medianApy": 4.21,
  "updatedAt": 1772000000,
  "provenance": {
    "selectionMethod": "confidence-weighted",
    "benchmark": { "key": "USD", "label": "USD 3M T-Bill", "currency": "USD", "rate": 4.25, "recordDate": "2026-03-25", "fetchedAt": 1774425600, "ageSeconds": 0, "source": "fred-dgs3mo", "isFallback": false, "fallbackMode": null, "isProxy": false },
    "benchmarks": {
      "USD": { "key": "USD", "label": "USD 3M T-Bill", "currency": "USD", "rate": 4.25, "recordDate": "2026-03-25", "fetchedAt": 1774425600, "ageSeconds": 0, "source": "fred-dgs3mo", "isFallback": false, "fallbackMode": null, "isProxy": false },
      "EUR": { "key": "EUR", "label": "EUR 3M compounded €STR", "currency": "EUR", "rate": 1.9358, "recordDate": "2026-03-26", "fetchedAt": 1774425600, "ageSeconds": 0, "source": "ecb-estr-3m", "isFallback": false, "fallbackMode": null, "isProxy": false },
      "CHF": { "key": "CHF", "label": "CHF 3M compounded SARON", "currency": "CHF", "rate": -0.0539, "recordDate": "2026-03-25", "fetchedAt": 1774425600, "ageSeconds": 0, "source": "six-sar3mc", "isFallback": false, "fallbackMode": null, "isProxy": false }
    },
    "dlPools": { "mode": "dex-cache", "ageSeconds": 240, "poolCount": 812 },
    "safetySnapshot": { "kind": "ok", "coverageRatio": 0.98 }
  },
  "_meta": { "updatedAt": 1710500000, "ageSeconds": 42, "status": "fresh" }
}
FieldTypeDescription
rankingsYieldRanking[]All ranked stablecoins, sorted by Pharos Yield Score descending
riskFreeRatenumberDefault USD benchmark rate (%) retained for backward compatibility and mixed-view fallback
benchmarksobject | nullBenchmark registry keyed by currency (USD, EUR, CHF) with label, rate, freshness, fallback, and proxy metadata
scalingFactornumberScaling factor applied in yield score computation
medianApynumberTVL-weighted median APY (30d) across best-source rows, used as a peer reference in warning heuristics
updatedAtnumberUnix seconds when the rankings were last computed
provenanceobject | nullSnapshot-level provenance for default benchmark freshness, full benchmark registry, DeFiLlama pool input freshness, safety coverage, and selection method

YieldRanking

FieldTypeDescription
idstringPharos stablecoin ID
symbolstringToken symbol
namestringFull name
currentApynumberCurrent APY (%)
apy7dnumber7-day average APY (%)
apy30dnumber30-day average APY (%)
apyBasenumber | nullBase APY component (%)
apyRewardnumber | nullReward APY component (%), null if none
yieldSourcestringHuman-readable yield source description
yieldSourceUrlstring | nullOfficial URL for the selected source when Pharos has a curated or metadata-derived link
yieldTypestringYield type classification (e.g. "lending-vault", "staking")
dataSourcestringData source identifier (e.g. "defillama")
sourceTvlUsdnumber | nullTVL of the yield source pool (USD)
pharosYieldScorenumber | nullComposite Pharos Yield Score (0–100), recomputed at read time from cached APY + benchmark inputs plus the current Safety Score
safetyScorenumber | nullCurrent Safety Score input used by Yield Intelligence. Rated coins match /api/report-cards; unrated coins use the default NR penalty input (40)
safetyGradestring | nullCurrent Safety Score letter grade ("A+" through "F", or "NR") from /api/report-cards
yieldToRisknumber | nullYield-to-risk ratio recomputed at read time from cached APY inputs plus the current Safety Score
excessYieldnumber | nullAPY above risk-free rate (percentage points)
benchmarkKey"USD" | "EUR" | "CHF" | undefinedBenchmark selected for this row's excessYield and any rate-derived APY logic
benchmarkLabelstring | undefinedHuman-readable benchmark label for the row
benchmarkCurrencystring | undefinedBenchmark currency code used for the row
benchmarkRatenumber | undefinedBenchmark rate (%) applied to this row
benchmarkRecordDatestring | null | undefinedMarket or policy record date for the selected benchmark
benchmarkIsFallbackboolean | undefinedWhether the row benchmark is currently on a fallback path
benchmarkFallbackModestring | null | undefinedFallback reason for the row benchmark when applicable
benchmarkSelectionMode"native" | "fallback-usd" | "manual-override" | undefinedHow the row benchmark was selected
benchmarkIsProxyboolean | undefinedTrue when the selected benchmark is an explicit proxy rather than the exact reference rate
yieldStabilitynumber | nullYield stability metric (0–1; higher = more stable)
apyVariance30dnumber | null30-day APY variance
apyMin30dnumber | nullMinimum APY in last 30 days (%)
apyMax30dnumber | nullMaximum APY in last 30 days (%)
warningSignalsstring[]Active warning-signal flags for the selected best source
altSourcesAltYieldSource[]Additional non-selected source rows for the same coin
provenanceobject | nullSource-level provenance: confidence tier, selection reason, benchmark state, source-switch metadata, source freshness, and optional anchor timing

When present, YieldRanking.provenance includes:

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

GET /api/yield-history

Historical yield data for a single stablecoin. If a stored warning_signals payload is malformed, the API treats it as an empty array rather than failing the entire response. Returned rows are capped at the latest published /api/yield-rankings snapshot so history cannot advance past an unpublished yield cache state. If the cached rankings payload is missing or malformed, the cap degrades to the latest successful sync-yield-data cron timestamp instead of wall-clock now.

Cache: slow — X-Data-Age and Warning headers included. Freshness threshold: 3600 s (1 hour, aligned to the hourly sync-yield-data publisher).

Required query parameter

ParamTypeDescription
stablecoinstringPharos stablecoin ID (required)

Optional query parameters

ParamTypeDefaultBoundsDescription
daysinteger901–365Lookback window in days
modestringbestbest for historically selected best-source rows
sourceKeystringWhen present, returns source-specific history for that source key

Response

{
  "current": {
    "date": 1772000000,
    "apy": 4.21,
    "sourceKey": "onchain:usde-ethena",
    "yieldSource": "Ethena staking (sUSDe)"
  },
  "history": [YieldHistoryPoint, "..."],
  "methodology": {
    "version": "7.4",
    "currentVersion": "7.4",
    "changelogPath": "/methodology/yield-changelog/"
  }
}
FieldTypeDescription
currentYieldHistoryPoint|nullLatest row in the returned history window, or null when no history exists
historyYieldHistoryPoint[]History rows sorted by date ASC
methodologyobjectYield methodology envelope for the response

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
}
FieldTypeDescription
datenumberUnix seconds
apynumberTotal APY at snapshot time (%)
apyBasenumber | nullBase APY component (%)
apyRewardnumber | nullReward APY component (%); null if none
exchangeRatenumber | nullExchange rate at snapshot time (e.g. sUSDe/USDe); null if not applicable
sourceTvlUsdnumber | nullTVL of the yield source pool at snapshot time (USD)
warningSignalsstring[]Active warning-signal flags at that snapshot
sourceKeystring | nullStable source identifier for this history row (for example a DL pool UUID or onchain:<stablecoinId>)
yieldSourcestring | nullHuman-readable source label at that snapshot
yieldSourceUrlstring | nullOfficial URL for that source when Pharos has a curated or metadata-derived link
yieldTypestring | nullYield type classification at that snapshot
dataSourcestring | nullUnderlying data-source family
isBestbooleanWhether this row was the selected best source at that timestamp
sourceSwitchbooleanTrue when the historically selected best source changed at this row

GET /api/mint-burn-flows

Mint/burn flow data across tracked stablecoins — aggregate gauge score, per-coin net-flow + pressure-shift signals, and hourly timeseries. Updated every 30 minutes by the sync cron.

Cache: standard

Error responses: 503 when the cached fallback payload is missing or malformed and live recomputation cannot satisfy the request. Malformed embedded freshness fields inside an otherwise valid cached payload no longer reset freshness to synthetic values; the API logs the corruption and falls back to the cache row timestamp.

Optional query parameters

ParamTypeDefaultBoundsDescription
stablecoinstringFilter to a single stablecoin ID. Changes response shape to per-coin mode
hoursinteger241–720Lookback window for the returned hourly[] series

Response (aggregate mode — no stablecoin param)

{
  "gauge": {
    "score": 2.3,
    "band": "NEUTRAL",
    "flightToQuality": false,
    "flightIntensity": 0,
    "trackedCoins": 8,
    "trackedMcapUsd": 215000000000,
    "intensitySemantics": "signed-v2",
    "classificationSource": "report-card-cache"
  },
  "coins": [CoinFlow, ...],
  "hourly": [HourlyFlow, ...],
  "updatedAt": 1772000000,
  "windowHours": 24,
  "scope": { "chainIds": ["ethereum", "arbitrum"], "label": "Configured issuance chains" },
  "sync": { "lastSuccessfulSyncAt": 1772000200, "freshnessStatus": "fresh", "warning": null, "criticalLaneHealthy": true }
}

gauge

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

Top-level metadata

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

CoinFlow

FieldTypeDescription
stablecoinIdstringPharos stablecoin ID
symbolstringToken symbol
flowIntensitynumber | nullDeprecated alias for pressureShiftScore; retained for compatibility
pressureShiftScorenumber | nullCanonical baseline-relative pressure score (-100 to +100). null if < 7 days of data or no current activity
pressureShiftState"improving" | "stable" | "worsening" | "nr"Interpreted pressure state from pressureShiftScore
netFlowDirection24h"minting" | "burning" | "flat" | "inactive"Current 24h direction derived from raw net flow + activity
has24hActivitybooleanWhether any 24h mint/burn events were recorded for the coin
baselineDailyNetUsdnumber | nullAverage daily net flow over the baseline window used for scoring
baselineDailyAbsUsdnumber | nullAverage daily absolute flow over the baseline window used for scoring
baselineDataDaysnumber | nullNumber of tracked days contributing to the baseline window
netFlow24hUsdnumberRaw 24h net flow (USD, positive = net minting, negative = net burning). Fixed to the canonical 24h window even when hours changes
mintVolume24hUsdnumberTotal mint volume in the canonical 24h window (USD)
burnVolume24hUsdnumberTotal burn volume in the canonical 24h window (USD)
mintCount24hnumberNumber of mint events in the canonical 24h window
burnCount24hnumberNumber of burn events in the canonical 24h window
netFlow7dUsdnumber7-day net flow (USD)
netFlow30dUsdnumber30-day net flow (USD)
netFlow90dUsdnumber90-day net flow (USD)
largestEvent24hobject | nullLargest event in the last 24h: { direction, amountUsd, txHash, timestamp }
coverageobject | undefinedCoverage metadata: startBlock, lastSyncedBlock, lagBlocks, historyStartAt, window booleans, adapter provenance (adapterKinds, startBlockSource, startBlockConfidence), and status

HourlyFlow

FieldTypeDescription
hourTsnumberUnix seconds (start of hour)
netFlowUsdnumberNet flow for this hour (USD)
mintVolumeUsdnumberMint volume for this hour (USD)
burnVolumeUsdnumberBurn volume for this hour (USD)

Response (per-coin mode — with stablecoin param)

Returns per-chain breakdown and hourly timeseries for a single coin. Returns 404 if the stablecoin is not tracked for mint/burn flows.

{
  "stablecoinId": "usdt-tether",
  "symbol": "USDT",
  "mintVolumeUsd": 50000000,
  "burnVolumeUsd": 30000000,
  "netFlowUsd": 20000000,
  "mintCount": 12,
  "burnCount": 8,
  "chains": [{ "chainId": "ethereum", "mintVolumeUsd": 40000000, ... }],
  "hourly": [HourlyFlow, ...],
  "updatedAt": 1772000000,
  "windowHours": 24,
  "scope": { "chainIds": ["ethereum"], "label": "Ethereum-only" },
  "sync": { "lastSuccessfulSyncAt": 1772000200, "freshnessStatus": "fresh", "warning": null, "criticalLaneHealthy": true }
}

GET /api/mint-burn-events

Paginated list of individual mint/burn events for a specific stablecoin. Events are sourced from on-chain logs via Alchemy JSON-RPC.

Cache: realtime

Required query parameter

ParamTypeDescription
stablecoinstringPharos stablecoin ID (required)

Optional query parameters

ParamTypeDefaultBoundsDescription
directionstring"mint" or "burn"Filter by direction
chainstringtracked chain IDs for the requested stablecoinFilter by chain ID within the stablecoin's configured issuance scope
burnTypestring"effective_burn", "bridge_burn", "review_required"Filter burn rows by classification
scopestring"all""all" or "counted"counted returns only rows used in economic-flow aggregates (flow_type='standard' and mint/effective-burn semantics)
minAmountnumberMinimum USD amount; unpriced rows are excluded when this filter is used
limitinteger501–500Max results
offsetinteger0Pagination offset

Response

{
  "events": [MintBurnEvent, ...],
  "total": 1234
}

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

MintBurnEvent

FieldTypeDescription
idstringComposite ID: {chainId}-{txHash}-{logIndex}
stablecoinIdstringPharos stablecoin ID
symbolstringToken symbol
chainIdstringChain identifier (e.g. "ethereum")
direction"mint" | "burn"Whether tokens were minted or burned
flowType"standard" | "bridge_transfer" | "atomic_roundtrip"Flow-noise classification; bridge_transfer and atomic_roundtrip rows are excluded from aggregate flow metrics
amountnumberAmount in native token units
amountUsdnumber | nullUSD value at time of event
burnType"effective_burn" | "bridge_burn" | "review_required" | nullBurn classification; null for mint rows
burnReviewReasonstring | nullReason emitted when a burn requires manual review classification
counterpartystring | nullNon-zero address (recipient for mint, sender for burn)
txHashstringTransaction hash
blockNumbernumberBlock number
timestampnumberUnix seconds
explorerTxUrlstringBlock explorer URL for the transaction
priceUsednumber | nullPrice used to derive amountUsd
priceTimestampnumber | nullUnix seconds of the price snapshot used
priceSourcestring | nullValuation provenance (supply_history, price_cache, price_cache_heal, etc.)

GET /api/stress-signals

Returns Depeg Early Warning Score (DEWS) data for active tracked stablecoins.

All coins (no params): Latest DEWS score + signal breakdown per coin.

Single coin: Add ?stablecoin=ID&days=30 for latest + daily history.

stablecoin must be an active tracked Pharos stablecoin ID. Unknown IDs return 404 with { "error": "Unknown stablecoin" }; tracked-but-non-active IDs return 404 with { "error": "Stablecoin not tracked" }.

Cache: standard (public, s-maxage=300, max-age=60). Freshness threshold: 1800 s (30 minutes, aligned to compute-dews).

Query parameters

ParamTypeDefaultDescription
stablecoinstringSingle coin mode: return latest + daily history
daysinteger30History lookback (max 365)

Aggregate responses are filtered to active tracked stablecoin IDs only, even if stale rows for non-active or de-tracked IDs still exist in storage. The aggregate response keeps updatedAt as the newest returned current row, but X-Data-Age / Warning freshness headers are based on oldestComputedAt so a stale per-coin row cannot be hidden by fresher rows for other coins.

Response (all coins)

{
  "signals": {
    "usdt-tether": {
      "score": 5,
      "band": "CALM",
      "signals": {
        "supply": { "value": 2, "available": true },
        "price": { "value": 1, "available": true }
      },
      "amplifiers": { "psi": 1, "contagion": 1 },
      "computedAt": 1740000000,
      "methodologyVersion": "5.95"
    }
  },
  "updatedAt": 1740000000,
  "oldestComputedAt": 1740000000,
  "malformedRows": 0,
  "methodology": {
    "version": "5.95",
    "versionLabel": "v5.95",
    "currentVersion": "5.95",
    "currentVersionLabel": "v5.95",
    "changelogPath": "/methodology/depeg-changelog/",
    "asOf": 1740000000,
    "isCurrent": true
  }
}

Response (single coin)

{
  "current": {
    "score": 5,
    "band": "CALM",
    "signals": {
      "supply": { "value": 2, "available": true },
      "price": { "value": 1, "available": true }
    },
    "amplifiers": { "psi": 1, "contagion": 1 },
    "computedAt": 1740000000,
    "methodologyVersion": "5.95"
  },
  "history": [
    {
      "date": 1739900000,
      "score": 3,
      "band": "CALM",
      "signals": {
        "supply": { "value": 1, "available": true },
        "price": { "value": 1, "available": true }
      },
      "amplifiers": { "psi": 1, "contagion": 1 },
      "methodologyVersion": "5.95"
    }
  ],
  "malformedRows": 0,
  "methodology": {
    "version": "5.95",
    "versionLabel": "v5.95",
    "currentVersion": "5.95",
    "currentVersionLabel": "v5.95",
    "changelogPath": "/methodology/depeg-changelog/",
    "asOf": 1740000000,
    "isCurrent": true
  }
}

malformedRows — count of DB rows with unparseable JSON signal data (expected 0 under normal operation)

oldestComputedAt — aggregate mode only; oldest returned current row and the timestamp used for response freshness headers

amplifiers — clamped multipliers that were applied on top of the base weighted score. psi is the systemic PSI amplifier (range [1.0, 1.3]); contagion is the per-peg-type cross-asset amplifier (range [1.0, 1.2]). Both default to 1.0 for legacy cached rows written before v5.95.

methodology — same fields and semantics as /api/depeg-events


POST /api/feedback

Public feedback ingestion endpoint used by the in-app feedback modal. Validates payloads, applies IP-based rate limiting, and forwards submissions to GitHub Issues.

Authentication: exempt

Cache: no edge cache (POST passthrough)

Rate limits

  • Global public API limiter: D1-backed per-IP-hash limiter (300 requests / 60 seconds) for non-admin requests. If the distributed limiter path fails, the worker fails open for the first two consecutive storage failures, then returns 503 with Retry-After: 60 until limiter storage recovers or the emergency counter decays.
  • Feedback endpoint limiter: 3 submissions / 10 minutes per salted IP hash in D1.

Request body

{
  "type": "bug",
  "title": "Optional short title",
  "description": "Required, 10-2000 characters",
  "expectedValue": "Optional expected behavior/value",
  "stablecoinId": "Optional canonical stablecoin id",
  "stablecoinName": "Optional stablecoin name",
  "pageUrl": "/stablecoin/usdt-tether",
  "pegValue": "Optional UI value snapshot",
  "contactHandle": "@pharos_user",
  "website": ""
}
FieldTypeRequiredNotes
type"bug" | "data-correction" | "feature-request"YesSubmission category
titlestringConditionalRequired for bug and feature-request (3–100 chars); optional for data-correction
descriptionstringYes10–2000 chars
pageUrlstringYesRelative app path (must start with /)
websitestringNoHoneypot field; non-empty is silently accepted/dropped
expectedValue, stablecoinId, stablecoinName, pegValuestringNoOptional metadata
contactHandlestringNoOptional Telegram/X handle that appears publicly on GitHub

Response

{ "ok": true }

Error responses

  • 400 invalid payload
  • 429 rate limited (3 submissions / 10 minutes per salted IP hash)
  • 500 forwarding/processing failure
  • 503 service misconfigured (missing FEEDBACK_IP_SALT or GITHUB_PAT)

POST /api/telegram-webhook

Telegram Bot API webhook endpoint. Receives user messages, processes bot commands, and manages subscriptions.

Authentication: exempt from X-API-Key; requires X-Telegram-Bot-Api-Secret-Token instead. Not the standard X-Admin-Key.

Rate limiting: Exempt from IP rate limiter (Telegram sends from fixed IPs).

Cache: no edge cache (POST passthrough)

Request body: Telegram Update object (JSON, sent by Telegram servers).

Response: Always 200 OK with plain-text body ok (Telegram retries on non-2xx).

Commands handled:

  • /start — Welcome message with onboarding examples plus @pharoswatch and @pharoswatchers links
  • /presets — List the preset watchlist catalog and example commands
  • /subscribe <types> <targets> — Subscribe to alerts for explicit coins or preset watchlists (types: dews, depeg, safety, launch)
  • /subscribe <types> all — Enable one or more alert types across all tracked stablecoins
  • /unsubscribe <targets> — Remove explicit coin subscriptions or the concrete coin rows covered by a preset watchlist
  • /unsubscribe all — Remove all per-coin subscriptions and disable every current alert flag, including launch
  • /set <ticker> <setting> <value> — Tune per-coin thresholds and modes
  • /set all <setting> <value> — Toggle global all-stablecoin alert types
  • /mute <start>-<end> — Enable UTC quiet hours
  • /unmutehours — Disable quiet hours
  • /status <ticker> — Read-only per-coin status summary
  • /cancel — Cancel a pending disambiguation flow
  • /list — Show current subscriptions, per-coin settings, and quiet hours
  • /help — Command reference

Preset watchlists currently resolve at command time into standard telegram_subscriptions rows. Supported aliases are usd-top10, usd-top25, usd-top50, eur-top10, gold-top5, mcap-ge-1b, and mcap-ge-100m. Presets are supported for dews, depeg, and safety; launch still requires explicit tickers or Pharos coin IDs.