Switch rate provider from CoinGecko to Kraken

Kraken public API is free and doesn't require an API key.
Supports XMR/USD and XMR/EUR pairs.
This commit is contained in:
jwinterm
2026-03-09 10:27:47 -04:00
parent 655d037c49
commit a69d199fab
5 changed files with 27 additions and 25 deletions
+1 -1
View File
@@ -10,7 +10,7 @@ BACKUP_RETENTION_DAYS=7
# API authentication # API authentication
API_KEYS=dev-key-1 API_KEYS=dev-key-1
API_KEY_ENCRYPTION_KEY=replace-with-fernet-key API_KEY_ENCRYPTION_KEY=replace-with-fernet-key
COINGECKO_API_KEY= RATE_PROVIDER=kraken
INVOICE_RECONCILE_INTERVAL_SECONDS=30 INVOICE_RECONCILE_INTERVAL_SECONDS=30
# Founder donation wallet (view-only) # Founder donation wallet (view-only)
+1 -1
View File
@@ -1,6 +1,6 @@
DATABASE_URL=postgresql://xmrcheckout:xmrcheckout@localhost:5432/xmrcheckout DATABASE_URL=postgresql://xmrcheckout:xmrcheckout@localhost:5432/xmrcheckout
API_KEYS=change-me-1,change-me-2 API_KEYS=change-me-1,change-me-2
COINGECKO_API_KEY= RATE_PROVIDER=kraken
INVOICE_RECONCILE_INTERVAL_SECONDS=30 INVOICE_RECONCILE_INTERVAL_SECONDS=30
FOUNDER_PAYMENT_ADDRESS= FOUNDER_PAYMENT_ADDRESS=
FOUNDER_VIEW_KEY= FOUNDER_VIEW_KEY=
+1 -1
View File
@@ -29,7 +29,7 @@ INVOICE_DEFAULT_EXPIRY_HOURS = int(os.getenv("INVOICE_DEFAULT_EXPIRY_HOURS", "1"
LATE_PAYMENT_LOOKBACK_HOURS = int(os.getenv("LATE_PAYMENT_LOOKBACK_HOURS", "48")) LATE_PAYMENT_LOOKBACK_HOURS = int(os.getenv("LATE_PAYMENT_LOOKBACK_HOURS", "48"))
DONATION_EXPIRY_MINUTES = int(os.getenv("DONATION_EXPIRY_MINUTES", "30")) DONATION_EXPIRY_MINUTES = int(os.getenv("DONATION_EXPIRY_MINUTES", "30"))
DONATION_ACTIVE_INVOICE_LIMIT = int(os.getenv("DONATION_ACTIVE_INVOICE_LIMIT", "25")) DONATION_ACTIVE_INVOICE_LIMIT = int(os.getenv("DONATION_ACTIVE_INVOICE_LIMIT", "25"))
COINGECKO_API_KEY = os.getenv("COINGECKO_API_KEY", "") RATE_PROVIDER = os.getenv("RATE_PROVIDER", "kraken")
DONATIONS_ENABLED = _get_bool_env("DONATIONS_ENABLED", False) DONATIONS_ENABLED = _get_bool_env("DONATIONS_ENABLED", False)
MONERO_WALLET_RPC_URLS = [ MONERO_WALLET_RPC_URLS = [
+23 -21
View File
@@ -9,12 +9,16 @@ from typing import Any
import requests import requests
from .config import COINGECKO_API_KEY KRAKEN_TICKER_URL = "https://api.kraken.com/0/public/Ticker"
KRAKEN_SOURCE = "kraken"
COINGECKO_URL = "https://api.coingecko.com/api/v3/simple/price"
COINGECKO_SOURCE = "coingecko"
RATE_TTL_SECONDS = 60 RATE_TTL_SECONDS = 60
# Kraken pair names for XMR against common fiat currencies
_KRAKEN_PAIRS: dict[str, str] = {
"usd": "XXMRZUSD",
"eur": "XXMRZEUR",
}
@dataclass(frozen=True) @dataclass(frozen=True)
class QuoteResult: class QuoteResult:
@@ -32,34 +36,32 @@ _cached_at: float | None = None
def get_xmr_rate(currency: str) -> QuoteResult: def get_xmr_rate(currency: str) -> QuoteResult:
global _cached_quote, _cached_at global _cached_quote, _cached_at
normalized_currency = currency.strip().lower() normalized_currency = currency.strip().lower()
if not COINGECKO_API_KEY:
raise RuntimeError("CoinGecko API key is not configured") pair = _KRAKEN_PAIRS.get(normalized_currency)
if pair is None:
raise ValueError(f"Unsupported fiat currency: {currency} (supported: {', '.join(_KRAKEN_PAIRS)})")
with _cache_lock: with _cache_lock:
if _cached_quote and _cached_at: if _cached_quote and _cached_at:
if time.monotonic() - _cached_at < RATE_TTL_SECONDS: if time.monotonic() - _cached_at < RATE_TTL_SECONDS:
return _cached_quote return _cached_quote
params = { response = requests.get(KRAKEN_TICKER_URL, params={"pair": pair}, timeout=5)
"vs_currencies": normalized_currency,
"ids": "monero",
"x_cg_demo_api_key": COINGECKO_API_KEY,
}
response = requests.get(COINGECKO_URL, params=params, timeout=5)
response.raise_for_status() response.raise_for_status()
data: dict[str, Any] = response.json() data: dict[str, Any] = response.json()
rate_value = ( if data.get("error"):
data.get("monero", {}).get(normalized_currency) raise RuntimeError(f"Kraken API error: {data['error']}")
if isinstance(data, dict) result = data.get("result", {})
else None ticker = result.get(pair, {})
) # 'c' is the last trade closed: [price, lot-volume]
if rate_value is None: last_price = ticker.get("c", [None])[0]
raise ValueError("Unsupported fiat currency at this time") if last_price is None:
rate = Decimal(str(rate_value)) raise ValueError("Could not get XMR rate from Kraken")
rate = Decimal(str(last_price))
quote = QuoteResult( quote = QuoteResult(
rate=rate, rate=rate,
currency=normalized_currency.upper(), currency=normalized_currency.upper(),
source=COINGECKO_SOURCE, source=KRAKEN_SOURCE,
quoted_at=datetime.now(timezone.utc), quoted_at=datetime.now(timezone.utc),
) )
with _cache_lock: with _cache_lock:
+1 -1
View File
@@ -22,7 +22,7 @@ services:
DATABASE_URL: ${DATABASE_URL} DATABASE_URL: ${DATABASE_URL}
API_KEYS: ${API_KEYS} API_KEYS: ${API_KEYS}
API_KEY_ENCRYPTION_KEY: ${API_KEY_ENCRYPTION_KEY} API_KEY_ENCRYPTION_KEY: ${API_KEY_ENCRYPTION_KEY}
COINGECKO_API_KEY: ${COINGECKO_API_KEY} RATE_PROVIDER: ${RATE_PROVIDER:-kraken}
FOUNDER_PAYMENT_ADDRESS: ${FOUNDER_PAYMENT_ADDRESS} FOUNDER_PAYMENT_ADDRESS: ${FOUNDER_PAYMENT_ADDRESS}
FOUNDER_VIEW_KEY: ${FOUNDER_VIEW_KEY} FOUNDER_VIEW_KEY: ${FOUNDER_VIEW_KEY}
DONATIONS_ENABLED: ${DONATIONS_ENABLED:-false} DONATIONS_ENABLED: ${DONATIONS_ENABLED:-false}