ENS metadata service running on Cloudflare Workers.
A subset of ensdomains/ens-metadata-service ported to Workers. Serves NFT metadata JSON, resolved avatar/header images, server-rendered ENS name cards (SVG + PNG), and an NFT lookup endpoint. Name cards are rendered with resvg-wasm (no node-canvas); the upstream Puppeteer-based /preview and /rasterize endpoints are not included.
| Endpoint | Upstream | Flarecloud |
|---|---|---|
GET / |
✓ | ✓ (Scalar docs) |
GET /docs |
✓ | ✓ (Scalar docs) |
GET /favicon.ico |
✓ | ✓ (204) |
GET /{network}/{contract}/{tokenId} |
✓ | ✓ |
GET /{network}/{contract}/{tokenId}/image |
✓ | ✓ (SVG card via resvg-wasm) |
GET /{network}/{contract}/{tokenId}/image/png |
✗ | ✓ (PNG card via resvg-wasm) |
GET /{network}/{contract}/{tokenId}/rasterize |
✓ | ✗ (needs Puppeteer) |
GET /{network}/avatar/{name} |
✓ | ✓ |
GET /{network}/avatar/{name}/meta |
✓ | ✓ |
GET /{network}/header/{name} |
✓ | ✓ |
GET /{network}/header/{name}/meta |
✓ | ✓ |
GET /queryNFT |
✓ | ✓ |
GET /preview/{name} |
✓ | ✗ |
POST /cache/invalidate |
✗ | ✓ (indexer-only) |
POST /cache/preload |
✗ | ✓ (indexer-only) |
GET /openapi.json |
✗ | ✓ |
GET /llms.txt |
✗ | ✓ |
network is one of mainnet, sepolia, holesky. contract is the BaseRegistrar (v1) or NameWrapper (v2) address.
Click the Deploy to Cloudflare button above. Cloudflare forks this repo into your GitHub and runs npm run deploy. The deploy script creates or reuses the R2 bucket (ens-metadata-ipfs-cache) and KV namespace (RESOLVER_CACHE) on your account, then writes the account-specific KV namespace ID into the build copy of wrangler.toml before publishing.
The setup page will prompt for these optional secrets — leave them blank unless you need them:
THE_GRAPH_API_KEY— one or more comma-separated Graph API keys. Requests are spread randomly across the keys and retried on a different key on rate-limit/transport failures. Blank uses the public subgraph endpoints.OPENSEA_API_KEY— OpenSea-hosted NFT metadata used by ERC-721 and ERC-1155 avatar/header lookups.
Public endpoints (ETH_RPC_URL, SUBGRAPH_URL_*, IPFS_GATEWAYS) come from [vars] in wrangler.toml and can be overridden on the same setup page. The indexer-only endpoints need their own secrets — see Indexer endpoints.
Requires Node 20+.
npm install
cp .dev.vars.example .dev.vars # fill in any secrets you have
npm run dev # wrangler dev on localhost:8787
npm test # vitest in workerd via @cloudflare/vitest-pool-workers
npm run typecheck # tsc --noEmitOpen http://localhost:8787 for the Scalar docs. Miniflare simulates R2 and KV locally — no manual resource creation needed for dev.
If you'd rather clone and deploy from the CLI instead of using the button, log in with Wrangler, add any optional secrets you need, then deploy. npm run deploy will create or reuse the KV namespace and R2 bucket for the authenticated account before publishing.
npx wrangler secret put THE_GRAPH_API_KEY # optional
npx wrangler secret put OPENSEA_API_KEY # optional
npm run deployPublic vars live in wrangler.toml under [vars] and can be edited freely:
ETH_RPC_URL,SEPOLIA_RPC_URL,HOLESKY_RPC_URLIPFS_GATEWAYS— comma-separated. All gateways are raced in parallel and the fastest successful response wins. The defaults inwrangler.tomlare public, best-effort endpoints.SUBGRAPH_URL_MAINNET,SUBGRAPH_URL_SEPOLIA,SUBGRAPH_URL_HOLESKY—{API_KEY}in a URL is substituted with aTHE_GRAPH_API_KEYkey.LOG_LEVEL—debug|info|warn|error(defaultinfo). See Observability.PUBLIC_BASE_URL— optional; the absolute origin/cache/preloadself-fetches to warm the edge cache. Falls back to the incoming request origin when unset (set it behind a custom domain / preview).
Cloudflare retired its public IPFS gateway in 2024. For production, point IPFS_GATEWAYS at reliable dedicated gateways or a self-hosted Kubo node rather than relying on the public defaults.
Two indexer-only POST endpoints keep caches in sync with on-chain changes. Each returns 503 until its secrets are set; every other route is unaffected.
POST /cache/invalidate— purge KV + R2 + the edge cache for a name/token. Secrets:CACHE_INVALIDATION_TOKEN,CF_API_TOKEN,CF_ZONE_ID. Spec:docs/cache-invalidation.md.POST /cache/preload— warm R2 (by CID) and the per-colo edge cache (bynetwork+name) so the first real user gets a hot response. Secret:CACHE_PRELOAD_TOKEN(optionalPUBLIC_BASE_URL). Spec:docs/cache-preload.md.
npx wrangler secret put CACHE_INVALIDATION_TOKEN
npx wrangler secret put CF_API_TOKEN
npx wrangler secret put CF_ZONE_ID
npx wrangler secret put CACHE_PRELOAD_TOKEN- Resolved avatar/header URIs are cached in KV for 15 minutes
- Image bytes are cached in R2, keyed by IPFS CID or by
sha256(url)for HTTPS sources - On a cache miss, IPFS/HTTPS images stream to the client while the R2 copy is written in the background, so time-to-first-byte does not wait on the full download
- HTTPS fetches are revalidated with
If-None-Match/If-Modified-Sincewhen an ETag orLast-Modifiedwas stored - Responses also opt into the Workers Cache API via
Cache-Control: public, max-age=900 - Sanitized SVG bytes are stored in R2 with a
sanitizerVersionfield. When the SVG sanitizer policy changes, bumpSANITIZER_VERSIONinsrc/services/sanitize.ts; older cached entries are lazily re-sanitized on first read. Note:*.workers.devpreview URLs cannot be tag-purged — stale Workers Cache responses expire naturally aftermax-age=900(15 min).
[observability] is enabled in wrangler.toml, so console output is ingested by Workers Logs. The service emits structured single-line JSON logs:
- One
request_completeline per request (method, path, status, duration) carrying areqId— Cloudflare'scf-raywhen present, a UUID otherwise — propagated to all service-layer andwaitUntillogs viaAsyncLocalStorage. - Unhandled errors log
unhandled_error; handled 5xx loghttp_error(4xx are not logged). - Cache-seam and IPFS gateway-race diagnostics (
r2_image_*,kv_resolver_*,ipfs_gateway_win) aredebug-level — setLOG_LEVEL=debugunder[vars]to surface them.
MIT, see LICENSE.