Skip to content
Closed
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 docs/guides/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion docs/guides/uprock.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

Expand Down Expand Up @@ -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.
12 changes: 7 additions & 5 deletions services/depin/uprock.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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]

Expand Down
76 changes: 76 additions & 0 deletions tests/test_catalog_minimum_matches_its_cons.py
Original file line number Diff line number Diff line change
@@ -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<usd>\$)?(?P<amount>\d[\d,]*(?:\.\d+)?) ?(?P<unit>[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<usd>\$)?(?P<amount>\d[\d,]*(?:\.\d+)?) ?(?P<unit>[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")]
Loading