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
2 changes: 1 addition & 1 deletion app/collectors/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down
55 changes: 44 additions & 11 deletions app/collectors/earnfm.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
Expand All @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion app/exchange_rates.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
4 changes: 2 additions & 2 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 <a href='https://nodes.bitping.com' target='_blank'>nodes.bitping.com</a>).",
"bytelixir": "Log in at <a href='https://dash.bytelixir.com' target='_blank'>dash.bytelixir.com</a> (tick Remember Me), press F12 → Application → expand <b>Cookies</b> in the left sidebar → click <code>https://dash.bytelixir.com</code> → find <b>bytelixir_session</b> → copy its Value.",
"earnapp": "Log in at <a href='https://earnapp.com' target='_blank'>earnapp.com</a>, press F12 → Application → Cookies, copy the <b>oauth-refresh-token</b> value.",
"earnfm": "Copy your UUID API key from <a href='https://app.earn.fm' target='_blank'>app.earn.fm</a> → Account Settings.",
"earnfm": "Use your Earn.fm account email and password (same as <a href='https://app.earn.fm' target='_blank'>app.earn.fm</a> login).",
"grass": "Log in at <a href='https://app.getgrass.io' target='_blank'>app.getgrass.io</a>, press F12 → Application → Local Storage, copy the <b>accessToken</b> value.",
"honeygain": "Use your Honeygain account email and password (same as <a href='https://dashboard.honeygain.com' target='_blank'>dashboard.honeygain.com</a>).",
"iproyal": "Use your IPRoyal Pawns account email and password (same as <a href='https://pawns.app' target='_blank'>pawns.app</a>).",
Expand Down
10 changes: 5 additions & 5 deletions services/bandwidth/earnfm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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."
Loading