Cloudflare Worker that serves Enso spot prices and historical prices from Neon Postgres. Historical rows are written by offline warmup and backfill jobs, not by the worker.
- Bun
- Wrangler (installed via
bun install) - A Neon (or other Postgres) database
- Cloudflare account access, for deploys
bun install
cp .env.example .dev.vars # wrangler dev reads secrets from .dev.vars
cp .env.example .env # scripts (warmup, backfill, migrate) read from .env via dotenvFill in .dev.vars and .env with real values: a DATABASE_URL, one API_KEY_* per consumer, ENSO_API_KEY, and an RPC_URL_<chainId> per supported chain. Both files are gitignored — never commit them.
bun run dev| Command | Purpose |
|---|---|
bun run dev |
Start the worker locally with wrangler dev |
bun run deploy |
Deploy the worker with wrangler deploy |
bun run typecheck |
Type-check with tsc --noEmit |
bun run test |
Run the Vitest suite |
bun run migrate |
Run pending Postgres migrations (db-migrate up) |
bun run migrate:create |
Scaffold a new SQL migration file |
bun run migrate:down |
Roll back the last migration |
bun run warmup |
Pre-populate today's prices for known vaults/tokens |
bun run backfill:token-address-checksums |
One-off backfill of checksummed token addresses |
bun run backfill:defillama-day-alignment |
One-off repair of DeFiLlama prices stored against the wrong day |
backfill:defillama-day-alignment takes a phase (prices, derived, verify, cleanup, default all) and
--out <file> (report path, default backfill-report.json), --retry[=db|<file>] (retry only tokens that failed,
from the progress table or a prior report), --concurrency <n> (tokens in flight, default 4). verify samples a
fixed YFI/WBTC 2025-08-16..21 window. Pause the hourly warmup workflow while prices/derived run.
Full route reference, request/response shapes, error codes, and caching behavior are documented in docs/routes.md.
- Spot prices: Enso (live prices for any token on supported chains)
- Historical prices: read from
token_pricesonly. No historical route calls an upstream provider; a row that is not in the table returns404(single token) or is omitted from the response (batch, range), and stays that way until an offline job writes it.
Historical rows are written by scripts/warmup-prices.ts (hourly: DefiLlama, Curve, derived), scripts/backfill-historical-gaps.ts and scripts/backfill-defillama-day-alignment.ts. One-off migrations (scripts/backfill-token-address-checksums.ts) copy existing rows and add no new prices. docs/routes.md lists which job writes each source value.
Sources are pluggable adapters under src/sources/. Spot sources register in src/registries/spot.ts and are served live. Historical sources register in src/registries/historical.ts but have no request-path consumer — extend a warmup or backfill job so rows reach the table. See src/sources/README.md.
All /api/prices/* routes require an API key, sent as either:
Authorization: Bearer <api-key>x-api-key: <api-key>
The worker has no token database — it checks the presented key against every worker environment variable/secret named API_KEY_* (see src/http/auth.ts). The matched variable's suffix, lowercased, becomes the client_id used in request logs (e.g. API_KEY_FRONTEND → frontend).
Production secrets, including every API_KEY_*, live in the 1Password vault webops-prod, item yearn-price. .github/workflows/deploy.yml pulls them via 1Password/load-secrets-action and uploads them to the Cloudflare Worker with wrangler secret bulk on every push to main.
- Generate a random secret.
openssl rand -base64 32
- Pick a client id for the consumer, e.g.
KONG,FRONTEND. The env var name will beAPI_KEY_<CLIENT_ID>(uppercase). - Add it to 1Password. In the
webops-prodvault,yearn-priceitem, add a new password field namedAPI_KEY_<CLIENT_ID>with the generated value. - Wire it into CI.
.github/workflows/deploy.ymllists each secret explicitly in two places — add the new key to both:- the
env:block of the "Load secrets from 1Password" step (API_KEY_<CLIENT_ID>: op://webops-prod/yearn-price/API_KEY_<CLIENT_ID>) - the
jqobject in the "Upload secrets to Cloudflare" step
- the
- Deploy. Merge to
main(or run theDeploy Workerworkflow manually) — CI loads the secret from 1Password and uploads it to the Worker viawrangler secret bulk. - Local dev: add the same
API_KEY_<CLIENT_ID>=<value>line to.dev.varssowrangler devcan validate it. - Hand off the token to the consuming team out-of-band (e.g. a 1Password share link) — never paste it into Slack, git, or a PR.
To rotate or add a key outside of a deploy (e.g. an emergency rotation), you can push directly to the live Worker without going through CI:
wrangler secret put API_KEY_<CLIENT_ID>This only updates the deployed Worker; remember to also update 1Password and deploy.yml so the next CI deploy doesn't overwrite or drop it.
Pushing to main runs .github/workflows/deploy.yml: install deps, load secrets from 1Password, upload them to the Worker, run migrations, warm the price cache, then wrangler deploy. .github/workflows/warmup.yml runs the warmup script hourly on a cron. .github/workflows/pr.yml runs typecheck and tests on every PR.
bun run typecheck
bun run test