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.
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.
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.
{
"kind": 17375,
"content": nip44_encrypt([
[ "privkey", "hexkey" ],
[ "mint", "https://mint1" ],
[ "mint", "https://mint2" ]
]),
"tags": []
}
| Entry | Rule |
|---|---|
| mint | A mint this wallet uses. There MUST be one or more mint entries. |
| privkey | Private key that unlocks P2PK-locked ecash. It MUST be stored encrypted in content. It is used only to receive NIP-61 nutzaps. |
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.
{
"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": []
}
| Field | Rule |
|---|---|
| mint | The mint the proofs belong to. |
| proofs | Unencoded Cashu proofs. Each proof has id, amount, secret, and C. |
| unit | The base unit of the proofs, for example sat, usd, or eur. Default is sat when omitted. |
| del | IDs 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:
- Delete the old token event with a NIP-09 deletion request.
- 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
delfield. - The
kind:5delete event from the NIP-09 process MUST carry the tag["k", "7375"]. This lets clients that watch state transitions filter for it.
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.
{
"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" ]
]
}
| Entry | Rule |
|---|---|
| direction | The direction of the transaction. in means received funds. out means sent funds. |
| amount | The amount of the transaction, as a string. |
| unit | The base unit of the amount, for example sat, usd, or eur. Default is sat when omitted. |
| e | Reference 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:
| Marker | Meaning |
|---|---|
| created | A new token event was created. |
| destroyed | A token event was destroyed. |
| redeemed | A 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.
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.
{
"kind": 7374,
"content": nip44_encrypt("quote-id"),
"tags": [
[ "expiration", "<expiration-timestamp>" ],
[ "mint", "<mint-url>" ]
]
}
| Tag | Rule |
|---|---|
| expiration | NIP-40 expiration timestamp. Set it about 2 weeks ahead. |
| mint | The 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:
"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.
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.
{
"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:
{
"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:
{
"kind": 7376,
"content": nip44_encrypt([
[ "direction", "out" ],
[ "amount", "4" ],
[ "unit", "sat" ],
[ "e", "<event-id-1>", "", "destroyed" ],
[ "e", "<event-id-2>", "", "created" ]
]),
"tags": []
}
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
unittag was added on the nutzap side. - 20252025-02fiatjaf simplified NIP-60/61: the wallet became a single replaceable
kind:17375event with encryptedmintandprivkeyentries. - 20252025-01State transitions became consistent: the
delfield 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
| Author | Pablo Fernandez |
| First commit | 2024-10-25 — “Cashu wallet + Nutzaps” (PR #1369) |
| Status | draft - optional |
| Source | 60.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
contentpayload here. - NIP-09 — deletion requests, required for token state transitions.
- NIP-40 — the
expirationtag used by quote events. - NIP-65 — the relay list used as a discovery fallback.