diff --git a/docs/guides/index.md b/docs/guides/index.md index dc0706c0..443e6261 100644 --- a/docs/guides/index.md +++ b/docs/guides/index.md @@ -83,7 +83,7 @@ Setup and configuration for every service CashPilot supports. | [Teneo Protocol](teneo.md) | Residential IP | App only | — | active | | [Theta Edge Node](theta-edge.md) | — | App only | — | active | | [Titan Network](titan.md) | Residential IP, Storage 50GB | App only | — | active | -| [Uprock](uprock.md) | Residential IP | App only | $5 | active | +| [Uprock](uprock.md) | Residential IP | App only | 250 UPT | active | | [Wipter](wipter.md) | Residential IP | App only | $20 | dead | ## GPU Compute diff --git a/docs/guides/uprock.md b/docs/guides/uprock.md index a4bd071e..e7eacdd8 100644 --- a/docs/guides/uprock.md +++ b/docs/guides/uprock.md @@ -14,7 +14,7 @@ Uprock is a DePIN (Decentralized Physical Infrastructure Network) platform that |--------|-------| | Monthly range | $0 - $5 (estimate) | | Per | device | -| Minimum payout | $5 | +| Minimum payout | 250 UPT | | Payout frequency | On request | | Payment methods | Crypto | @@ -56,6 +56,8 @@ No Docker image available. Uprock is a browser extension and mobile app only. ## Important Notes +- **Withdrawals**: rewards are UPT tokens, withdrawn to a Solana wallet once 250 UPT are available to withdraw, with two-factor authentication on. A withdrawal fee applies ([UpRock help centre](https://help.uprock.com/en/articles/2057635-security-and-user-asset-protection-2fa-and-withdrawals)). Earnings from an Android device stay locked until UpRock's device verification completes, so they do not count toward the 250 UPT until then ([device verification](https://help.uprock.com/en/help/articles/8124029-what-is-device-verification)). + - **Android works well**: The Android app runs reliably in the background with proper battery optimization settings. - **No IP conflicts**: Can run alongside other bandwidth-sharing services on the same device/network. - **Extension + App**: You can run both the browser extension and mobile app simultaneously on different devices for additional earnings. diff --git a/services/depin/uprock.yml b/services/depin/uprock.yml index 02a1bd5e..a8fea42e 100644 --- a/services/depin/uprock.yml +++ b/services/depin/uprock.yml @@ -31,8 +31,10 @@ requirements: payment: methods: [crypto] - minimum_payout: "$5" - currency: USD + # help.uprock.com/en/articles/2057635: "The minimum withdrawal is 250 UPT", + # to a Solana wallet (checked 2026-11-07). + minimum_payout: "250 UPT" + currency: UPT frequency: "On request" earnings: @@ -45,9 +47,9 @@ earnings: cashout: method: redirect dashboard_url: "https://uprock.com" - min_amount: 5.0 - currency: USD - notes: "Crypto token-based rewards. Claim from dashboard." + min_amount: 250.0 + currency: UPT + notes: "UPT tokens, withdrawn to a Solana wallet from 250 UPT, with two-factor authentication on. A withdrawal fee applies." platforms: [browser-extension, android] diff --git a/tests/test_catalog_minimum_matches_its_cons.py b/tests/test_catalog_minimum_matches_its_cons.py new file mode 100644 index 00000000..3e4fa0eb --- /dev/null +++ b/tests/test_catalog_minimum_matches_its_cons.py @@ -0,0 +1,76 @@ +"""A minimum quoted in a rating con must be the minimum the catalog uses. + +UpRock's payout_terms con quoted its help centre ("a 250 UPT minimum") while +``payment.minimum_payout`` said "$5" and ``cashout.min_amount`` said 5 USD, so +the payout card counted down to a threshold the provider does not have. +""" + +from __future__ import annotations + +import re +from pathlib import Path + +import pytest +import yaml + +ROOT = Path(__file__).resolve().parents[1] +QUOTED = re.compile(r"(?P\$)?(?P\d[\d,]*(?:\.\d+)?) ?(?P[A-Z]{2,6})? minimum") + + +def quoted_minimums(entry: dict) -> list[tuple[float, str]]: + found = [] + for con in (entry.get("rating") or {}).get("cons") or []: + for m in QUOTED.finditer(str(con.get("text") or "")): + unit = "USD" if m.group("usd") else (m.group("unit") or "") + if unit: + found.append((float(m.group("amount").replace(",", "")), unit)) + return found + + +def _stated(text: object) -> tuple[float, str] | None: + """payment.minimum_payout as (amount, unit): "$5" is (5.0, "USD"), "250 UPT" is (250.0, "UPT").""" + m = re.fullmatch(r"(?P\$)?(?P\d[\d,]*(?:\.\d+)?) ?(?P[A-Z]{2,6})?", str(text or "").strip()) + if not m: + return None + unit = "USD" if m.group("usd") else (m.group("unit") or "") + return float(m.group("amount").replace(",", "")), unit + + +def mismatches(entry: dict) -> list[tuple[float, str]]: + """Quoted minimums that disagree with cashout.min_amount or payment.minimum_payout.""" + cashout = entry.get("cashout") or {} + derived = (cashout.get("min_amount"), str(cashout.get("currency") or "").upper()) + stated = _stated((entry.get("payment") or {}).get("minimum_payout")) + return [q for q in quoted_minimums(entry) if q != derived or q != stated] + + +ENTRIES = {p.stem: yaml.safe_load(p.read_text(encoding="utf-8")) or {} for p in sorted(ROOT.glob("services/*/*.yml"))} + + +@pytest.mark.parametrize("slug", [s for s, e in ENTRIES.items() if quoted_minimums(e)]) +def test_the_quoted_minimum_is_the_catalog_minimum(slug: str): + assert mismatches(ENTRIES[slug]) == [] + + +def test_some_entry_quotes_a_minimum(): + """Otherwise the test above has nothing to check.""" + assert "uprock" in [s for s, e in ENTRIES.items() if quoted_minimums(e)] + + +def test_the_check_catches_the_old_uprock_entry(): + old = { + "payment": {"minimum_payout": "$5"}, + "cashout": {"min_amount": 5.0, "currency": "USD"}, + "rating": {"cons": [{"text": "withdrawals are UPT tokens to a Solana wallet, with a 250 UPT minimum."}]}, + } + assert mismatches(old) == [(250.0, "UPT")] + + +def test_the_check_catches_a_stale_payment_minimum(): + """cashout.min_amount is derived from payment.minimum_payout; a stale source is caught too.""" + entry = { + "payment": {"minimum_payout": "$5"}, + "cashout": {"min_amount": 250.0, "currency": "UPT"}, + "rating": {"cons": [{"text": "with a 250 UPT minimum."}]}, + } + assert mismatches(entry) == [(250.0, "UPT")]