Reference for limits we can verify from repo code and checked-in config.
This document intentionally focuses on:
- limits enforced in code
- budgets encoded in config
- runtime assumptions the scheduler is explicitly designed around
It intentionally does not treat vendor pricing-plan quotas as source of truth. Cloudflare, CoinGecko, Alchemy, Etherscan, Anthropic, X, and similar providers can change those independently of this repo. Re-check official vendor docs or your live account dashboard before making spend-sensitive or capacity-sensitive changes.
Primary Sources
worker/wrangler.tomlshared/lib/cron-jobs.tsworker/src/lib/rate-limit.tsshared/lib/ops-limits.tsworker/src/lib/api-keys.tsworker/src/lib/circuit-breaker.tsworker/src/handlers/http/gates.tsworker/src/cron/sync-blacklist.tsworker/src/cron/sync-mint-burn.tsworker/src/cron/sync-live-reserves.tsworker/src/cron/sync-live-reserves-config.tsworker/src/lib/address-price-providers/index.tsworker/src/lib/authoritative-price-sources/index.tsworker/src/cron/sync-stablecoins/enrich-prices-cmc-pass.tsworker/src/cron/sync-stablecoins/post-enrichment.tsworker/src/cron/dex-discovery/orchestrator.tsworker/src/cron/measured-execution/sync.tsworker/src/cron/measured-execution/profiles.tsworker/src/cron/sync-stablecoins/enrich-prices.tsworker/src/cron/sync-fx-rates.tsworker/src/cron/publish-report-card-cache.tsworker/src/cron/daily-digest.tsworker/src/cron/sync-yield-data.tsworker/src/cron/sync-yield-supplemental.tsworker/src/cron/fetch-tbill-rate.ts
Worker Runtime
| Constraint | Current repo value | Source | Notes |
|---|---|---|---|
| Worker CPU budget per invocation | 30000 ms | worker/wrangler.toml | Hard repo-configured CPU cap via [limits].cpu_ms. Telegram dispatch (C102) applies the fresh-send budget before formatting and check-telegram-load.ts models per-invocation estimatedCpuMs (format-count capped at the fresh budget); the harness fails if the 5,000-watcher burst exceeds 0.5×cpu_ms (15,000 ms). |
| Cron expressions / trigger slots | Source-owned; run the cron checks | worker/wrangler.toml, shared/lib/cron-jobs.ts, shared/lib/scheduled-runner-registry.ts | The shared runner registry is the dispatch authority checked by npm run check:cron-sync; check:cron-connections also models budget-only scheduled surfaces with connectionGroup metadata. Failure-isolated reserve recovery remains independent of the interrupted invocation. |
| Status-tracked and budget-only jobs | Source-owned; exposed by status and budget registries | shared/lib/cron-jobs.ts, shared/lib/scheduled-runner-registry.ts | Runtime scheduling matches the shared status metadata expected by /api/status; budget-only work is modeled separately by the connection-budget registry. |
| API key default limiter | 120 requests / 60 seconds per key | shared/lib/ops-limits.ts, worker/src/lib/api-keys.ts | Non-exempt /api/* requests require a valid X-API-Key; the no-key public exceptions are health, OG images, feedback, self-serve key request/verification, the Telegram webhook, and Telegram Mini App session/mutation endpoints authenticated by signed initData. The D1-backed api_key_rate_limit table enforces per-key quotas in the normal path, and per-key overrides live in api_keys.rate_limit_per_minute. Protected cacheable GET edge-cache hits can use a bounded isolate-local fast path only for recently verified non-self-serve keys; cold, unknown, self-serve, expired auth-cache, cache-miss, cache-bypass, and non-GET requests stay on the D1-backed path or fail closed. After repeated D1 limiter failures, protected cacheable GET routes open a 60-second isolate-local fallback circuit capped at the self-serve quota (30/min); cold or unknown keys still fail closed with 503 and Retry-After: 60. Last-used metadata writes are best-effort. |
| Self-serve API key policy | 30 requests / 60 seconds, 60 days expiry | shared/lib/ops-limits.ts, worker/src/api/api-key-requests.ts | Email-verified key issuance through /api/; one active/pending self-serve key claim per normalized email. |
| API key revocation latency | ≤5 seconds for protected cacheable edge-cache hits; immediate on the D1-backed path | worker/src/lib/api-key-core.ts (API_KEY_AUTH_CACHE_TTL_MS) | Normal API-key authentication consults D1 and fails closed if lookup storage is unavailable. A recently verified non-self-serve key can use the bounded fresh auth cache only on the narrow protected cacheable GET edge-cache-hit fast path, and the cache is not accepted as stale authentication material after lookup failures. |
| Feedback limiter | 3 submissions / 10 minutes per salted IP hash | worker/src/api/feedback.ts, worker/src/lib/rate-limit.ts | Separate from the per-key limiter |
| Self-serve request limiter | 5/hour per salted IP hash, 3/day per private email hash | shared/lib/ops-limits.ts, worker/src/api/api-key-requests.ts | Protects POST /api/api-key-requests; dependency failures fail closed with 503 and Retry-After: 60. |
| Self-serve verification limiter | 20/10 minutes per salted IP hash, 5/10 minutes per token hash | shared/lib/ops-limits.ts, worker/src/api/api-key-requests.ts | Protects POST /api/api-key-requests/verify; issuance is also capped to one creation per salted IP hash per 24 hours. |
| Request attribution telemetry retention | 35 days | worker/src/lib/request-source-attribution.ts, functions/lib/request-attribution.ts | Total site-vs-external demand, worker-lane load, and per-key public-API load buckets in api_request_consumer_stats / site_data_request_stats / api_key_request_stats are pruned opportunistically. Worker route/source, Worker per-key, and Pages site-data counters use short isolate-local batching before D1 upsert. Set REQUEST_SOURCE_ATTRIBUTION_DISABLED=true on Worker and/or Pages to pause low-value route/source writes without disabling API-key auth, D1-backed rate limiting, or per-key public API load telemetry. Set API_KEY_REQUEST_ATTRIBUTION_DISABLED=true on the Worker only for keyed public-API spikes where per-key observability writes also need a D1 pressure relief valve; auth, rate limiting, and last-used metadata stay enabled. |
Connection-budget operating assumption
Cloudflare currently limits each invocation to six simultaneous outbound requests that are still waiting for response headers. A request stops occupying that header-wait slot once headers arrive. Pharos deliberately uses a stricter trigger-wide six-connection model in its static scheduler budget so nested and chained fetch phases cannot accidentally overlap at the platform ceiling. See the Workers limits and the April 2026 connection-limit change.
The scheduler is structured around that conservative repo constraint:
- heavy lanes get isolated trigger slots (
sync-blacklist,sync-mint-burn,sync-mint-burn-extended,sync-dex-discovery,sync-dex-liquidity) - shared slots bundle only related work
- the quarter-hourly handler sequences jobs instead of fanning them out blindly
npm run check:cron-connectionsfails any trigger at or above6/6and reports5/6triggers as headroom full- the connection check includes budget-only scheduled surfaces that do not create separate
cron_runsrows:telegram-registration-reconciliation,telegram-digest-outbox-drain, anddigest-trigger-poll
Treat any new fetch-heavy work added to an existing trigger slot as competing for the same trigger-wide outbound connection budget. A trigger at 5/6 must be treated as full for new fetch-heavy work unless the change also reduces existing peak usage or moves work to a different slot.
Current state: sync-dex-liquidity is intentionally modeled at 5/6 because its nested direct-API phase can reach that static peak while staying below both the platform header-wait ceiling and the repo budget. Treat the halfHourlyOffset DEX scoring trigger as full for new fetch-heavy work. Other formerly full slots were given headroom by reducing Telegram send batches to 4, running supplemental yield source families serially, and moving discovery-scan from the 08:05 daily lane to its own 08:10 trigger.
For sync-stablecoins, failed upstream responses must still be consumed or canceled before later passes start. That rule bounds unread bytes, completes transport cleanup, and keeps fetch phases deterministic; it is not a claim that an unread body still occupies Cloudflare's header-wait slot. The late fallback phase remains CoinMarketCap -> Jupiter -> DexScreener.
The same cleanup rule applies to Worker-side integration clients. Telegram delivery, X posting, and GitHub feedback submission should consume or cancel response bodies before returning so transports do not retain unread streams or unbounded response bytes.
Cron Budgeting
| Area | Current repo budget | Source | Notes |
|---|---|---|---|
| DEX discovery overall deadline | 12 minutes | worker/src/cron/dex-discovery/orchestrator.ts | Shared deadline for the discovery pass before persistence/cleanup tail work |
| DEX discovery per-coin budget | 25 seconds | worker/src/cron/dex-discovery/orchestrator.ts | Prevents one slow coin from consuming the whole staging lane |
| Live reserve sync outer deadline | 12 minutes | worker/src/lib/cron-lease.ts | Explicit wrapper budget for the serialized reserve loop before the rest of the 4-hourly slot |
| Live reserve sync internal run budget | 9 minutes | worker/src/cron/sync-live-reserves-config.ts | Default cursoring budget; if the remaining budget drops below one adapter attempt, the untouched tail is marked deferred and resumed from cursor on the next run, leaving at least two minutes of wrapper headroom for D1 cleanup and cron logging. Optional finalization cleanup/history pruning is skipped when the D1 tail budget is already exhausted, and the skip is recorded in cron metadata. |
| Live reserve adapter I/O peak | 2 outbound operations per adapter attempt | worker/src/cron/reserve-adapters/concurrency.ts, shared/lib/cron-jobs.ts | Coin loop is serialized, but individual adapters can fan out internally; shared fetch/RPC helpers enforce the per-attempt limiter |
| Live reserve recovery poll / deadline | 5 minutes / 13 minutes; 2/6 connection peak | worker/src/handlers/scheduled/reserve-recovery.ts, worker/src/lib/scheduled-recovery-checkpoint.ts, shared/lib/cron-jobs.ts | WORKER_RESERVE_RECOVERY_MODE gates the isolated lane: off skips scans, shadow is read-only, reconcile prepares an exact successor without claiming, and recover claims/replays. Active child/recovery leases and queue-hash drift fail closed. |
| Yield publication overall deadline | 10 minutes | worker/src/lib/cron-lease.ts | Dedicated hourly sync-yield-data timeout after moving off the half-hourly lane |
| Yield supplemental overall deadline | 12 minutes | worker/src/lib/cron-lease.ts | Dedicated 4-hour sync-yield-supplemental timeout for optional protocol families |
| Telegram dispatch overall deadline | 14 minutes hard timeout; 4 minutes send-loop soft deadline | shared/lib/telegram-delivery-policy.ts, worker/src/lib/cron-timeouts.ts, worker/src/handlers/scheduled/context.ts | Dedicated five-minute Telegram lane timeout plus 30-second lease heartbeat; pending-drain and fresh-send loops stop starting Telegram batches near the four-minute mark, releasing pending claims or queueing the untouched fresh tail so slow Bot API runs yield the next trigger interval |
| Telegram authoritative target planning | 32 durable transitions per dispatch invocation; 100 statements per D1 transaction | shared/lib/telegram-delivery-policy.ts, worker/src/cron/telegram-alert-target-plans/materialization.ts | Existing due deliveries drain before subscriber capture and target planning. Page materialization packs complete idempotent plan units into bounded D1 transactions, avoiding a database round trip per subscriber while preserving atomic units and page-level reconciliation. |
| Telegram personalized recap planning | 90 due preferences/page, 10 pages (900 recipients), 500 Tape rows/page cap-plus-one, 90 min Tape freshness, 6 h pending TTL | shared/lib/telegram-recap-policy.ts, worker/src/cron/telegram-recap-planner.ts | D1-only deterministic planning with zero AI/external planning calls. Priority 100 keeps recap work below risk, legacy, and admin pending rows; incomplete Tape windows defer instead of silently truncating. |
| Telegram eventless dispatch fast path | 5-minute dispatch cadence with fan-out skipped on healthy no-change runs | worker/src/cron/dispatch-telegram-alerts.ts, worker/src/cron/dispatch-telegram-queue-paths.ts | Quiet runs still refresh snapshots and drain/clean due or expired pending rows, but skip subscriber fan-out, alert-job manifests, backoff reads, and fresh-send assembly when all alert families are unchanged and the safety source is healthy. |
| Telegram safety source stale threshold | 2 producer intervals (30 minutes) | worker/src/lib/alert-safety-source-cache.ts, shared/lib/cron-jobs.ts | Safety alerts remain suppressed until publish-report-card-cache republishes a fresh generation-valid source snapshot |
| Report-card full snapshot cache budget | <= 1.5 MB stored; <= 1.1 MB compressed; <= 8 MB uncompressed | worker/src/lib/report-cards-snapshot-cache.ts | The full public payload is checksum-verified gzip/base64 in D1 while the decoded V8 API contract remains unchanged. The stored ceiling leaves 500 KB of headroom below D1's 2,000,000-byte string/row limit; the new reader also accepts legacy plain envelopes during rolling deploys. |
| Telegram safety source serialized cache budget | <= 1.5 MB for 401 report-card rows in tests | worker/src/lib/__tests__/alert-safety-source-cache.test.ts | The optional safety explain payload is kept compact so cache["alert:safety-source-cache"] remains a single D1 cache row with operational headroom |
| Telegram registration reconciliation peak | 1 outbound Bot API call at a time | worker/src/lib/telegram-webhook-registration.ts, shared/lib/cron-jobs.ts | Runs serially before dispatch-telegram-alerts when 15-minute cache markers expire; modeled as the budget-only telegram-registration-reconciliation entry in the same five-minute Telegram connection group and surfaced through /api/status.budgetOnlySurfaces. |
| Telegram digest outbox retry peak | 1 Bot API call at a time; up to 4 due editions per five-minute poll | worker/src/lib/telegram-digest-outbox.ts, worker/src/handlers/scheduled/digest-trigger-poll.ts | Runs serially on the existing */5 digest-trigger slot. Retryable HTTP failures retain the exact stored chunks and honor retry_after; ambiguous and permanent outcomes stop for operator review. Retained terminal backlog degrades budget-surface telemetry but does not repeatedly feed the Telegram provider circuit when no network attempt occurred. |
| Persisted cron metadata ceiling | <64 KiB per cron_runs.metadata payload | worker/src/lib/cron-metadata-persistence.ts, worker/src/lib/cron-logger.ts | Global compaction retains counts, bounded samples, and latest drill-down state while preventing large asset/config arrays from driving unbounded D1 growth. |
| GBP SONIA retained-fallback alert | 2 consecutive daily retained-fallback runs; 24 hours re-alert cooldown | worker/src/cron/fetch-tbill-rate.ts | The first gbp-sonia-compounded-index-failed-retained run writes cache["fetch-tbill-rate:gbp-retained-fallback-streak"]; the second consecutive run sends the shared webhook alert and records a cron event. The 24-hour cooldown is keyed only to successful webhook delivery, so failed webhook attempts remain retryable. Fresh GBP market data resets the streak. |
| Telegram pulse heavy-section cadence | 15 minutes | worker/src/api/telegram-pulse.ts, worker/src/lib/telegram-usage-analytics.ts | Current aggregate pulse counts still refresh every 5 minutes. Top coins, lifecycle history/fallback, and Mini App daily counters reuse a rendered pulse section for up to 15 minutes; pending-delivery count can reuse the dispatch lane's pending-capacity snapshot. |
| Telegram load simulation targets | 500, 1,000, 5,000, 10,000 active watchers | shared/lib/telegram-delivery-policy.ts, shared/lib/telegram-recap-policy.ts, scripts/ci/check-telegram-load.ts | npm run check:telegram-load estimates drain time, per-invocation estimatedCpuMs, and D1 operations for risk, admin, and personalized-recap scenarios. Recap scenarios enforce a 5,000-recipient target across all-due, risk burst, 429 storm, preset-heavy, global-scope, no-change, and stale-Tape cases, with aiCalls=0 and externalPlanningFetches=0; they also emit a non-enforced 10,000-recipient advisory. The recap planner uses a 900-recipient/5-minute-run page budget, a 6-hour pending TTL, and priority 100. The CPU dimension (C102) reads cpu_ms from worker/wrangler.toml and fails when required scenarios exceed 0.5×cpu_ms. scripts/lib/telegram-load-guard.mjs owns the reviewed dependency triggers for local and CI execution. |
| Telegram group admin membership cache | 5 minutes for cached denial/admin-list copy; mutating auth revalidates per webhook | worker/src/lib/telegram-chat-member.ts, worker/src/api/telegram-webhook-auth.ts | /subscribe, /unsubscribe, /set in group/supergroup chats use a fresh getChatMember lookup for mutation authorization and fail closed if Telegram cannot confirm admin status. Soft-launch warnings and denial copy can still use the cached getChatAdministrators list (telegram:chat-admins:<chat_id>). These calls happen on webhook ingress, not inside the dispatch cron lane. |
| Live reserve history retention | 90 days | worker/src/lib/live-reserves-store-write.ts | reserve_composition_history and reserve_sync_attempt_history are pruned during reserve-sync cleanup |
| Redemption route-status producer | D1-free/static plus live-reserve metadata | worker/src/lib/redemption-backstop-route-status.ts, worker/src/cron/sync-redemption-backstops.ts | v4 route status remains four-hour snapshot data. sync-redemption-backstops does not add outbound route-status feed fetches; route availability comes from existing live-reserve adapter metadata, reviewed static policy, and market-implied severe-depeg overlays. |
| Blacklist sync runtime budget | 10 minutes | worker/src/cron/sync-blacklist.ts | Guardrail below the 12-minute trigger wrapper timeout |
| Blacklist sync subrequest budget | 900 | worker/src/cron/sync-blacklist.ts, worker/src/lib/evm-logs.ts | Covers explorer/RPC calls for a single run |
| Blacklist provider pacing/concurrency | 3 requests/second, 1 live request | worker/src/cron/sync-blacklist.ts, worker/src/lib/evm-logs.ts | The serial limiter's declared concurrency matches the live connection posture; throughput and live connections are reported separately in cron metadata |
| Blacklist RPC topic aggregation | One OR-topic eth_getLogs request per range when supported | worker/src/cron/blacklist/evm-source.ts, worker/src/lib/alchemy-logs.ts | Required topic0 signatures share one completeness frontier; explorer fallback remains per-topic where OR filters are unavailable |
| Blacklist Alchemy split cap | 64 provider calls per log scan, depth 8 | worker/src/lib/alchemy-logs.ts | Split traversal is sequential and also bounded by deadline and the shared 900-subrequest budget; hitting any cap returns incomplete coverage without advancing beyond proof |
| Blacklist Arbitrum scan window | 25,000,000 explorer/Alchemy-primary blocks; 250,000 fallback-RPC blocks per config/run | worker/src/cron/blacklist/evm-source.ts | Every topic shares the minimum proven frontier; the stored cursor never exceeds the 15-minute safe head |
| Blacklist amount-recovery batch | 100 queued rows / pass | worker/src/cron/blacklist/amount-recovery.ts, worker/src/cron/blacklist/amount-repair-queue.ts | Priority/retry state is durable; event and queue outcomes are paired in shared D1 batches and stay under the sync subrequest/runtime budget |
| Blacklist provider telemetry retention | 14 days; at most 4 x 120-character failure samples/config/run | worker/src/cron/blacklist/provider-telemetry.ts | Retains fetched/inserted rows, call count, split depth, safe frontier, and bounded diagnostics without allowing cron metadata or D1 rows to grow unbounded |
| Mint/burn global request budget | 200 | worker/src/cron/sync-mint-burn.ts | Shared per-run request ceiling |
| Mint/burn runtime self-budget | 9 minutes; 60 seconds minimum next-config window | worker/src/cron/mint-burn/run-configs.ts, worker/src/lib/alchemy-logs.ts | Guardrail below the 10-minute cron wrapper timeout so the runner can stop before starting a config that would not leave enough persistence/logging tail room. Mint/burn eth_getLogs calls receive the same runtime deadline so a started config cannot continue log-scan recursion past the self-budget. |
| Mint/burn per-config budget (critical) | 60; 150 for bridge-aware critical configs | worker/src/cron/sync-mint-burn.ts | Prevents one hot config from consuming the full run while allowing bridge-aware high-volume configs enough tx-context headroom |
| Mint/burn tx-context batch size | 20 tx hashes/request; 3 concurrent batch requests | worker/src/lib/mint-burn-pipeline/classification.ts | Keeps bridge-aware classification under the Worker connection pool and avoids one HTTP request per parsed USDC-style mint/burn transaction |
| Mint/burn per-config budget (extended) | 25 | worker/src/cron/sync-mint-burn.ts | Lower ceiling for long-tail backlog drain |
| Mint/burn max scan range | 50,000 blocks | worker/src/cron/sync-mint-burn.ts | Keeps per-request log scans bounded |
| Mint/burn timestamp resolution scope | Non-dust candidate logs only | worker/src/cron/mint-burn/sync-config.ts | Dust-only blocks are excluded before eth_getBlockByNumber lookups so irrelevant logs cannot block sync-state advancement |
Mint/burn SQL IN chunk size | 90 ids | worker/src/cron/sync-mint-burn.ts | Current safeguard for large batched SQL |
| Mint/burn event insert batch size | 50 statements | worker/src/lib/mint-burn-pipeline/persistence.ts | Each insert binds 18 values; chunked to stay below D1 batch bind ceilings |
| Mint/burn extended attempt SLO | 2 due runs / 75 minutes | worker/src/cron/mint-burn/run-state.ts | The next run resumes at the first capacity-deferred config; active provider deferrals are explicitly exempted until their recorded expiry |
| Stablecoin producer metadata ceiling | <60 KiB before scheduler enrichment; <64 KiB persisted | worker/src/cron/sync-stablecoins/metadata.ts, worker/src/lib/cron-metadata-persistence.ts | The stablecoin producer reserves 4 KiB for wrapper-owned lease, slot, invocation, and timeout metadata. Its size guard compacts diagnostics before that enrichment so top-level publication and active-price coverage survive unchanged into cron_runs; the global <64 KiB persistence guard remains the final fallback. |
D1 overload retry posture
Cron persistence helpers retry transient D1 queue pressure through runWithOverloadRetry() in worker/src/lib/cron-lease.ts. Retried errors include D1 DB is overloaded, Requests queued for too long, D1 storage-operation reset timeouts (D1 DB storage operation exceeded timeout ...), and Cloudflare D1 internal-reference errors (D1_ERROR: internal error; reference = ...). Retry backoff is abort-aware, and batchExecute() can accept an AbortSignal so chunked persistence can stop before the next D1 batch after cron timeout or lease loss. Every status-tracked job in shared/lib/cron-jobs.ts must have an explicit CRON_TIMEOUT_MS ceiling; the scheduled-runner contract test fails when a new cron job would fall back implicitly. If a leased job ignores timeout or lease-loss aborts, runCronWithLease() records an abandoned failure and leaves the lease row to expire by TTL instead of releasing it while late writes may still be running. Long-job lease heartbeats renew every 30 seconds and abort controlled work after three consecutive renewal failures. The duration watchdog excludes stale-slot reconciled synthetic child rows from runtime averages, tracks repeated runBudgetTruncated metadata as real capacity pressure, and uses cron_slot_executions for the separate scheduled-slot abandonment signal. DEX-liquidity persistence writes candidate rows to a per-run table and validates the active generation before updating the public current table. Live reserve, redemption-backstop, cache-sentinel, and DEWS persistence paths should route bursty run-manifest writes, cleanup, prune, and chunked batch work through this helper or batchExecute() so one transient D1 queue spike does not fail a whole scheduled run.
D1 query budgeting
Public health/status diagnostics intentionally trade a few minutes of operator telemetry freshness for lower D1 pressure:
queryBlacklistGapMetrics()supports a core diagnostic mode that skips the amount-status and amount-source distribution scans when callers only need total/recoverable/recent gap counts for health scoring.sync-blacklistmaterializes producer-owned blacklist gap snapshots into the existing D1cachetable only after every required config completes successfully in the run, no state CAS conflicts occur, and enough snapshot budget remains. Any skipped or incomplete config degrades the run and withholds publication. The snapshot timestamp is the oldest required config's successful-scan time, preventing a recent cron finish from masking a stale source cohort. It writes both full summary metrics and core health/status metrics from one live full query./api/health,/api/status, and/api/blacklist-summaryprefer those producer snapshots, then fall back to the short request cache and finally the live query if the producer row is missing or stale./api/blacklist-summaryalso has a producer-owned summary payload snapshot in the same D1cachetable. The handler preserves the public response shape, serves a fresh producer payload directly, serves a stale-but-present producer payload without rewriting the producer cache from public traffic, and falls back to live construction only when no valid snapshot has ever been written. Freshness headers are based on the producer snapshot timestamp, so stale-but-served fallback snapshots still emit the normalWarningheader once they exceed the blacklist summary max age.- The short 5-minute D1 request cache remains only as a fallback pressure valve. It should not be treated as the freshness authority for blacklist health; producer materialization is the preferred source when present.
- Cron health reads the latest 10 runs per tracked job through the existing
(job, started_at DESC)index instead of applying a window function across the retainedcron_runstable. - Tron blacklist amount recovery mirrors
blacklist_current_balancesthrough deterministic current-balance IDs and primary-keyid IN (...)lookups instead of correlatedLOWER()subqueries against the ledger table. The mirror is capped at 100 candidate events per pass and receives the same runtime budget assync-blacklist. - DEWS producers maintain
stress_signals_latestas a latest-row materialization for public stress-signal, Telegram dispatch, and DEWS smoothing reads. Readers merge latest rows over canonicalstress_signalslatest-history rows bystablecoin_idso missing, stale, unreadable, or partially written latest materialization cannot suppress fresher canonical history during rollout or D1 chunk failures. A missingdews:published-generationpointer is treated as first-run bootstrap and can use the unbounded latest-row fallback; corrupt or unreadable publication pointers return unavailable current rows instead of silently switching to an unbounded generation. - Migration
0144_worker_hot_query_indexes_and_stress_latest.sqladds chain/hourmint_burn_hourly, stablecoin/time yield, and depeg pagination/open-event indexes for the current hot query families. Keep matching SQL comments stable so D1 insights can attribute reads to source-owned query families. - D1 capacity samples are hourly and bounded. File-size states cross at 60%, 75%, and 90% of Cloudflare's 10 GB paid-plan ceiling. Exhaustion forecasts require at least three observations spanning 24 hours and never extrapolate a flat or shrinking trend. The scheduled status lane refreshes the control-plane observation; public health reads only the bounded cached assessment.
- The five-minute Telegram lane reuses same-run pending-capacity and safety-source reads between dispatch, degradation watchdog, and pulse publication. Healthy no-change dispatch runs skip fan-out and fresh-send assembly while preserving pending drain, TTL cleanup, snapshot freshness, and watchdog-visible metadata.
- Telegram registration reconciliation still keeps separate webhook, command, profile, and menu cache checks so each helper remains directly callable and testable. Coalesce those cache reads only with a helper that preserves the current standalone semantics and Bot API rate-limit backoff behavior.
- Cron progress writes still go through the shared scheduled-runner logging path. There is no Telegram-specific progress-write opt-out today; add a generic, documented opt-out before reducing progress persistence for one lane.
When adding a new health/status loader, prefer a producer-cadence cache or an indexed top-N read over table-wide aggregates. Table scans are acceptable for low-frequency admin-only diagnostics, but not for public health probes, status self-checks, or same-origin browser polling.
Large cache-backed endpoints can opt into a response-ready companion cache when the producer has already validated the canonical payload. /api/stablecoins writes stablecoins:response-ready:v2 alongside the canonical stablecoins cache row after schema validation; the reader serves that raw body only when both rows share the same updated_at, then injects freshness metadata without reparsing the full payload. Companion rows are best-effort optimizations: read or write failures fall back to the canonical cache path and should not fail the producer. Endpoints with live transforms should not use this shortcut unless the companion body already includes those transforms.
Upstream Fetch Budgets
| Path | Current repo throttle / budget | Source | Notes |
|---|---|---|---|
| CoinGecko onchain discovery | 250 ms between requests | worker/src/lib/rate-limit.ts | Used by discovery crawlers |
| CoinGecko onchain crawl budget | 5 minutes | worker/src/lib/rate-limit.ts | Per-source crawl budget, not full-run deadline |
| CoinGecko backfill throttle | 200 ms between requests | worker/src/lib/rate-limit.ts | Used by CoinGecko backfill/admin flows |
| GeckoTerminal crawl throttle | 2000 ms between requests | worker/src/lib/rate-limit.ts | Conservative crawl pacing |
| GeckoTerminal crawl budget | 3 minutes | worker/src/lib/rate-limit.ts | Per-source crawl budget |
| GeckoTerminal probe budget | 90 seconds max per sync-stablecoins run; dynamically clipped to preserve a 90 seconds sync tail | worker/src/lib/constants.ts, worker/src/cron/sync-stablecoins/stages.ts | Prevents the serialized soft-source cross-check from consuming the full 8-minute stablecoin sync timeout |
| Protocol-redeem live override stage | 10 s total budget; grouped circuit for most external live RPC-backed providers, with dedicated Kava, JUSD, USX, AZND, and Mento circuits | worker/src/lib/authoritative-price-sources/index.ts | Missing-price candidates run before already-priced candidates and provider families are interleaved within each partition. Missing-only thin routes are excluded when a usable incumbent exists. Local par and inherited tracked-base overrides are cache/local decisions; circuit-open RPC providers fail closed. |
| Kava USDX oracle adapter | 4 sequential requests, 2.2 s timeout/request, 0 retries, 256 KiB max response/request; inside the shared 10 s live-override budget | worker/src/lib/authoritative-price-sources/kava-pricefeed.ts | Reads head, market, aggregate, and raw-oracle state serially. Wrong chain/market/oracle identity, a head older than 2 minutes, insufficient oracle expiry, or excessive dispersion fails closed. |
| Reviewed exact price routes | Inside the shared 10 s live-override budget; adapter-specific bounded RPC calls and executable notionals | worker/src/lib/authoritative-price-sources/ | sAID, PHPm, USX, AZND, and JUSD pin exact contracts/tokens and reject stale state, identity drift, missing trusted parents, insufficient capacity/depth, route-specific impact/divergence failures, failed public redemption, or RPC failure. AZND calls stay serial to preserve connection headroom. |
| Reviewed Balancer USP route | 3 sequential hosted Balancer API requests: exact pool state, one pool-constrained 1 USP reference quote, and one pool-constrained 1,000 USP bounded quote; inside the direct Balancer provider budget | worker/src/cron/dex-liquidity/fetch-balancer.ts | Requires the exact USP -> waEthUSDC -> USDC buffer path, at least $50,000 pool TVL, bounded output no greater than 5% of TVL, calculated reference-to-bounded impact no greater than 2%, and reported impact no greater than 2% when numeric. Generic balanceUSD par is suppressed; this is not direct same-block Vault verification. |
| DexScreener discovery fallback budget | 2 minutes shared fallback window | worker/src/lib/rate-limit.ts | Shared with other late-stage discovery fallbacks |
| Jupiter price fallback | 50 ids/request, 5 s timeout/request, 0 retries; up to 25 low-depth primary augmentation targets/run; at most 3 sequential 3 s slot-RPC probes/pass | worker/src/cron/sync-stablecoins/enrich-prices-jupiter-pass.ts | Solana-only enrichment pass between CMC and DexScreener; can append agreeing Jupiter evidence to low-depth primary prices and fails closed when every bounded slot reference is unavailable |
| DexScreener price-enrichment pass | 10 total requests, 5 s timeout/request, 45 s total budget, 0 retries | worker/src/cron/sync-stablecoins/enrich-prices-dexscreener-pass.ts | Best-effort final fallback for missing prices through exact token-address lookups; symbol search is retired |
| Address-price augmentation group | 90 s total budget, 5 s timeout/request, 0 retries | worker/src/lib/address-price-providers/index.ts | Optional group currently disabled in production Worker config for quarter-hour sync headroom. When enabled, it runs during primary pricing for assets with missing prices, observations expiring before the next generation, low-confidence prices, or previous source depth below 3. Durable target cursors rotate only inside missing, expiring, low-depth, and remaining-priced cohorts. |
| vaults.fyi supplemental yield | 13 estimated credits/run on the four-hour lane; 2,500 estimated credits/UTC month | worker/src/cron/yield-sync/vaults-fyi.ts | Generation-fences one reservation owner with compare-and-swap, finalizes only the matching owner, dynamically throttles to the sustainable remainder, and fails closed before paid requests when the monthly ledger is corrupt; no provider-authoritative usage counter is available on this path |
| Coinbase CEX ticker products | 1 product request in flight, 10 s timeout/request, 1 retry | worker/src/lib/cex-tickers.ts | Keeps the Coinbase thunk inside the sync-stablecoins primary-provider cap instead of multiplying the quarter-hourly trigger fanout |
| Secondary FX mirror race | 3 mirror requests in parallel | worker/src/cron/sync-fx-rates-sources.ts, shared/lib/cron-jobs.ts | jsDelivr @latest, direct Pages mirror, and date-pinned jsDelivr package race inside the sync-fx-rates max connection declaration |
| Chainlink FX/commodity overlay | 3 feed pipelines in parallel | worker/src/lib/chainlink-feeds.ts, shared/lib/cron-jobs.ts | Keeps the five-feed overlay inside the sync-fx-rates three-connection cron metadata while preserving bounded parallel recovery |
| DexScreener address augmentation | opt-in; 1 request/run, 30 addresses/request | worker/src/lib/address-price-providers/dexscreener.ts | Exact /tokens/v1/{chain}/{addresses} lane; no symbol search; stops on hard upstream refusal |
| DexPaprika address augmentation | 60 token-detail requests/run | worker/src/lib/address-price-providers/index.ts | Public exact token-detail lookup |
| CoinGecko Onchain address augmentation | 5 requests/run, 30 addresses/request | worker/src/lib/address-price-providers/index.ts | Keyed exact onchain token lookup, separate from the serialized GeckoTerminal pool probe |
| Alchemy Prices address augmentation | 20 requests/run, 25 addresses/request | worker/src/lib/address-price-providers/index.ts | Optional keyed lookup, grouped by Alchemy network id |
| Moralis address augmentation | 3 requests/run, 100 addresses/request | worker/src/lib/address-price-providers/moralis.ts | Optional keyed EVM batch lookup; capped below the free-plan 40k CU/day envelope for the 15-minute sync cadence |
| Birdeye address augmentation | 10 Solana requests/run, 1000 ms between requests | worker/src/lib/address-price-providers/index.ts | Optional keyed Solana-only targeted gap lookup; known no-price payloads count as missing-quote, while 429 or provider-wide quota/compute-unit exhaustion stops the remaining request tail immediately |
| CoinMarketCap fallback | Up to 2 requests in an eligible hour: 1 category request plus 1 targeted request for at most 25 rotated unresolved slugs; 10 s timeout/request, 0 retries | worker/src/cron/sync-stablecoins/enrich-prices-cmc-pass.ts | Usable returned category rows survive an unseen tail. Targeted quotes require exact identity, active status, freshness, positive volume, required supplied-contract agreement for known deployments, and peg validation. Verified targeted rows preserve their upstream timestamp through the one-hour cooldown; 429 still honors Retry-After. |
| DEX primary source JSON reads | 30 s per attempt through fetchJsonWithRetry() | worker/src/cron/dex-liquidity/fetch-primary.ts | DeFiLlama Yields, DeFiLlama Protocols, and Curve API body reads stay inside the request timeout; the defillama-protocols cache stores a compact slug/category snapshot for yield coverage audits, and raw Curve response trees are released once their derived lookup maps are built |
| Direct DEX API fetch phase | 1 protocol family at a time, 5 nested connection static peak within a provider, 90 s provider timeout wrapping 15 s per-request timeouts, deterministic page caps (50 default); Fluid ticker retries cap provider Retry-After sleeps at 5 s per retry | worker/src/lib/provider-execution.ts, worker/src/cron/dex-liquidity/direct-api-policy.ts, worker/src/cron/dex-liquidity/direct-api-paginated.ts, protocol fetchers, shared/lib/cron-jobs.ts | Runs inside the existing sync-dex-liquidity trigger through a provider execution context derived from the declared cron connection budget; each provider result is reduced to tracked pools plus compact raw counts and authoritative exact keys before the next family starts. check:cron-connections still treats the trigger as 5/6 and headroom-full because one provider can use the nested peak; page-cap errors include resume markers for large or drifting upstream responses |
| Measured DEX execution RPC lane | 3 chain lanes, one sequential RPC stream per chain; 800 actual JSON-RPC requests, 6,400 quote subcalls, and 8 minutes per run; quote batches start at 8 and halve adaptively | worker/src/cron/measured-execution/sync.ts, worker/src/cron/measured-execution/profiles.ts, shared/lib/cron-jobs.ts | Runs in the isolated 0,30 * * * * trigger. Every block/code/factory/quote request is clipped to the absolute producer deadline; the producer stops and records bounded failure reasons instead of opening more requests. |
| Shared retry JSON/text response body | 16 MiB default; configurable per request with maxResponseBytes | worker/src/lib/fetch-retry.ts, worker/src/lib/response-body.ts | fetchJsonWithRetry() and fetchTextWithRetry() reject declared or streamed overflow, cancel the body, and retry through the existing warning path; JSON is parsed only after a complete in-cap body. Raw fetchWithRetry() responses remain caller-owned and uncapped. |
| Generic circuit breaker | opens after 3 consecutive failures, probes every 30 minutes | worker/src/lib/circuit-breaker.ts | Used to stop hammering degraded upstreams |
JSON/text fetch callers that need per-request timeout coverage across body consumption should use fetchJsonWithRetry() or fetchTextWithRetry() rather than calling fetchWithRetry() and then consuming the returned Response separately. Provider execution wrappers (providerJson() and providerTextBounded()) keep their provider timeout active through body reads and record body-read timeouts as provider failures.
What this means operationally
sync-dex-liquidityno longer owns discovery. It consumes staged output written bysync-dex-discovery.sync-cl-exit-depthconsumes the previously published retained-pool target generation and atomically publishes the next measured-quote generation. The10,40scoring run only joins a fresh published generation and then publishes the next target inventory, so a torn or same-run self-join cannot activate partial evidence.sync-dex-discoveryis deliberately best-effort. Short per-source request timeouts and the 12-minute shared budget are there to force a partialdegradedresult before the platform can hard-kill the invocation. Lower-priority tier-2/tier-3 candidates are deterministically sharded across their cadence windows so one modulo run does not inherit the entire tier queue at once.- Missing-price fallback is intentionally time-bounded so a bad upstream day cannot consume the whole
sync-stablecoinsslot. - Replay-safe price continuity has a hard six-hour ceiling and also obeys any shorter per-source
maxTrustedAgeSec. Low/fallback or non-replay-safe sources are never eligible. The separate verified-CMC provider cache is limited to the original quote's one-hour age, preserves its upstream timestamp, stays fallback confidence, and revalidates identity and peg bounds on reuse. activePriceCoverageis independent from cron execution and cache publication. A missing active price degrades public health while the valid remainder of thestablecoinspayload stays available and a successfully published cron run remainsok. Two consecutive published missing generations trigger an asset-attributable webhook alert; only successful delivery starts the 24-hour per-asset cooldown.- Exact adapters are fail-closed availability paths. A stale head, RPC/provider outage, identity drift, missing parent, depleted bridge, insufficient pool depth, or excessive quote impact produces no price; operators must not compensate by increasing replay age or substituting nominal parity.
- Any new provider added to discovery or price enrichment should come with both a throttle and a hard stop budget.
Request Timeouts Worth Preserving
| Area | Current timeout | Source |
|---|---|---|
| CoinMarketCap price fallback | 10_000 ms | worker/src/cron/sync-stablecoins/enrich-prices-cmc-pass.ts |
| Jupiter price fallback | 5_000 ms | worker/src/cron/sync-stablecoins/enrich-prices-jupiter-pass.ts |
| DexScreener price fallback requests | up to 5_000 ms per request | worker/src/cron/sync-stablecoins/enrich-prices-dexscreener-pass.ts |
| Direct DEX API requests | 15_000 ms per request | worker/src/cron/dex-liquidity/direct-api-policy.ts |
| Measured DEX execution RPC requests | up to 15_000 ms, further clipped to the run's absolute 8 minute deadline | worker/src/cron/measured-execution/sync.ts, worker/src/cron/measured-execution/profiles.ts |
| Ops admin proxy reads | 20_000 ms for /api/status and /api/status-history; 45_000 ms for /api/audit-depeg-history | shared/lib/api-endpoints/definitions.ts (opsProxyTimeoutMs) |
| Live reserve adapter attempt | 20_000 ms | worker/src/cron/sync-live-reserves-config.ts |
| Live reserve D1 finalize timeout | 30_000 ms | worker/src/cron/sync-live-reserves-config.ts |
| Public dataset snapshot outer deadline | 10 * 60_000 ms | worker/src/lib/public-dataset-snapshot-budget.ts, worker/src/lib/cron-lease.ts |
| Blacklist explorer / RPC reads | 15_000 ms | worker/src/lib/fetch-retry.ts (default timeout) |
| Daily digest LLM call (outer) | 12 * 60_000 ms | worker/src/lib/constants.ts |
| Daily digest per-attempt fetch | 11 * 60_000 ms | worker/src/cron/digest/platform.ts (DIGEST_FETCH_PER_ATTEMPT_TIMEOUT_MS) |
Live reserve timeout values are resolved through LiveReserveSyncBudgetConfig. Production uses the checked-in defaults above; tests and operational wrappers can inject smaller or larger positive finite values to validate deferred-tail and D1-finalize behavior without changing cron code.
/api/audit-depeg-history is also hard-capped at 25 items per request (limit default and max) so the CoinGecko-backed admin audit stays page-sized over the Pages proxy.
Anthropic / Digest Runtime
Current digest generation constraints that are actually encoded in repo code:
- model:
claude-opus-4-7 - thinking: adaptive (
thinking.type = "adaptive") - reasoning effort: xhigh (
output_config.effort = "xhigh") — dropped frommaxon 2026-04-18 after runaway-thinking exhaustedmax_tokenstwice (stopReason=max_tokenswith only asignature_deltaat both 16k and 32k).maxhas no constraint on thinking depth on Opus 4.7;xhighis Anthropic's recommended level for complex editorial work and Claude Code's own default. - Anthropic outer timeout:
12 * 60_000 ms(12 min), bound byAbortSignal.timeout(ANTHROPIC_TIMEOUT_MS)inplatform.ts - per-attempt fetch timeout:
11 * 60_000 ms(11 min), local torequestDigestCopy; safety net so a single stalled attempt cannot consume the outer budget - retry depth for the digest Anthropic call:
2(max 3 attempts); the outerAbortSignalcaps total wall time regardless - corrective retry skip: if first-pass elapsed
>= 50%of the outer budget (6 min), the in-process retry after quality failures is skipped; the parse is accepted withqualityIssuesflaggeddegraded - daily cron lease (wrapper timeout):
14 * 60_000 ms(14 min), leaves ~2 min under Cloudflare's 15-min scheduled-event ceiling for D1 persistence, Twitter/Telegram delivery, and cron_runs logging. - daily-digest heartbeat override:
heartbeatSec = 30,maxRenewFailures = 3(seeworker/src/handlers/scheduled/context.ts— default policy unchanged for other jobs) - weekly cron lease:
12 * 60_000 ms(12 min) - max_tokens:
64000daily,64000weekly — Anthropic's documented floor for Opus 4.7 atxhigh/maxeffort with adaptive thinking. Earlier settings of 16k → 32k ateffort: "max"both hitstop_reason=max_tokenswith no text emitted; the root-cause fix on 2026-04-18 lowered effort toxhighand raised the ceiling to 64k in one change. - cadence: daily scheduled run plus deferred manual admin trigger (see "Manual trigger runtime model" below)
- cost envelope (approximate, assuming single-attempt runs): Opus 4.7 input ~$5/Mtok, output ~$25/Mtok. Daily worst-case at 64k tokens ≈ $4.80; weekly worst-case at 64k ≈ $4.80. Annualized ≈ $2000 at cap. Actual usage is typically much lower since most runs don't approach the cap; the ceiling exists to survive adaptive-thinking-heavy runs. The current worker does not persist token-usage telemetry in
digest:last-trigger-resultorcron_runs; use provider-side Anthropic usage logs for exact spend.
Manual trigger runtime model
POST /api/trigger-digest does not execute the digest synchronously. It writes a digest:force-run-request flag into the cache D1 table and returns 202. A dedicated */5 * * * * cron slot (digestTriggerPoll) reads the flag on its next tick and runs the digest under scheduled-event wall-clock (15 min). Outcome is persisted to digest:last-trigger-result for D1 inspection and future ops-UI surfacing; the current admin panel still shows only the enqueue result from the browser session.
This two-step model exists because the repo treats long HTTP-triggered ctx.waitUntil() digest execution as unsafe on Cloudflare Workers. The external platform assumption is that HTTP request tail work can be canceled after a short post-response window, while scheduled events get the full scheduled-event wall-clock; an Opus 4.7 digest run takes 5–10 min, so enqueue + scheduled polling is the repo-verified safe path.
Source: worker/src/api/admin-actions.ts (enqueue-only HTTP handler), worker/src/handlers/scheduled/digest-trigger-poll.ts (polling consumer).
Source: worker/src/lib/constants.ts (Anthropic timeout/retries), worker/src/lib/cron-lease.ts (CRON_TIMEOUT_MS per-job lease budget), worker/src/cron/digest/platform.ts (model/thinking/effort, per-attempt timeout, corrective-retry skip), worker/src/cron/daily-digest.ts and worker/src/cron/weekly-recap.ts (max_tokens), worker/src/handlers/scheduled/context.ts (PER_JOB_LEASE_OPTIONS heartbeat override)
This doc deliberately does not restate Anthropic account-tier RPM / token-plan numbers because those are not repo-enforced.
Design Guidance
Before adding a worker feature that touches external services:
- Pick the trigger slot first. Shared slots are a capacity decision, not just a schedule decision.
- Add explicit throttle constants and an overall time budget before writing the fetch loop.
- Prefer chunked / batched writes and bounded SQL fan-out.
- Add or reuse a circuit breaker when the feature depends on a flaky upstream.
- Run
npm run check:cron-connectionsand document the trigger-slot impact for any new outbound I/O. - Update this doc only with limits the repo actually enforces or depends on architecturally.
If you need current provider-plan quotas, verify them outside the repo before relying on them.