diff --git a/app/collectors/__init__.py b/app/collectors/__init__.py index 87fcd69..39cbcfd 100644 --- a/app/collectors/__init__.py +++ b/app/collectors/__init__.py @@ -21,6 +21,7 @@ from app.collectors.packetstream import PacketStreamCollector from app.collectors.proxyrack import ProxyRackCollector from app.collectors.repocket import RepocketCollector +from app.collectors.salad import SaladCollector from app.collectors.storj import StorjCollector from app.collectors.traffmonetizer import TraffmonetizerCollector @@ -41,6 +42,7 @@ "packetstream": PacketStreamCollector, "grass": GrassCollector, "bytelixir": BytelixirCollector, + "salad": SaladCollector, } # Map of slug -> list of config keys needed to instantiate the collector @@ -58,6 +60,7 @@ "packetstream": ["auth_token"], "grass": ["access_token"], "bytelixir": ["session_cookie", "?remember_web", "?xsrf_token"], + "salad": ["auth_cookie"], } diff --git a/app/collectors/salad.py b/app/collectors/salad.py new file mode 100644 index 0000000..f22143c --- /dev/null +++ b/app/collectors/salad.py @@ -0,0 +1,70 @@ +"""Salad earnings collector. + +Authenticates via the ``auth`` cookie and fetches the current balance +from the Salad API at app-api.salad.com. + +Salad uses ASP.NET Core anti-forgery: the ``auth`` cookie value must +also be sent as the ``X-XSRF-TOKEN`` header (double-submit pattern). + +To get the token: open salad.com in your browser, log in, press F12, +go to Application > Cookies > .salad.com, and copy the ``auth`` cookie. +""" + +from __future__ import annotations + +import logging + +import httpx + +from app.collectors.base import BaseCollector, EarningsResult + +logger = logging.getLogger(__name__) + +API_BASE = "https://app-api.salad.com/api/v1" + + +class SaladCollector(BaseCollector): + """Collect earnings from Salad's API using the auth cookie.""" + + platform = "salad" + + def __init__(self, auth_cookie: str) -> None: + self.auth_cookie = auth_cookie + + async def collect(self) -> EarningsResult: + """Fetch current Salad balance.""" + try: + cookies = {"auth": self.auth_cookie} + headers = {"X-XSRF-TOKEN": self.auth_cookie} + + async with httpx.AsyncClient(timeout=30) as client: + resp = await client.get( + f"{API_BASE}/profile/balance", + cookies=cookies, + headers=headers, + ) + + if resp.status_code in (401, 403): + return EarningsResult( + platform=self.platform, + balance=0.0, + error="Auth cookie expired — get a new 'auth' cookie from salad.com", + ) + + resp.raise_for_status() + data = resp.json() + + balance = float(data.get("currentBalance", 0)) + + return EarningsResult( + platform=self.platform, + balance=round(balance, 4), + currency="USD", + ) + except Exception as exc: + logger.error("Salad collection failed: %s", exc) + return EarningsResult( + platform=self.platform, + balance=0.0, + error=str(exc), + ) diff --git a/app/main.py b/app/main.py index 65181d7..72b4a17 100644 --- a/app/main.py +++ b/app/main.py @@ -1271,6 +1271,7 @@ async def api_collectors_meta(request: Request) -> list[dict[str, Any]]: "access_token", "api_key", "session_cookie", + "auth_cookie", "oauth_token", "brd_sess_id", } @@ -1318,6 +1319,30 @@ class ConfigUpdate(BaseModel): async def api_set_config(request: Request, body: ConfigUpdate) -> dict[str, str]: _require_owner(request) await database.set_config_bulk(body.data) + + # Auto-create "external" deployment records for manual-only services + # whose collector credentials were just saved. Without a deployment + # row, _run_collection() will never instantiate the collector. + from app.collectors import _COLLECTOR_ARGS + + for slug, arg_keys in _COLLECTOR_ARGS.items(): + required_keys = [f"{slug}_{a.lstrip('?')}" for a in arg_keys if not a.startswith("?")] + if not required_keys: + continue + if not all(body.data.get(k) for k in required_keys): + continue + svc = catalog.get_service(slug) + if not svc: + continue + docker_conf = svc.get("docker", {}) + has_image = bool(docker_conf and docker_conf.get("image")) + if has_image: + continue # Docker services get deployed normally + existing = await database.get_deployment(slug) + if not existing: + await database.save_deployment(slug=slug, container_id="", status="external") + logger.info("Auto-created external deployment for %s", slug) + return {"status": "saved"} diff --git a/docs/guides/salad.md b/docs/guides/salad.md index 5a193e0..a333201 100644 --- a/docs/guides/salad.md +++ b/docs/guides/salad.md @@ -33,20 +33,27 @@ Salad.io lets you share your GPU for distributed AI workloads and earn Salad bal ### 1. Create an account -Sign up at [Salad](https://salad.io). +Sign up at [Salad](https://salad.io) and install the desktop application on your Windows machine. -### 2. Get your credentials +### 2. Get your auth cookie -After signing up, locate the credentials needed for Docker deployment. These are typically your email/password or an API token found in the dashboard. +Salad runs as a native Windows app — there is no Docker image. To let CashPilot track your earnings: -### 3. Deploy with CashPilot +1. Open [salad.com](https://salad.com) in your browser and log in. +2. Press **F12** to open DevTools. +3. Go to **Application > Cookies > salad.com**. +4. Copy the value of the `auth` cookie (starts with `CfDJ8...`). -In the CashPilot web UI, find **Salad** in the service catalog and click **Deploy**. Enter the required credentials and CashPilot will handle the rest. +### 3. Configure CashPilot -## Docker Configuration +Add the cookie to your CashPilot configuration: + +``` +salad_auth_cookie= +``` -- **Image:** `` +CashPilot will poll `app-api.salad.com/api/v1/profile/balance` to fetch your current and lifetime balance. -### Environment Variables +## Docker Configuration -No environment variables required. +Salad is a native Windows desktop application — no Docker image is available. CashPilot monitors earnings via the Salad API only. diff --git a/services/compute/salad.yml b/services/compute/salad.yml index 5491480..4dd39e4 100644 --- a/services/compute/salad.yml +++ b/services/compute/salad.yml @@ -53,4 +53,4 @@ platforms: [windows] collector: type: api - notes: "API at app.salad.io. Auth via session token. Earnings and machine status endpoints available." + notes: "API at app-api.salad.com/api/v1/profile/balance. Auth via 'auth' cookie + X-XSRF-TOKEN header (double-submit). Returns currentBalance (USD) and lifetimeBalance."