From 86bdd490ac2abd2744c33ec9fc2a8963879fc435 Mon Sep 17 00:00:00 2001
From: GeiserX <9169332+GeiserX@users.noreply.github.com>
Date: Sat, 23 May 2026 22:01:54 +0200
Subject: [PATCH] fix: Earn.fm collector (Supabase auth), exchange rates, and
CSP
- Rewrite Earn.fm collector to use Supabase email/password auth instead
of the broken UUID token (which is only for the Docker bandwidth client)
- Fix exchange rates: Frankfurter API returns 301 but httpx wasn't
following redirects, causing empty fiat rates (only USD available)
- Fix CSP: allow connect-src to cdn.jsdelivr.net for Chart.js source maps
---
app/collectors/__init__.py | 2 +-
app/collectors/earnfm.py | 55 ++++++++++++++++++++++++++++-------
app/exchange_rates.py | 2 +-
app/main.py | 4 +--
services/bandwidth/earnfm.yml | 10 +++----
5 files changed, 53 insertions(+), 20 deletions(-)
diff --git a/app/collectors/__init__.py b/app/collectors/__init__.py
index 1a5d0c6..1f470fd 100644
--- a/app/collectors/__init__.py
+++ b/app/collectors/__init__.py
@@ -56,7 +56,7 @@
"repocket": ["email", "password"],
"proxyrack": ["api_key"],
"bitping": ["email", "password"],
- "earnfm": ["token"],
+ "earnfm": ["email", "password"],
"packetstream": ["auth_token"],
"grass": ["access_token"],
"bytelixir": ["session_cookie"],
diff --git a/app/collectors/earnfm.py b/app/collectors/earnfm.py
index 075b33d..88d9601 100644
--- a/app/collectors/earnfm.py
+++ b/app/collectors/earnfm.py
@@ -1,7 +1,7 @@
"""Earn.fm earnings collector.
-Authenticates via a UUID API key (EARNFM_TOKEN) obtained from the
-Earn.fm dashboard at app.earn.fm > Account Settings.
+Authenticates via Supabase (email/password) at sb.earn.fm, then uses
+the access token to query the harvester balance API.
"""
from __future__ import annotations
@@ -14,35 +14,67 @@
logger = logging.getLogger(__name__)
+SUPABASE_URL = "https://sb.earn.fm"
+SUPABASE_ANON_KEY = (
+ "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
+ "ewogICJyb2xlIjogImFub24iLAogICJpc3MiOiAic3VwYWJhc2UiLAogICJpYXQiOiAxNjkyNjU1MjAwLAogICJleHAiOiAxODUwNTA4MDAwCn0."
+ "jp-Uj5ro0jj7MHnlE8HHZRsZAFOI1d_T9n_9tnE09vM"
+)
API_BASE = "https://api.earn.fm/v2"
class EarnFMCollector(BaseCollector):
- """Collect earnings from Earn.fm's API using a token."""
+ """Collect earnings from Earn.fm using Supabase email/password auth."""
platform = "earnfm"
- def __init__(self, token: str) -> None:
+ def __init__(self, email: str, password: str) -> None:
super().__init__()
- self._token = token.strip()
+ self._email = email.strip()
+ self._password = password.strip()
+ self._access_token: str = ""
+
+ async def _authenticate(self) -> str | None:
+ """Sign in via Supabase and return the access token."""
+ client = self._get_client(timeout=30)
+ resp = await client.post(
+ f"{SUPABASE_URL}/auth/v1/token?grant_type=password",
+ headers={
+ "apikey": SUPABASE_ANON_KEY,
+ "Content-Type": "application/json",
+ },
+ json={"email": self._email, "password": self._password},
+ )
+ if resp.status_code == 400:
+ return None
+ resp.raise_for_status()
+ data = resp.json()
+ return data.get("access_token")
async def collect(self) -> EarningsResult:
"""Fetch current Earn.fm balance."""
- if not self._token:
+ if not self._email or not self._password:
return EarningsResult(
platform=self.platform,
balance=0.0,
- error="No token configured — copy API key from app.earn.fm > Settings",
+ error="No credentials configured — enter Earn.fm email and password",
)
try:
+ token = await self._authenticate()
+ if not token:
+ return EarningsResult(
+ platform=self.platform,
+ balance=0.0,
+ error="Invalid credentials — check Earn.fm email/password in Settings",
+ )
+
client = self._get_client(timeout=30)
- headers = {"X-API-Key": self._token}
async def _fetch_balance() -> httpx.Response:
return await client.get(
f"{API_BASE}/harvester/view_balance",
- headers=headers,
+ headers={"X-API-Key": token},
)
resp = await self._retry(_fetch_balance)
@@ -51,13 +83,14 @@ async def _fetch_balance() -> httpx.Response:
return EarningsResult(
platform=self.platform,
balance=0.0,
- error="Token invalid or expired — refresh API key from app.earn.fm",
+ error="Auth token rejected — check Earn.fm email/password in Settings",
)
resp.raise_for_status()
data = resp.json()
- balance = float((data.get("data") or {}).get("totalBalance", 0))
+ balance_data = data.get("data") or {}
+ balance = float(balance_data.get("totalBalance", 0))
return EarningsResult(
platform=self.platform,
diff --git a/app/exchange_rates.py b/app/exchange_rates.py
index fd22fd4..4b03edb 100644
--- a/app/exchange_rates.py
+++ b/app/exchange_rates.py
@@ -39,7 +39,7 @@ async def refresh() -> None:
global _fiat_rates, _crypto_usd, _last_fetch
try:
- async with httpx.AsyncClient(timeout=15) as client:
+ async with httpx.AsyncClient(timeout=15, follow_redirects=True) as client:
# --- Crypto rates from CoinGecko (free, no key) ---
if CRYPTO_IDS:
ids = ",".join(CRYPTO_IDS.values())
diff --git a/app/main.py b/app/main.py
index 3830cc1..d4150b7 100644
--- a/app/main.py
+++ b/app/main.py
@@ -309,7 +309,7 @@ async def dispatch(self, request, call_next):
"style-src 'self' https://fonts.googleapis.com 'unsafe-inline'; "
"font-src 'self' https://fonts.gstatic.com; "
"img-src 'self' data:; "
- "connect-src 'self'; "
+ "connect-src 'self' https://cdn.jsdelivr.net; "
"frame-ancestors 'none'"
)
return response
@@ -1491,7 +1491,7 @@ async def api_collectors_meta(request: Request) -> list[dict[str, Any]]:
"bitping": "Use your Bitping account email and password (same as nodes.bitping.com).",
"bytelixir": "Log in at dash.bytelixir.com (tick Remember Me), press F12 → Application → expand Cookies in the left sidebar → click https://dash.bytelixir.com → find bytelixir_session → copy its Value.",
"earnapp": "Log in at earnapp.com, press F12 → Application → Cookies, copy the oauth-refresh-token value.",
- "earnfm": "Copy your UUID API key from app.earn.fm → Account Settings.",
+ "earnfm": "Use your Earn.fm account email and password (same as app.earn.fm login).",
"grass": "Log in at app.getgrass.io, press F12 → Application → Local Storage, copy the accessToken value.",
"honeygain": "Use your Honeygain account email and password (same as dashboard.honeygain.com).",
"iproyal": "Use your IPRoyal Pawns account email and password (same as pawns.app).",
diff --git a/services/bandwidth/earnfm.yml b/services/bandwidth/earnfm.yml
index 84931a1..2fa221f 100644
--- a/services/bandwidth/earnfm.yml
+++ b/services/bandwidth/earnfm.yml
@@ -4,10 +4,10 @@ category: bandwidth
status: active
website: https://earn.fm
description: >
- Earn.fm pays you for sharing your internet bandwidth. Authenticates via
- a UUID API key from the dashboard. Lightweight Docker image with
- straightforward deployment.
-short_description: Bandwidth sharing - API key auth, lightweight client
+ Earn.fm pays you for sharing your internet bandwidth. Earnings collector
+ uses email/password auth via Supabase. Lightweight Docker image with
+ straightforward deployment (Docker client uses separate UUID token).
+short_description: Bandwidth sharing - email/password auth, lightweight client
referral:
signup_url: "https://earn.fm/ref/GEISYB91"
@@ -57,4 +57,4 @@ platforms: [docker, linux]
collector:
type: api
- notes: "API key auth. Dashboard at app.earn.fm shows earnings."
+ notes: "Supabase email/password auth. Docker client uses separate EARNFM_TOKEN (UUID) for bandwidth sharing only."