A pay-what-you-want crypto tip + download tool for musicians (Bandcamp-style). Non-custodial.
Fans name their price, get sent to a pay.u.cash checkout to pay in cryptocurrency, and the download is revealed when they return. Payments land directly in the musician's own wallet. No platform custody, no payout wait, no middleman between the fan and the artist.
This is a small static page plus JavaScript. No build step, no backend required for the happy path. Drop it on any static host (GitHub Pages, Netlify, Cloudflare Pages, S3, nginx).
- The fan picks an amount (or
0for a free download). app.jsbuilds a hosted pay.u.cash checkout link with the amount, the track title, and anexternal_reference.- The fan pays in crypto on pay.u.cash. pay.u.cash forwards the funds straight to the address you configured in your store.
- On return (
?paid=<external_reference>), the download is revealed.
The store Cloud Token used to open the checkout is publishable: it only authorizes receiving funds into your wallet. It is not a private key and cannot move funds out. It is safe to ship in config.js in the browser.
index.html- the Bandcamp-style pay-what-you-want form.config.js- the only file an operator edits (Cloud Token, download URL, track metadata).app.js- builds the pay.u.cash embed link and reveals the download on return.package.json- static-site manifest and a one-command local server.
-
Clone the repo:
git clone https://github.com/UdotCASH/bandcamp-ucashpay.git cd bandcamp-ucashpay -
Edit
config.js:window.BANDCAMP_UCASHPAY_CONFIG = { cloudToken: "st_your_store_cloud_token_here", downloadUrl: "./downloads/my-track.zip", trackTitle: "My Track", trackArtist: "My Name", minimumAmount: 0 };
-
Put your download file in place, e.g.
./downloads/my-track.zip. -
Serve the folder. Any static host works. For local testing:
npm start # then open http://localhost:8080For GitHub Pages: push to
main, then enable Pages on the repo settings (source:main/ root). The page is fully static. -
Open the page, set a price, and complete a checkout to confirm the return flow reveals your download.
- Sign up at pay.u.cash, then click the verification link in the email.
- Set receive addresses under Settings -> Addresses (raw address, ENS, Unstoppable Domains, or FIO).
- Create a store under Account -> Stores and copy its Store Cloud Token (use the store-level token, not the account-wide one).
- For fiat cards, connect your own Stripe under Settings -> Payment processors.
Two surfaces are used. The browser uses the first; the second is for operators who want server-side tracking/verification.
Client-side hosted pay link (publishable Cloud Token, called straight from the browser, no server secret):
GET https://pay.u.cash/embed.php
?cloud=<store Cloud Token>
&amount=<number>
¤cy=<USD|EUR|...> (default USD)
&title=<track title>
&external_reference=<your id>
&redirect=<return URL>
app.js builds exactly this URL and navigates to it.
Server-side tracked checkout (call from a server route; idempotent per external_reference):
POST https://pay.u.cash/payment/ajax.php
Content-Type: application/x-www-form-urlencoded
function=create-transaction
&amount=<number>
¤cy_code=<USD|EUR|...>
&cryptocurrency_code= (empty string)
&external_reference=<your id>
&title=<track title>
&redirect=<return URL>
&cloud=<store Cloud Token>
&idempotent=1
Response JSON: { "success": true, "response": [paymentUrl, transactionId, ...] }. The payment URL is the array element that starts with http:// or https://.
By default, the download is revealed purely on the return redirect. That is fine for a tip jar, but a determined fan could fake the return URL. For a hardened download gate, verify the transaction on your server before revealing the file:
- Open the checkout using the server-side
create-transactioncall above (idempotent perexternal_reference), store thetransactionIdagainst the track. - After the fan returns, call pay.u.cash to confirm that transaction is paid (the idempotent
create-transactioncall with the sameexternal_referencereturns the live status). - Only stream/serve the download file from your server when the status is paid.
A minimal Node example:
// server/verify.js (run on your own backend; never expose your account-wide key)
const params = new URLSearchParams({
function: "create-transaction",
amount: "5.00",
currency_code: "USD",
cryptocurrency_code: "", // empty: let the fan pick the coin
external_reference: req.body.ref, // the id you minted for this checkout
title: "My Track",
redirect: "https://yoursite.band/thanks",
cloud: process.env.STORE_CLOUD_TOKEN,
idempotent: 1
});
const res = await fetch("https://pay.u.cash/payment/ajax.php", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: params.toString()
});
const json = await res.json();
// json.response[0] starts with http(s):// -> the payment URL
// check the transaction status, then stream the file if paid- No automatic recurring payments. pay.u.cash is pay-per-checkout and non-custodial, so subscription-style billing is not supported. For ongoing support, fans simply pay what they want each time, or use a separate patron setup. This tool is intentionally a one-time tip-and-download flow.
- Default reveal is trust-the-return. See "Verifying payment server-side" above to harden it.
- The download file itself is static. Once a fan has the URL they can re-download it. For DRM-free music this is expected (Bandcamp works the same way). Put the file behind your server's verify route if you want a true gate.
This is a static site, not a registry package, so there is nothing to publish to npm. The npm start script just pulls in a zero-config static server on demand. If you want to publish it as an npm skeleton anyway:
npm login
npm publish --access publicMIT. See LICENSE.