11"""Bytelixir earnings collector.
22
3- Uses session cookie from the browser to fetch balance. Bytelixir is a
4- Laravel app with hCaptcha on login, so automated email/password login
3+ Uses session cookie from the browser to fetch balance via /api/v1/user.
4+ Bytelixir is a Laravel app with hCaptcha on login, so automated login
55is not possible. Users must extract session cookies from their browser.
66
77To get the cookie: open dash.bytelixir.com, log in (tick "Remember Me"),
88press F12 > Application > Cookies, and copy the `bytelixir_session` value.
9+
10+ Note: session expires after ~3.5 hours. When expired, the collector
11+ returns an error that surfaces as a notification in the CashPilot UI.
912"""
1013
1114from __future__ import annotations
1215
1316import logging
14- import re
1517
1618import httpx
1719
1820from app .collectors .base import BaseCollector , EarningsResult
1921
2022logger = logging .getLogger (__name__ )
2123
22- DASH_BASE = "https://dash.bytelixir.com"
24+ API_BASE = "https://dash.bytelixir.com"
2325
2426
2527class BytelixirCollector (BaseCollector ):
@@ -33,61 +35,42 @@ def __init__(self, session_cookie: str) -> None:
3335 async def collect (self ) -> EarningsResult :
3436 """Fetch current Bytelixir balance."""
3537 try :
36- cookies = {"bytelixir_session" : self .session_cookie }
38+ cookies = httpx .Cookies ()
39+ cookies .set ("bytelixir_session" , self .session_cookie , domain = "dash.bytelixir.com" )
40+
3741 headers = {
38- "User-Agent" : "Mozilla/5.0" ,
39- "Accept" : "application/json, text/html " ,
42+ "User-Agent" : "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 " ,
43+ "Accept" : "application/json, text/plain, */* " ,
4044 "X-Requested-With" : "XMLHttpRequest" ,
41- "Referer" : f"{ DASH_BASE } /" ,
45+ "Referer" : f"{ API_BASE } /en" ,
46+ "Origin" : API_BASE ,
4247 }
4348
44- async with httpx .AsyncClient (timeout = 30 , follow_redirects = True , cookies = cookies ) as client :
45- # Try the JSON API first
49+ async with httpx .AsyncClient (timeout = 30 , cookies = cookies ) as client :
4650 resp = await client .get (
47- f"{ DASH_BASE } /api/v1/user" ,
51+ f"{ API_BASE } /api/v1/user" ,
4852 headers = headers ,
4953 )
5054
51- if resp .status_code == 200 :
52- try :
53- data = resp .json ()
54- balance = _extract_balance (data )
55- if balance is not None :
56- return EarningsResult (
57- platform = self .platform ,
58- balance = round (balance , 4 ),
59- currency = "USD" ,
60- )
61- except Exception :
62- pass
63-
64- # Fallback: scrape the dashboard HTML for data-balance
65- resp = await client .get (
66- f"{ DASH_BASE } /en" ,
67- headers = {** headers , "Accept" : "text/html" },
68- )
69-
70- if resp .status_code == 200 :
71- balance = _extract_balance_from_html (resp .text )
72- if balance is not None :
73- return EarningsResult (
74- platform = self .platform ,
75- balance = round (balance , 4 ),
76- currency = "USD" ,
77- )
78-
79- # Check if session expired (redirected to login)
80- if "/sign-in" in str (resp .url ):
55+ if resp .status_code == 401 :
8156 return EarningsResult (
8257 platform = self .platform ,
8358 balance = 0.0 ,
84- error = "Session expired — get a new bytelixir_session cookie from your browser " ,
59+ error = "Session expired — refresh bytelixir_session cookie in Settings " ,
8560 )
8661
62+ resp .raise_for_status ()
63+ data = resp .json ()
64+
65+ # Response shape: {"data": {"balance": "0.0000000000", ...}}
66+ user_data = data .get ("data" , {})
67+ balance_str = user_data .get ("balance" , "0" )
68+ balance = float (balance_str )
69+
8770 return EarningsResult (
8871 platform = self .platform ,
89- balance = 0.0 ,
90- error = "Could not extract balance from Bytelixir dashboard " ,
72+ balance = round ( balance , 4 ) ,
73+ currency = "USD " ,
9174 )
9275 except Exception as exc :
9376 logger .error ("Bytelixir collection failed: %s" , exc )
@@ -96,47 +79,3 @@ async def collect(self) -> EarningsResult:
9679 balance = 0.0 ,
9780 error = str (exc ),
9881 )
99-
100-
101- def _extract_balance (data : dict ) -> float | None :
102- """Try to extract a USD balance from JSON response."""
103- for key in ("balance" , "total_balance" , "earnings" , "total_earnings" ):
104- val = data .get (key )
105- if val is not None :
106- try :
107- return float (val )
108- except (ValueError , TypeError ):
109- continue
110-
111- # Nested under 'data' or 'user'
112- for wrapper in ("data" , "user" ):
113- inner = data .get (wrapper , {})
114- if isinstance (inner , dict ):
115- for key in ("balance" , "total_balance" , "earnings" , "total_earnings" ):
116- val = inner .get (key )
117- if val is not None :
118- try :
119- return float (val )
120- except (ValueError , TypeError ):
121- continue
122- return None
123-
124-
125- def _extract_balance_from_html (html : str ) -> float | None :
126- """Extract balance from data-balance attribute in dashboard HTML."""
127- match = re .search (r'data-balance=["\']([^"\']+)["\']' , html )
128- if match :
129- try :
130- return float (match .group (1 ))
131- except (ValueError , TypeError ):
132- pass
133-
134- # Also try matching a balance-like number near "balance" text
135- match = re .search (r"(?:balance|earnings)[^>]*>\s*\$?\s*([\d.]+)" , html , re .IGNORECASE )
136- if match :
137- try :
138- return float (match .group (1 ))
139- except (ValueError , TypeError ):
140- pass
141-
142- return None
0 commit comments