Skip to content

fix(api): serve stale or empty data instead of 5xx when Dune or CoinGecko fail (DEV-1163) - #2149

Merged
brunod-e merged 15 commits into
devfrom
brunod-e/dev-1163-proposals-endpoint
Sep 15, 2026
Merged

brunod-e merged 15 commits into
devfrom
brunod-e/dev-1163-proposals-endpoint

Conversation

@brunod-e

@brunod-e brunod-e commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Causa raiz das quedas de 26-28/08

Reconstruído pelos access logs do gateful e logs/traces das DAO APIs (a métrica http_server_requests_total está inflada por scrape de réplicas e não foi usada). O volume da plataforma é ~3 req/s, flat. As falhas reais vieram de duas rotas com dependência externa quebrada, que juntas são 95% de todos os 5xx das DAO APIs em 14 dias:

Rota Upstream Falhas/dia Efeito
/ens/revenue/renewal-tenure Dune responde HTTP 404 para a query de renewalTenure 25-60 503 -> breaker do ENS -> 722 x 503 em /ENS/* em 26/08 17-19h
/fluid/token/historical-data CoinGecko 404 em /coins/fluid/market_chart (id errado) 50-267 500 -> breaker do FLUID -> 13h fora em 27-28/08

Cada 5xx conta no circuit breaker por DAO do gateful (5 consecutivas -> 5-40min de 503 para todas as rotas). O dashboard faz 3 retries por padrão, então um único usuário abrindo a página de Revenue do ENS gera as falhas necessárias.

Mudanças

  • CoinGecko: FLUID mapeado para instadapp (id real; fluid não existe). O serviço guarda a última resposta boa por janela e a serve quando o CoinGecko falha; a rota /token/historical-data responde 200 [] em vez de 5xx quando não há dado (404 de token não listado é preservado).
  • Dune: RevenueDuneClient serve a última resposta boa (cache de 24h passa a reter entradas expiradas para isso) ou um result set vazio quando o Dune falha, em vez de 503. As 7 rotas /revenue/* passam a degradar para 200 com items: [], igual ao comportamento quando o client não está configurado. O erro continua logado com a URL.

Pendente fora do código (Railway)

REVENUE_DUNE_RENEWAL_TENURE_QUERY_ID aponta para uma query que o Dune responde 404. Com este PR a rota deixa de derrubar o ENS, mas o gráfico de renewal tenure só volta a ter dados quando o id for corrigido.

Verificação

  • pnpm api typecheck, pnpm api lint: OK
  • Testes: coingecko, revenue (client, cache, controller) e a nova suíte do controller de token: 72 passando

Relacionado: PR de resiliência do gateful/API (breaker por rota, RPC fora do hot path).

🤖 Generated with Claude Code

Dune (revenue) and CoinGecko (token price history) failures now serve the
last successful result, or an empty series when there is none, instead of
503/500. Those two routes produced 95% of all DAO API 5xx in the last two
weeks and each 5xx counts against the gateway circuit breaker for the whole
DAO, which is how ENS and FLUID went offline in late August. Also maps FLUID
to its actual CoinGecko id (instadapp): the previous id 404ed on every call.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@vercel

vercel Bot commented Sep 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
anticapture-storybook Ready Ready Preview Sep 15, 2026 6:57pm UTC
1 Skipped Deployment
Project Deployment Actions Updated
anticapture Ignored Ignored Sep 15, 2026 6:57pm UTC

Request Review

@railway-app

railway-app Bot commented Sep 3, 2026

Copy link
Copy Markdown

🚅 Deployed to the anticapture-pr-2149 environment in anticapture-infra

Service Status Web Updated
uniswap-api ✅ Success (View Logs) Sep 15, 2026 at 6:59 pm UTC
lil-nouns-api ✅ Success (View Logs) Sep 15, 2026 at 6:58 pm UTC
ens-api ✅ Success (View Logs) Sep 15, 2026 at 6:57 pm UTC
prometheus ✅ Success (View Logs) Sep 15, 2026 at 6:57 pm UTC
loki ✅ Success (View Logs) Sep 15, 2026 at 6:57 pm UTC
nouns-api ✅ Success (View Logs) Sep 15, 2026 at 6:57 pm UTC
compound-api ✅ Success (View Logs) Sep 15, 2026 at 6:57 pm UTC
mcp ✅ Success (View Logs) Web Sep 15, 2026 at 6:57 pm UTC
gitcoin-api ✅ Success (View Logs) Sep 15, 2026 at 6:57 pm UTC
authful ✅ Success (View Logs) Web Sep 15, 2026 at 6:57 pm UTC
fluid-api ✅ Success (View Logs) Sep 15, 2026 at 6:57 pm UTC
alertmanager ✅ Success (View Logs) Web Sep 15, 2026 at 6:57 pm UTC
aave-api ✅ Success (View Logs) Sep 15, 2026 at 6:57 pm UTC
gateful ✅ Success (View Logs) Web Sep 15, 2026 at 6:57 pm UTC
tornado-api ✅ Success (View Logs) Sep 15, 2026 at 6:57 pm UTC
shutter-api ✅ Success (View Logs) Sep 15, 2026 at 6:56 pm UTC
obol-api ✅ Success (View Logs) Sep 15, 2026 at 6:56 pm UTC
scroll-api ✅ Success (View Logs) Sep 15, 2026 at 6:56 pm UTC
otelcol ✅ Success (View Logs) Sep 15, 2026 at 6:56 pm UTC
tempo ✅ Success (View Logs) Sep 15, 2026 at 6:56 pm UTC
grafana ✅ Success (View Logs) Web Sep 15, 2026 at 6:56 pm UTC
docs ✅ Success (View Logs) Web Sep 15, 2026 at 6:55 pm UTC
ens-relayer ✅ Success (View Logs) Sep 15, 2026 at 6:52 pm UTC
ens-indexer-offchain ✅ Success (View Logs) Sep 3, 2026 at 6:14 pm UTC
compound-indexer-offchain ✅ Success (View Logs) Sep 3, 2026 at 6:14 pm UTC
gitcoin-indexer-offchain ✅ Success (View Logs) Sep 3, 2026 at 6:09 pm UTC
shutter-indexer-offchain ✅ Success (View Logs) Sep 3, 2026 at 6:07 pm UTC
uniswap-indexer-offchain ✅ Success (View Logs) Sep 3, 2026 at 6:07 pm UTC
gitcoin-indexer ✅ Success (View Logs) Sep 3, 2026 at 12:26 pm UTC
tornado-indexer ✅ Success (View Logs) Sep 3, 2026 at 12:25 pm UTC
obol-indexer ✅ Success (View Logs) Sep 3, 2026 at 12:25 pm UTC
lil-nouns-indexer ✅ Success (View Logs) Sep 3, 2026 at 12:25 pm UTC
fluid-indexer ✅ Success (View Logs) Sep 3, 2026 at 12:25 pm UTC
compound-indexer ✅ Success (View Logs) Sep 3, 2026 at 12:25 pm UTC
shutter-indexer ✅ Success (View Logs) Sep 3, 2026 at 12:25 pm UTC
aave-indexer ✅ Success (View Logs) Sep 3, 2026 at 12:25 pm UTC
uniswap-indexer ✅ Success (View Logs) Sep 3, 2026 at 12:25 pm UTC
nouns-indexer ✅ Success (View Logs) Sep 3, 2026 at 12:25 pm UTC
address-enrichment ✅ Success (View Logs) Web Sep 3, 2026 at 12:25 pm UTC
scroll-indexer ✅ Success (View Logs) Sep 3, 2026 at 12:25 pm UTC
ens-indexer ✅ Success (View Logs) Sep 3, 2026 at 12:25 pm UTC
erpc ✅ Success (View Logs) Sep 3, 2026 at 12:25 pm UTC
nodeful ✅ Success (View Logs) Sep 3, 2026 at 12:25 pm UTC
user-api ✅ Success (View Logs) Web Sep 3, 2026 at 12:24 pm UTC

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f144c5c5e7

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread apps/api/src/services/revenue/dune-client.ts Outdated
Comment thread apps/api/src/controllers/token/token-historical-data.ts Outdated
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

🔍 Vercel preview: https://anticapture-dqz8k8txw-ful.vercel.app

…ut of caches

Review feedback: fetchJson is now generic over the row type so the empty
fallback needs no unknown cast, setCacheControl no longer overrides a
Cache-Control header set by the handler, and the empty series returned when
CoinGecko is unavailable is sent with no-store so the gateway does not cache
it for the route's one hour max-age after the provider recovers.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Comment thread apps/api/src/controllers/token/token-historical-data.ts Outdated
The Dune and CoinGecko fallbacks answer with 200s, so the stale and empty
responses they serve are invisible to the HTTP error metrics and to the
HighErrorRate alert. Every fallback site now increments the
degraded_upstream_responses_total counter, labelled by upstream, resource
and mode, alongside the structured warn log carrying the upstream error.

The DegradedUpstreamData rule in infra/monitoring/alerts.yml fires on that
counter and routes through the existing Alertmanager Slack receiver.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a4ae59291c

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread apps/api/src/controllers/token/token-historical-data.ts Outdated
The token history route degraded on any non-client error. For NOUNS and
LIL_NOUNS it is wired with NFTPriceService, which reads auction prices from
PostgreSQL before calling CoinGecko, so a database error or a mapping bug
was returned as an empty 200 labelled as a CoinGecko failure and hidden
from error monitoring.

UpstreamUnavailableError now carries the failing upstream and is thrown only
by the provider calls themselves: the CoinGecko market chart request, the ETH
price requests in the NFT path, and the Dune result requests. The controller
and both degraded clients fall back only on that error and rethrow everything
else, so our own failures stay a 5xx and are counted by the HTTP metrics.
It extends HTTPException with 503, so callers that do not degrade keep the
status and body they returned before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e0a61b1482

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread apps/api/src/services/revenue/dune-client.ts Outdated
Comment thread apps/api/src/services/nft-price/index.ts Outdated
brunod-e and others added 2 commits September 14, 2026 15:24
Malformed 2xx bodies escaped the upstream classification. The Dune revenue
client accepted and cached any JSON, so a body without result.rows was kept
for 24h and failed in the mapper on every later call, outside the classified
path. It now validates the envelope before anything is cached and degrades
on a mismatch. The NFT price wrapper returned CoinGecko's body unchecked, so
a missing, mistyped or too short prices array blew up in the mapping and
became a 500 instead of degrading; it now validates the shape and the
minimum length the caller's mapping needs.

The CoinGecko token price call is now classified like the others, and every
live provider call has a 15s deadline, an AbortSignal for fetch and the
axios timeout option for the axios clients. Without one a hanging provider
is neither degraded nor errored.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…routes

Dune rows are now validated per query key inside the fetch, before anything
is cached, so a renamed or nulled column degrades once instead of being
stored for 24h and then failing in the mappers on every later request. The
row cast is gone and nullable aggregate columns are modelled explicitly, with
the mappers defaulting them to zero.

Only transport errors, timeouts, 408, 429 and 5xx count as an outage. A
provider rejecting our request, an expired key or a deleted query id or a
wrong token id, now surfaces as a 502 with an error log naming the upstream,
status and path, so HighErrorRate catches it rather than an empty 200 being
served behind a warning.

Treasury and token properties degrade the same way, which is what stops a
CoinGecko outage from opening the DAO circuit breaker in the gateway. Stale
responses carry no-store like the empty ones, so day-old prices do not sit in
a downstream cache for the route's hour.

The per-days price cache became one bounded last-good series with a 24h max
age, sliced to the window being asked for, and the NFT path gained the same.
A Dune outage now leases the stale value for 60s and shares one in-flight
call per key, instead of every request opening its own.

Also removes the dead DuneService, drops `for` from the DegradedUpstreamData
alert so it pages on the first degraded response, and restores spies between
tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…outage

The treasury path degraded straight to empty while the price routes served
their last good value, so an outage blanked the chart rather than showing
slightly old numbers.

TreasuryProviderCache now keeps fetchedAt and exposes a bounded stale read,
capped at the fresh TTL plus one day, the same rule and reasoning as the
revenue cache. All three providers expose it through getStaleTreasury, and
the one degrade path in the service serves it marked degraded, which the
controller answers with no-store. Empty is the fallback only when nothing
fresh enough is held. Nothing is written to the cache on a failure, so a
degraded response can never become the next response's source of truth.

The token treasury path needs no change: its staleness lives in the price
provider, and its other input is the database.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a2c7e27a87

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread apps/api/src/services/nft-price/index.ts
Comment thread apps/api/src/services/treasury/providers/defillama-provider.ts
Comment thread apps/api/src/services/coingecko/index.ts
Comment thread apps/api/src/controllers/treasury/treasury.integration.test.ts Outdated
The fake now implements ITreasuryRepository, the interface TreasuryService
depends on, so the five `as unknown as TreasuryRepository` casts go away
and the test double is type-checked against what the service reads.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6370a90d10

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread apps/api/src/services/treasury/treasury.service.ts Outdated
…nd label stale treasury honestly

Follow-ups from a regression pass over yesterday's commits:

- Splitting the empty check in two made the token treasury return nothing
  whenever no transfer fell inside the window, skipping the balance carried
  in from before it. It now returns empty only when there is no balance to
  carry either, as dev did.
- A 404 from CoinGecko, DefiLlama or Compound became a 502, which the
  gateway counts against the DAO's breaker. It degrades under `not_found`
  now, the rule the Dune client already followed; 401 and 403 stay a 502.
- A cached empty treasury series was reported as `mode=stale` while serving
  nothing; it is reported and served as `empty`.
- A vacuous `every()` over an empty array in the unit tests asserts the
  empty result instead.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 98e5254409

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread apps/api/src/controllers/token/token-historical-data.ts
Comment thread apps/api/src/controllers/token/token-historical-data.unit.test.ts Outdated
Comment thread apps/api/src/services/coingecko/index.unit.test.ts Outdated
…he controller for real

The NFT price service used a hand-rolled copy of the axios classification
that never got the 404 branch, so a delisted ETH price returned a 502 for
NOUNS and LIL_NOUNS while every other DAO degraded; it now uses the shared
classifyAxiosFailure. The historical-data controller passes the upstream
reason to the degraded counter, so a persistent not_found is not counted as
a transient outage. The controller test runs the real CoingeckoService with
MSW at the HTTP boundary instead of a fake client, the CoinGecko test
asserts the whole result, and redactUrl keeps the path of the relative axios
paths every provider logs instead of a placeholder.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2bdee77394

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread apps/api/src/lib/upstream-error.ts Outdated
Comment thread infra/monitoring/alerts.yml Outdated
Comment thread apps/api/src/services/revenue/dune-client.ts Outdated
Comment thread apps/api/src/controllers/treasury/treasury.integration.test.ts Outdated
…e and harden fallbacks

Upstream failures are logged as a description (status, code, redacted url,
cause chain capped at three) instead of the raw AxiosError, whose config
carried the CoinGecko and Dune API keys. DegradedUpstreamData gains an arm
for counter series born inside the window, so the first fallback of a new
label set pages. Dune month values must be real calendar dates before they
are cached. The empty Dune fallback is leased like the stale one, for
transport and status failures only. The NFT spot price is quoted in the
requested currency so its stale tag is truthful. The total treasury refuses
a half that degraded to nothing instead of serving a token-only sum, joins
the two halves by date rather than by position, and a DAO holding none of
its token no longer loses its liquid half in a CoinGecko outage. The stale
treasury test runs through the real provider factory with MSW.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…osals-endpoint

# Conflicts:
#	infra/monitoring/alerts.yml

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e8b51a66af

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread apps/api/src/services/treasury/treasury.service.ts Outdated
Comment thread apps/api/src/services/nft-price/index.unit.test.ts Outdated
…t NFT results whole

A DAO with neither in-window transfers nor a carried balance has nothing to
price, yet the token treasury still waited on CoinGecko and recorded a
degraded treasury on outage before discarding the result. Fetch the balances
first and return before asking the provider.

The NFT price happy-path tests destructured `degraded` away, so a regression
flagging every healthy response as degraded would have passed. Assert the
full `{ data, degraded: false }` object.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@railway-app
railway-app Bot temporarily deployed to anticapture-infra / anticapture-pr-2149 September 15, 2026 18:55 Destroyed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1b229951cf

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread apps/api/src/controllers/token/token-properties.integration.test.ts
Comment thread apps/api/src/controllers/treasury/treasury.integration.test.ts
Comment thread apps/api/src/services/revenue/dune-client.ts
@brunod-e
brunod-e merged commit 4229b08 into dev Sep 15, 2026
59 of 60 checks passed
@brunod-e
brunod-e deleted the brunod-e/dev-1163-proposals-endpoint branch September 15, 2026 19:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants