Skip to content

External dependencies

Everything the app and its refreshers reach outside the box: third-party HTTP services, the on-chain RPC endpoints, and the npm runtime. For where these are called from in the pipeline see Data pipeline; for the table shapes they write into see Database & schema; for the server/deploy mechanics see Deployment.

Three ground rules that hold across every dependency here:

  • The app reads only its own Postgres at request time. Off-chain services are touched by the refreshers (scripts/refreshers/, run via cron) and by one-shot backfills, not on the request path. The exceptions are the on-chain RPC (used by per-asset adapters during ISR revalidation), DefiLlama current-prices (used by the carry simulator API), the KyberSwap aggregator (the simulator's swap-cost quote route), and Google Fonts (fetched by the OG-image route). Everything else is snapshotted into Postgres ahead of time.
  • No external read failure is allowed to write a wrong value. Refreshers retry with backoff, then either skip the row (leaving the prior snapshot standing) or write null. A failed read is never persisted as 0.
  • Every outbound HTTP call on the request path carries an explicit timeout (AbortSignal.timeout). A wedged upstream socket must not hang a bounded API route or an ISR page render, nor keep a live portfolio refresh running past its route deadline while holding a DB client. Budgets: on-chain reads 15s (archive-block eth_call 20s), the Kyber and Pendle swap quotes 10s per attempt, the simulator's Llama current-price read 10s, the yoETH Base-chain TVL read 15s; the batched RPC helper the refreshers share (rpc-batch.ts) uses 60s. A timeout rejects the fetch the same way a non-2xx does, so the existing handler degrades it (skip / null / retry), never a wrong value.

Service inventory

ServiceUsed forEndpoint (default)AuthCalled from
publicnode RPCCurrent-state eth_call (ERC-20 / ERC-4626 reads)https://ethereum-rpc.publicnode.com (ETHEREUM_RPC_URL)nonesrc/lib/data/rpc.ts → per-asset adapters; api/sim/swap-cost (request time: SY getTokensOut + decimals, cached per process, for the PT redeem-and-swap exit)
dRPC archive RPCHistorical eth_call / eth_getStorageAt / eth_getBlockByNumber at past blockshttps://eth.drpc.org (ETHEREUM_ARCHIVE_RPC_URL)nonesrc/lib/data/rpc.ts → refreshers + backfills
DefiLlama Coins APIUSD prices (current + historical), token decimals, block-by-timestamp. Not a mark source — portfolio + token_basis MARKET marks come from the Dune mirror (Milestone C); the historical-price use is Fluid DEX fee/TVL onlyhttps://coins.llama.finoneprices.ts (Fluid DEX fee/TVL), llama-prices.ts (simulator current prices), rpc.ts (block-by-timestamp), the DEPRECATED backfill-token-basis.ts
DefiLlama Stablecoins APITop-20 mainnet stablecoins by Ethereum circulating (ranking) + per-stablecoin mainnet address (--approve only)https://stablecoins.llama.finonescripts/sync-portfolio-tokens.ts (weekly cron in PROPOSE mode + the manual --approve apply)
NY Fed reference ratesSOFR daily rate + compounded averages + SOFR Indexhttps://markets.newyorkfed.org/api/rates/securednonescripts/refreshers/sofr-rates.ts
Morpho Blue GraphQLMorpho market posted-collateral USD; curator-vault fee / netApy / allocationhttps://blue-api.morpho.org/graphqlnonescripts/refreshers/morpho.ts, curator-vault-state.ts
Euler (Goldsky subgraph)Euler curator-vault interestFee + accepted-collateral sethttps://api.goldsky.com/api/public/project_cm4iagnemt1wp01xn4gh1agft/subgraphs/euler-v2-mainnet/latest/gnnonescripts/refreshers/curator-vault-state.ts
KyberSwap aggregatorRound-trip swap quotes behind the simulator's execution-cost card (non-PT legs)https://aggregator-api.kyberswap.com/ethereum/api/v1/routesnone (x-client-id header)src/app/api/sim/swap-cost/route.ts (request time)
Fluid public APICarry-registry vault candidates + smart-debt leg compositionhttps://api.fluid.instadapp.io/v2/1/vaultsnonescripts/fluid-discovery.ts, scripts/refreshers/collateral-exposure.ts
vaults.fyiCurator-fund vault discovery only (registry regen)https://api.vaults.fyi/v2VAULTS_FYI_API_KEYscripts/sync-curator-vaults.ts (manual tooling, never the app or a cron)
Google FontsTTF fetch for the OG-image renderer (Satori)https://fonts.googleapis.com/css2nonesrc/app/opengraph-image.tsx (request time)
Pendle hosted APIActive-market registry (new-market discovery, on-chain verified before insert), liquidity_usd display aid, one-shot daily history backfill (underlying_apy), and the simulator's PT-leg swap quotes (v2 SDK /swap — Kyber has no real PT route)https://api-v2.pendle.finance/core (v1 active-list + v2 history/SDK)nonescripts/refreshers/pendle-markets.ts, scripts/backfill-pendle-history.ts, src/app/api/sim/swap-cost/route.ts (request time)
Dune AnalyticsThe token_price_bars mirror: hourly USD bars (prices.hour) as the historical market-price source for portfolio marks + token_basis, plus exact 5-min bars (prices.minute) for narrow flow true-up; plus ad-hoc analytics / reconciliationhttps://api.dune.com/api/v1 (DUNE_API_KEY, DUNE_QUERY_ID_HOURLY, DUNE_QUERY_ID_MINUTES)API key (scarce credits)src/lib/data/dune.ts → the 6h token-basis refresher (incremental sync) + scripts/backfill-token-price-bars.ts (bulk load) + fetch-through on a read miss
CloudflareDNS + CDN + TLS in front of creddit.xyz (and Cloudflare Pages + Access for docs.creddit.xyz)Infrastructure (no app code)

All defaults are public, keyless endpoints, so the app boots with only DATABASE_URL set. The two RPC env vars only need setting to override the public defaults (e.g. to a paid endpoint with higher limits). The one keyed service, vaults.fyi, is manual registry tooling — neither the app nor any cron calls it.


Ethereum RPC

Defined in src/lib/data/rpc.ts — a zero-dependency raw JSON-RPC helper, split into two endpoints by access pattern:

  • ETHEREUM_RPC_URL (default publicnode) — current state only, low latency. Used by ethCall / ethCallRaw against the latest block, which back the per-asset on-chain adapters during page ISR revalidation.
  • ETHEREUM_ARCHIVE_RPC_URL (default dRPC) — historical / archive. Used by ethCallAt, ethCallAtRaw, ethGetStorageAt, and getBlockTimestamp, which read state at a specific historical block. This is the endpoint the refreshers and backfills hit, since the canonical APY (annualizeRatio in src/lib/data/apy.ts) needs the compounding index at two past blocks.

ethGetStorageAt reads packed storage slots directly (e.g. Fluid DEX dexVariables2, which has no clean resolver getter).

Failure behaviour. Every helper throws on a non-2xx response (RPC <status>), a JSON-RPC error, or an empty result. The caller (an adapter or refresher) catches and skips that asset/snapshot; it never writes a partial or zeroed row.

Timeouts. Every fetch here carries an explicit per-request timeout so a wedged socket cannot hang an ISR render or a bounded route: 15s for the latest eth_call, a storage slot, a block number / timestamp, and the Llama block-by-timestamp lookup; 20s for an archive-block eth_call (ethCallAt), which executes contract code at a historical block. A timeout rejects the fetch just like a non-2xx, so the same catch skips the read (and the block-by-timestamp ladder counts it as one transient attempt and retries).

publicnode getLogs size limit. publicnode rejects oversized getLogs ranges with HTTP 400, which is the signal used in the position-attribution scans (lending-positions) to back off / narrow the block window. Treat an HTTP 400 from publicnode on a log query as "range too large," not as a hard error. (Note: rpc.ts itself only does eth_call-class methods; the log scanning lives in the lending-positions pipeline.)


DefiLlama Coins API

Free and keyless; used in four places. Their rate limit is aggressive enough to break long backfills, so size matters.

  • Current pricesprices/current/{coins} in src/lib/data/llama-prices.ts, backing the carry simulator's live position-size readout (called during the /carries ISR render). Dedupes + validates addresses, revalidate: 300, a 10s request timeout, and returns {} on any failure, a timeout included (the UI degrades gracefully rather than erroring).
  • Historical prices + decimalsprices/historical/{ts}/{coins} in src/lib/data/prices.ts (getHistoricalPrices). Batches up to ~50 coins per request (URL-length bound), revalidate: 86400. Throws on non-2xx so the caller skips that snapshot. Maps the native-ETH sentinel 0xEee...EEeE → WETH for lookups. Since Milestone C this is out of the mark pipeline — its only live callers are the Fluid DEX fee/TVL enrichment (fluid-dex.ts, backfill-fluid-core.ts, display/TVL, not a portfolio or token_basis mark). prices.ts carries a header forbidding new mark-pipeline use.
  • Block-by-timestampblock/ethereum/{ts} in rpc.ts (blockByTimestamp), used to resolve the two window-endpoint blocks for every 6h APY snapshot. Retries up to 5x with exponential backoff (1/2/4/8s) on 429 / 5xx; fails fast on other 4xx; throws after the last attempt.
  • Token basisno longer a DefiLlama consumer (Milestone C). The 6h token-basis refresher now sources its market + ETH/BTC numeraire quotes from the Dune price mirror (getBarSeriesAt over token_price_bars, same-bar division), not getHistoricalPrices + a coingecko:bitcoin call. Only the DEPRECATED legacy scripts/backfill-token-basis.ts still uses the DefiLlama path, and it refuses to run without --i-know-deprecated (it would write cross-vintage rows into the mirror-derived served table). See Data pipeline → "Token price bars (Dune mirror)".

batchHistorical (avoid the 429)

This describes the deprecated legacy token_basis backfill, retained only to reconstruct pre-mirror history (see the Token-basis note above); the live pipeline no longer uses DefiLlama for marks.

Requesting thousands of individual historical timestamps gets hard rate-limited (429). The legacy backfill therefore uses the batch endpoint coins.llama.fi/batchHistorical?coins={...} (scripts/backfill-token-basis.ts), which encodes {coin: [ts, ...]} and prefetches everything in ~90 batched calls instead of thousands of singles. The URL grows with coins x timestamps, so it is capped at 40 timestamps x 6 coins per call — past roughly 12 coins x 40 ts DefiLlama returns 414 (URI Too Long). Each batch retries up to 5x on failure. Rule of thumb: any DefiLlama backfill at scale must use batchHistorical, never per-timestamp loops.


NY Fed SOFR

scripts/refreshers/sofr-rates.ts, run weekdays 13:00 UTC (~09:00 ET, after the ~08:00 ET NY Fed publish). Two endpoints under https://markets.newyorkfed.org/api/rates/secured, joined by date:

  • /sofr/... — daily overnight rate (ACT/360, stored in %).
  • /sofrai/... — compounded 30d / 90d / 180d averages + the canonical SOFR Index (the compounding accrual factor used as the risk-free benchmark on the USD-denominated charts).

Daily mode pulls the last ~30 business days (.../last/30.json) for a generous revision-overlap window; backfill mode ({ since }) walks the range in 1-year chunks (/search.json?startDate=&endDate=) back to the SOFR genesis 2018-04-03. Writes upsert into onchain_credit.sofr_rates keyed by rate_date. On a non-2xx or network error fetchJson logs a warning and returns null, which collapses to an empty merge for that endpoint (no bad rows). Reader side (src/lib/data/sofr.ts) returns [] if the DB read fails.


Morpho Blue GraphQL API

https://blue-api.morpho.org/graphql, POSTed from three places. Keyless.

  1. Market discovery — veto only (scripts/morpho-discovery.ts, fetchApiMarkets). Pulls the top mainnet markets by borrowed USD for the admission rule (processes.md A.7). The API is advisory / veto-only: it supplies the veto signals (listed, RED warnings, badDebt, supplyingVaults count) and can keep a market out, but every listing number (sizes, params, oracle) is re-read on-chain — the chain stays canonical. Fail closed for additions (API down → hold new proposals), fail open for removals.
  2. Market collateral USD (scripts/refreshers/morpho.ts, fetchCollateralUsdByMarket). The on-chain market() struct carries no collateral aggregate (it is per-position state), so total posted-collateral USD for the "Underwritten capital" overcollateralization figure comes from Morpho's own indexer (state.collateralAssetsUsd). Best-effort on live runs only; the historical backfill passes fetchCollateralUsd:false because the API exposes current state only. On any failure the map comes back empty and collateral_usd is written null (the overcoll column simply hides rather than lying).
  3. Curator-vault fee / netApy / allocation (scripts/refreshers/curator-vault-state.ts, fetchMorpho). Chunked at 20 vaults per query, 4 retries with backoff, 300ms between chunks. A vault whose state doesn't come back is counted failed and skipped (prior snapshot stands).

Gotchas

  • Address filters need checksummed casing. The GraphQL address-type filters only match checksummed-case addresses. The market query sidesteps this by filtering on uniqueKey_in (the lowercase bytes32 market ids, matched verbatim) instead of address. If you add an address filter, checksum it first.
  • Schema drifts (2026-07). On the Market type the uniqueKey field was renamed marketId and whitelisted became listed. The uniqueKey_infilter still exists (so the collateral-USD query is unaffected), but the discovery query uses the current field names — treat the API as drift-prone and re-check fields ({ __type(name:"Market"){ fields{ name } } }) if a query starts erroring.
  • V1 API does not index Morpho V2 vaults. blue-api.morpho.org is the V1 Blue API; V2 vaults are absent from it, which is a known coverage gap in the curator-funds registry. Such vaults appear in the registry but get no fee / allocation snapshot until V2 indexing exists.

Euler (Goldsky subgraph)

scripts/refreshers/curator-vault-state.ts, fetchEuler / eulerGql. POSTs GraphQL to the public Goldsky subgraph euler-v2-mainnet. Euler is an isolated single-pool model, so there is no per-collateral $ allocation: the refresher exposes only the interestFee (scaled by 1e4, e.g. 1000 = 10%) and the accepted-collateral set (collateral vault ids resolved to asset symbols, e.g. eWETH-1WETH, batched 50 at a time). 4 retries with backoff; on persistent failure eulerGql returns null and the vault is skipped. Stored as allocation entries with supplyUsd = 0, which the UI renders as an unweighted set rather than a weighted bar.


KyberSwap aggregator

https://aggregator-api.kyberswap.com/ethereum/api/v1/routes, called at request time from src/app/api/sim/swap-cost/route.ts (force-dynamic POST, invoked by the carry simulator's execution-cost card). Keyless; sends an x-client-id header (KYBER_CLIENT_ID, default creddit).

The route quotes each leg as a reciprocal round trip (A→B then B→A at the quoted output) and reports the round-trip loss as the execution cost — one round trip for a single-token leg, two for T2/T3 pool legs, zero for mirrored T4 legs. A non-positive round-trip loss (cross-venue spread noise) returns {ok:false} ("Quote not available") — there is deliberately no estimated-cost fallback anywhere in the simulator. Kyber was chosen over Enso after evaluation: Enso's priceImpact is unstable and size-invariant, while its amountOut is a Kyber drop-in, so quoting Kyber directly is strictly simpler. Each attempt carries a 10s timeout (AbortSignal.timeout); a timed-out attempt is treated as transient and retried within the 3-attempt loop, exactly like a 429 / 5xx, and returns null if all three fail (no fabricated quote).

Pendle PT legs quote on Pendle, not Kyber. Kyber has no real route to a Pendle AMM (verified: quoting 100 PT-srUSDe, worth roughly $98, it returned a dust path paying ~22 USDC, with its own USD estimate off by three orders of magnitude — the round-trip check correctly refused it, which is why PT rows showed "Quote not available"). When a leg's token spec carries a pendleMarket tag (wired from the carry registry config), venuePrice dispatches that leg to Pendle's hosted SDK (/core/v2/sdk/1/markets/{market}/swap, enableAggregator=true so a USDC/USDT side routes through the underlying inside the quote) and uses its amountOut with the identical round-trip method. Same retry policy (3 attempts, each with a 10s timeout), same no-fallback rule. The client-supplied market is validated against pendle_markets (10-minute per-process cache) before any outbound call, so the unauthenticated endpoint cannot be used as a quote proxy for arbitrary markets — Pendle rate-limits per IP, shared with our own refresher.

Per-IP rate limited (protects the quota). The swap-cost route is now wrapped in a per-IP in-memory limiter (10 requests/min/IP, src/lib/rate-limit.ts), so a single caller cannot burn Kyber (or Pendle) quote quota on this unauthenticated, outbound-heavy endpoint. Over-limit returns HTTP 429 with Retry-After. See Architecture → API routes for the full per-endpoint budget table and the single-process caveat.


Fluid public API

https://api.fluid.instadapp.io/v2/1/vaults, keyless. Two consumers:

  • scripts/fluid-discovery.ts (via the manual sync-carries.ts flow) — lists every live Fluid vault as carry-registry candidates, with TVL from the API's own embedded token prices. Partial-response guard: if the API returns fewer than MIN_EXPECTED_VAULTS (80) vaults, discovery refuses to classify — otherwise the sync's gone-marking would mass-mark live vaults as gone and strip /carries.
  • scripts/refreshers/collateral-exposure.ts (6h cron) — vault set + smart debt leg composition for the Fluid underwritten-capital slices; the risk layer itself reads on-chain storage, not the API.

vaults.fyi

https://api.vaults.fyi/v2, requires VAULTS_FYI_API_KEY. Discovery only: scripts/sync-curator-vaults.ts (manual, --write to regen src/data/curator-vaults.ts) lists USD curator vaults to propose registry candidates. The app and crons never call it; once a vault is in the registry, everything comes from on-chain reads + the Morpho/Euler APIs.

Gotchas: rate-limits aggressively (429 and 403 both mean back off — the script retries 5x with pacing); the list endpoint carries corrupted/phantom duplicate entries (filter isCorrupted !== true && holders > 0) and low-holder feeder vaults next to the real "Main" vault.


Google Fonts (OG image)

src/app/opengraph-image.tsx fetches the display font at request time (fonts.googleapis.com/css2 → font file) for the Satori-rendered social card. Two hard-won rules, documented in the file itself:

  • Do not send a browser User-Agent on the CSS request — Google then serves WOFF2, which Satori cannot parse (this broke a deploy once). The default fetch UA gets TTF.
  • The font URL is regex-pinned to a truetype/opentype src block as a second guard against format drift.

Dune Analytics

A runtime dependency of the 6h token-basis refresher + the mirror backfill tooling (dune-price-mirror-plan.md Phase A), plus the older out-of-band analytics use. The client is src/lib/data/dune.ts — a plain HTTP call against the Dune API (no SDK): execute a saved parameterized query → poll its execution (reading the execution_cost_credits off the status, free) → page the results (32k rows/page) → idempotent UPSERT into onchain_credit.token_price_bars.

Two saved queries (a prices.minute WINDOW execution scans the whole time partition, ~110-170 credits/month — ~100x the estimate — so it is NOT used for windows):

  • HOURLYprices.hour, DUNE_QUERY_ID_HOURLY (query 8033816), params tokens CSV / from_iso / to_iso, bars on the hour. Cost is SUBLINEAR in span: 1y × ~40 tokens ≈ 16 credits in ONE execution, 1 month ≈ 1.5, a 6h window ≈ 0.25 (floor ~0.22). Every window path — sync, preload, fetch-through — uses it.
  • EXACT-MINUTESprices.minute, DUNE_QUERY_ID_MINUTES (query 8033818), params tokens / minutes (CSV of 5-min-aligned ISO timestamps). Its cost scales with the calendar SPAN of the requested minutes, not their count (~1.5 credits clustered ≤1h, ~22+ scattered over a year), so syncExactMinutes reserves it for ≤6h clusters — the Milestone C/D flow true-up path only.

Call sites:

  • Incremental sync — the token-basis refresher prepends syncBars(tracked, MAX(bar_ts), now) on its existing 6h grid (HOURLY), so the newest window lands for ~0.25 credits/run (~30/month steady state); no crontab change.
  • Bulk loadscripts/backfill-token-price-bars.ts seeds ~1 year of hourly bars in ONE execution (~16 credits; --halves splits into two if it pages awkwardly), idempotent + resumable, with a hard abort if a chunk returns zero rows for all tokens (a query/param bug) or the credit guard stops it.
  • Fetch-through — a read miss beyond the 6h walk-back triggers ONE HOURLY syncBars for a ±1h window (cheap floor), then re-reads; a second miss returns null (M9 — never a fabricated mark). Fetch-throughs are logged; alert if > 20/day (a coverage gap — fix the tracked set, not the budget).

Guard rails, because credits are scarce (~2,500/month on the mirror plan; steady state ~30-60/month):

  • Window clampsyncBars refuses a window > 48h (a [fail]-style log) and clamps to the most recent 48h unless allowLongWindow (bulk load only): a stale mirror is caught up by the bulk tool, never by one giant cron execution.
  • Credit guard — a per-process accumulator (DUNE_MAX_CREDITS_PER_RUN, default 50) stops issuing further executions once reached, returning partial with a [fail] line; the per-execution credit cost is logged always.
  • Reads never overwrite a stored bar (ON CONFLICT DO NOTHING), so a Dune history restatement cannot move a frozen bar — a stable audit trail.
  • Keyless is graceful: with DUNE_API_KEY / the relevant DUNE_QUERY_ID_* unset every network path degrades to a logged skip + DB-only reads, so local dev / CI / a de-provisioned box still valuate from stored bars.
  • For the surviving ad-hoc analytics (e.g. Fluid NFT-PnL): inspect a query and its cached results before executing, partition-prune before windowing, reuse cached results.

ToS note. The credit model here is built on RETAINING query results in our own DB (the mirror is a persisted copy of Dune bars). Confirm the Dune API terms permit that use before the prod bulk load — a one-time check (Dune API terms: https://dune.com/terms).


Cloudflare

Fronts the production app: creddit.xyz resolves to Cloudflare, which proxies to nginx on the Hetzner box, which proxies to the pm2 onchain-credit process on localhost:3001. The docs site docs.creddit.xyz is served from Cloudflare Pages and gated by a Cloudflare Access policy while private. No application code touches Cloudflare; it is pure edge infrastructure (DNS, CDN, TLS). See Deployment.


npm runtime dependencies

From package.json (Node + tsx for the TypeScript crons):

PackageVersionRole
next16.2.5App Router framework, Server Components, per-page ISR
react / react-dom19.2.4React 19
tailwindcss + @tailwindcss/postcssv4Styling (@theme inline in globals.css)
@base-ui/react1.4.xUI primitives (Base UI, not Radix), wrapped in src/components/ui/*
recharts3.xCharts
viem2.xABI encode/decode for on-chain reads (used alongside the raw rpc.ts helpers)
pg8.xPostgres client (src/lib/data/postgres.ts)
tsx (dev)4.xRuns the .ts refreshers / backfills directly under the cron wrapper
lucide-react, clsx, class-variance-authority, tailwind-merge, tw-animate-cssUI utilities
typescript (dev)5.xStrict type checking

node_modules is gitignored, so the deploy runs npm ci on the server. A transient gotcha: during npm ci tsx is briefly absent, so a refresher launched mid-deploy can fail with MODULE_NOT_FOUND — wait out the deploy before running a cron by hand.


Environment variables

VarRequiredDefaultPurpose
DATABASE_URLyesPostgres connection string. App throws DATABASE_URL not set without it. Prod points at role onchain_credit on db creddit; staging at onchain_credit_staging on creddit_staging.
ETHEREUM_RPC_URLnohttps://ethereum-rpc.publicnode.comCurrent-state RPC override
ETHEREUM_ARCHIVE_RPC_URLnohttps://eth.drpc.orgArchive RPC override
KYBER_CLIENT_IDnocredditx-client-id header on KyberSwap quote requests
VAULTS_FYI_API_KEYtooling onlyscripts/sync-curator-vaults.ts discovery; not read by the app or any cron
DUNE_API_KEYmirrorDune API key for the token_price_bars mirror (6h token-basis sync + backfill + fetch-through). Unset ⇒ the mirror degrades to DB-only reads (no error).
DUNE_QUERY_ID_HOURLYmirrorSaved Dune prices.hour query id (query 8033816; params tokens CSV, from_iso, to_iso). The WINDOW path: sync / preload / fetch-through. Unset degrades to DB-only reads.
DUNE_QUERY_ID_MINUTESmirrorSaved Dune prices.minute query id (query 8033818; params tokens, minutes CSV). The exact-minute flow-true-up path (syncExactMinutes). Unset degrades to DB-only reads.
DUNE_MAX_CREDITS_PER_RUNno50Per-process Dune credit ceiling; the client stops issuing further executions once a run reaches it (returns partial).

DefiLlama, NY Fed, Morpho Blue, Euler/Goldsky, Kyber, the Fluid API, and Google Fonts are all keyless, so they need no env vars. DUNE_API_KEY + DUNE_QUERY_ID_HOURLY / DUNE_QUERY_ID_MINUTES now drive the token_price_bars mirror (the token-basis refresher + the bulk load); with the key or the relevant query id unset the mirror degrades to DB-only reads rather than erroring. On the server these live in .env.local (gitignored, so a deploy git reset --hard preserves them); the cron wrapper scripts/run-cron.sh loads the same env before invoking a refresher.


Deploy / verification caveat

The CI deploy is code-only (git reset --hard + npm ci + npm run build

  • pm2 restart); it does not run migrations, backfills, or refreshers. Pages are prerendered at build with ISR revalidate of 1800/3600s, so if a refresher runs after a deploy build, re-run the deploy to re-prerender or data stays stale up to the revalidate window. See Data pipeline and Deployment.

Two things to know when verifying a live deploy:

  • Egress firewall. This dev environment's egress sits behind an SSL- inspecting firewall that blocks creddit.xyz (you get TLS errors). You cannot curl / WebFetch prod from here; rely on the green deploy workflow + local route validation, and ask a human to confirm the live URL.
  • Early-access gate. src/components/EarlyAccessGate.tsx overlays the whole app until a code is entered. It is a soft gate (friction + early-access signal, code ships in the client bundle); the page content is still server-rendered underneath so SEO/structured data is unaffected. It blocks human visual access, not crawlers or the data path, so it does not affect any external dependency or refresher.

Anthropic API (AI assistant)

The Creddit Agent assistant (/agent) calls the Anthropic Messages API through the Vercel AI SDK (ai + @ai-sdk/anthropic), server-side in src/app/api/chat/route.ts.

Used forStreaming the tool-using chat response over the creddit intelligence layer.
ConfigANTHROPIC_API_KEY (secret), CHAT_MODEL (default claude-sonnet-5), CHAT_ENABLED (kill switch), plus CHAT_SESSION_SECRET and the CHAT_*_TOKEN_BUDGET caps. See deployment.
Failure modeThe assistant degrades (route returns 503) if CHAT_ENABLED is off or the key is missing; the rest of the app is unaffected (it does not depend on this key). Model errors surface as an error line in the chat, not a 500.
Cost controlPer-user + whole-app daily token budgets (chat_usage, cost-weighted), a stepCountIs(5) tool-loop cap, prompt caching on the static system prompt (1h TTL, shared across users) + a rolling 5m breakpoint in the tool loop, and a CHAT_EFFORT thinking-depth ceiling (default medium; adaptive thinking still decides per step whether to think at all; model-default omits the param for models that reject effort).
NotesThe methodology system prompt is prompts/assistant-metrics.md (a condensed derivative of docs/metrics.md), cached. Model choice is env-driven so it can change without a code deploy. Anthropic pricing: https://www.anthropic.com/pricing.

Private documentation. creddit.xyz