From da8e10f537c648375ca41dd83ba3b742ada6e4e6 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Tue, 12 May 2026 23:13:06 +0200 Subject: [PATCH] Route GeckoTerminal via pricing-proxy (#34) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Route GeckoTerminal calls through optional base-URL Mirrors the COINGECKO_BASE_URL plumbing already in place: the GeckoTerminal upstream URL becomes a config-driven base path rather than a hardcoded api.geckoterminal.com literal. Defaults to the public host so existing deployments without the env var keep working; setting GECKOTERMINAL_BASE_URL points the service at the in-cluster pricing-proxy on https://github.com/DFXswiss/pricing-proxy, which adds a 60 s shared cache, request coalescing and validation on top of the shared free-tier 30 req/min IP quota. With three monitoring stacks on the same host (d-EURO, jdt, jdm) all hitting GT directly, the shared anonymous IP quota burns out under load and restart-bursts. Behind the proxy, the three streams collapse to one upstream call per 60 s. Non-breaking: the literal default keeps every existing deployment on the direct path until its compose adds GECKOTERMINAL_BASE_URL. * Make GECKOTERMINAL_BASE_URL required Hard-fail at construction when GECKOTERMINAL_BASE_URL is unset, mirroring the existing COINGECKO_BASE_URL check in the same constructor. The previous `?? 'https://api.geckoterminal.com'` default silently fell back to the public host — the exact pattern the CoinGecko refactor was meant to remove for that route. Treating both upstreams the same way removes a class of silent anonymous fallbacks that surface later as sporadic 429s once the shared host quota is exhausted. `.env.example` upgraded from optional comment to required entry, again matching the COINGECKO_BASE_URL phrasing. --- .env.example | 11 +++++++++++ src/config/config.service.ts | 4 ++++ src/config/monitoring.config.ts | 5 +++++ src/monitoringV2/price.service.ts | 7 ++++++- 4 files changed, 26 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index bb7ff9e..b2e7809 100644 --- a/.env.example +++ b/.env.example @@ -30,6 +30,17 @@ COINGECKO_BASE_URL=http://pricing-proxy:8080/coingecko # own key) or to the public host anonymously. # COINGECKO_API_KEY= +# GeckoTerminal Configuration. +# +# GECKOTERMINAL_BASE_URL: required. The origin used for GeckoTerminal token +# price calls. Recommended is the in-cluster pricing-proxy +# (https://github.com/DFXswiss/pricing-proxy), which gives cache, coalescing +# and validation on top of the shared free-tier 30 req/min quota. Anything +# GeckoTerminal-compatible works (api.geckoterminal.com directly is fine for +# single-consumer setups, but the free-tier quota is shared across the whole +# host IP). +GECKOTERMINAL_BASE_URL=http://pricing-proxy:8080/geckoterminal + # Telegram Bot Configuration (optional) # TELEGRAM_BOT_TOKEN=5123456789:ABCdefGHIjklMNOpqrsTUVwxyz # Path to the persisted subscribers file. Each operator subscribes themselves by diff --git a/src/config/config.service.ts b/src/config/config.service.ts index 72582bd..80c8759 100644 --- a/src/config/config.service.ts +++ b/src/config/config.service.ts @@ -92,6 +92,10 @@ export class AppConfigService { return this.monitoringConfig.coingeckoBaseUrl || undefined; } + get geckoTerminalBaseUrl(): string | undefined { + return this.monitoringConfig.geckoTerminalBaseUrl || undefined; + } + get environment(): string | undefined { return this.monitoringConfig.environment; } diff --git a/src/config/monitoring.config.ts b/src/config/monitoring.config.ts index b64a1f8..5cc0a97 100644 --- a/src/config/monitoring.config.ts +++ b/src/config/monitoring.config.ts @@ -75,6 +75,10 @@ export class MonitoringConfig { @IsString() coingeckoApiKey?: string; + @IsOptional() + @IsString() + geckoTerminalBaseUrl?: string; + @IsOptional() @IsString() environment?: string; @@ -105,6 +109,7 @@ export default registerAs('monitoring', () => { config.alertTimeframeHours = parseInt(process.env.ALERT_TIMEFRAME_HOURS || '12'); config.coingeckoBaseUrl = process.env.COINGECKO_BASE_URL || ''; config.coingeckoApiKey = process.env.COINGECKO_API_KEY || ''; + config.geckoTerminalBaseUrl = process.env.GECKOTERMINAL_BASE_URL || ''; config.environment = process.env.ENVIRONMENT?.toLowerCase(); config.chain = process.env.CHAIN; diff --git a/src/monitoringV2/price.service.ts b/src/monitoringV2/price.service.ts index 5bf4e91..63fd721 100644 --- a/src/monitoringV2/price.service.ts +++ b/src/monitoringV2/price.service.ts @@ -64,6 +64,9 @@ export class PriceService { if (!this.appConfigService.coingeckoBaseUrl) { throw new Error('COINGECKO_BASE_URL is not set'); } + if (!this.appConfigService.geckoTerminalBaseUrl) { + throw new Error('GECKOTERMINAL_BASE_URL is not set'); + } } registerWcbtcAddress(address: string): void { @@ -225,9 +228,11 @@ export class PriceService { const remaining = addresses.filter((addr) => !cached[addr]); if (remaining.length === 0) return cached; + const baseUrl = this.appConfigService.geckoTerminalBaseUrl; + try { const response = await axios.get( - `https://api.geckoterminal.com/api/v2/simple/networks/citrea/token_price/${remaining.map((a) => a.toLowerCase()).join(',')}`, + `${baseUrl}/api/v2/simple/networks/citrea/token_price/${remaining.map((a) => a.toLowerCase()).join(',')}`, { headers: { accept: 'application/json' }, timeout: 10000,