Agent navigation — Grep the heading you need: API Endpoints · Route Definition Model · Telegram Subsystem Tables · Telegram Alert Cron Job · File Tree Guide · Frontend Runtime And SEO Surface · CSS Build Pipeline · Coverage Subsystem · Worker Coding Conventions · TypeScript Target Constraints · Architectural Decision Records · Stablecoin lifecycle phases · Funding page.
API Endpoints
Curated architecture-significant routes. Start with the Documentation Index for the full docs map, or go straight to the API Reference for the exhaustive HTTP contract.
Route Definition Model
Static route metadata is declared once in the folderized shared/lib/api-endpoints/ module surface (@shared/lib/api-endpoints). That shared descriptor list carries path, method, admin/cache/probe/status-action metadata, shared dynamic-admin path matching, plus the worker dependency-hydration hints needed for static routes. Worker route primitives now live in worker/src/routes/shared.ts, domain route arrays are split under worker/src/routes/, and worker/src/routes/registry.ts composes them into the dispatch map that worker/src/router.ts consumes for method validation and generic dispatch. Dependency hydration lives in worker/src/routes/dependency-hydrators.ts and stays exhaustive/keyed by EndpointDependency, so adding a new dependency without wiring hydration still fails at compile time instead of silently defaulting.
Cron trigger metadata follows the same single-source pattern. shared/lib/cron-jobs.ts remains the schedule authority, while shared/lib/scheduled-runner-registry.ts binds each cron expression to a symbolic scheduled-runner key that both the worker scheduler and scripts/ci/check-cron-schedule-sync.ts consume. That keeps worker/wrangler.toml, shared cron metadata, and scheduled-runner dispatch in lockstep.
The architecture doc no longer carries a hand-maintained endpoint inventory. Use the generated OpenAPI artifact (public/openapi.json), the generated quick-reference block in API Reference, and the source route registries (shared/lib/api-endpoints/ plus worker/src/routes/) for current route membership. The architecture contract is the routing model above, not the full route list.
Telegram Subsystem Tables
| Table | Description |
|---|---|
telegram_subscribers | Bot subscriber preferences (chat_id, alert type flags) |
telegram_subscriptions | Per-user coin subscriptions (chat_id, stablecoin_id) |
telegram_pending_disambiguation | Ephemeral mid-conversation state for ticker disambiguation |
telegram_pending_alerts | Overflow subscriber-alert delivery queue drained by the 5-minute alert cron |
The Telegram subscriber, disambiguation, and overflow-queue tables are part of the squashed worker baseline in worker/migrations/0000_baseline.sql; see worker/migrations/MANIFEST.md for the pre-squash lineage and current post-baseline files. For the full bot flow, see PharosWatchBot and Telegram Alerts.
Telegram Alert Cron Job
| Job | Description |
|---|---|
dispatch-telegram-alerts | Detects DEWS/depeg/safety/launch/reserve/freeze changes and fans out alerts to subscribers on the dedicated 2,7,12,17,22,27,32,37,42,47,52,57 * * * * trigger |
This is a curated subsystem summary, not the canonical Telegram schema or job inventory. Use PharosWatchBot and Telegram Alerts and the shared Telegram manifests for current tables, families, and commands.
File Tree Guide
This section is intentionally high-level. For the exhaustive current source inventory, run:
rg --files src shared worker scripts data functions
| Area | Primary paths | Notes |
|---|---|---|
| Frontend routes | src/app/**/page.tsx, route client.tsx, route layout.tsx / error.tsx files | Static Next.js export surfaces. Route-specific contracts live in the route docs linked from Documentation Index. |
| Shared UI components | src/components/**, excluding shadcn primitives in src/components/ui/** | Product components, charts, page sections, status surfaces, and stablecoin-detail modules. Preserve local design patterns before introducing new abstractions. |
| Frontend hooks and helpers | src/hooks/**, src/lib/** | TanStack Query wrappers, stale/refetch policy, view-model builders, route metadata, API helpers, and pure UI derivations. |
| Shared runtime contracts | shared/lib/**, shared/types/**, shared/data/stablecoins/** | Runtime-neutral scoring, classification, endpoint metadata, cron metadata, stablecoin data, schemas, and types imported by both frontend and worker. Stablecoin metadata is authored in shared/data/stablecoins/coins/*.json; shared/data/stablecoins/coins.generated.json is the generated runtime aggregate. Legacy category shards were emptied and then deleted; npm run check:stablecoin-data guards against recreating those retired filenames. |
| API endpoint registry | shared/lib/api-endpoints/**, worker/src/routes/**, worker/src/router.ts | Shared endpoint definitions drive method/auth/cache metadata; worker route arrays bind those definitions to handlers. |
| Worker API handlers | worker/src/api/** | Public, admin, messaging, and dynamic OG/API handlers. Exact HTTP contracts are canonical in API Reference. |
| Worker scheduled runtime | shared/lib/cron-jobs.ts, shared/lib/scheduled-runner-registry.ts, worker/src/handlers/scheduled/**, worker/src/cron/** | Cron schedules, slot dispatch, leases, progress, domain ingestion/scoring jobs, freshness watchdogs, and reserve adapters. Run npm run check:cron-sync and npm run check:cron-connections for the authoritative schedule and connection-budget reports. |
| Worker support libraries | worker/src/lib/** | D1 helpers, auth, rate limits, circuit breakers, fetch/RPC helpers, stores, scoring support, request attribution, and runtime credentials. |
| Pages Functions | functions/** | Same-origin site-data and ops proxy surfaces for Cloudflare Pages. Host/origin behavior is documented in Worker Infrastructure and Operator Origin Access. |
| Static/generated data | data/**, public/**, src/generated/** | Build-time digest data, logos, redirects, public assets, generated docs metadata/sitemap dates, /llms.txt, public cemetery dataset exports, OpenAPI/Postman artifacts, and markdown exports. See Scripts for the generator/check commands. |
| Operational scripts | scripts/**, worker/scripts/** | CI guardrails, smoke tests, static export serving, data refresh helpers, and worker-bound maintenance tools. See Scripts. |
| D1 migrations | worker/migrations/** | Backward-compatible migration tree plus baseline lineage in worker/migrations/MANIFEST.md. Standard deploy applies migrations before the new Worker deployment. |
The status contract surface is organized under shared/types/status/ by ownership: core health state, cron telemetry, D1 capacity forecasting, Telegram, yield/liquidity diagnostics, operational publication/dependency/canary state, response schemas, and public health/history. shared/types/status.ts remains the compatibility barrel used by existing frontend and Worker consumers, including lazy schema loaders. Domain modules import lower-level siblings directly and never import the compatibility barrel; internal schema primitives stay out of the public export surface. shared/types/__tests__/status-barrel.test.ts guards both the legacy runtime export list and schema-instance identity through the barrel.
Stablecoin catalog identity, routing, contracts, classifications, and other scalar metadata remain in shared/data/stablecoins/coins/*.json. Research-heavy domains may move as a unit into strict sidecars under shared/data/stablecoins/domains/: reserves, mint authority, GENIUS/MiCA compliance, and bridge/oracle/blacklistability risk review. scripts/lib/stablecoin-catalog-sources.ts validates each base source shape, claims whole-domain ownership for any present sidecar, merges deterministically, and then applies the full StablecoinMeta invariants. Generated registries consume only that merged projection.
Shared runtime host/origin defaults live in shared/lib/runtime-origins.json and shared/lib/runtime-origins.ts. Frontend API-base inference, /_site-data/* Pages Functions, ops-host Pages Functions, worker self/probe URLs, and local static-export tooling should consume that shared source instead of embedding production origins ad hoc.
The Stablecoin Cemetery public dataset export is static Pages data, not a Worker API route. scripts/maintenance/generate-cemetery-dataset.ts consumes the merged cemetery entry registry from shared/lib/cemetery-merged.ts, backed by curated dead rows in shared/data/dead-stablecoins.json and frozen tracked rows from shared/data/stablecoins/coins.generated.json. It writes public/datasets/stablecoin-cemetery.json plus public/datasets/stablecoin-cemetery.csv as an explicit maintenance artifact, with per-source checksums recorded in the JSON metadata; npm run check:generated-artifacts -- --only=cemetery-dataset guards drift in CI. Provenance pins the curated dead-stablecoin file and the buildFrozenCemeteryProjection() frozen-row projection — not the whole generated catalog — so live-coin curation cannot rotate a published cemetery checksum while no cemetery row moves.
The API integration artifacts follow the same static-export pattern. scripts/maintenance/generate-postman-collection.ts writes public/postman/pharos-api.postman_collection.json plus public/postman/pharos-api.postman_environment.json, and scripts/maintenance/generate-openapi-spec.ts writes public/openapi.json during prebuild; npm run check:generated-artifacts -- --only=postman and npm run check:generated-artifacts -- --only=openapi guard drift.
Worker cron refactors should place reusable stage helpers under worker/src/cron/shared/. The general abort vocabulary is throwIfAborted/rethrowIfAborted in worker/src/lib/abort.ts; cron families that need a terminal abort result (rather than a throw) build it beside their own stage runner, as sync-stablecoins does in worker/src/cron/sync-stablecoins/runtime.ts.
Frontend Runtime And SEO Surface
- Indexable route membership is owned by
src/lib/public-route-inventory.ts.PUBLIC_ROUTE_INVENTORYpairs every public path with aPublicRouteKind, andPUBLIC_ROUTE_PATHSis its flat path projection.src/app/sitemap.tsmust agree with it exactly —src/app/__tests__/sitemap-frozen.test.tsasserts the two path sets are equal — so a hand-listed page has to be added to both the registry's primary/reference lists and the sitemap; the derived families (stablecoins, chains, taxonomy, mechanisms, case studies, blog, comparisons, digests, depeg events, public docs, methodology changelogs) flow from their own registries into both sides at once./sitemap-tree/renders its indexed-archive rows from the same entries, labelled by kind, for every path its hand-curated tiers do not already list (see Sitemap Tree Page). Route metadata modules and the route-specific docs linked from Documentation Index own per-route detail. Do not mirror the full route inventory here; use the registry andnpm run seo:checkfor current crawlability coverage. - Legacy aliases are maintained through
public/_redirectsand are not sitemap entries:/telegramand/telegram/*redirect to/pharoswatchbot/;/micaand/mica/*redirect to/compliance/;/blacklistand/blacklist/*redirect to/freezewatch/;/report-cardsand/risk-labredirect to/safety-scores/;/peg-trackerredirects to/;/stability-index-altredirects to/stability-index/;/tapeand/tape/*redirect to/timeline/;/stablecoins/protocol/*(the Liquity-v1/v2 lineage paths) redirect to/stablecoins/infrastructure/*. This list is representative;public/_redirectsis the source of truth. - Tool roots intentionally marked
noindex,follow:/portfolio//screener/picker/(profile-driven shortlist; Pages-only; KV-backed snapshot pinning at same-origin/selector-snapshot/; see Screener Picker Page)
- Public noindex utility route:
/pharoswatchbot/app/is the Telegram Mini App control panel and is markednoindex,nofollow.
- Tracked-variant browse ownership stays on the homepage query state (
/?variant=...). The repo does not ship a dedicated/stablecoins/variants/*family. - Legacy numeric stablecoin URLs from the pre-canonical-ID era (
/stablecoin/<DefiLlama id>/) redirect to the matching canonical/stablecoin/[id]/route throughfunctions/stablecoin/[[path]].ts. - Private operator routes marked
noindex,nofollow:/admin//admin-api//api/admin/
- Crawlable server-rendered link hubs now live on the compare root, digest archive, depeg event archive, safety scores, liquidity, taxonomy landing pages, and stablecoin detail pages. These hubs are part of the static export and are what
npm run seo:checkvalidates for orphan routes, sitemap coverage, and click depth. /llms.txtis regenerated in the Pages post-refresh artifact pass from checked-in route/data sources as a curated LLM-facing index. It is a community proposal/inference aid, not a robots or sitemap replacement.- Markdown content negotiation for agents is handled by
functions/_middleware.tsfor/methodology/,/stablecoin/<id>/,/changelog/,/digest/<date>/, and/docs/*. The.mdvariants are generated byscripts/maintenance/generate-markdown-exports.tsduringpostbuildand are written asout/<route>/index.md. Responses includeVary: Acceptplus CDN no-store headers because Cloudflare's default CDN cache does not key on arbitraryVary: Accept. public/_routes.jsonuses a single broad/*include so exported document routes pass through that middleware, while static asset prefixes such as/_next/*,/logos/*,/dexes/*, and/featured/*stay excluded from function routing; Cloudflare rejects overlapping include splats, so the exclude list is the only lever. Cloudflare Pages static headers live inpublic/_headers, where the broad fallbackCache-Controlallows CDN compression for HTML responses and static assets with their own cache policy detach the broad rule with! Cache-Controlso Pages does not comma-join duplicate values. The middleware also nonce-authorizes inline scripts and overwrites the CSP; that contract, thepublic/_headersfallback policy, and its route exceptions are owned by Security & Governance: CSP posture.
Runtime host and env rules
src/lib/api-url.tsis the frontend runtime source of truth for API origin selection;src/lib/api.tsre-exports those helpers and layers request/freshness handling on top.src/lib/request.tsowns bespoke frontend JSON, text, blob, and raw-response lifecycles outside the endpoint-query registry. Its timeout covers response-body consumption, caller and timeout signals are merged, failures are classified, andRequestSequencecancels superseded UI requests and rejects late completions. TanStack endpoint reads continue throughsrc/lib/api.tsanduseRegisteredApiQuery(), with the query signal forwarded to the transport.NEXT_PUBLIC_API_BASEis an optional explicit override, mainly for localnext devagainstwrangler dev.- When
NEXT_PUBLIC_API_BASEis unset,buildRequestUrl()maps public browser reads onpharos.watch,ops.pharos.watch,stablecoin-dashboard.pages.dev, and*.stablecoin-dashboard.pages.devto same-origin/_site-data/*, whilebuildApiUrl()still points explicit public-API callsites (for example feedback, API-key self-serve, and OG fetches) athttps://api.pharos.watch. functions/_site-data/[[path]].tsis the browser-facing proxy contract for the website data lane: same-origin browser reads go through/_site-data/*rather than toapi.pharos.watch. The lane contract — allowed method and routes, the exact HTTPSSITE_API_ORIGINrequirement, origin gating, cache-age forwarding, and bounded body consumption — lives in Worker Infrastructure: Site-Data Auth.site-api.pharos.watchis an internal Worker host, not a browser surface.worker/src/handlers/http/gates.tsallowsGETallowlisted site-data paths plus the shared-secret header on that lane, and the exactPOST /api/telegram-adoptionmutation (or the corresponding Worker preview URL during CI rehearsal).NEXT_PUBLIC_FORCE_SITE_DATA_PROXYis a build-time browser data-origin switch: when it is set totrue,buildRequestUrl()sends public browser reads to same-origin/_site-data/*on any hostname, bypassing the allowlist above..github/workflows/pages-release.ymlhardcodes it totrue, so the production bundle always uses the proxy lane and the host allowlist never decides there. It does not override an explicitNEXT_PUBLIC_API_BASE(that still wins and disables the lane), does not affectbuildApiUrl()callsites that intentionally target the public API, and does not touch/api/admin/*requests, which stay same-origin regardless. Unlike the otherNEXT_PUBLIC_*build inputs it is not a GitHub repository Variable, so flipping it requires a workflow edit rather thangh variable set.NEXT_PUBLIC_GA_IDgates GA4 script injection insrc/app/layout.tsx. When it is unset, the site still renders normally and no browser analytics events are emitted fromsrc/lib/analytics.ts.
Metadata and crawl ownership
src/lib/page-metadata.tsis the shared helper for per-route canonical metadata, Open Graph images, Twitter cards, indexable robots preview directives, and sentence-aware description trimming.src/app/layout.tsxowns the sitewide metadata baseline, icons, RSS alternates, and root JSON-LD (WebSite,Organization,Person,WebApplication) with stable#website,#organization,#person-tokenbrice, and#webappanchors. It intentionally does not emitSearchActionuntil the site has a real query handler, and it does not preload GA becausesrc/components/google-analytics.tsxloads analytics from the runtime component after the page shell is interactive.- Dataset JSON-LD nodes must remain crawlable in isolation for Google Search Console: emit explicit Pharos
Organizationobjects forcreatorandpublisher, a URL-valued license, a Pharos URNidentifier, andsameAswhere the dataset has a canonical page or public export. When a Dataset usesincludedInDataCatalog, the nestedDataCatalogreference must include the catalog@id,name, andurl, not only an ID reference. Datasetdistribution.contentUrlvalues must point only at public crawlable API/static-export URLs, never same-origin/_site-data/*. src/app/sitemap.tsowns sitemap output for indexable routes./compare/and/compare/[slug]/static comparison pages are included;/digest/and every generated/digest/[date]/detail page are included because daily and weekly digests are durable archive/citation pages with unique editorial text and snapshots; methodology changelog sitemap membership is the explicitMETHODOLOGY_CHANGELOG_SITEMAP_PATHSallowlist./portfolio/,/admin/,/admin-api/,/screener/picker/,/stablecoin/[id]/yield/, and/pharoswatchbot/app/are omitted./funding/uses the latest of route edit time and checked-in funding data timestamps forlastModified.LAST_EDITEDdates are auto-generated from git history (scripts/maintenance/generate-sitemap-dates.ts) and written to a gitignored JSON file plus its gitignored.d.tstype sidecar; locallynpm run bootstrap:generated:historymaterializes it, and the production release regenerates it duringprebuildfrom a full-history checkout. Public docs usescripts/maintenance/generate-docs-metadata.tsfor git-derived first/last modified dates and the same gitignored JSON-plus-sidecar pattern, materialized by the same commands. Both generators fail fast on a shallow checkout rather than emitting filesystem-timestamp dates.src/app/robots.tspublishes the sitemap location and disallows crawling of/admin/,/admin-api/, and/pharoswatchbot/app/; everything else stays crawlable./admin/and/admin-api/are operator surfaces with host gates plusX-Robots-Tag: noindex, nofollow;/pharoswatchbot/app/is a Telegram Mini App surface with noindex metadata/headers and Telegram frame ancestors, but it is not host-gated. The robots.txt block means crawlers can no longer observe those noindex responses — accepted tradeoff: already-indexed operator URLs cannot be deindexed via observed noindex while the disallow stands. Routes that rely on crawlers observing a noindex response (/portfolio/, yield subpages, datasets) remain crawlable.
Standalone PharosVille
PharosVille now lives in the separate TokenBrice/pharosville repository and
is deployed through its own Cloudflare Pages project at
https://pharosville.pharos.watch/. The Pharos.watch host keeps only temporary
redirects from /pharosville/ and /lighthouse/ plus the shared API contract
schemas that the standalone app validates against.
The standalone app reads Pharos data through its own same-origin Pages Function
proxy. That proxy owns the PharosVille API key server-side and calls only the
allowlisted public read endpoints on https://api.pharos.watch, so the host
Worker does not need a CORS allowlist change for the split.
Pages Function endpoints (not Worker API)
These are same-origin Pages Functions backed by bindings that vary by function (ASSETS, KV, D1, Access/service-token configuration). They do not appear in the Worker API catalogue and are not part of the api.pharos.watch surface.
| Endpoint | Description |
|---|---|
GET /_site-data/* | Pages Function (functions/_site-data/[[path]].ts): same-origin website data lane. It validates the caller origin, signs the server-to-server hop with SITE_API_SHARED_SECRET, forwards allowed read requests to SITE_API_ORIGIN, and records site-data attribution when the DB binding is present. |
POST /selector-snapshot | Pages Function (functions/selector-snapshot/[[path]].ts): projects SelectorInput, loads seven schema-validated canonical sources through the authenticated site-data lane in two fully consumed batches (four, then three), runs the shared selector engine, and stores a KV-attested schema-v3 pharos-verified output. The 100 KB cap is streaming; HMAC-IP rate/quota keys require a dedicated pepper. |
GET /selector-snapshot/:sid | Pages Function: returns the KV-attested verified frozen projection or an explicitly client-unverified legacy projection, or 404. It recomputes the sid, returns 502 for corrupt/mismatched trusted values, and returns 503 when a first-read five-year retention extension cannot be confirmed. |
GET /stablecoin/:legacy-id | Pages Function (functions/stablecoin/[[path]].ts): redirect shim for legacy stablecoin IDs before the App Router handles canonical detail routes. |
GET /admin/*, GET /admin-api/* | Pages Functions (functions/admin/[[path]].ts, functions/admin-api/[[path]].ts): operator-host asset gates for the Access-protected admin surfaces on ops.pharos.watch. |
/api/admin/* | Pages Function (functions/api/admin/[[path]].ts): same-origin operator proxy to ops-api.pharos.watch, using Cloudflare Access service-token bindings and the shared proxy helpers. |
GET /safety-scores/map.json | Pages Function (functions/safety-scores/map.json.ts): GET/HEAD readiness manifest for the daily Safety Score map, read from the Pages-only SELECTOR_SNAPSHOTS KV binding with no-store. A missing binding or object is a 404, not a 500. |
GET /safety-scores/map.png | Pages Function (functions/safety-scores/map.png.ts): GET/HEAD map poster from the same KV namespace — latest on a short edge TTL, ?date=YYYY-MM-DD as an immutable dated archive. See Safety Score Map. |
POST /pharoswatchbot-adoption | Pages Function (functions/pharoswatchbot-adoption.ts): same-origin adoption-telemetry shim that forwards to the Worker's /api/telegram-adoption through the site-data lane, adding a dedicated IP-hash header. |
functions/_middleware.ts | Pages middleware: markdown content negotiation on the negotiable route prefixes, canonical/noindex headers on direct .md asset paths, and CSP nonce injection on HTML responses — all before route-specific Pages Functions run. Host gating lives in functions/lib/ops-asset-host-gate.ts and the per-route functions. |
Static feed route handlers
The App Router feed handlers under src/app/feed/**/route.ts emit static RSS/XML feeds from checked-in or generated build inputs. Treat that source tree, the sitemap, and npm run seo:check as the current feed-route inventory.
CSS Build Pipeline
Styling runs through PostCSS with the @tailwindcss/postcss plugin (configured in postcss.config.mjs). This is the Tailwind CSS v4 integration path -- there is no standalone tailwind.config file; Tailwind v4 reads design tokens and @theme directives directly from src/app/globals.css. The cn() utility in src/lib/utils.ts uses tailwind-merge for safe class deduplication at runtime.
Reminder: Tailwind classes must be static strings -- never construct class names dynamically, as the CSS purge pass cannot detect them.
src/app/globals.css also owns Tailwind v4 @source not exclusions for non-browser prose, data fixtures, and tests. Keep runtime class maps included, but do not let docs, scripts, worker code, or shared data text inflate the generated utility CSS.
Coverage Subsystem
The /coverage page model lives under src/lib/coverage/ as one module per feature (price, safety, dex, reserves, redemption, yield, flows, blacklist, mica, genius, dependency, mint-authority), plus a shared.ts with primitives (createStatus, createPresetStatus, createDataUnavailableStatus, resolveBooleanCoverageStatus). Each per-feature module owns:
- The feature's preset table (when applicable).
- The
resolve<Feature>Coverage(...)function that maps aStablecoinMeta(plus auxiliary inputs) to aCoverageStatus. - A
format<Feature>Breakdown(rows, breakdownMap)callback returningCoverageBreakdownItem[]. - The
statusKindsproperty on the exportedcoverageFeatureobject — derived from the module's preset table withstatusKindsFromPresets(...)(plus any literal kinds the resolver produces outside the presets) — enumerating everykindthe resolver can produce, used by the legend invariant. - The
legendItemsproperty on the exportedcoverageFeatureobject (backed by a module-private<FEATURE>_LEGENDconst) — a list ofCoverageLegendItementries of shape{ term, description, kinds[] }aggregated into the global legend.
src/lib/coverage.ts is a thin orchestrator + partial barrel — it re-exports the shared coverage types plus COVERAGE_FEATURES so consumers can keep importing those from @/lib/coverage, and it owns the two exported cross-feature helpers (buildCoverageRow, buildCoverageFeatureSummary) alongside COVERAGE_BADGE_TONE_CLASS. The headline/availability counters (countAvailableFeatures, countHeadlineFeatures, isHeadlineFeatureCovered) are module-private and reachable only through the counts buildCoverageRow puts on each row; per-feature resolvers, shared.ts status primitives, and the legend/status-kind tables are imported from their own modules rather than re-exported here. buildCoverageFeatureSummary calls feature.formatBreakdown(rows, breakdownMap) inline — adding a new feature requires providing the callback, otherwise TypeScript fails the build.
src/lib/coverage-features.ts wires each feature key to its per-feature module exports (formatter, status kinds, legend items); resolvers are wired directly in coverage.ts's buildCoverageRow via per-feature module imports, not through coverage-features.ts. src/lib/coverage-page-config.ts derives LEGEND_ITEMS from those per-feature exports plus a small fixed set of general entries (NR / Data n/a / —). The coverage legend invariant test in src/lib/__tests__/coverage.test.ts asserts every kind any resolver can produce has a matching legend entry. The former declared-vs-observed exhaustiveness test was removed: deriving statusKinds from the preset tables makes the declaration and the resolver output the same source.
Worker Coding Conventions
Loose-equality null guard (!= null)
The worker codebase deliberately uses != null (loose equality) as the standard null/undefined guard for D1 query results. D1 can return either null or undefined for absent column values depending on the query path and column type, and value != null catches both in a single check. This is intentional -- do not "fix" these to !== null or !== undefined.
Worker import boundary waiver
eslint.config.mjs enforces the worker/frontend/shared import boundary through its no-restricted-imports and pharos/worker-import-boundaries rules. The only named non-test waiver is frozen-invariants-lifecycle-registry-check for scripts/ci/check-frozen-invariants.ts, which imports worker and frontend registries to prove frozen stablecoin IDs were removed from lifecycle surfaces. Keep that waiver documented in the script header and in docs/process/boundary-waivers.md; new cross-layer checks should move runtime-neutral metadata into shared/ instead of expanding the waiver set.
TypeScript Target Constraints
Both the root tsconfig and worker tsconfig target ES2022, but the worker config pins lib: ["ES2021"]. Shared modules in shared/lib/ compile under both configs and may use ES2022 syntax (nullish assignment ??=, logical assignment ||=, top-level await, etc.) — but not the ES2022 library surface (Array.at(), Object.hasOwn(), …), which does not typecheck under the worker config — and must remain runtime-neutral — no DOM APIs, no Node-only APIs, no Cloudflare-only APIs.
Architectural Decision Records
Load-bearing, deliberately-locked decisions and why they exist, so the rationale survives independent of the people/agents who set them. These are constraints, not aspirations — change them only with a deliberate, documented follow-up. The CLAUDE.md / agent instructions hard-rules block is the enforceable summary; this section is the rationale.
Active Records
- ADR-1 — Root tsconfig excludes
worker/.worker/tsconfig.jsonsets"types": ["@cloudflare/workers-types", "node"], whose global types (andD1Databaseet al.) conflict with the frontend's DOM lib. Compiling both under one config produces ambient-type clashes, so the root config (tsconfig.jsonexclude) dropsworker/and the worker compiles under its own config. Consequence: code shared by both runtimes must be runtime-neutral and live inshared/lib/, never reach into worker- or DOM-only APIs. - ADR-2 —
@shared/*alias boundary. Frontend and worker both import shared runtime/types through the@shared/*/@shared/data/*path alias (tsconfig.jsonpaths), not relative cross-boundary paths. This keeps the runtime-neutral boundary explicit and greppable; the ESLint import-boundary rules enforce it (see Worker import boundary waiver above for the single named exception). - ADR-3 — Methodology versioning is numeric-decimal, not semver minor. Methodology versions increase as decimals: after
v5.9the next step isv5.91orv6.0, neverv5.10. This avoids the5.9 < 5.10ambiguity that bit earlier integer-segment version comparisons; compare versions numerically. A methodology change updates four places, and this record owns that enumeration: the owning domain's runtime version source (the per-domain constant inshared/lib/methodology-versions/constants.ts, surfaced throughshared/lib/methodology-versions/registry.ts; for the Safety Score domain that constant readsshared/lib/methodology-versions/current-version.json, which belongs to that domain only), the structured changelog entry undershared/data/methodology-changelogs/, the owning methodology document, and the relevant/methodologysection when public explanation changes. Skipping the runtime version source ships a changelog entry for a version the runtime never adopts. - ADR-4 — One Worker for OG + API + cron. A single Cloudflare Worker serves the public/site/ops API surfaces, dynamic OG image generation, and all scheduled (cron) runtime work rather than splitting into per-concern Workers. This keeps one deploy/rollback unit and one D1 binding set. Cloudflare limits each invocation to six simultaneous outbound requests waiting for response headers; Pharos conservatively budgets the whole trigger at six and requires response-body cleanup before later phases to bound resources and preserve deterministic sequencing (see
worker-infrastructure.md). - ADR-6 — Squashed D1 baseline.
worker/migrations/0000_baseline.sqlconsolidates migrations 0001–0227 into one fresh-DB schema script; existing databases never run the baseline and continue from their last-applied migration, while fresh databases apply the baseline then 0228 onward. The legacy-duplicate allowlist is frozen and must not expand. Cadence/procedure for the next squash lives indocs/process/d1-baseline-squash-plan.md; lineage is inworker/migrations/MANIFEST.md. - ADR-7 — Single-source route and cron metadata. Static route metadata is declared once in
shared/lib/api-endpoints/and cron schedules once inshared/lib/cron-jobs.ts(bound to runner keys viashared/lib/scheduled-runner-registry.ts); worker route/dispatch and CI sync checks consume those single sources. This keepswrangler.toml, shared metadata, and dispatch in lockstep and makes drift a compile/CI failure rather than a silent default. See Route Definition Model above. - ADR-8 — DefiLlama list supply is read as-is. Use
getCirculatingRaw()fromshared/lib/supply.ts; DefiLlama list-endpointcirculatingvalues are already USD-denominated, so they must never be multiplied by price or replaced with a manual/on-chain/CMC/DEX override. Separate supplemental supply admission paths for assets missing from, or broken in, the list are allowed only when source-owned, documented, fail closed, and conserve supply without double-counting. This keeps the primary DefiLlama basis stable while letting explicit fallback lanes repair covered gaps. - ADR-9 — Per-cache availability ratio overrides. Public cache availability normally tolerates the global
>8×degraded />12×stale age bands (STATUS_CACHE_RATIO_THRESHOLDS), but a cache whose slow stale window under-reports real user impact can tighten those bands viaSTATUS_CACHE_RATIO_OVERRIDESinshared/lib/status-thresholds.ts.yield-dataoverrides to>2×/>4×against its post-V9 producer so two missed publishes make the public status degrade (ruling R3, 2026-07-19) — previously the global bands let multi-hour-stale rankings still readhealthywhile the admin endpoint-budget lane already flagged the lane at 1×. Overrides are resolved throughgetCacheRatioThresholds/getCacheHealthyMaxRatioand threaded (as an optional cache key) intogetCacheFreshnessStatus/getCacheImpactStatusand the workerbuildCacheStatuseshealthy/statusFloorpath; the per-record freshness layer (FRESHNESS_RATIOS) and all non-overridden caches are unaffected. Seedocs/status-dashboard.md. - ADR-10 — Cron growth headroom is topology-reviewed, not a single expression count. The former 20-expression soft cap predated the fully source-owned schedule registry, slot fencing, per-trigger connection-budget validation, and compact producer-history read model. The audited topology distinguishes deployed Cloudflare trigger expressions from logical scheduled work:
CRON_GROWTH_HEADROOM_POLICYinshared/lib/cron-jobs.tscaps the reviewed physical topology, fetch-capable scheduled entries, and headroom-full (5/6) trigger slots; read the current ceilings from that constant rather than from prose, and the rationale for each raise from the ADR that made it. Crossing any gate requires another ADR and consolidation/rebalance plan. These caps are internal operability guards, not permission to add unaudited triggers. - ADR-19 — Domain-composed Safety Score. Safety Score V9 is the single judgment of asset risk, composed from domain producers rather than competing with them. Three rules are load-bearing. (i) One engine per sub-dimension. A risk sub-dimension has exactly one scoring implementation; a second surface may present a different view of that engine's output but must not carry its own formula. The exit route-scoring engine (safety
9.07wave) and the mint grader (safety9.1) were each consolidated after the two copies had silently diverged — the mint pair shared five posture-enum names with different values and had opposite incident semantics, and the exit pair mirrored every scoring constant with no test pinning them equal. (ii) Anti-double-counting. A signal enters the composition exactly once, at the layer that owns it. This is why the retired Mint Authority engine's route-family pricing was not merged into the control pillar at9.1:capSemanticsalready prices the same risk, and pricing it twice would double-count. (iii) Downstream consumers re-bin, never re-derive. DDR is the model: it reads published outputs and re-bins them. Any consumer that recomputes a published score is a divergence waiting to happen, which is why DDR's last re-derivation (the mint band) moved to the published posture band at9.1. Consequences: curated fields that duplicate a derived judgment become validated annotations with a curation queue rather than score inputs (mintAuthority.authorityPosture); every scoring constant lives in the V9 policy asset so the determinism contract is one digest; and any commit touching a manifest-pinned scoring source regenerates the V9 evaluation-build manifest in the same commit. Seereport-cards.mdandmint-authority-scoring.md. - ADR-20 — Cron CPU-class rebalance and physical-trigger freeze. Cloudflare's Workers limits give Paid-plan Cron Triggers a 30-second CPU class for expressions with intervals below one hour and a 15-minute class for hourly-or-longer intervals; an invocation that consistently exceeds its CPU limit is terminated. Production D1 measurement on 2026-08-21, using
cron_runssyntheticplatform-abandonedrows written byworker/src/lib/scheduled-slot-reconciliation.ts, verified abandonment in the three single-expression sub-hourly lanes:statusSelfCheckOffset(9,24,39,54 * * * *) abandoned 28/96 slots (29.2%),v9SupplyAttributionOffset(8,23,38,53 * * * *) abandoned 28/96 (29.2%), andquarterHourly(*/15 * * * *) abandoned 27/96 (28.1%). In 140/146 rows,slotWorkerVersion == reconciledByWorkerVersion, which rules out deploy eviction as the main explanation. The existing paired-hourly DEX lanes had near-zero loss:halfHourlyChartsOffset(16+46) was 0/48 (0.0%) andhalfHourlyOffset(10+40) was 1/48 (2.1%). Loss landed on serial-chain tails:compute-depeg-resolverlost 49/192 runs (25.5%), including six consecutive slots with no depeg resolution for 90 minutes;cron-staleness-watchdoganddata-invariant-canarywere starved for 90 and 75 minutes. The reviewed fix adds hourlytriggerSchedulesaliases inshared/lib/cron-jobs.tsand the matching single-minute expressions inworker/wrangler.tomlwhile leaving logical schedules and cadence definitions unchanged. The resulting physical expressions were the reviewed topology at the time of this record (ADR-21 later raised it), not general-purpose spare capacity: splitting an existing comma expression into hourly aliases is a topology rebalance of existing logical work, not new scheduled work, and does not add logical slots, fetch surface, or connection pressure. Further physical-trigger growth is frozen pending a consolidation or Cloudflare Queues/Workflows decision. ThefiveMinuteTelegramAlertslane is explicitly deferred as the next review target: its measured abandonment was 44/288 (15.3%), and the same alias approach would cost +11 expressions. Child-level attribution over seven days shows the same monotonic chain-position signature (dispatch-telegram-alerts37,telegram-personalized-recap-planner40,telegram-degradation-watchdog44,telegram-disambiguation-cleanup51,telegram-pulse-snapshot68), butdispatch-telegram-alertsdrains a durable pending queue, so an abandoned slot costs delivery latency rather than lost alerts. That durability, not a low abandonment rate, is what makes deferring this lane acceptable; a one-shot writer at the same rate would not be. - ADR-21 — V9 publication CPU-class rebalance. ADR-20 measured
platform-abandonedrows on 2026-08-21 at 28/96 (29.2%) for bothstatusSelfCheckOffsetandv9SupplyAttributionOffset, and 27/96 (28.1%) forquarterHourly, versus 0/48 forhalfHourlyChartsOffsetand 1/48 forhalfHourlyOffseton paired-hourly aliases.v9PublicationOffsethad the same proven-bad single-expression sub-hourly shape (22,52 * * * *) but was neither included in that rebalance nor named as its deferred follow-up; it was missed. This lane cannot use ADR-20's deferral rationale: that record permits deferral forfiveMinuteTelegramAlertsbecause it drains a durable pending queue, and states outright that "a one-shot writer at the same rate would not be" acceptable.compute-safety-score-v9is exactly such a one-shot publication writer, so platform termination can lose a publication attempt rather than merely delay durable queued work. The reviewed correction deploys22 * * * *and52 * * * *as physical aliases while preserving the logical22,52 * * * *schedule, slot key, half-hour cadence, publication window, and zero-connection declaration. Its cost is +1 physical expression, moving the reviewed topology from 34 to 35, versus the +11 expressions that justified deferring the Telegram lane. This is a topology rebalance of existing logical work: it adds no scheduled work, fetch surface, or connection pressure. Further growth beyond 35 remains frozen pending another ADR and consolidation or execution-substrate review. - ADR-22 — DDR run-scoped heap separation and mint/burn CPU-class rebalance. The hourly-alias correction removed the 30-second CPU-class fault from
v9SupplyAttributionOffset, but seven-day production evidence on 2026-08-27 still showed 82/671 abandoned slots. Eighty were same-version in-place deaths, and the post-rebalance child split was decisive: 43 of 45 losses occurred after attribution whencompute-depeg-resolverloaded its review graph in the same isolate, versus two attribution losses. Commit74ca9d2b57recorded Cloudflare's confirmedexceededMemoryoutcome for this chain; both dynamic graphs include the large generated stablecoin catalog, so serial ordering and cooperative timeouts cannot provide a memory boundary. DDR therefore moves unchanged to the independent logicaldepegResolverOffsetlane at13,28,43,58, deployed as four hourly aliases for the hourly CPU class. This prevents the two large run-scoped graphs from executing in one invocation; the same Worker service may reuse a warm isolate, so observed production runs remain the acceptance criterion. It remains D1-only (0/6), keeps its 900-second freshness contract, and retains the core/version and V9 memory-lane admission fence. The same review pairshalfHourlyMintBurnCriticalas hourly4and34aliases: 17/332 healthy runs exceeded 30 seconds and three same-version 2026-08-21 abandonments ended at 42–133 seconds, making the single sub-hourly expression a proven CPU-class hazard even though the rolling abandonment ratio was below the topology watchdog threshold. These changes add one logical zero-fetch plan and five net physical expressions, moving the current reviewed topology from 34 to 39 without increasing the 32 fetch-capable entries or the two5/6slots. DDR scans roughly 8,800 incident rows, so the Queue/Workflow fan-out review threshold was considered; a separate D1-only snapshot-compiler invocation with sub-30-second p95 is materially simpler than introducing queue orchestration, while removing run-scoped overlap without claiming service-level isolate separation. Further physical-trigger growth beyond 39 remains frozen pending another ADR and consolidation or execution-substrate review. - ADR-23 — Extended mint/burn CPU-class rebalance. ADR-22 corrected only
halfHourlyMintBurnCritical;halfHourlyMintBurnExtendedremained on the combined18,48 * * * *expression and therefore in Cloudflare's 30-second CPU class. Production acceptance on 2026-09-01 proved the omission after commita988acf22removed the 15-MiB rich stablecoin registry from the extended lane: Worker version7c171ad5-6421-49bd-8a5e-78408420bf87completed the 14:18 UTC attempt in 61.3 seconds but its 14:48 and 15:18 attempts stopped heartbeating after 39 seconds atscan-configand were reconciled as same-versionplatform-abandoned, withinterruptedByWorkerVersionChange=false. The code-level memory reduction was necessary but could not override the platform class. The reviewed correction preserves the logical18,48 * * * *cadence and slot identity while deploying hourly18 * * * *and48 * * * *aliases. This adds one physical expression, moving the reviewed topology from 39 to 40, but adds no logical work, fetch-capable entry, or connection pressure: the lane remains isolated at1/6, with 122 configured units and normal runtime far below the 10-minute and 1,000-unit Queue/Workflow review thresholds. A topology alias is therefore materially simpler than changing ingestion semantics or adding orchestration. Further physical-trigger growth beyond 40 remains frozen pending another ADR and consolidation or execution-substrate review. - ADR-24 — Five-minute Telegram lane runs on the narrow Worker runtime registry. ADR-20 deferred the
fiveMinuteTelegramAlertsCPU-class rebalance on the grounds thatdispatch-telegram-alerts"drains a durable pending queue, so an abandoned slot costs delivery latency rather than lost alerts". Production evidence on 2026-09-02 falsified both halves. Cloudflare invocation analytics (the workersInvocationsAdaptive dataset, 2026-08-26 to 2026-09-02) recorded 196exceededMemoryoutcomes and zero exceededCpu outcomes for the Worker; the D1platform-abandonedrows for the lane land on the:27/:57runs that carry safety fan-out after the:22/:52V9 publication (20 of 96 such runs abandoned in 48 hours, versus 2 of 480 eventless runs), always atfanout-builtafter 17-39 seconds, i.e. during the pending drain. Every kill reconciled the in-flightsendingrows toexecution_unknownwithpending_effect_owner_lost: 317 such rows accumulated since 2026-07-29. That state means the Bot API effect is unconfirmed and, by contract, is never retried, so each abandonment converts due deliveries into permanent ambiguity rather than latency. The cause was the lane's dynamic-import graph: eleven Telegram modules importedshared/lib/stablecoins/registry.ts, which inlines the 15.8 MBcoins.generated.json, giving the five-minute bundle 19.0 MB of inputs inside the 128 MB isolate (the same hazard commita988acf22removed from the extended mint/burn lane). The lane reads onlyid,symbol,name,status, andflags.pegCurrency, so the reviewed fix addsnameandpegCurrencyto thecoins.worker-runtime.generated.jsonprojection, adds pre-launch and active-meta indexes toworker-runtime-registry.ts, moves all eleven modules onto it (bundle inputs 19.0 MB to 3.2 MB), and extendscheck:mint-burn-runtime-importsto bundlefive-minute-telegram.tsso the full registry cannot re-enter. No trigger, schedule, slot, or connection budget changes; the physical topology stays at 40. The first production observation on Worker07c1ac69proved the registry removal necessary but not sufficient: the 20:27 and 20:57 UTC safety fan-out dispatches now completed, but the isolate still died ofexceededMemoryimmediately afterwards and abandoned the watchdog, cleanup, and pulse sidecars. The second heap source wasworker/src/cron/telegram-alert-context.ts, which built the alertContext:line by callingloadActiveSafetyScoreSource— a full ~8 MB V9 publication gunzip and parse on every event-carrying dispatch — bypassing the thinalert-safety-v9-sourceenvelope thatalert-safety-source-cache.tsintroduced after the 2026-08-19 OOM for exactly this lane. Worker647e5b01reads grade, score, and publication identity from that envelope instead. Acceptance is therefore two-part: the first week of production:27/:57safety-event dispatches on647e5b01or later completing with their sidecars, withoutplatform-abandonedrows,exceededMemoryoutcomes, or newpending_effect_owner_lostrows. If abandonment persists, the next candidates are bounding thedrainPendingQueueclaim and materialization window, then the hourly-alias rebalance ADR-20 costed at +11 expressions.
Superseded Records
- ADR-5 — Historical penalty-only score blends (superseded by ADR-19). Safety Score V8 applied reviewed Mint Authority, CDP oracle, and bridge-route scores to its Decentralization dimension through penalty-only blends. Canonical V9 retires that numeric blend and compiles the underlying reviewed evidence into typed Backing, Exit, and Economic Control facts, gaps, and caps. The historical rule remains documented so archived V8 results are reproducible; it is not an active V9 scoring path. ADR-19 states the positive rule that replaced it. See
report-cards.mdandclassification.md.
Stablecoin lifecycle phases
Every entry in TRACKED_STABLECOINS is in one of five lifecycle phases. The phase controls write-side collection, live aggregates, and public presentation. Phase transitions are a catalog policy change, not a scoring-algorithm change, so per-domain methodology versions are unaffected unless the scoring formula also changes.
| Phase | status field | New data collected? | Score recomputation? | Public treatment |
|---|---|---|---|---|
| Active | "active" (or omitted) | Yes | Yes | Live tables, analytics, aggregates, alerts, and detail page |
| Pre-launch | "pre-launch" | No | No | /upcoming/ and pre-launch detail variant |
| Quarantined | "quarantined" plus listingStatusReview | No | No | Static read-only detail record with reason and review date |
| Delisted | "delisted" plus sourced listingStatusReview | No | No | Static historical detail record |
| Frozen | "frozen" plus frozenAt and obituary | No | No | /cemetery/ and preserved archive detail page |
The main registry universes from shared/lib/stablecoins/registry.ts are:
TRACKED_STABLECOINS— the complete catalog across all five phases. Use for canonical identity, schema validation, static detail params, sitemap entries, and known provider IDs in discovery.ACTIVE_STABLECOINS— active or omitted status only. Every write-side cron, live aggregator, PSI/DEWS/Bank-Run-Gauge input, and Telegram alert target must use this universe.PRE_LAUNCH_STABLECOINS,QUARANTINED_STABLECOINS,DELISTED_STABLECOINS, andFROZEN_STABLECOINS— explicit lifecycle partitions that drive their respective static or archive surfaces.READABLE_STABLECOINS— all post-launch records: active, quarantined, delisted, and frozen. Use only for historical/read-only navigation and identity resolution, never live collection or cache publication.
Listing scope, classes, quarantine, and delisting are defined in Stablecoin Listing Policy. The freeze procedure is documented in Freezing a Tracked Stablecoin.
Funding page
The /funding route is a static page backed by the hand-maintained cost and donation records under shared/data/funding/; it has no cron, D1, or API surface. Use the project funding-update skill for the approval-gated research workflow and Funding Page for the data contract.