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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ Platform notes, system requirements and the full walkthrough are in [Getting sta

## Supported services

CashPilot bundles a catalog of 39 active passive-income services across multiple categories (the catalog also carries 11 retired entries, which the app hides and refuses to deploy). Some links below are affiliate/referral links -- see the [disclosure](docs/services.md#disclosure). Read [how bandwidth-sharing works](docs/services.md#before-you-start-how-bandwidth-sharing-works) before you opt in; per-service limits, payouts and caveats are in [docs/services.md](docs/services.md).
CashPilot bundles a catalog of 41 active passive-income services across multiple categories (the catalog also carries 11 retired entries, which the app hides and refuses to deploy). Some links below are affiliate/referral links -- see the [disclosure](docs/services.md#disclosure). Read [how bandwidth-sharing works](docs/services.md#before-you-start-how-bandwidth-sharing-works) before you opt in; per-service limits, payouts and caveats are in [docs/services.md](docs/services.md).

- **Docker-deployable:** [Anyone Protocol](https://anyone.io), [Bitping](https://app.bitping.com), [Earn.fm](https://earn.fm/ref/GEISYB91), [EarnApp](https://earnapp.com/i/TSMD9wSm), [Honeygain](https://dashboard.honeygain.com/ref/SERGIB4014), [IPRoyal Pawns](https://pawns.app?r=19266874), [MystNodes](https://mystnodes.co/?referral_code=do7v7YOoBBpbOstKQovX2pUvZYKia4ZhH3QIdNtE), [PacketStream](https://packetstream.io/?psr=7xgZ), [ProxyBase](https://peer.proxybase.org?referral=nXzS3c6iTO), [ProxyBase Markets](https://proxybase.xyz?referral=nXzS3c6iTO), [ProxyLite](https://proxylite.ru/?r=KMUPRZIZ), [ProxyRack](https://peer.proxyrack.com/ref/mpwiok3xlaxeycnn5znqlg7ipjeutxyxr6xl7vmn), [Repocket](https://repocket.com/), [Storj](https://storj.dev/node/get-started/setup), [Traffmonetizer](https://traffmonetizer.com/?aff=2111758), [URnetwork](https://ur.io/?referral_code=1Q3G19)
- **Browser extension / desktop only:** [Bytebenefit](https://bytebenefit.io/invited?ref=Brl4z3), [Bytelixir](https://bytelixir.com/r/OYEIRE0VSZBZ), [Dawn Internet](https://dawninternet.com/?code=2QLQV97F), [Deeper Network](https://deeper.network), [Ebesucher](https://www.ebesucher.com/?ref=geiserx), [Gradient Network](https://app.gradient.network/signup?referralCode=YSKMY7), [Grass](https://app.grass.io/register?referralCode=kn8FNEPnUr2tMqE), [Helium](https://helium.com), [Nodepay](https://app.nodepay.ai/register?ref=0wzzyznen64j9zx), [Nodle](https://nodle.com), [PassiveApp](https://passiveapp.com/i/bqpC4M), [Sentinel dVPN](https://sentinel.co), [Spide](https://spide.network/register.html?f3bc51), [Teneo Protocol](https://dashboard.teneo.pro/?code=CAqef), [Theta Edge Node](https://thetatoken.org), [Titan Network](https://edge.titannet.info/signup?inviteCode=2GKKJ495), [Uprock](https://link.uprock.com/i/33e8492e)
- **Docker-deployable:** [Anyone Protocol](https://anyone.io), [Bitping](https://app.bitping.com), [Earn.fm](https://earn.fm/ref/GEISYB91), [EarnApp](https://earnapp.com/i/TSMD9wSm), [Honeygain](https://dashboard.honeygain.com/ref/SERGIB4014), [IPRoyal Pawns](https://pawns.app?r=19266874), [MystNodes](https://mystnodes.co/?referral_code=do7v7YOoBBpbOstKQovX2pUvZYKia4ZhH3QIdNtE), [PacketStream](https://packetstream.io/?psr=7xgZ), [ProxyBase](https://peer.proxybase.org?referral=nXzS3c6iTO), [ProxyBase Markets](https://proxybase.xyz?referral=nXzS3c6iTO), [ProxyLite](https://proxylite.ru/?r=KMUPRZIZ), [ProxyRack](https://peer.proxyrack.com/ref/mpwiok3xlaxeycnn5znqlg7ipjeutxyxr6xl7vmn), [Repocket](https://repocket.com/), [Sia hostd](https://docs.sia.tech/provide-storage/setting-up-hostd/docker), [Storj](https://storj.dev/node/get-started/setup), [Traffmonetizer](https://traffmonetizer.com/?aff=2111758), [URnetwork](https://ur.io/?referral_code=1Q3G19)
- **Browser extension / desktop only:** [Bytebenefit](https://bytebenefit.io/invited?ref=Brl4z3), [Bytelixir](https://bytelixir.com/r/OYEIRE0VSZBZ), [Dawn Internet](https://dawninternet.com/?code=2QLQV97F), [Deeper Network](https://deeper.network), [Ebesucher](https://www.ebesucher.com/?ref=geiserx), [Gradient Network](https://app.gradient.network/signup?referralCode=YSKMY7), [Grass](https://app.grass.io/register?referralCode=kn8FNEPnUr2tMqE), [Helium](https://helium.com), [Nodepay](https://app.nodepay.ai/register?ref=0wzzyznen64j9zx), [Noderr Micronodes](https://micronodes.noderr.xyz/), [Nodle](https://nodle.com), [PassiveApp](https://passiveapp.com/i/bqpC4M), [Sentinel dVPN](https://sentinel.co), [Spide](https://spide.network/register.html?f3bc51), [Teneo Protocol](https://dashboard.teneo.pro/?code=CAqef), [Theta Edge Node](https://thetatoken.org), [Titan Network](https://edge.titannet.info/signup?inviteCode=2GKKJ495), [Uprock](https://link.uprock.com/i/33e8492e)
- **GPU compute:** [Flux](https://runonflux.io), [Golem Network](https://golem.network), [io.net](https://io.net), [Nosana](https://nosana.io), [Salad](https://salad.io), [Vast.ai](https://cloud.vast.ai/?ref_id=452772)

## Documentation
Expand Down
3 changes: 3 additions & 0 deletions catalog-overlay/image-pins.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ pins:
repocket:
image: "repocket/repocket@sha256:5888d756de7e0e74b95b5eb08bb93f4240a0ce840c910a7e528d1143ad0c502b"
why: "Upstream ships no tag at all on this entry."
sia-hostd:
image: "ghcr.io/siafoundation/hostd@sha256:2f27742e103cf79cf1bc818820af936fb342ee435e7bd5ee4f0193f7cd03e2cd"
why: "Upstream ships no tag at all on this entry."
storj:
image: "storjlabs/storagenode@sha256:21ca0ac819d1d66240830d28097eeb693923e51383aa3bb07bf823f9fad1f6e9"
why: "Upstream ships no tag at all on this entry."
Expand Down
10 changes: 10 additions & 0 deletions frontend/scripts/fields_check.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -125,5 +125,15 @@ check(

check("stripHtml leaves plain text alone", stripHtml("no markup here") === "no markup here");

// A catalog pattern must survive into the field, or the wizard cannot render it.
check(
"a docker.env pattern reaches the rendered field",
serviceFormFields(service("s", [envVar("K", { pattern: "[a-f0-9-]+" })]), {})[0].pattern === "[a-f0-9-]+",
);
check(
"a field without a pattern carries none",
serviceFormFields(service("s", [envVar("K")]), {})[0].pattern === undefined,
);

console.log(`${checks - failures}/${checks} checks passed`);
if (failures > 0) process.exit(1);
23 changes: 23 additions & 0 deletions frontend/scripts/wizard_render_check.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ function inputs(html) {
type: attr("type"),
value: attr("value"),
placeholder: attr("placeholder"),
pattern: attr("pattern"),
autocomplete: attr("autocomplete"),
};
});
}
Expand Down Expand Up @@ -121,6 +123,7 @@ const repocket = service({
label: "API Key",
secret: true,
description: 'Your Repocket API key — log in at <a href="https://app.repocket.com/b">app.repocket.com</a> and copy it',
pattern: "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}",
}),
],
},
Expand Down Expand Up @@ -314,6 +317,26 @@ const manual = renderWizardServiceSetup(
check("a manual-only service still renders its collector input", inputs(manual)[0]?.env === "PAWNS_EMAIL", manual);
check("its Deploy button is disabled", manual.includes('data-wizard-action="deploy" data-slug="pawns" disabled'), manual);

// ---------------------------------------------------------------------------
// RULE 6: a catalog pattern reaches the input, so the browser can point at a
// value of the wrong shape (a password pasted where a UUID API key belongs)
// before anything is saved. The backend check stays the gate.
// ---------------------------------------------------------------------------
const field = (html, key) => inputs(html).find((input) => input.env === key);
check(
"RULE 6: a docker.env field with a pattern renders it on the input",
field(rp, "RP_API_KEY")?.pattern === "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}",
field(rp, "RP_API_KEY")?.tag,
);
check("RULE 6: a field without a pattern renders none", field(rp, "RP_EMAIL")?.pattern === undefined, field(rp, "RP_EMAIL")?.tag);
check("RULE 6: nor does a collector field", field(rp, "REPOCKET_PASSWORD")?.pattern === undefined, field(rp, "REPOCKET_PASSWORD")?.tag);
check(
"RULE 6: a secret asks the browser for a new value, never a saved password",
field(rp, "RP_API_KEY")?.autocomplete === "new-password" && field(hg, "HONEYGAIN_PASSWORD")?.autocomplete === "new-password",
rp,
);
check("RULE 6: a non-secret field carries no autocomplete hint", field(rp, "RP_EMAIL")?.autocomplete === undefined, field(rp, "RP_EMAIL")?.tag);

// Nothing to ask for must say so rather than render an empty grid.
const none = renderWizardServiceSetup(service({ slug: "quiet", docker: { env: [] } }), {}, "macmini");
check("a service with no credentials renders none", inputs(none).length === 0, none);
Expand Down
17 changes: 17 additions & 0 deletions frontend/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1229,8 +1229,25 @@ function readWizardForm(slug: string): Record<string, string> {
return values;
}

// A typed value the catalog constrains (the input carries its pattern) is checked
// in the browser first, so a password pasted where a UUID API key belongs is
// pointed at in the form instead of coming back as a backend error after a save.
// The backend check stays authoritative: this is the early warning, not the gate.
function firstInvalidWizardInput(slug: string): HTMLInputElement | null {
for (const input of document.querySelectorAll<HTMLInputElement>(`[data-form-slug="${slug}"] [data-wizard-env]`)) {
if (input.pattern && input.value.trim() && !input.checkValidity()) return input;
}
return null;
}

async function runWizardAction(slug: string, action: string) {
const output = document.querySelector<HTMLPreElement>(`[data-output-slug="${slug}"]`);
const invalid = firstInvalidWizardInput(slug);
if (invalid && (action === "save" || action === "deploy")) {
invalid.reportValidity();
if (output) output.textContent = `${invalid.dataset.wizardEnv} does not have the shape this provider expects.`;
return;
}
const values = readWizardForm(slug);
try {
if (action === "save") {
Expand Down
1 change: 1 addition & 0 deletions frontend/src/render/fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export function serviceFormFields(
secret: item.secret,
required: item.required,
default: item.default,
pattern: item.pattern,
}));
const envKeys = new Set(env.map((item) => item.key));
const collector = (collectorFields || {})[service.slug] || [];
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/render/wizard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export function renderWizardServiceSetup(
${fields.map((item) => `
<label>
<span>${escapeHtml(item.label)}${item.required ? " *" : ""}</span>
<input data-wizard-env="${escapeHtml(item.key)}" type="${item.secret ? "password" : "text"}" placeholder="${escapeHtml(item.description)}" value="${escapeHtml((item.default || "").replaceAll("{hostname}", hostname || "desktop"))}" />
<input data-wizard-env="${escapeHtml(item.key)}" type="${item.secret ? "password" : "text"}"${item.secret ? ' autocomplete="new-password"' : ""}${item.pattern ? ` pattern="${escapeHtml(item.pattern)}"` : ""} placeholder="${escapeHtml(item.description)}" value="${escapeHtml((item.default || "").replaceAll("{hostname}", hostname || "desktop"))}" />
</label>
`).join("") || `<p class="muted">No credentials are required by the catalog for this service.</p>`}
</div>
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/wails.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,8 @@ export interface CollectorField {
description: string;
secret?: boolean;
required?: boolean;
/** A regular expression the whole value must match (catalog docker.env pattern). */
pattern?: string;
}

export interface EnvVar {
Expand All @@ -385,6 +387,7 @@ export interface EnvVar {
secret: boolean;
description: string;
default: string;
pattern?: string;
}

// PreflightFinding is one thing that could stop a service earning on this machine,
Expand Down
3 changes: 2 additions & 1 deletion services/bandwidth/repocket.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@ docker:
secret: false
description: "Your Repocket account email"
- key: RP_API_KEY
label: "API Key"
label: "API Key (UUIDv4, not your password)"
required: true
secret: true
pattern: '[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}'
description: "Your Repocket API key — log in at <a href=\"https://app.repocket.com/bandwidth-earnings\">app.repocket.com</a> and copy the API key shown on the bandwidth-earnings page (this is NOT your account password)"
ports: []
volumes: []
Expand Down
110 changes: 110 additions & 0 deletions services/depin/noderr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
name: Noderr Micronodes
slug: noderr
category: depin
status: beta
# Listed on request of the project (issue #409). Link-only: CashPilot does not
# deploy, run or collect anything for it. Everything below comes from
# micronodes.noderr.xyz and docs.noderr.xyz as read on 2026-10-07.
website: https://micronodes.noderr.xyz
description: >
Noderr Micronodes is the free entry tier of the Noderr Protocol, a DeFi
project on Base L2. A micronode is a small agent on a spare Android phone,
a Windows PC, a Mac or a Chromium browser. On Android it shares the phone's
mobile connection: other Noderr traffic exits through the phone's carrier
IP, so it needs a SIM with mobile data. On a PC or in the browser it runs
automation tasks for the protocol with spare compute. Rewards are designed
to be paid in NODR, scaled by an on-chain uptime and reliability score, but
the network is on the Base Sepolia testnet during its beta: node rewards
are unfunded and none have been paid yet.
short_description: Free micronode (Android connection sharing, Windows, Mac, browser) - testnet, rewards unfunded so far

referral:
signup_url: "https://micronodes.noderr.xyz/"
# The project confirmed on 2026-10-07 that there is no referral programme
# and no invite parameter on the download page (issue #409).
program: false

docker:
image: ""
platforms: []
env: []
ports: []
volumes: []
command: ""
network_mode: ""

disclosure:
sells: >-
Depends on the device. An Android phone sells its mobile connection: other
Noderr traffic exits through the phone's carrier IP address and counts
against its data plan. A Windows PC, a Mac or a browser sells spare compute
for what the provider calls social media and content automation tasks.
third_party_traffic: >-
Yes. On Android, other Noderr traffic exits through your phone's mobile IP
address; the app asks for a recorded consent before it starts and you can
stop sharing from the same screen. For the Windows, Mac and browser
clients the provider describes automation tasks and does not say whether
those carry traffic for other people.
data_collected: >-
The node's wallet address and its uptime and task-completion figures go
on-chain as a reputation score (the TrustFingerprint). The provider says
tasks are anonymised, run in a sandbox and that wallet keys never leave
the device. Nothing more specific is published.
isp_risk: >-
On Android the shared connection is a carrier mobile plan: the provider's
own docs warn that sharing may not be allowed under some carriers'
acceptable-use policies, and shared traffic uses the plan's allowance. For
the desktop and browser clients the provider says nothing about ISP terms.
account_rules: >-
One node per device, each with its own soulbound Utility NFT; running
several devices is allowed and encouraged. No per-IP limit is published.
There is no account in the usual sense: the identity is a wallet the app
generates on first launch.

requirements:
# Android: a SIM with mobile data is required (Wi-Fi alone shares nothing).
# Desktop and browser: the provider publishes no IP requirement, so neither
# residential_ip nor vps_ip is asserted here.
devices_per_account: 0
min_bandwidth: ""
gpu: false
min_storage: ""
note: "Android needs a SIM with mobile data; the desktop and browser clients publish no IP requirement."

payment:
methods: [crypto]
crypto_token: NODR
minimum_payout: ""
currency: NODR
frequency: "On request"

earnings:
# Not an estimate: the provider's own docs state that node rewards are
# unfunded on testnet and none have been paid (2026-10-07). The 5-10% APY
# the site mentions is a design target, not observed earnings.
monthly_low: 0
monthly_high: 0
currency: USD
per: "device"
notes: "Testnet beta: node rewards are unfunded and none have been paid yet. The 5-10% APY on the site is a design target. Revisit when the network leaves Base Sepolia."

cashout:
method: manual
dashboard_url: "https://dapp.noderr.xyz/nodes"
# Undocumented: the provider publishes no minimum. NOT 0, which would mean
# "no minimum, cash out anything".
min_amount: null
currency: NODR
notes: "Rewards accrue to the node's own wallet and are claimed from the app or the dApp. During the beta they are Base Sepolia testnet tokens with no market value."

payout:
model: minted
chain: base
address_source: app
notes: "The app creates the node wallet on first launch and mints a free soulbound Utility NFT to it. Back up the wallet from the app: losing the device loses the node identity. Rewards are on Base Sepolia (testnet) during the beta."

platforms: [android, windows, macos, browser-extension]

collector:
type: manual
notes: "No public API found. The dashboard at dapp.noderr.xyz/nodes sits behind a wallet signature. Nothing to collect while rewards are unfunded on testnet."
Loading
Loading