Chapter 60 - Payments
60
Nostr Implementation Possibility - Hex № 60

Cashu Wallets

Store a Cashu ecash wallet on relays as encrypted events, so the same wallet follows the user across every application.

1 Overview

A Cashu wallet holds ecash proofs issued by a mint. This NIP stores the state of such a wallet on relays. Every event is encrypted with NIP-44 to the user's own key. Only the owner can read the wallet.

The goal is twofold. Ease of use: a new user can receive funds at once, with no account at any service. Interoperability: the wallet follows the user across applications, because every client reads the same relay state.

This NIP does not define how a user receives money from someone else. It only keeps the state of the user's own wallet. Payments to the user are defined in NIP-61.

The wallet uses four event kinds. A kind:17375 event describes the wallet. kind:7375 events hold the unspent proofs. kind:7376 events record spending history; they are informational and fully optional. kind:7374 events keep the state of a mint quote; they are optional as well.

Hint!

Read NIP-44 before this chapter. Every content payload here travels through nip44_encrypt, keyed to the user's own Nostr key. Relays only ever see ciphertext.

Client user's key user's relays (ciphertext only) 17375 wallet 7375 tokens 7376 history 7374 quotes ×1 (replaceable) optional optional nip44
Fig. 60-1 — Wallet on relays. One wallet event, many token events, optional history and quote events. All content is NIP-44 encrypted.

2 Wallet Event — Kind 17375

The wallet event is a replaceable event of kind:17375. It describes the wallet itself. Its content is a NIP-44 encrypted array of tag-like pairs. The outer tags array stays empty.

Wallet event - kind 17375
{
  "kind": 17375,
  "content": nip44_encrypt([
    [ "privkey", "hexkey" ],
    [ "mint", "https://mint1" ],
    [ "mint", "https://mint2" ]
  ]),
  "tags": []
}
Wallet content entries
EntryRule
mintA mint this wallet uses. There MUST be one or more mint entries.
privkeyPrivate key that unlocks P2PK-locked ecash. It MUST be stored encrypted in content. It is used only to receive NIP-61 nutzaps.
Warning!

The privkey entry is a separate key made only for the wallet. It is not the user's Nostr private key, and it has no association with it. Never reuse the Nostr key here.

3 Token Events — Kind 7375

Token events record the unspent proofs of the wallet. A wallet can have many kind:7375 events for the same mint. Each event can hold many proofs. The content is a NIP-44 encrypted JSON object. The outer tags array stays empty.

Token event - kind 7375
{
  "kind": 7375,
  "content": nip44_encrypt({
    "mint": "https://stablenut.umint.cash",
    "unit": "sat",
    "proofs": [
      // one or more proofs in the default cashu format
      {
        "id": "005c2502034d4f12",
        "amount": 1,
        "secret": "z+zyxAVLRqN9lEjxuNPSyRJzEstbl69Jc1vtimvtkPg=",
        "C": "0241d98a8197ef238a192d47edf191a9de78b657308937b4f7dd0aa53beae72c46"
      }
    ],
    // token events destroyed while creating this one
    "del": [ "token-event-id-1", "token-event-id-2" ]
  }),
  "tags": []
}
Token content fields (inside the NIP-44 payload)
FieldRule
mintThe mint the proofs belong to.
proofsUnencoded Cashu proofs. Each proof has id, amount, secret, and C.
unitThe base unit of the proofs, for example sat, usd, or eur. Default is sat when omitted.
delIDs of token events destroyed by the creation of this token. This assists with state transitions.

State transition on spend

When the wallet spends one or more proofs of a token event, the client must move the state forward:

  1. Delete the old token event with a NIP-09 deletion request.
  2. If some proofs of that event are still unspent, create a new token event. Roll the unspent proofs into it. Add any change outputs to it. The change output should include a del field.
  3. The kind:5 delete event from the NIP-09 process MUST carry the tag ["k", "7375"]. This lets clients that watch state transitions filter for it.
7375 (old) proofs: 1+2+4+8 7375 (new) rollover + del 5 delete 7376 history MUST roll over MUST ["k","7375"] SHOULD record unspent proofs NIP-09 optional
Fig. 60-2 — Token state transition. A spend destroys the old token event and creates a rollover, a delete, and an optional history entry.

4 Spending History — Kind 7376

Clients SHOULD publish a kind:7376 event whenever the wallet balance changes. Together these events form a transaction history. The history is for informational purposes only, and it is completely optional.

Spending history event - kind 7376
{
  "kind": 7376,
  "content": nip44_encrypt([
    [ "direction", "in" ], // in = received, out = sent
    [ "amount", "1" ],
    [ "unit", "sat" ],
    [ "e", "<event-id-of-created-token>", "", "created" ]
  ]),
  "tags": [
    [ "e", "<event-id-of-created-token>", "", "redeemed" ]
  ]
}
History content entries
EntryRule
directionThe direction of the transaction. in means received funds. out means sent funds.
amountThe amount of the transaction, as a string.
unitThe base unit of the amount, for example sat, usd, or eur. Default is sat when omitted.
eReference to a token event, with a marker that states its role. See below.

Clients MUST add e tags that reference the destroyed and created token events. Each e tag carries a marker as its fourth element:

e-tag markers
MarkerMeaning
createdA new token event was created.
destroyedA token event was destroyed.
redeemedA NIP-61 nutzap was redeemed.

Encryption rules for the history event: all tags can be NIP-44 encrypted, that is, moved into the encrypted content array. Multiple e tags can be added, and they should be encrypted. The exception is the redeemed marker. Clients SHOULD leave e tags with a redeemed marker unencrypted, in the plain tags array.

Hint!

Why keep redeemed tags public? They point at NIP-61 nutzap events. A public link proves the nutzap was claimed, without exposing the rest of the wallet history.

5 Quote Events — Kind 7374 (Optional)

When the wallet creates a quote at a mint, the client can store the quote ID in a kind:7374 event. The client later uses the quote ID to check when the quote has been paid. The content is the NIP-44 encrypted quote ID.

Create these events with a NIP-40 expiration tag of 2 weeks. That is around the maximum time a Lightning payment can stay in flight. Application developers SHOULD use local state when possible. Publish this event only when it makes sense in the context of the application.

Quote event - kind 7374
{
  "kind": 7374,
  "content": nip44_encrypt("quote-id"),
  "tags": [
    [ "expiration", "<expiration-timestamp>" ],
    [ "mint", "<mint-url>" ]
  ]
}
Quote event tags
TagRule
expirationNIP-40 expiration timestamp. Set it about 2 weeks ahead.
mintThe URL of the mint that issued the quote.

6 Client Flow

A client that wants to load a user's wallet starts with relay discovery. Fetch the user's kind:10019 events from the user's relays. Kind 10019 is the nutzap relay list from NIP-61. If no event is found, fall back to the user's NIP-65 relays.

From those relays, fetch the wallet and token events with one filter:

Fetch wallet and token list
"kinds": [17375, 7375], "authors": ["<my-pubkey>"]

Decrypt each event with the user's key. The wallet event yields the mint list and the P2PK private key. The token events yield the unspent proofs. Their sum is the wallet balance.

Tech Note

The source document has an empty section named "Fetch proofs" inside the Flow chapter. It defines no rules. The filter above already returns the token events that carry the proofs. Also note: kind 10019 is defined in NIP-61, so wallet discovery depends on that NIP even for wallets that never receive nutzaps.

Validating proofs (Appendix 1)

Clients can optionally validate proofs against the mint. This guards against working from an old state. The spec leaves the timing and the reason to each implementation. If checked proofs turn out to be spent, the client should delete the token event and roll over any unspent proof, as in §3.

7 Worked Example

Alice holds one token event with proofs of 1, 2, 4, and 8 sats. She spends 4 sats. Her client performs the full state transition.

Before - token event "event-id-1"
{
  "kind": 7375,
  "id": "event-id-1",
  "content": nip44_encrypt({
    "mint": "https://stablenut.umint.cash",
    "unit": "sat",
    "proofs": [
      { "id": "1", "amount": 1 },
      { "id": "2", "amount": 2 },
      { "id": "3", "amount": 4 },
      { "id": "4", "amount": 8 }
    ]
  }),
  "tags": []
}

Her client MUST roll over the unspent proofs into a new token event. The del array names the destroyed event:

After - rollover token event "event-id-2"
{
  "kind": 7375,
  "id": "event-id-2",
  "content": nip44_encrypt({
    "mint": "https://stablenut.umint.cash",
    "unit": "sat",
    "proofs": [
      { "id": "1", "amount": 1 },
      { "id": "2", "amount": 2 },
      { "id": "4", "amount": 8 }
    ],
    "del": [ "event-id-1" ]
  }),
  "tags": []
}

Then the client MUST delete event-id-1 with a NIP-09 request that carries ["k", "7375"]. It SHOULD add event-id-1 to the del array, as shown above. It SHOULD also record the spend:

History entry - kind 7376
{
  "kind": 7376,
  "content": nip44_encrypt([
    [ "direction", "out" ],
    [ "amount", "4" ],
    [ "unit", "sat" ],
    [ "e", "<event-id-1>", "", "destroyed" ],
    [ "e", "<event-id-2>", "", "created" ]
  ]),
  "tags": []
}
Warning!

Never publish a proof twice. If two token events carry the same proof, a stale client can try a double-spend and fail at the mint. Always pair the new event with the delete and the del field.

8 Version History

NIP-60 arrived in one commit together with NIP-61. Notable changes, newest first:

  • 20252025-10Companion update with NIP-61: a base unit tag was added on the nutzap side.
  • 20252025-02fiatjaf simplified NIP-60/61: the wallet became a single replaceable kind:17375 event with encrypted mint and privkey entries.
  • 20252025-01State transitions became consistent: the del field and the mandatory ["k", "7375"] tag on delete events.
  • 20252025-01Terminology updated: "parameterized replaceable" became "addressable" across the spec.
  • 20252025Typo, link, heading-level, and syntax-highlighting fixes.
  • Origin2024-10-25Pablo Fernandez proposed Cashu wallets and nutzaps in one PR (#1369).

Original Proposal - Credits

AuthorPablo Fernandez
First commit2024-10-25 — “Cashu wallet + Nutzaps” (PR #1369)
Statusdraft - optional
Source60.md on GitHub - full change history

9 See Also

  • NIP-01 — the event object, replaceable kinds, and filters used throughout this chapter.
  • NIP-61 — nutzaps: how other users pay P2PK-locked ecash into this wallet; defines kind:10019.
  • NIP-44 — the encryption scheme for every content payload here.
  • NIP-09 — deletion requests, required for token state transitions.
  • NIP-40 — the expiration tag used by quote events.
  • NIP-65 — the relay list used as a discovery fallback.