From 30c738b7f98f8da00029f4538874163c9ea02503 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Tue, 12 May 2026 22:58:56 +0200 Subject: [PATCH 1/2] 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. --- .env.example | 9 +++++++++ src/config/config.service.ts | 4 ++++ src/config/monitoring.config.ts | 5 +++++ src/monitoringV2/price.service.ts | 4 +++- 4 files changed, 21 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index bb7ff9e..91ec056 100644 --- a/.env.example +++ b/.env.example @@ -30,6 +30,15 @@ COINGECKO_BASE_URL=http://pricing-proxy:8080/coingecko # own key) or to the public host anonymously. # COINGECKO_API_KEY= +# GeckoTerminal Configuration. +# +# GECKOTERMINAL_BASE_URL: optional. 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. Leave unset +# to call api.geckoterminal.com directly. +# 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..0fb81d5 100644 --- a/src/monitoringV2/price.service.ts +++ b/src/monitoringV2/price.service.ts @@ -225,9 +225,11 @@ export class PriceService { const remaining = addresses.filter((addr) => !cached[addr]); if (remaining.length === 0) return cached; + const baseUrl = this.appConfigService.geckoTerminalBaseUrl ?? 'https://api.geckoterminal.com'; + 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, From 3b40fcbdaf1c63e478b8d3157ac2bac5fc0fbb46 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Tue, 12 May 2026 23:08:07 +0200 Subject: [PATCH 2/2] Make GECKOTERMINAL_BASE_URL required MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 | 10 ++++++---- src/monitoringV2/price.service.ts | 5 ++++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.env.example b/.env.example index 91ec056..b2e7809 100644 --- a/.env.example +++ b/.env.example @@ -32,12 +32,14 @@ COINGECKO_BASE_URL=http://pricing-proxy:8080/coingecko # GeckoTerminal Configuration. # -# GECKOTERMINAL_BASE_URL: optional. The origin used for GeckoTerminal token +# 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. Leave unset -# to call api.geckoterminal.com directly. -# GECKOTERMINAL_BASE_URL=http://pricing-proxy:8080/geckoterminal +# 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 diff --git a/src/monitoringV2/price.service.ts b/src/monitoringV2/price.service.ts index 0fb81d5..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,7 +228,7 @@ export class PriceService { const remaining = addresses.filter((addr) => !cached[addr]); if (remaining.length === 0) return cached; - const baseUrl = this.appConfigService.geckoTerminalBaseUrl ?? 'https://api.geckoterminal.com'; + const baseUrl = this.appConfigService.geckoTerminalBaseUrl; try { const response = await axios.get(