diff --git a/apps/api/src/controllers/account-balance/historical.ts b/apps/api/src/controllers/account-balance/historical.ts
index 4136f8964..d77e3fbb0 100644
--- a/apps/api/src/controllers/account-balance/historical.ts
+++ b/apps/api/src/controllers/account-balance/historical.ts
@@ -6,6 +6,7 @@ import {
HistoricalBalanceRequestQuerySchema,
HistoricalBalancesResponseSchema,
} from "@/mappers";
+import { setCacheControl } from "@/middlewares";
import { HistoricalBalancesService } from "@/services";
export function historicalBalances(
@@ -21,6 +22,7 @@ export function historicalBalances(
description:
"Returns historical balance deltas for one account, enriched with the transfer that caused each change.",
tags: ["account-balances"],
+ middleware: [setCacheControl(60)],
request: {
params: HistoricalBalanceRequestParamsSchema,
query: HistoricalBalanceRequestQuerySchema,
diff --git a/apps/api/src/controllers/account-balance/interactions.ts b/apps/api/src/controllers/account-balance/interactions.ts
index 826297e08..8de5f4fd5 100644
--- a/apps/api/src/controllers/account-balance/interactions.ts
+++ b/apps/api/src/controllers/account-balance/interactions.ts
@@ -1,6 +1,8 @@
import { Address } from "viem";
import { OpenAPIHono as Hono, createRoute } from "@hono/zod-openapi";
+import { setCacheControl } from "@/middlewares";
+
import {
AccountInteractions,
AccountInteractionsMapper,
@@ -33,6 +35,7 @@ export function accountInteractions(app: Hono, service: InteractionsService) {
description: `Returns a mapping of the largest interactions between accounts.
Positive amounts signify net token transfers FROM
, whilst negative amounts refer to net transfers TO `,
tags: ["account-balances"],
+ middleware: [setCacheControl(60)],
request: {
params: AccountInteractionsParamsSchema,
query: AccountInteractionsQuerySchema,
diff --git a/apps/api/src/controllers/account-balance/listing.ts b/apps/api/src/controllers/account-balance/listing.ts
index 0e9873d58..7c3496803 100644
--- a/apps/api/src/controllers/account-balance/listing.ts
+++ b/apps/api/src/controllers/account-balance/listing.ts
@@ -10,6 +10,7 @@ import {
AccountBalanceWithVariationResponseMapper,
AccountBalanceWithVariationResponseSchema,
} from "@/mappers";
+import { setCacheControl } from "@/middlewares";
import { AccountBalanceService } from "@/services";
export function accountBalances(
@@ -25,6 +26,7 @@ export function accountBalances(
summary: "Get account balance records",
description: "Returns sorted and paginated account balance records",
tags: ["account-balances"],
+ middleware: [setCacheControl(60)],
request: {
query: AccountBalancesRequestSchema,
},
@@ -94,6 +96,7 @@ export function accountBalances(
summary: "Get account balance",
description: "Returns account balance information for a specific address",
tags: ["account-balances"],
+ middleware: [setCacheControl(60)],
request: {
params: AccountBalanceRequestParamSchema,
query: AccountBalanceRequestQuerySchema,
diff --git a/apps/api/src/controllers/account-balance/variations.ts b/apps/api/src/controllers/account-balance/variations.ts
index 564c36100..c1ebe9ae7 100644
--- a/apps/api/src/controllers/account-balance/variations.ts
+++ b/apps/api/src/controllers/account-balance/variations.ts
@@ -9,6 +9,7 @@ import {
AccountBalanceVariationsByAccountIdResponseMapper,
AccountBalanceVariationsByAccountIdResponseSchema,
} from "@/mappers";
+import { setCacheControl } from "@/middlewares";
import { BalanceVariationsService } from "@/services";
export function accountBalanceVariations(
@@ -24,6 +25,7 @@ export function accountBalanceVariations(
description:
"Returns a mapping of the biggest variations to account balances associated by account address",
tags: ["account-balances"],
+ middleware: [setCacheControl(60)],
request: {
query: AccountBalanceVariationsRequestQuerySchema,
},
@@ -66,6 +68,7 @@ export function accountBalanceVariations(
summary: "Get changes in balance for a given period for a single account",
description: "Returns a the changes to balance by period and accountId",
tags: ["account-balances"],
+ middleware: [setCacheControl(60)],
request: {
params: AccountBalanceVariationsByAccountIdRequestParamsSchema,
query: AccountBalanceVariationsByAccountIdRequestQuerySchema,
diff --git a/apps/api/src/controllers/dao/index.ts b/apps/api/src/controllers/dao/index.ts
index 9d40648b1..351b35d6e 100644
--- a/apps/api/src/controllers/dao/index.ts
+++ b/apps/api/src/controllers/dao/index.ts
@@ -1,6 +1,7 @@
import { OpenAPIHono as Hono, createRoute } from "@hono/zod-openapi";
import { DaoResponseSchema } from "@/mappers";
+import { setCacheControl } from "@/middlewares";
import { DaoService } from "@/services";
export function dao(app: Hono, service: DaoService) {
@@ -12,6 +13,7 @@ export function dao(app: Hono, service: DaoService) {
summary: "Get DAO governance parameters",
description: "Returns current governance parameters for this DAO",
tags: ["governance"],
+ middleware: [setCacheControl(3600)],
responses: {
200: {
description: "DAO governance parameters",
diff --git a/apps/api/src/controllers/delegation-percentage/index.ts b/apps/api/src/controllers/delegation-percentage/index.ts
index 09847dcdc..dc2b11ba2 100644
--- a/apps/api/src/controllers/delegation-percentage/index.ts
+++ b/apps/api/src/controllers/delegation-percentage/index.ts
@@ -5,6 +5,7 @@ import {
DelegationPercentageResponseSchema,
toApi,
} from "@/mappers/";
+import { setCacheControl } from "@/middlewares";
import { DelegationPercentageService } from "@/services";
export function delegationPercentage(
@@ -18,6 +19,7 @@ export function delegationPercentage(
path: "/delegation-percentage",
summary: "Get delegation percentage day buckets with forward-fill",
tags: ["metrics"],
+ middleware: [setCacheControl(3600)],
request: {
query: DelegationPercentageRequestSchema,
},
diff --git a/apps/api/src/controllers/delegations/delegations.ts b/apps/api/src/controllers/delegations/delegations.ts
index 6adcdaedc..4d9cefaf6 100644
--- a/apps/api/src/controllers/delegations/delegations.ts
+++ b/apps/api/src/controllers/delegations/delegations.ts
@@ -5,6 +5,7 @@ import {
DelegationsResponseSchema,
} from "@/mappers/delegations";
import {} from "@/mappers";
+import { setCacheControl } from "@/middlewares";
import { DelegationsService } from "@/services/delegations/current";
@@ -17,6 +18,7 @@ export function delegations(app: Hono, service: DelegationsService) {
summary: "Get delegations",
description: "Get current delegations for an account",
tags: ["delegations"],
+ middleware: [setCacheControl(60)],
request: {
params: DelegationsRequestParamsSchema,
},
diff --git a/apps/api/src/controllers/delegations/delegators.ts b/apps/api/src/controllers/delegations/delegators.ts
index ef1f19580..0df9291c0 100644
--- a/apps/api/src/controllers/delegations/delegators.ts
+++ b/apps/api/src/controllers/delegations/delegators.ts
@@ -6,6 +6,7 @@ import {
DelegatorsRequestQuerySchema,
} from "@/mappers/delegations/delegators";
import {} from "@/mappers";
+import { setCacheControl } from "@/middlewares";
import { DelegatorsService } from "@/services/delegations/delegators";
export function delegators(app: Hono, service: DelegatorsService) {
@@ -17,6 +18,7 @@ export function delegators(app: Hono, service: DelegatorsService) {
summary: "Get delegators",
description: "Get current delegators of an account with voting power",
tags: ["delegations"],
+ middleware: [setCacheControl(60)],
request: {
params: DelegatorsRequestParamsSchema,
query: DelegatorsRequestQuerySchema,
diff --git a/apps/api/src/controllers/delegations/historical.ts b/apps/api/src/controllers/delegations/historical.ts
index 46e398207..3edcfc513 100644
--- a/apps/api/src/controllers/delegations/historical.ts
+++ b/apps/api/src/controllers/delegations/historical.ts
@@ -6,6 +6,7 @@ import {
DelegationsResponseSchema,
} from "@/mappers/delegations";
import {} from "@/mappers";
+import { setCacheControl } from "@/middlewares";
import { HistoricalDelegationsService } from "@/services/delegations";
@@ -22,6 +23,7 @@ export function historicalDelegations(
description:
"Get historical delegations for an account, with optional filtering and sorting",
tags: ["delegations"],
+ middleware: [setCacheControl(60)],
request: {
params: HistoricalDelegationsRequestParamsSchema,
query: HistoricalDelegationsRequestQuerySchema,
diff --git a/apps/api/src/controllers/event-relevance/index.ts b/apps/api/src/controllers/event-relevance/index.ts
index 87b2a7a44..6b6c5eb22 100644
--- a/apps/api/src/controllers/event-relevance/index.ts
+++ b/apps/api/src/controllers/event-relevance/index.ts
@@ -6,6 +6,7 @@ import {
EventRelevanceThresholdQuerySchema,
EventRelevanceThresholdResponseSchema,
} from "@/mappers";
+import { setCacheControl } from "@/middlewares";
import { EventRelevanceService } from "@/services";
export function eventRelevance(app: Hono, service: EventRelevanceService) {
@@ -16,6 +17,7 @@ export function eventRelevance(app: Hono, service: EventRelevanceService) {
path: "/event-relevance/threshold",
summary: "Get event relevance threshold",
tags: ["feed"],
+ middleware: [setCacheControl(3600)],
request: {
query: EventRelevanceThresholdQuerySchema,
},
diff --git a/apps/api/src/controllers/feed/index.ts b/apps/api/src/controllers/feed/index.ts
index c08398a8e..d33979835 100644
--- a/apps/api/src/controllers/feed/index.ts
+++ b/apps/api/src/controllers/feed/index.ts
@@ -1,6 +1,7 @@
import { OpenAPIHono as Hono, createRoute } from "@hono/zod-openapi";
import { FeedRequestSchema, FeedResponseSchema } from "@/mappers";
+import { setCacheControl } from "@/middlewares";
import { FeedService } from "@/services";
export function feed(app: Hono, service: FeedService) {
@@ -11,6 +12,7 @@ export function feed(app: Hono, service: FeedService) {
path: "/feed/events",
summary: "Get feed events",
tags: ["feed"],
+ middleware: [setCacheControl(60)],
request: {
query: FeedRequestSchema,
},
diff --git a/apps/api/src/controllers/governance-activity/controller.ts b/apps/api/src/controllers/governance-activity/controller.ts
index 4e09ba123..da2a339c0 100644
--- a/apps/api/src/controllers/governance-activity/controller.ts
+++ b/apps/api/src/controllers/governance-activity/controller.ts
@@ -2,6 +2,7 @@ import { OpenAPIHono as Hono, createRoute } from "@hono/zod-openapi";
import { formatEther } from "viem";
import { DaysEnum } from "@/lib/enums";
+import { setCacheControl } from "@/middlewares";
import {
ActiveSupplyResponseSchema,
AverageTurnoutComparisonResponseSchema,
@@ -40,6 +41,7 @@ export function governanceActivity(
path: "/active-supply/compare",
summary: "Get active token supply for DAO",
tags: ["governance"],
+ middleware: [setCacheControl(60)],
request: {
query: GovernanceActivityDaysQuerySchema,
},
@@ -68,6 +70,7 @@ export function governanceActivity(
path: "/proposals/compare",
summary: "Compare number of proposals between time periods",
tags: ["governance"],
+ middleware: [setCacheControl(60)],
request: {
query: GovernanceActivityDaysQuerySchema,
},
@@ -117,6 +120,7 @@ export function governanceActivity(
path: "/votes/compare",
summary: "Compare number of votes between time periods",
tags: ["governance"],
+ middleware: [setCacheControl(60)],
request: {
query: GovernanceActivityDaysQuerySchema,
},
@@ -165,6 +169,7 @@ export function governanceActivity(
path: "/average-turnout/compare",
summary: "Compare average turnout between time periods",
tags: ["governance"],
+ middleware: [setCacheControl(60)],
request: {
query: GovernanceActivityDaysQuerySchema,
},
diff --git a/apps/api/src/controllers/last-update/index.ts b/apps/api/src/controllers/last-update/index.ts
index 5ae72d016..a8a3754fc 100644
--- a/apps/api/src/controllers/last-update/index.ts
+++ b/apps/api/src/controllers/last-update/index.ts
@@ -2,6 +2,7 @@ import { OpenAPIHono as Hono, createRoute } from "@hono/zod-openapi";
import { Drizzle } from "@/database";
import { LastUpdateQuerySchema, LastUpdateResponseSchema } from "@/mappers/";
+import { setCacheControl } from "@/middlewares";
import { LastUpdateRepositoryImpl } from "@/repositories";
import { LastUpdateService } from "@/services";
@@ -15,6 +16,7 @@ export function lastUpdate(app: Hono, db: Drizzle) {
path: "/last-update",
summary: "Get the last update time",
tags: ["metrics"],
+ middleware: [setCacheControl(30)],
request: {
query: LastUpdateQuerySchema,
},
diff --git a/apps/api/src/controllers/proposals/offchainProposals.ts b/apps/api/src/controllers/proposals/offchainProposals.ts
index 81d7195f7..9f7311038 100644
--- a/apps/api/src/controllers/proposals/offchainProposals.ts
+++ b/apps/api/src/controllers/proposals/offchainProposals.ts
@@ -8,6 +8,7 @@ import {
OffchainProposalsResponseSchema,
OffchainProposalsRequestSchema,
} from "@/mappers";
+import { setCacheControl } from "@/middlewares";
import { OffchainProposalsService } from "@/services";
export function offchainProposals(
@@ -22,6 +23,7 @@ export function offchainProposals(
summary: "Get offchain proposals",
description: "Returns a list of offchain (Snapshot) proposals",
tags: ["offchain"],
+ middleware: [setCacheControl(60)],
request: {
query: OffchainProposalsRequestSchema,
},
@@ -97,6 +99,7 @@ export function offchainProposals(
summary: "Get an offchain proposal by ID",
description: "Returns a single offchain (Snapshot) proposal by its ID",
tags: ["offchain"],
+ middleware: [setCacheControl(60)],
request: {
params: OffchainProposalRequestSchema,
},
diff --git a/apps/api/src/controllers/proposals/onchainProposals.ts b/apps/api/src/controllers/proposals/onchainProposals.ts
index e4146336b..558b7726e 100644
--- a/apps/api/src/controllers/proposals/onchainProposals.ts
+++ b/apps/api/src/controllers/proposals/onchainProposals.ts
@@ -10,6 +10,7 @@ import {
ProposalResponseSchema,
ProposalMapper,
} from "@/mappers";
+import { setCacheControl } from "@/middlewares";
import { ProposalsService } from "@/services";
export function proposals(
@@ -26,6 +27,7 @@ export function proposals(
summary: "Get proposals",
description: "Returns a list of proposal",
tags: ["proposals"],
+ middleware: [setCacheControl(60)],
request: {
query: ProposalsRequestSchema,
},
@@ -134,6 +136,7 @@ export function proposals(
summary: "Get a proposal by ID",
description: "Returns a single proposal by its ID",
tags: ["proposals"],
+ middleware: [setCacheControl(60)],
request: {
params: ProposalRequestSchema,
},
diff --git a/apps/api/src/controllers/proposals/proposals-activity.ts b/apps/api/src/controllers/proposals/proposals-activity.ts
index 0c9b755fc..f4c3f337a 100644
--- a/apps/api/src/controllers/proposals/proposals-activity.ts
+++ b/apps/api/src/controllers/proposals/proposals-activity.ts
@@ -7,6 +7,7 @@ import {
ProposalActivityRequestSchema,
ProposalActivityResponseSchema,
} from "@/mappers";
+import { setCacheControl } from "@/middlewares";
import { DrizzleProposalsActivityRepository } from "@/repositories/";
import { ProposalsActivityService } from "@/services";
@@ -27,6 +28,7 @@ export function proposalsActivity(
description:
"Returns proposal activity data including voting history, win rates, and detailed proposal information for the specified delegate within the given time window",
tags: ["proposals"],
+ middleware: [setCacheControl(60)],
request: {
query: ProposalActivityRequestSchema,
},
diff --git a/apps/api/src/controllers/token-metrics/index.ts b/apps/api/src/controllers/token-metrics/index.ts
index 64ac5dced..f0d035337 100644
--- a/apps/api/src/controllers/token-metrics/index.ts
+++ b/apps/api/src/controllers/token-metrics/index.ts
@@ -6,6 +6,7 @@ import {
toTokenMetricsApi,
} from "@/mappers/token-metrics";
import {} from "@/mappers";
+import { setCacheControl } from "@/middlewares";
import { TokenMetricsService } from "@/services/token-metrics";
@@ -18,6 +19,7 @@ export function tokenMetrics(app: Hono, service: TokenMetricsService) {
summary: "Get token related metrics",
description: `Returns token related metrics for a single metric type.`,
tags: ["metrics"],
+ middleware: [setCacheControl(3600)],
request: {
query: TokenMetricsRequestSchema,
},
diff --git a/apps/api/src/controllers/token/token-distribution.ts b/apps/api/src/controllers/token/token-distribution.ts
index 600c4b7cd..0d19682b4 100644
--- a/apps/api/src/controllers/token/token-distribution.ts
+++ b/apps/api/src/controllers/token/token-distribution.ts
@@ -3,6 +3,7 @@ import { formatUnits, parseEther } from "viem";
import { MetricTypesEnum } from "@/lib/constants";
import { DaysEnum, SECONDS_IN_DAY } from "@/lib/enums";
+import { setCacheControl } from "@/middlewares";
import {
SupplyComparisonResponseSchema,
TokenDistributionComparisonQuerySchema,
@@ -42,6 +43,7 @@ export function tokenDistribution(
path: `/${path}/compare`,
summary: `Compare ${path.replace(/-/g, " ")} between periods`,
tags: ["tokens"],
+ middleware: [setCacheControl(60)],
request: {
query: TokenDistributionComparisonQuerySchema,
},
diff --git a/apps/api/src/controllers/token/token-historical-data.ts b/apps/api/src/controllers/token/token-historical-data.ts
index cfa274ca3..d2b9d43c0 100644
--- a/apps/api/src/controllers/token/token-historical-data.ts
+++ b/apps/api/src/controllers/token/token-historical-data.ts
@@ -4,6 +4,7 @@ import {
TokenHistoricalPriceRequest,
TokenHistoricalPriceResponse,
} from "@/mappers";
+import { setCacheControl } from "@/middlewares";
export interface TokenHistoricalDataClient {
getHistoricalTokenData(
@@ -24,6 +25,7 @@ export function tokenHistoricalData(
summary: "Get historical token data",
description: "Get historical market data for a specific token",
tags: ["tokens"],
+ middleware: [setCacheControl(3600)],
request: {
query: TokenHistoricalPriceRequest,
},
diff --git a/apps/api/src/controllers/token/token-properties.ts b/apps/api/src/controllers/token/token-properties.ts
index 0406c86c5..93b3ff2e1 100644
--- a/apps/api/src/controllers/token/token-properties.ts
+++ b/apps/api/src/controllers/token/token-properties.ts
@@ -7,6 +7,7 @@ import {
TokenPropertiesResponseSchema,
TokenMapper,
} from "@/mappers";
+import { setCacheControl } from "@/middlewares";
import { TokenService } from "@/services";
export interface TokenPriceClient {
@@ -39,6 +40,7 @@ export function token(
summary: "Get token properties",
description: "Get property data for a specific token",
tags: ["tokens"],
+ middleware: [setCacheControl(3600)],
request: {
query: TokenPropertiesQuerySchema,
},
diff --git a/apps/api/src/controllers/transactions/index.ts b/apps/api/src/controllers/transactions/index.ts
index ac14f8785..d635da975 100644
--- a/apps/api/src/controllers/transactions/index.ts
+++ b/apps/api/src/controllers/transactions/index.ts
@@ -4,6 +4,7 @@ import {
TransactionsRequestSchema,
TransactionsResponseSchema,
} from "@/mappers/";
+import { setCacheControl } from "@/middlewares";
import { TransactionsService } from "@/services";
export function transactions(app: Hono, service: TransactionsService) {
@@ -16,6 +17,7 @@ export function transactions(app: Hono, service: TransactionsService) {
description:
"Get transactions with their associated transfers and delegations, with optional filtering and sorting",
tags: ["transactions"],
+ middleware: [setCacheControl(60)],
request: {
query: TransactionsRequestSchema,
},
diff --git a/apps/api/src/controllers/transfers/index.ts b/apps/api/src/controllers/transfers/index.ts
index 012fa3bdb..8728d504d 100644
--- a/apps/api/src/controllers/transfers/index.ts
+++ b/apps/api/src/controllers/transfers/index.ts
@@ -5,6 +5,7 @@ import {
TransfersRequestQuerySchema,
TransfersResponseSchema,
} from "@/mappers/";
+import { setCacheControl } from "@/middlewares";
import { TransfersService } from "@/services";
export function transfers(app: Hono, service: TransfersService) {
@@ -16,6 +17,7 @@ export function transfers(app: Hono, service: TransfersService) {
summary: "Get transfers",
description: "Get transfers of a given address",
tags: ["transfers"],
+ middleware: [setCacheControl(60)],
request: {
params: TransfersRequestRouteSchema,
query: TransfersRequestQuerySchema,
diff --git a/apps/api/src/controllers/treasury/index.ts b/apps/api/src/controllers/treasury/index.ts
index 8d5d3b842..4a85db8f9 100644
--- a/apps/api/src/controllers/treasury/index.ts
+++ b/apps/api/src/controllers/treasury/index.ts
@@ -5,6 +5,7 @@ import {
TreasuryQuerySchema,
} from "@/mappers/treasury";
import {} from "@/mappers";
+import { setCacheControl } from "@/middlewares";
import { TreasuryService } from "@/services/treasury";
export function treasury(
@@ -21,6 +22,7 @@ export function treasury(
description:
"Get historical Liquid Treasury (treasury without DAO tokens) from external providers (DefiLlama/Dune)",
tags: ["treasury"],
+ middleware: [setCacheControl(60)],
request: {
query: TreasuryQuerySchema,
},
@@ -54,6 +56,7 @@ export function treasury(
description:
"Get historical DAO Token Treasury value (governance token quantity × token price)",
tags: ["treasury"],
+ middleware: [setCacheControl(60)],
request: {
query: TreasuryQuerySchema,
},
@@ -88,6 +91,7 @@ export function treasury(
description:
"Get historical Total Treasury (liquid treasury + DAO token treasury)",
tags: ["treasury"],
+ middleware: [setCacheControl(60)],
request: {
query: TreasuryQuerySchema,
},
diff --git a/apps/api/src/controllers/votes/offchainVotes.ts b/apps/api/src/controllers/votes/offchainVotes.ts
index 5cc5cc589..fe24e5495 100644
--- a/apps/api/src/controllers/votes/offchainVotes.ts
+++ b/apps/api/src/controllers/votes/offchainVotes.ts
@@ -5,6 +5,7 @@ import {
OffchainVotesRequestSchema,
OffchainVotesResponseSchema,
} from "@/mappers";
+import { setCacheControl } from "@/middlewares";
import { OffchainVotesService } from "@/services";
export function offchainVotes(app: Hono, service: OffchainVotesService) {
@@ -16,6 +17,7 @@ export function offchainVotes(app: Hono, service: OffchainVotesService) {
summary: "Get offchain votes",
description: "Returns a list of offchain (Snapshot) votes",
tags: ["offchain"],
+ middleware: [setCacheControl(60)],
request: {
query: OffchainVotesRequestSchema,
},
@@ -64,6 +66,7 @@ export function offchainVotes(app: Hono, service: OffchainVotesService) {
description:
"Returns a paginated list of offchain (Snapshot) votes for a specific proposal",
tags: ["offchain"],
+ middleware: [setCacheControl(60)],
request: {
params: OffchainProposalRequestSchema,
query: OffchainVotesRequestSchema,
diff --git a/apps/api/src/controllers/votes/onchainVotes.ts b/apps/api/src/controllers/votes/onchainVotes.ts
index 7a76d5822..6fb41c336 100644
--- a/apps/api/src/controllers/votes/onchainVotes.ts
+++ b/apps/api/src/controllers/votes/onchainVotes.ts
@@ -7,6 +7,7 @@ import {
VotesRequestSchema,
VotesResponseSchema,
} from "@/mappers";
+import { setCacheControl } from "@/middlewares";
import { VotesService } from "@/services";
export function votes(app: Hono, service: VotesService) {
@@ -19,6 +20,7 @@ export function votes(app: Hono, service: VotesService) {
description:
"Returns a paginated list of votes cast on a specific proposal",
tags: ["proposals"],
+ middleware: [setCacheControl(60)],
request: {
params: ProposalRequestSchema,
query: VotesRequestSchema,
@@ -71,6 +73,7 @@ export function votes(app: Hono, service: VotesService) {
summary: "Get all votes",
description: "Get all votes ordered by timestamp or voting power",
tags: ["votes"],
+ middleware: [setCacheControl(60)],
request: {
query: VotesRequestSchema,
},
@@ -119,6 +122,7 @@ export function votes(app: Hono, service: VotesService) {
description:
"Returns the active delegates that did not vote on a given proposal",
tags: ["proposals"],
+ middleware: [setCacheControl(60)],
request: {
params: ProposalRequestSchema,
query: VotersRequestSchema,
diff --git a/apps/api/src/controllers/voting-power/historical.ts b/apps/api/src/controllers/voting-power/historical.ts
index fd8baec82..21483a95d 100644
--- a/apps/api/src/controllers/voting-power/historical.ts
+++ b/apps/api/src/controllers/voting-power/historical.ts
@@ -8,6 +8,7 @@ import {
HistoricalVotingPowerGlobalQuerySchema,
DBHistoricalVotingPowerWithRelations,
} from "@/mappers";
+import { setCacheControl } from "@/middlewares";
import { Address } from "viem";
export interface HistoricalVotingPowerService {
@@ -40,6 +41,7 @@ export function historicalVotingPower(
description:
"Returns a list of voting power changes for a specific account",
tags: ["voting-power"],
+ middleware: [setCacheControl(60)],
request: {
params: HistoricalVotingPowerRequestParamsSchema,
query: HistoricalVotingPowerRequestQuerySchema,
@@ -94,6 +96,7 @@ export function historicalVotingPower(
summary: "Get voting power changes",
description: "Returns a list of voting power changes.",
tags: ["voting-power"],
+ middleware: [setCacheControl(60)],
request: {
query: HistoricalVotingPowerGlobalQuerySchema,
},
diff --git a/apps/api/src/controllers/voting-power/listing.ts b/apps/api/src/controllers/voting-power/listing.ts
index a44713012..25ea99d6d 100644
--- a/apps/api/src/controllers/voting-power/listing.ts
+++ b/apps/api/src/controllers/voting-power/listing.ts
@@ -10,6 +10,7 @@ import {
AmountFilter,
DBAccountPowerWithVariation,
} from "@/mappers/";
+import { setCacheControl } from "@/middlewares";
interface VotingPowerService {
getVotingPowers(
@@ -45,6 +46,7 @@ export function votingPowers(app: Hono, service: VotingPowerService) {
summary: "Get voting powers",
description: "Returns sorted and paginated account voting power records",
tags: ["voting-power"],
+ middleware: [setCacheControl(60)],
request: {
query: VotingPowersRequestSchema,
},
@@ -111,6 +113,7 @@ export function votingPowers(app: Hono, service: VotingPowerService) {
description:
"Returns voting power information for a specific address (account)",
tags: ["voting-power"],
+ middleware: [setCacheControl(60)],
request: {
params: VotingPowerByAccountIdRequestParamsSchema,
query: VotingPowerByAccountIdRequestQuerySchema,
diff --git a/apps/api/src/controllers/voting-power/variations.ts b/apps/api/src/controllers/voting-power/variations.ts
index 34011c902..88fbeacd7 100644
--- a/apps/api/src/controllers/voting-power/variations.ts
+++ b/apps/api/src/controllers/voting-power/variations.ts
@@ -11,6 +11,7 @@ import {
VotingPowerVariationsByAccountIdRequestParamsSchema,
DBVotingPowerVariation,
} from "@/mappers/";
+import { setCacheControl } from "@/middlewares";
export interface VotingPowerVariationsService {
getVotingPowerVariations(
@@ -43,6 +44,7 @@ export function votingPowerVariations(
description:
"Returns a mapping of the voting power changes within a time frame for the given addresses",
tags: ["voting-power"],
+ middleware: [setCacheControl(60)],
request: {
query: VotingPowerVariationsRequestQuerySchema,
},
@@ -87,6 +89,7 @@ export function votingPowerVariations(
description:
"Returns a the changes to voting power by period and accountId",
tags: ["voting-power"],
+ middleware: [setCacheControl(60)],
request: {
params: VotingPowerVariationsByAccountIdRequestParamsSchema,
query: VotingPowerVariationsByAccountIdRequestQuerySchema,
diff --git a/apps/api/src/middlewares/cacheControl.ts b/apps/api/src/middlewares/cacheControl.ts
new file mode 100644
index 000000000..89f66fe0a
--- /dev/null
+++ b/apps/api/src/middlewares/cacheControl.ts
@@ -0,0 +1,8 @@
+import { createMiddleware } from "hono/factory";
+
+export function setCacheControl(seconds: number) {
+ return createMiddleware(async (c, next) => {
+ await next();
+ c.header("Cache-Control", `public, max-age=${seconds}`);
+ });
+}
diff --git a/apps/api/src/middlewares/index.ts b/apps/api/src/middlewares/index.ts
index 3d8e8e9a4..14f74b74d 100644
--- a/apps/api/src/middlewares/index.ts
+++ b/apps/api/src/middlewares/index.ts
@@ -1,2 +1,3 @@
+export { setCacheControl } from "./cacheControl";
export { errorHandler } from "./errorHandler";
export { metricsMiddleware } from "./metricsMiddleware";
diff --git a/apps/gateful/package.json b/apps/gateful/package.json
index b7f207547..c7855bb4e 100644
--- a/apps/gateful/package.json
+++ b/apps/gateful/package.json
@@ -15,6 +15,7 @@
"license": "ISC",
"devDependencies": {
"@types/node": "^25.4.0",
+ "msw": "^2.12.10",
"openapi-types": "^12.1.3",
"tsx": "^4.19.4",
"typescript": "^5.8.3",
@@ -27,6 +28,7 @@
"dotenv": "^16.5.0",
"hono": "^4.12.7",
"openapi3-ts": "^4.5.0",
+ "redis": "^4.7.1",
"zod": "^4.3.6"
}
}
diff --git a/apps/gateful/src/cache/redis.ts b/apps/gateful/src/cache/redis.ts
new file mode 100644
index 000000000..c04ed8e55
--- /dev/null
+++ b/apps/gateful/src/cache/redis.ts
@@ -0,0 +1,28 @@
+import { createClient } from "redis";
+
+/**
+ * Creates a Redis client for the given URL.
+ * Connects asynchronously — errors are logged but do not block startup.
+ */
+export function createRedisClient(url: string) {
+ const client = createClient({ url });
+
+ client.on("connect", () => {
+ console.log("[redis] connected");
+ });
+
+ client.on("reconnecting", () => {
+ console.log("[redis] reconnecting");
+ });
+
+ client.on("error", (err: Error) => {
+ console.error(`[redis] error: ${err.message}`);
+ });
+
+ // Connect in the background — startup is not blocked.
+ client.connect().catch((err: Error) => {
+ console.error(`[redis] initial connection failed: ${err.message}`);
+ });
+
+ return client;
+}
diff --git a/apps/gateful/src/config.ts b/apps/gateful/src/config.ts
index 21f706d0e..766ae9c5f 100644
--- a/apps/gateful/src/config.ts
+++ b/apps/gateful/src/config.ts
@@ -7,6 +7,7 @@ const envSchema = z.object({
PORT: z.coerce.number().default(4001),
ADDRESS_ENRICHMENT_API_URL: z.url().optional(),
BLOCKFUL_API_TOKEN: z.string().optional(),
+ REDIS_URL: z.string().optional(),
});
function loadDaoApis(
@@ -35,5 +36,6 @@ export const config = {
port: env.PORT,
addressEnrichmentUrl: env.ADDRESS_ENRICHMENT_API_URL,
blockfulApiToken: env.BLOCKFUL_API_TOKEN,
+ redisUrl: env.REDIS_URL,
daoApis: loadDaoApis(),
};
diff --git a/apps/gateful/src/index.ts b/apps/gateful/src/index.ts
index 62ce9dcda..49feae143 100644
--- a/apps/gateful/src/index.ts
+++ b/apps/gateful/src/index.ts
@@ -8,6 +8,8 @@ import { cors } from "hono/cors";
import { logger } from "hono/logger";
import { config } from "./config.js";
+import { createRedisClient } from "./cache/redis.js";
+import { cacheMiddleware } from "./middlewares/cache.js";
import { health } from "./health/route.js";
import { proxy } from "./proxy/route.js";
import { addressEnrichment } from "./resolvers/address-enrichment/route.js";
@@ -28,6 +30,10 @@ app.use("*", logger());
if (config.blockfulApiToken) {
app.use("*", bearerAuth({ token: config.blockfulApiToken }));
}
+if (config.redisUrl) {
+ const redis = createRedisClient(config.redisUrl);
+ app.use("*", cacheMiddleware(redis));
+}
console.log(
`Discovered ${config.daoApis.size} DAO APIs: [${Array.from(config.daoApis.keys()).join(", ")}]`,
diff --git a/apps/gateful/src/middlewares/cache.test.ts b/apps/gateful/src/middlewares/cache.test.ts
new file mode 100644
index 000000000..74db4bf86
--- /dev/null
+++ b/apps/gateful/src/middlewares/cache.test.ts
@@ -0,0 +1,158 @@
+import { OpenAPIHono } from "@hono/zod-openapi";
+import { vi } from "vitest";
+import { type CacheStore, cacheMiddleware } from "./cache";
+
+// ---------------------------------------------------------------------------
+// Fake Redis
+// ---------------------------------------------------------------------------
+
+class FakeRedis implements CacheStore {
+ store = new Map();
+
+ async get(key: string): Promise {
+ return this.store.get(key)?.value ?? null;
+ }
+
+ async set(
+ key: string,
+ value: string,
+ options?: { EX: number },
+ ): Promise {
+ this.store.set(key, { value, ttl: options?.EX });
+ return "OK";
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Helpers
+// ---------------------------------------------------------------------------
+
+const defaultHandler = (c: import("hono").Context) => {
+ c.header("Cache-Control", "public, max-age=60");
+ return c.json({ ok: true }, 200);
+};
+
+/** Builds a minimal Hono app wired with the cache middleware and one GET route. */
+function buildApp(
+ redis: CacheStore,
+ handler: (
+ c: import("hono").Context,
+ ) => Response | Promise = defaultHandler,
+): OpenAPIHono {
+ const app = new OpenAPIHono();
+ app.use("*", cacheMiddleware(redis));
+ app.get("/test", handler);
+ return app;
+}
+
+async function readResponse(res: Response) {
+ return {
+ status: res.status,
+ xCache: res.headers.get("X-Cache"),
+ cacheControl: res.headers.get("Cache-Control"),
+ body: await res.text(),
+ };
+}
+
+// ---------------------------------------------------------------------------
+// Tests
+// ---------------------------------------------------------------------------
+
+describe("cacheMiddleware", () => {
+ let redis: FakeRedis;
+
+ beforeEach(() => {
+ redis = new FakeRedis();
+ });
+
+ // -------------------------------------------------------------------------
+ // Cache HIT
+ // -------------------------------------------------------------------------
+
+ it("returns cached response with X-Cache: HIT header on cache hit", async () => {
+ const handler = vi.fn((c: import("hono").Context) => {
+ c.header("Cache-Control", "public, max-age=60");
+ return c.json({ ok: true }, 200);
+ });
+ const app = buildApp(redis, handler);
+
+ await app.request("/test"); // first request: MISS — primes the cache
+ const res = await app.request("/test"); // second request: HIT
+
+ expect(await readResponse(res)).toEqual({
+ status: 200,
+ xCache: "HIT",
+ cacheControl: "public, max-age=60",
+ body: '{"ok":true}',
+ });
+ // Handler was only invoked for the first (MISS) request.
+ expect(handler).toHaveBeenCalledOnce();
+ });
+
+ // -------------------------------------------------------------------------
+ // Redis read error → fail open
+ // -------------------------------------------------------------------------
+
+ it("fails open when Redis throws on read (calls next() normally)", async () => {
+ vi.spyOn(redis, "get").mockRejectedValueOnce(
+ new Error("connection refused"),
+ );
+
+ const app = buildApp(redis);
+
+ const res = await app.request("/test");
+
+ expect(await readResponse(res)).toEqual({
+ status: 200,
+ xCache: null,
+ cacheControl: "public, max-age=60",
+ body: '{"ok":true}',
+ });
+ });
+
+ // -------------------------------------------------------------------------
+ // Redis write error → fail open
+ // -------------------------------------------------------------------------
+
+ it("fails open when Redis throws on write (response still returned)", async () => {
+ vi.spyOn(redis, "set").mockRejectedValueOnce(new Error("write error"));
+
+ const app = buildApp(redis);
+
+ const res = await app.request("/test");
+
+ expect(await readResponse(res)).toEqual({
+ status: 200,
+ xCache: null,
+ cacheControl: "public, max-age=60",
+ body: '{"ok":true}',
+ });
+ });
+
+ // -------------------------------------------------------------------------
+ // Non-2xx → not cached
+ // -------------------------------------------------------------------------
+
+ it("does not cache non-2xx responses", async () => {
+ const app = buildApp(redis, (c) => {
+ c.header("Cache-Control", "public, max-age=60");
+ return c.json({ error: "not found" }, 404);
+ });
+
+ await app.request("/test");
+
+ expect(redis.store.size).toBe(0);
+ });
+
+ // -------------------------------------------------------------------------
+ // Missing Cache-Control → not cached
+ // -------------------------------------------------------------------------
+
+ it("does not cache responses without a Cache-Control header", async () => {
+ const app = buildApp(redis, (c) => c.json({ ok: true }, 200));
+
+ await app.request("/test");
+
+ expect(redis.store.size).toBe(0);
+ });
+});
diff --git a/apps/gateful/src/middlewares/cache.ts b/apps/gateful/src/middlewares/cache.ts
new file mode 100644
index 000000000..4299b27b6
--- /dev/null
+++ b/apps/gateful/src/middlewares/cache.ts
@@ -0,0 +1,89 @@
+import type { Context, Next } from "hono";
+
+/** Minimal interface the middleware actually needs */
+export interface CacheStore {
+ get(key: string): Promise;
+ set(key: string, value: string, options?: { EX: number }): Promise;
+}
+
+type CachedEntry = {
+ body: string;
+ status: number;
+ contentType: string;
+ cacheControl: string;
+};
+
+function safeParse(raw: string): T | null {
+ try {
+ return JSON.parse(raw) as T;
+ } catch {
+ return null;
+ }
+}
+
+/**
+ * Cache-aside middleware using Redis.
+ *
+ * - Skips non-GET requests.
+ * - On cache hit: returns the stored response with `X-Cache: HIT`.
+ * - On cache miss: passes through, then stores the response when:
+ * - The status is 2xx.
+ * - The upstream set a `Cache-Control: max-age=` header with n > 0.
+ * - All Redis errors are swallowed (fail open) to preserve availability.
+ */
+export function cacheMiddleware(redis: CacheStore) {
+ return async (c: Context, next: Next) => {
+ // Only cache GET requests.
+ if (c.req.method !== "GET") return next();
+
+ const key = c.req.url;
+
+ // --- Request phase: check for a cached response ---
+ // Fail open: if Redis is unavailable, .catch returns null and we proceed normally.
+ const raw = await redis.get(key).catch(() => null);
+ if (raw) {
+ const entry = safeParse(raw);
+ if (!entry) return next();
+ return new Response(entry.body, {
+ status: entry.status,
+ headers: {
+ "Content-Type": entry.contentType,
+ "Cache-Control": entry.cacheControl,
+ "X-Cache": "HIT",
+ },
+ });
+ }
+
+ await next();
+
+ // --- Response phase: store the response if eligible ---
+ if (c.res.status < 200 || c.res.status >= 300) return;
+
+ const cacheControl = c.res.headers.get("Cache-Control");
+ if (!cacheControl) return;
+
+ const match = /max-age=(\d+)/.exec(cacheControl);
+ // No max-age directive or explicitly zero → do not cache.
+ if (!match || match[1] === "0") return;
+
+ const ttl = parseInt(match[1], 10);
+ if (ttl <= 0) return;
+
+ // Fail open: response is still returned even if we fail to cache it.
+ await c.res
+ .clone()
+ .text()
+ .then((body) => {
+ const contentType =
+ c.res.headers.get("Content-Type") ?? "application/json";
+ const entry: CachedEntry = {
+ body,
+ status: c.res.status,
+ contentType,
+ cacheControl: cacheControl!,
+ };
+ return redis.set(key, JSON.stringify(entry), { EX: ttl });
+ })
+ .catch(() => null);
+ };
+}
diff --git a/apps/gateful/src/resolvers/daos/route.ts b/apps/gateful/src/resolvers/daos/route.ts
index b62daa36a..0e381de4d 100644
--- a/apps/gateful/src/resolvers/daos/route.ts
+++ b/apps/gateful/src/resolvers/daos/route.ts
@@ -36,7 +36,8 @@ const route = createRoute({
export function daos(app: OpenAPIHono, service: DaosService) {
app.openapi(route, async (c) => {
- const result = await service.getAllDaos();
- return c.json(result);
+ const { cacheControl, ...body } = await service.getAllDaos();
+ if (cacheControl) c.header("Cache-Control", cacheControl);
+ return c.json(body, 200);
});
}
diff --git a/apps/gateful/src/resolvers/daos/service.ts b/apps/gateful/src/resolvers/daos/service.ts
index d06742ff3..174be79b5 100644
--- a/apps/gateful/src/resolvers/daos/service.ts
+++ b/apps/gateful/src/resolvers/daos/service.ts
@@ -1,17 +1,23 @@
import { fanOutGet } from "../../shared/fan-out.js";
-
import { DaoResponse, DaosResponse } from "./route.js";
+export type DaosResult = DaosResponse & { cacheControl: string | null };
+
export class DaosService {
constructor(private readonly daoApis: Map) {}
- async getAllDaos(): Promise {
- const responses = await fanOutGet(this.daoApis, "/dao");
- const items = Array.from(responses.values());
+ async getAllDaos(): Promise {
+ const { data, cacheControl } = await fanOutGet(
+ this.daoApis,
+ "/dao",
+ );
+
+ const items = Array.from(data.values());
return {
items,
totalCount: items.length,
+ cacheControl,
};
}
}
diff --git a/apps/gateful/src/resolvers/delegation/route.ts b/apps/gateful/src/resolvers/delegation/route.ts
index ccdfdff09..0cf3cb0c4 100644
--- a/apps/gateful/src/resolvers/delegation/route.ts
+++ b/apps/gateful/src/resolvers/delegation/route.ts
@@ -69,6 +69,8 @@ export function averageDelegation(
limit,
});
- return c.json(result);
+ const { cacheControl, ...body } = result;
+ if (cacheControl) c.header("Cache-Control", cacheControl);
+ return c.json(body, 200);
});
}
diff --git a/apps/gateful/src/resolvers/delegation/service.ts b/apps/gateful/src/resolvers/delegation/service.ts
index 20be09ce1..3e883b9d7 100644
--- a/apps/gateful/src/resolvers/delegation/service.ts
+++ b/apps/gateful/src/resolvers/delegation/service.ts
@@ -11,6 +11,10 @@ export type DelegationPercentageResponse = {
};
};
+export type DelegationResult = DelegationPercentageResponse & {
+ cacheControl: string | null;
+};
+
export class DelegationService {
constructor(private readonly daoApis: Map) {}
@@ -21,7 +25,7 @@ export class DelegationService {
before?: string;
orderDirection?: string;
limit?: number;
- }) {
+ }): Promise {
const params = new URLSearchParams();
params.set("startDate", args.startDate);
if (args.endDate) params.set("endDate", args.endDate);
@@ -30,11 +34,12 @@ export class DelegationService {
if (args.orderDirection) params.set("orderDirection", args.orderDirection);
if (args.limit) params.set("limit", String(args.limit));
- const daoResponses = await fanOutGet(
- this.daoApis,
- "/delegation-percentage",
- params.toString(),
- );
+ const { data: daoResponses, cacheControl } =
+ await fanOutGet(
+ this.daoApis,
+ "/delegation-percentage",
+ params.toString(),
+ );
const hasNextPage = Array.from(daoResponses.values()).some(
(response) => response?.pageInfo?.hasNextPage ?? false,
@@ -50,7 +55,12 @@ export class DelegationService {
? []
: this.aggregateMeanPercentage(alignedResponses);
- return this.buildPaginatedResponse(aggregated, args, hasNextPage);
+ const paginatedResponse = this.buildPaginatedResponse(
+ aggregated,
+ args,
+ hasNextPage,
+ );
+ return { ...paginatedResponse, cacheControl };
}
private getFirstDate(
diff --git a/apps/gateful/src/shared/fan-out.test.ts b/apps/gateful/src/shared/fan-out.test.ts
new file mode 100644
index 000000000..71f9044b8
--- /dev/null
+++ b/apps/gateful/src/shared/fan-out.test.ts
@@ -0,0 +1,100 @@
+import { http, HttpResponse } from "msw";
+import { setupServer } from "msw/node";
+
+import { fanOutGet } from "./fan-out";
+
+// ---------------------------------------------------------------------------
+// MSW server
+// ---------------------------------------------------------------------------
+
+const server = setupServer();
+
+beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
+afterEach(() => server.resetHandlers());
+afterAll(() => server.close());
+
+// ---------------------------------------------------------------------------
+// fanOutGet
+// ---------------------------------------------------------------------------
+
+describe("fanOutGet", () => {
+ it("returns data from all upstreams and cacheControl from the first fulfilled", async () => {
+ server.use(
+ http.get("http://ens-api/dao", () =>
+ HttpResponse.json(
+ { id: "ens" },
+ { headers: { "Cache-Control": "public, max-age=120" } },
+ ),
+ ),
+ http.get("http://uni-api/dao", () =>
+ HttpResponse.json(
+ { id: "uni" },
+ { headers: { "Cache-Control": "public, max-age=120" } },
+ ),
+ ),
+ );
+
+ const daoApis = new Map([
+ ["ens", "http://ens-api"],
+ ["uni", "http://uni-api"],
+ ]);
+
+ const result = await fanOutGet(daoApis, "/dao");
+
+ expect(result).toEqual({
+ data: new Map([
+ ["ens", { id: "ens" }],
+ ["uni", { id: "uni" }],
+ ]),
+ cacheControl: "public, max-age=120",
+ });
+ });
+
+ it("returns null cacheControl when all upstreams omit Cache-Control header", async () => {
+ server.use(
+ http.get("http://ens-api/dao", () => HttpResponse.json({ id: "ens" })),
+ http.get("http://uni-api/dao", () => HttpResponse.json({ id: "uni" })),
+ );
+
+ const daoApis = new Map([
+ ["ens", "http://ens-api"],
+ ["uni", "http://uni-api"],
+ ]);
+
+ const result = await fanOutGet(daoApis, "/dao");
+
+ expect(result).toEqual({
+ data: new Map([
+ ["ens", { id: "ens" }],
+ ["uni", { id: "uni" }],
+ ]),
+ cacheControl: null,
+ });
+ });
+
+ it("excludes failed upstreams from results", async () => {
+ server.use(
+ http.get("http://ens-api/dao", () =>
+ HttpResponse.json({}, { status: 500 }),
+ ),
+ http.get("http://uni-api/dao", () =>
+ HttpResponse.json(
+ { id: "uni" },
+ { headers: { "Cache-Control": "public, max-age=30" } },
+ ),
+ ),
+ );
+
+ const daoApis = new Map([
+ ["ens", "http://ens-api"],
+ ["uni", "http://uni-api"],
+ ]);
+
+ const result = await fanOutGet(daoApis, "/dao");
+
+ expect(result).toEqual({
+ data: new Map([["uni", { id: "uni" }]]),
+ cacheControl: "public, max-age=30",
+ });
+ });
+});
diff --git a/apps/gateful/src/shared/fan-out.ts b/apps/gateful/src/shared/fan-out.ts
index 34a7af5dd..8b0ccf932 100644
--- a/apps/gateful/src/shared/fan-out.ts
+++ b/apps/gateful/src/shared/fan-out.ts
@@ -1,12 +1,13 @@
/**
- * Fetches a path from all configured DAO APIs in parallel
- * Returns a Map of dao name → parsed JSON response (only successful ones)
+ * Fetches a path from all configured DAO APIs in parallel.
+ * Returns both the parsed response data and the Cache-Control header from
+ * the first successful upstream so callers can propagate the TTL downstream.
*/
export async function fanOutGet(
daoApis: Map,
path: string,
queryString?: string,
-): Promise