Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 10 additions & 11 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,17 @@ MAX_BLOCKS_PER_BATCH=1000
PRICE_CACHE_TTL_MS=120000
PG_MAX_CLIENTS=10

# CoinGecko Configuration (optional)
# CoinGecko Configuration.
#
# Three deployment modes, in priority order:
# 1. Caching pricing proxy: set COINGECKO_BASE_URL to the proxy origin, leave
# COINGECKO_API_KEY empty. The proxy injects the upstream key itself.
# COINGECKO_BASE_URL=http://pricing-proxy:8080/coingecko
# 2. Direct Pro tier: set COINGECKO_API_KEY to a Pro key, leave
# COINGECKO_BASE_URL empty. Calls go to pro-api.coingecko.com with
# `x-cg-pro-api-key`.
# COINGECKO_API_KEY=CG-xxxxxxxxxxxxxxxxxxxxxxxx
# 3. Anonymous: leave both empty — calls hit api.coingecko.com unauthenticated.
# COINGECKO_BASE_URL=
# COINGECKO_BASE_URL: required. The origin the service calls. Recommended is
# the in-cluster pricing-proxy (https://github.com/DFXswiss/pricing-proxy),
# which holds the upstream Pro key and serves a 60 s shared cache. Anything
# CoinGecko-compatible works (pro-api.coingecko.com, api.coingecko.com, …).
COINGECKO_BASE_URL=http://pricing-proxy:8080/coingecko
#
# COINGECKO_API_KEY: optional. If set, attached as `x-cg-pro-api-key` to every
# request. Leave unset when talking to the pricing-proxy (proxy injects its
# own key) or to the public host anonymously.
# COINGECKO_API_KEY=

# Telegram Bot Configuration (optional)
Expand Down
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ cp .env.example .env
# - DATABASE_URL: PostgreSQL connection string
# - RPC_URL: Ethereum mainnet RPC endpoint
# - BLOCKCHAIN_ID: Must be 1 (Ethereum mainnet)
# - COINGECKO_BASE_URL: required, see "CoinGecko" section below

# Generate Prisma client
npm run prisma:generate
Expand Down Expand Up @@ -67,6 +68,38 @@ docker rm -f deuro-test

Swagger documentation available at: `http://localhost:3001/swagger`

## CoinGecko

The monitoring service needs a CoinGecko-compatible endpoint for USD/EUR
and USD/CHF FX rates (drives EUR-denominated price conversions and the
staleness watchdog) and the daily Pro quota probe. Configuration is two
env vars:

| Var | Required | Purpose |
|---|---|---|
| `COINGECKO_BASE_URL` | yes | Origin the service calls. |
| `COINGECKO_API_KEY` | no | Attached as the `x-cg-pro-api-key` header on every request when set. |

The recommended deployment is the
[**pricing-proxy**](https://github.com/DFXswiss/pricing-proxy) — a small
caching reverse-proxy in front of CoinGecko Pro. It holds the upstream key,
serves a 60 s shared cache, validates upstream error envelopes, and
coalesces concurrent identical requests. When you use the proxy:

```env
COINGECKO_BASE_URL=http://pricing-proxy:8080/coingecko
# COINGECKO_API_KEY left unset — the proxy injects its own key
```

Without the proxy you can talk to CoinGecko directly:

```env
COINGECKO_BASE_URL=https://pro-api.coingecko.com
COINGECKO_API_KEY=CG-xxxxxxxxxxxxxxxxxxxxxxxx
```

The service refuses to start without `COINGECKO_BASE_URL`.

## Deployment

- **Development**: Push to `develop` branch → auto-deploys to `dev.monitoring.deuro.com`
Expand Down
6 changes: 3 additions & 3 deletions src/config/monitoring.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,11 @@ export class MonitoringConfig {

@IsOptional()
@IsString()
coingeckoApiKey?: string;
coingeckoBaseUrl?: string;

@IsOptional()
@IsString()
coingeckoBaseUrl?: string;
coingeckoApiKey?: string;

@IsOptional()
@IsString()
Expand Down Expand Up @@ -103,8 +103,8 @@ export default registerAs('monitoring', () => {
config.telegramGroupsJson = process.env.TELEGRAM_GROUPS_JSON;
config.telegramAlertsEnabled = (process.env.TELEGRAM_ALERTS_ENABLED || 'false').toLowerCase() === 'true';
config.alertTimeframeHours = parseInt(process.env.ALERT_TIMEFRAME_HOURS || '12');
config.coingeckoApiKey = process.env.COINGECKO_API_KEY || '';
config.coingeckoBaseUrl = process.env.COINGECKO_BASE_URL || '';
config.coingeckoApiKey = process.env.COINGECKO_API_KEY || '';
config.environment = process.env.ENVIRONMENT?.toLowerCase();
config.chain = process.env.CHAIN;

Expand Down
46 changes: 22 additions & 24 deletions src/monitoringV2/price.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ export class PriceService {
private readonly telegramService: TelegramService
) {
this.CACHE_TTL_MS = this.appConfigService.priceCacheTtlMs;
if (!this.appConfigService.coingeckoBaseUrl) {
throw new Error('COINGECKO_BASE_URL is not set');
}
}

async getTokenPricesInEur(addresses: string[]): Promise<{ [key: string]: string }> {
Expand Down Expand Up @@ -202,27 +205,30 @@ export class PriceService {
}

/**
* Resolve which CoinGecko endpoint and authentication header to use.
* Resolve the CoinGecko endpoint.
*
* `COINGECKO_BASE_URL` is required and points at the origin the service
* talks to — typically the in-cluster pricing-proxy
* (https://github.com/DFXswiss/pricing-proxy), but any CoinGecko-compatible
* origin works (e.g. `https://pro-api.coingecko.com` or
* `https://api.coingecko.com`).
*
* Three modes, in priority order:
* 1. `COINGECKO_BASE_URL` set → trust the caller (typically a pricing proxy
* that injects the upstream key itself); send no auth header.
* 2. `COINGECKO_API_KEY` set → Pro tier: pro-api.coingecko.com with
* `x-cg-pro-api-key`.
* 3. Otherwise → unauthenticated public endpoint.
* `COINGECKO_API_KEY` is optional and is attached as the
* `x-cg-pro-api-key` header on every request when set. Leave it unset when
* talking to the pricing-proxy (the proxy injects its own key) or when
* hitting the public host anonymously.
*/
private resolveCoingeckoEndpoint(): CoingeckoEndpoint {
const headers: Record<string, string> = { accept: 'application/json' };
const explicitBase = this.appConfigService.coingeckoBaseUrl;
if (explicitBase) {
return { baseUrl: explicitBase, headers };
const baseUrl = this.appConfigService.coingeckoBaseUrl;
if (!baseUrl) {
throw new Error('COINGECKO_BASE_URL is not set');
}
const headers: Record<string, string> = { accept: 'application/json' };
const apiKey = this.appConfigService.coingeckoApiKey;
if (apiKey) {
headers['x-cg-pro-api-key'] = apiKey;
return { baseUrl: 'https://pro-api.coingecko.com', headers };
}
return { baseUrl: 'https://api.coingecko.com', headers };
return { baseUrl, headers };
}

private async fetchFxRates(
Expand Down Expand Up @@ -297,20 +303,12 @@ export class PriceService {
}

/**
* Daily probe of /api/v3/key. Emits a critical alert when the monthly
* remaining call credit drops below QUOTA_REMAINING_ALERT_THRESHOLD.
*
* Routes through the same endpoint resolution as price calls so a proxy
* deployment (key held only by the proxy) is still covered. Skipped only
* when the service runs fully anonymous (no proxy and no key) — in that
* case there is no Pro account to monitor.
* Daily probe of /api/v3/key through the pricing proxy. Emits a critical
* alert when the monthly remaining call credit drops below
* QUOTA_REMAINING_ALERT_THRESHOLD.
*/
@Cron(CronExpression.EVERY_DAY_AT_NOON)
async checkCoingeckoQuota(): Promise<void> {
const explicitBase = this.appConfigService.coingeckoBaseUrl;
const apiKey = this.appConfigService.coingeckoApiKey;
if (!explicitBase && !apiKey) return;

try {
const { baseUrl, headers } = this.resolveCoingeckoEndpoint();
const response = await axios.get<CoingeckoKeyInfo>(`${baseUrl}/api/v3/key`, {
Expand Down
Loading