Skip to content

NIP-60 ​

Cashu Wallets - defines how to store and manage Cashu eCash wallets using Nostr events. Wallet metadata (kind 17375), unspent token proofs (kind 7375), spending history (kind 7376), and pending mint quotes (kind 7374). All sensitive data is NIP-44 encrypted to self.

Import ​

ts
import { nip60 } from 'nostr-core'
// or import individual functions
import {
  createWalletEventTemplate,
  createWalletEvent,
  parseWalletEvent,
  createTokenEventTemplate,
  createTokenEvent,
  parseTokenEvent,
  createHistoryEventTemplate,
  createHistoryEvent,
  parseHistoryEvent,
  createQuoteEventTemplate,
  createQuoteEvent,
  parseQuoteEvent,
  createTokenDeleteTemplate,
  createTokenDeleteEvent,
  getWalletFilters,
  getHistoryFilter,
  getProofsBalance,
  WALLET_KIND,
  TOKEN_KIND,
  HISTORY_KIND,
  QUOTE_KIND,
} from 'nostr-core'

Event Kinds ​

ConstantValueDescription
WALLET_KIND17375Wallet metadata (replaceable)
TOKEN_KIND7375Unspent proofs/tokens
HISTORY_KIND7376Spending history
QUOTE_KIND7374Pending mint quotes

CashuProof Type ​

ts
type CashuProof = {
  id: string
  amount: number
  secret: string
  C: string
}
FieldTypeDescription
idstringKeyset ID from the mint
amountnumberDenomination of this proof
secretstringUnique secret for this proof
CstringBlinded signature from the mint

CashuToken Type ​

ts
type CashuToken = {
  mint: string
  proofs: CashuProof[]
  unit?: string
  del?: string[]
}
FieldTypeDescription
mintstringMint URL these proofs belong to
proofsCashuProof[]Array of unspent Cashu proofs
unitstring (optional)Denomination unit: "sat", "usd", "eur". Default: "sat"
delstring[] (optional)Event IDs of token events destroyed to create this one

CashuWallet Type ​

ts
type CashuWallet = {
  privkey: string
  mints: string[]
}
FieldTypeDescription
privkeystringWallet-exclusive private key (hex), distinct from user's Nostr key. Used for NIP-61 nutzaps.
mintsstring[]One or more mint URLs

CashuHistory Type ​

ts
type CashuHistory = {
  direction: 'in' | 'out'
  amount: string
  unit: string
  events: CashuHistoryRef[]
}

type CashuHistoryRef = {
  id: string
  relay?: string
  marker: 'created' | 'destroyed' | 'redeemed'
}
FieldTypeDescription
direction'in' | 'out'Whether tokens were received or sent
amountstringTransaction amount
unitstringDenomination unit (default: "sat")
eventsCashuHistoryRef[]Referenced token event IDs with markers

CashuQuote Type ​

ts
type CashuQuote = {
  quoteId: string
  mint: string
  expiration: number
}
FieldTypeDescription
quoteIdstringMint quote identifier
mintstringMint URL
expirationnumberUnix timestamp when the quote expires

nip60.createWalletEvent ​

ts
function createWalletEvent(wallet: CashuWallet, secretKey: Uint8Array): NostrEvent
function createWalletEventTemplate(wallet: CashuWallet, secretKey: Uint8Array): EventTemplate

Creates a kind 17375 replaceable wallet event. Content is NIP-44 encrypted to self.

ts
const walletEvent = nip60.createWalletEvent({
  privkey: 'wallet-exclusive-private-key-hex',
  mints: ['https://mint.example.com', 'https://mint2.example.com'],
}, secretKey)

nip60.parseWalletEvent ​

ts
function parseWalletEvent(event: NostrEvent, secretKey: Uint8Array): CashuWallet

Decrypts and parses a kind 17375 wallet event.

ts
const wallet = nip60.parseWalletEvent(walletEvent, secretKey)
console.log(wallet.mints) // ['https://mint.example.com', ...]

nip60.createTokenEvent ​

ts
function createTokenEvent(token: CashuToken, secretKey: Uint8Array): NostrEvent
function createTokenEventTemplate(token: CashuToken, secretKey: Uint8Array): EventTemplate

Creates a kind 7375 token event storing unspent Cashu proofs. Content is NIP-44 encrypted to self.

ts
const tokenEvent = nip60.createTokenEvent({
  mint: 'https://mint.example.com',
  unit: 'sat',
  proofs: [
    { id: '005c2502034d4f12', amount: 1, secret: 'secret1', C: '02abc...' },
    { id: '005c2502034d4f12', amount: 4, secret: 'secret2', C: '03def...' },
  ],
  del: ['previous-token-event-id'],  // ID of destroyed token event
}, secretKey)

nip60.parseTokenEvent ​

ts
function parseTokenEvent(event: NostrEvent, secretKey: Uint8Array): CashuToken

Decrypts and parses a kind 7375 token event.

ts
const token = nip60.parseTokenEvent(tokenEvent, secretKey)
console.log(token.mint)    // 'https://mint.example.com'
console.log(token.proofs)  // [{ id, amount, secret, C }, ...]
console.log(nip60.getProofsBalance(token.proofs)) // 5

nip60.createHistoryEvent ​

ts
function createHistoryEvent(history: CashuHistory, secretKey: Uint8Array): NostrEvent
function createHistoryEventTemplate(history: CashuHistory, secretKey: Uint8Array): EventTemplate

Creates a kind 7376 spending history event. Event references with "redeemed" marker are placed in unencrypted tags; all others are NIP-44 encrypted.

ts
const historyEvent = nip60.createHistoryEvent({
  direction: 'out',
  amount: '4',
  unit: 'sat',
  events: [
    { id: 'old-token-event-id', marker: 'destroyed' },
    { id: 'new-token-event-id', marker: 'created' },
  ],
}, secretKey)

nip60.parseHistoryEvent ​

ts
function parseHistoryEvent(event: NostrEvent, secretKey: Uint8Array): CashuHistory

Decrypts and parses a kind 7376 spending history event.

ts
const history = nip60.parseHistoryEvent(historyEvent, secretKey)
console.log(history.direction) // 'out'
console.log(history.amount)    // '4'
console.log(history.events)    // [{ id, marker: 'destroyed' }, { id, marker: 'created' }]

nip60.createQuoteEvent ​

ts
function createQuoteEvent(quote: CashuQuote, secretKey: Uint8Array): NostrEvent
function createQuoteEventTemplate(quote: CashuQuote, secretKey: Uint8Array): EventTemplate

Creates a kind 7374 quote event for tracking a pending mint quote during Lightning payment. Uses NIP-40 expiration tag.

ts
const quoteEvent = nip60.createQuoteEvent({
  quoteId: 'quote-abc-123',
  mint: 'https://mint.example.com',
  expiration: Math.floor(Date.now() / 1000) + 86400,
}, secretKey)

nip60.parseQuoteEvent ​

ts
function parseQuoteEvent(event: NostrEvent, secretKey: Uint8Array): CashuQuote

Decrypts and parses a kind 7374 quote event.

nip60.createTokenDeleteEvent ​

ts
function createTokenDeleteEvent(tokenEventIds: string[], secretKey: Uint8Array): NostrEvent
function createTokenDeleteTemplate(tokenEventIds: string[]): EventTemplate

Creates a kind 5 deletion event for spent token events. Per NIP-60, the deletion includes ["k", "7375"] for filtering.

ts
const deleteEvent = nip60.createTokenDeleteEvent(['spent-token-event-id'], secretKey)
// Publish to relay to mark the token as spent

nip60.getWalletFilters ​

ts
function getWalletFilters(pubkey: string): Filter[]

Returns filters to fetch a user's wallet event and all unspent token events.

ts
const filters = nip60.getWalletFilters(pubkey)
// [{ kinds: [17375], authors: [pubkey] }, { kinds: [7375], authors: [pubkey] }]

nip60.getHistoryFilter ​

ts
function getHistoryFilter(pubkey: string): Filter

Returns a filter to fetch a user's spending history events.

nip60.getProofsBalance ​

ts
function getProofsBalance(proofs: CashuProof[]): number

Sums the total amount of a set of Cashu proofs.

ts
const total = nip60.getProofsBalance(token.proofs)
console.log(`Balance: ${total} sats`)

Full Example ​

ts
import { generateSecretKey, getPublicKey, nip60, Relay } from 'nostr-core'
import { randomBytes, bytesToHex } from 'nostr-core'

const sk = generateSecretKey()
const pk = getPublicKey(sk)

// Step 1: Create a wallet with a dedicated private key
const walletPrivkey = bytesToHex(randomBytes(32))
const walletEvent = nip60.createWalletEvent({
  privkey: walletPrivkey,
  mints: ['https://mint.minibits.cash'],
}, sk)

// Step 2: Store token proofs received from a mint
const tokenEvent = nip60.createTokenEvent({
  mint: 'https://mint.minibits.cash',
  unit: 'sat',
  proofs: [
    { id: '009a1f293253e41e', amount: 1, secret: 'secret1', C: '02...' },
    { id: '009a1f293253e41e', amount: 2, secret: 'secret2', C: '03...' },
    { id: '009a1f293253e41e', amount: 8, secret: 'secret3', C: '02...' },
  ],
}, sk)
console.log('Total balance:', nip60.getProofsBalance([
  { id: '009a1f293253e41e', amount: 1, secret: 'secret1', C: '02...' },
  { id: '009a1f293253e41e', amount: 2, secret: 'secret2', C: '03...' },
  { id: '009a1f293253e41e', amount: 8, secret: 'secret3', C: '02...' },
])) // 11

// Step 3: Spend some proofs (e.g. send 3 sats)
// After swapping with the mint, create new token with change
const newTokenEvent = nip60.createTokenEvent({
  mint: 'https://mint.minibits.cash',
  unit: 'sat',
  proofs: [
    { id: '009a1f293253e41e', amount: 8, secret: 'secret3', C: '02...' },
  ],
  del: [tokenEvent.id],  // Reference the destroyed token event
}, sk)

// Delete the old token event
const deleteEvent = nip60.createTokenDeleteEvent([tokenEvent.id], sk)

// Record the transaction
const historyEvent = nip60.createHistoryEvent({
  direction: 'out',
  amount: '3',
  unit: 'sat',
  events: [
    { id: tokenEvent.id, marker: 'destroyed' },
    { id: newTokenEvent.id, marker: 'created' },
  ],
}, sk)

// Step 4: Fetch wallet from relays
const relay = new Relay('wss://relay.example.com')
await relay.connect()

const filters = nip60.getWalletFilters(pk)
// Query relay with filters to get wallet + tokens
// Parse each event with nip60.parseWalletEvent / nip60.parseTokenEvent

relay.close()

How It Works ​

  • Kind 17375 is a replaceable wallet event storing the wallet's private key and mint list. Only one per user.
  • Kind 7375 stores unspent Cashu proofs for a single mint. Multiple token events may exist.
  • Kind 7376 records spending history with direction, amount, and event references.
  • Kind 7374 tracks pending mint quotes during Lightning payments (uses NIP-40 expiration).
  • All content is NIP-44 encrypted to self (using your own keypair).
  • When proofs are spent, the old token event is NIP-09 deleted and a new one created with remaining proofs.
  • The del field in token events references destroyed event IDs for state tracking.
  • Deletion events MUST include ["k", "7375"] for relay filtering.
  • The wallet privkey is a dedicated key for receiving NIP-61 nutzaps, not the user's Nostr key.
  • History event references with "redeemed" marker are stored unencrypted; all others are encrypted.

Released under the MIT License.