Starter kit

Add Cashu to a small app without guessing the moving parts.

Start with one test mint, one local wallet state model, the five cashu-ts flows most apps need, and a 60-second no-signup paid-download demo using a fresh test token.

cashu-tstest mint: nofee.testnut.cashu.spaceexample app: 60-second paid downloads

Hosted example

Try the Cashu-paid download

Open Cashu zine demo

No signup, no account setup: mint a fresh 21 sat test token, paste it into the hosted demo, and see the server record a short-lived grant before returning the sample file link.

Builder help

Building this into a real product?

Ask in the Cashu Telegram group

Share what you are building and where you are stuck in the Cashu community Telegram group. Maintainers and other builders are active there, so you get real answers without waiting on a single inbox.

1 / install

Install the library

npm install @cashu/cashu-ts

2 / wallet

Create a wallet context

Make the mint and unit explicit. Production apps should treat mint choice as a product trust decision.

import { Wallet } from '@cashu/cashu-ts'

const mintUrl = 'https://nofee.testnut.cashu.space'
const wallet = new Wallet(mintUrl, { unit: 'sat' })
await wallet.loadMint()

const mintInfo = wallet.getMintInfo()
// getKeyset() returns the active keyset bound to the wallet's unit.
const activeKeyset = wallet.getKeyset()

3 / state

Persist proof state deliberately

Proofs are bearer money. Track spendable, pending, and spent states so refreshes and retries do not lose value.

type StoredProof = {
  amount: number
  id: string
  secret: string
  C: string
  state: 'spendable' | 'pending' | 'spent'
}

type WalletRecord = {
  mintUrl: string
  unit: 'sat'
  keysetId?: string
  proofs: StoredProof[]
}

const sumBalance = (proofs: StoredProof[]) =>
  proofs.filter((proof) => proof.state === 'spendable').reduce((sum, proof) => sum + proof.amount, 0)

4 / app pattern

Paid unlock / digital download

A small useful pattern: buyer pays sats, the app records the payment, then returns a short-lived download URL.

type DigitalAsset = {
  id: string
  title: string
  priceSats: number
  fileKey: string
}

type UnlockGrant = {
  id: string
  assetId: string
  amountSats: number
  mintUrl: string
  paymentRef: string
  expiresAt: string
}

async function createUnlockGrant(asset: DigitalAsset, paymentRef: string): Promise<UnlockGrant> {
  return {
    id: crypto.randomUUID(),
    assetId: asset.id,
    amountSats: asset.priceSats,
    mintUrl: 'https://nofee.testnut.cashu.space',
    paymentRef,
    expiresAt: new Date(Date.now() + 10 * 60 * 1000).toISOString(),
  }
}

Build order

Copy these recipes in order

Back to playground