Cashu Tokens (Tickets)
Use Cashu ecash tokens as tickets, vouchers, or transferable access tokens in your application.
What is Cashu?
Cashu is an ecash protocol built on Bitcoin's Lightning Network. It provides:
- Privacy: Tokens are untraceable bearer instruments
- Offline Transfers: Can be sent peer-to-peer without internet
- Interoperability: Works across different applications
- Bitcoin-Backed: Each token is backed by real sats
- Blind Signatures: Mint cannot track token usage
Use Cases
Event Tickets
Issue tokens that grant access to events. Users present the token at entry, and you burn it to verify authenticity.
Vouchers & Gift Cards
Create tokens worth a specific amount that users can redeem for products or services.
Access Tokens
Grant temporary or permanent access to premium features using tokens.
Transferable Subscriptions
Allow users to share or resell access by transferring tokens.
How It Works
- Mint Tokens: Create Cashu tokens backed by sats
- Send to Users: Transfer tokens to authenticated users
- Users Hold Tokens: Users store tokens in their Cashu-compatible wallet
- Redeem/Burn: Verify and burn tokens when user accesses service
Requesting Cashu Tokens from Users
Ask users to send you Cashu tokens (e.g., as payment or ticket redemption):
import { PortalSDK } from 'portal-sdk';
const client = new PortalSDK({
serverUrl: 'ws://localhost:3000/ws'
});
await client.connect();
await client.authenticate(process.env.AUTH_TOKEN);
const userPubkey = 'user-public-key-hex';
// Request Cashu tokens from user
const result = await client.requestCashu(
userPubkey,
[], // subkeys
'https://mint.example.com', // mint URL
'sat', // unit (usually 'sat')
10000 // amount in millisats (10 sats)
);
if (result.status === 'success') {
console.log('Received Cashu token:', result.token);
// Now burn the token to claim the sats
const amount = await client.burnCashu(
'https://mint.example.com',
'sat',
result.token,
undefined // static_token (optional, for private mints)
);
console.log('Claimed amount:', amount);
} else if (result.status === 'insufficient_funds') {
console.log('User has insufficient funds');
} else if (result.status === 'rejected') {
console.log('User rejected the request');
}
Sending Cashu Tokens to Users
Send tokens directly to authenticated users:
// First, mint a token
const cashuToken = await client.mintCashu(
'https://mint.example.com', // mint URL
undefined, // static auth token (if mint requires it)
'sat', // unit
10000, // amount in millisats (10 sats)
'Event Ticket - VIP Access' // description
);
console.log('Minted token:', cashuToken);
// Send token to user
const userPubkey = 'user-public-key-hex';
const message = await client.sendCashuDirect(
userPubkey,
[], // subkeys
cashuToken
);
console.log('Sent to user:', message);
Complete Ticket Issuance Flow
Here's a complete example of issuing event tickets:
import { PortalSDK, Currency } from 'portal-sdk';
class TicketSystem {
private client: PortalSDK;
private mintUrl = 'https://mint.example.com';
constructor(wsUrl: string, authToken: string) {
this.client = new PortalSDK({ serverUrl: wsUrl });
this.init(authToken);
}
private async init(authToken: string) {
await this.client.connect();
await this.client.authenticate(authToken);
}
async issueTicket(userPubkey: string, ticketType: string, price: number): Promise<boolean> {
try {
// 1. Request payment from user
const paymentReceived = await this.requestPayment(userPubkey, price);
if (!paymentReceived) {
console.log('Payment failed or rejected');
return false;
}
// 2. Mint a Cashu token as the ticket
const ticket = await this.client.mintCashu(
this.mintUrl,
undefined,
'sat',
price,
`Ticket: ${ticketType}`
);
// 3. Send ticket to user
await this.client.sendCashuDirect(userPubkey, [], ticket);
console.log('Ticket issued successfully!');
return true;
} catch (error) {
console.error('Failed to issue ticket:', error);
return false;
}
}
private async requestPayment(userPubkey: string, amount: number): Promise<boolean> {
return new Promise((resolve) => {
this.client.requestSinglePayment(
userPubkey,
[],
{
amount,
currency: Currency.Millisats,
description: 'Event ticket purchase'
},
(status) => {
if (status.status === 'paid') {
resolve(true);
} else if (status.status === 'user_rejected' || status.status === 'timeout') {
resolve(false);
}
}
);
});
}
async verifyAndRedeemTicket(userPubkey: string): Promise<boolean> {
try {
// Request the ticket back from user
const result = await this.client.requestCashu(
userPubkey,
[],
this.mintUrl,
'sat',
1000 // ticket value
);
if (result.status === 'success') {
// Burn the token to verify it and prevent reuse
const amount = await this.client.burnCashu(
this.mintUrl,
'sat',
result.token
);
console.log('Ticket verified and redeemed!');
return true;
} else {
console.log('Invalid or already used ticket');
return false;
}
} catch (error) {
console.error('Ticket verification failed:', error);
return false;
}
}
}
// Usage
const ticketSystem = new TicketSystem(
process.env.PORTAL_WS_URL!,
process.env.PORTAL_AUTH_TOKEN!
);
// Issue a VIP ticket
await ticketSystem.issueTicket(
userPubkey,
'VIP Access',
50000 // 50 sats
);
// Later, when user arrives at event
await ticketSystem.verifyAndRedeemTicket(userPubkey);
Voucher System Example
Create a gift voucher system:
class VoucherSystem {
private client: PortalSDK;
private mintUrl = 'https://mint.example.com';
async createVoucher(value: number, description: string): Promise<string> {
// Mint a token worth the voucher value
const voucher = await this.client.mintCashu(
this.mintUrl,
undefined,
'sat',
value,
`Voucher: ${description}`
);
return voucher;
}
async sendVoucher(recipientPubkey: string, voucher: string) {
await this.client.sendCashuDirect(recipientPubkey, [], voucher);
console.log('Voucher sent to recipient');
}
async redeemVoucher(userPubkey: string, voucherValue: number): Promise<number> {
const result = await this.client.requestCashu(
userPubkey,
[],
this.mintUrl,
'sat',
voucherValue
);
if (result.status === 'success') {
// Burn and claim the value
const amount = await this.client.burnCashu(
this.mintUrl,
'sat',
result.token
);
return amount;
}
return 0;
}
}
// Create and send a $5 voucher (assuming 1 sat = $0.0001)
const voucher = await voucherSystem.createVoucher(50000, '$5 Store Credit');
await voucherSystem.sendVoucher(recipientPubkey, voucher);
// Redeem voucher
const value = await voucherSystem.redeemVoucher(userPubkey, 50000);
console.log('Voucher redeemed for:', value, 'millisats');
API Reference
mintCashu()
Mint new Cashu tokens from a mint:
await client.mintCashu(
mintUrl: string, // Mint URL
staticAuthToken?: string, // Optional: auth token for private mints
unit: string, // Usually 'sat'
amount: number, // Amount in millisats
description?: string // Optional description
): Promise<string>
Returns: Cashu token as a string
burnCashu()
Burn (redeem) a Cashu token at a mint:
await client.burnCashu(
mintUrl: string, // Mint URL
unit: string, // Usually 'sat'
token: string, // Cashu token to burn
staticAuthToken?: string // Optional: auth token for private mints
): Promise<number>
Returns: Amount claimed in millisats
requestCashu()
Request Cashu tokens from a user:
await client.requestCashu(
recipientKey: string, // User's public key
subkeys: string[], // Optional subkeys
mintUrl: string, // Mint URL
unit: string, // Usually 'sat'
amount: number // Amount to request
): Promise<CashuResponseStatus>
Returns:
{
status: 'success' | 'insufficient_funds' | 'rejected',
token?: string // If status is 'success'
reason?: string // If status is 'rejected'
}
sendCashuDirect()
Send Cashu tokens directly to a user:
await client.sendCashuDirect(
mainKey: string, // User's public key
subkeys: string[], // Optional subkeys
token: string // Cashu token to send
): Promise<string>
Returns: Success message
Setting Up Your Own Mint
For complete control over token issuance and custom ticket types, run your own Cashu mint using Portal's enhanced CDK implementation.
Full Guide: See Running a Custom Mint for detailed instructions on:
- Docker deployment with
getportal/cdk-mintd - Creating custom units (VIP, General, etc.)
- Adding metadata and images to tokens
- Event ticket configuration
- Authentication and security
Quick Start:
docker pull getportal/cdk-mintd:latest
# Configure and run - see full guide for details
Public Mints
If you don't want to run your own mint, you can use public Cashu mints:
- https://mint.minibits.cash
- https://mint.bitcoinmints.com
- https://stablenut.umint.cash
To find more mints, check: https://bitcoinmints.com/
Security Considerations
1. Token Storage
Cashu tokens are bearer instruments. Anyone with the token can spend it:
// ❌ Don't log tokens in production
console.log('Token:', cashuToken);
// ❌ Don't store in plain text
fs.writeFileSync('token.txt', cashuToken);
// ✅ Handle securely
// Only send directly to users, don't store
2. Double-Spending Prevention
Always burn tokens immediately after receiving:
const result = await client.requestCashu(user, [], mintUrl, 'sat', 1000);
if (result.status === 'success') {
// Burn immediately to prevent reuse
await client.burnCashu(mintUrl, 'sat', result.token);
}
3. Mint Trust
You must trust the mint operator:
- Use reputable, well-known mints for production
- Consider running your own mint for full control
- Diversify across multiple mints for redundancy
4. Amount Validation
Always validate amounts before minting:
function validateAmount(amount: number): boolean {
return amount > 0 && amount <= 100000000; // Max 100k sats
}
if (validateAmount(ticketPrice)) {
await client.mintCashu(mintUrl, undefined, 'sat', ticketPrice);
}
Troubleshooting
"Insufficient funds" Error
User's wallet doesn't have enough Cashu tokens at that mint:
- They may need to mint tokens first
- They may have tokens at a different mint
Mint Connection Issues
try {
await client.mintCashu(mintUrl, undefined, 'sat', 1000);
} catch (error) {
console.error('Mint error:', error);
// Try alternative mint or notify user
}
Token Already Spent
If burning fails, the token may have already been redeemed:
- Tokens can only be spent once
- Implement proper tracking to prevent this
Next Steps:
- JWT Tokens - Session management with JWTs
- Single Payments - Accept Lightning payments
- Production Deployment - Deploy securely