diff --git a/server/_shared/constants.ts b/server/_shared/constants.ts index 4c1bb6d53..ffec1fc4c 100644 --- a/server/_shared/constants.ts +++ b/server/_shared/constants.ts @@ -24,3 +24,23 @@ export function yahooGate(): Promise { }); return yahooQueue; } + +/** + * Global Finnhub request gate. + * Free-tier Finnhub keys are sensitive to burst concurrency; spacing requests + * reduces 429 cascades that otherwise spill into Yahoo fallback. + */ +let finnhubLastRequest = 0; +const FINNHUB_MIN_GAP_MS = 350; +let finnhubQueue: Promise = Promise.resolve(); + +export function finnhubGate(): Promise { + finnhubQueue = finnhubQueue.then(async () => { + const elapsed = Date.now() - finnhubLastRequest; + if (elapsed < FINNHUB_MIN_GAP_MS) { + await new Promise(r => setTimeout(r, FINNHUB_MIN_GAP_MS - elapsed)); + } + finnhubLastRequest = Date.now(); + }); + return finnhubQueue; +} diff --git a/server/worldmonitor/market/v1/_shared.ts b/server/worldmonitor/market/v1/_shared.ts index f15050525..984e88156 100644 --- a/server/worldmonitor/market/v1/_shared.ts +++ b/server/worldmonitor/market/v1/_shared.ts @@ -1,7 +1,7 @@ /** * Shared helpers, types, and constants for the market service handler RPCs. */ -import { CHROME_UA, yahooGate } from '../../../_shared/constants'; +import { CHROME_UA, finnhubGate, yahooGate } from '../../../_shared/constants'; import cryptoConfig from '../../../../shared/crypto.json'; import stablecoinConfig from '../../../../shared/stablecoins.json'; export { parseStringArray } from '../../../_shared/parse-string-array'; @@ -108,6 +108,7 @@ export async function fetchFinnhubQuote( apiKey: string, ): Promise<{ symbol: string; price: number; changePercent: number } | null> { try { + await finnhubGate(); const url = `https://finnhub.io/api/v1/quote?symbol=${encodeURIComponent(symbol)}`; const resp = await fetch(url, { headers: { Accept: 'application/json', 'User-Agent': CHROME_UA, 'X-Finnhub-Token': apiKey },