feat: add NIP-60/61 and applesauce-wallet skills

Add comprehensive skill documentation for:
- NIP-60: Cashu wallets on Nostr (kind:17375, 7375, 7376, 7374)
- NIP-61: Nutzaps - P2PK-locked Cashu payments (kind:9321, 10019)
- applesauce-wallet: hzrd149's package for NIP-60 implementation
  including actions, casts, helpers, and integration patterns
This commit is contained in:
Claude
2026-01-23 22:42:54 +00:00
parent f60964d210
commit 65aff7cc87
2 changed files with 955 additions and 0 deletions

View File

@@ -0,0 +1,586 @@
---
name: applesauce-wallet
description: This skill should be used when building NIP-60 Cashu wallets using the applesauce-wallet package. Covers wallet actions (CreateWallet, UnlockWallet, ReceiveNutzaps, TokensOperation), casts (Wallet, WalletToken, Nutzap), helpers (IndexedDBCouch), and integration with the applesauce ecosystem.
---
# applesauce-wallet Package
## Purpose
This skill provides expert-level assistance with `applesauce-wallet`, a TypeScript package for building NIP-60 Cashu wallets and NIP-61 nutzaps on Nostr. Part of the applesauce ecosystem by hzrd149.
## When to Use
Activate this skill when:
- Building a NIP-60 wallet with applesauce
- Implementing nutzap sending/receiving
- Managing Cashu tokens on Nostr
- Working with wallet actions and casts
- Integrating wallet functionality into a Nostr client
## Installation
```bash
npm install applesauce-wallet
# Also need related packages:
npm install applesauce-core applesauce-actions applesauce-react @cashu/cashu-ts
```
## Package Structure
### Imports
```typescript
// Actions - wallet operations
import {
CreateWallet,
UnlockWallet,
ReceiveNutzaps,
ReceiveToken,
ConsolidateTokens,
RecoverFromCouch,
SetWalletMints,
SetWalletRelays,
AddNutzapInfoMint,
RemoveNutzapInfoMint,
TokensOperation,
} from "applesauce-wallet/actions";
// Casts - reactive data wrappers
import {
Wallet,
WalletToken,
WalletHistory,
Nutzap,
} from "applesauce-wallet/casts";
// Helpers - utilities and constants
import {
getWalletRelays,
IndexedDBCouch,
WALLET_KIND,
WALLET_TOKEN_KIND,
WALLET_HISTORY_KIND,
NUTZAP_KIND,
} from "applesauce-wallet/helpers";
// IMPORTANT: Import casts to enable user.wallet$ property
import "applesauce-wallet/casts";
```
## Event Kind Constants
```typescript
const WALLET_KIND = 17375; // Wallet configuration
const WALLET_TOKEN_KIND = 7375; // Token storage
const WALLET_HISTORY_KIND = 7376; // Spending history
const NUTZAP_KIND = 9321; // Nutzap events
const NUTZAP_INFO_KIND = 10019; // Nutzap recipient info
```
## Core Setup
### Prerequisites
```typescript
import { EventStore, EventFactory } from "applesauce-core";
import { ActionRunner } from "applesauce-actions";
import { RelayPool } from "applesauce-relay";
import { ProxySigner } from "applesauce-accounts";
import { persistEncryptedContent } from "applesauce-common/helpers";
import { IndexedDBCouch } from "applesauce-wallet/helpers";
// Create singletons
const eventStore = new EventStore();
const pool = new RelayPool();
const couch = new IndexedDBCouch();
// Setup encrypted content persistence
const storage$ = new BehaviorSubject<SecureStorage | null>(null);
persistEncryptedContent(eventStore, storage$.pipe(defined()));
// Create action runner
const factory = new EventFactory({
signer: new ProxySigner(signer$.pipe(defined()))
});
const actions = new ActionRunner(eventStore, factory, async (event) => {
// Publish handler - determine relays and publish
const relays = getPublishRelays(event);
await pool.publish(relays, event);
});
```
## Wallet Casts
### Wallet
The main wallet cast provides reactive observables for wallet state.
```typescript
import { castUser } from "applesauce-common/casts";
import "applesauce-wallet/casts"; // Enable wallet$ property
const user = castUser(pubkey, eventStore);
// Access wallet
const wallet = use$(user.wallet$);
// Wallet properties (all are observables)
wallet.unlocked // boolean - whether wallet is decrypted
wallet.mints$ // string[] - configured mints
wallet.relays$ // string[] - wallet relays
wallet.balance$ // { [mint: string]: number } - balance by mint
wallet.tokens$ // WalletToken[] - all token events
wallet.received$ // string[] - received nutzap IDs
```
### WalletToken
Represents a kind:7375 token event.
```typescript
interface WalletToken {
id: string; // Event ID
event: NostrEvent; // Raw event
unlocked: boolean; // Is content decrypted
mint?: string; // Mint URL (when unlocked)
proofs?: Proof[]; // Cashu proofs (when unlocked)
seen?: string[]; // Relays where seen
// Observables
meta$: Observable<TokenMeta>; // Parsed metadata
amount$: Observable<number>; // Total amount
}
```
### WalletHistory
Represents a kind:7376 history event.
```typescript
interface WalletHistory {
id: string;
event: NostrEvent;
unlocked: boolean;
// Observable
meta$: Observable<{
direction: "in" | "out";
amount: number;
mint?: string;
unit?: string;
}>;
}
```
### Nutzap
Represents a kind:9321 nutzap event.
```typescript
interface Nutzap {
id: string;
event: NostrEvent;
amount: number; // Total nutzap amount
mint?: string; // Mint URL
comment?: string; // Message content
createdAt: Date; // Event timestamp
// Cast references
sender: User; // Sender user cast
// Observables
zapped$: Observable<NostrEvent | undefined>; // Referenced event
}
```
## User Extensions
When you import `"applesauce-wallet/casts"`, the User cast is extended with:
```typescript
// Available on User cast
user.wallet$ // Observable<Wallet | undefined>
user.nutzap$ // Observable<NutzapInfo | undefined>
// NutzapInfo structure
interface NutzapInfo {
mints: Array<{ mint: string; units: string[] }>;
relays?: string[];
pubkey?: string; // P2PK pubkey for receiving
}
```
## Wallet Actions
### CreateWallet
Creates a new NIP-60 wallet.
```typescript
import { CreateWallet } from "applesauce-wallet/actions";
import { generateSecretKey } from "nostr-tools";
await actions.run(CreateWallet, {
mints: ["https://mint1.example.com", "https://mint2.example.com"],
privateKey: generateSecretKey(), // For nutzap reception (optional)
relays: ["wss://relay1.example.com", "wss://relay2.example.com"]
});
```
### UnlockWallet
Decrypts wallet content using NIP-44.
```typescript
import { UnlockWallet } from "applesauce-wallet/actions";
await actions.run(UnlockWallet, {
history: true, // Also unlock history events
tokens: true // Also unlock token events
});
```
### ReceiveToken
Receives a Cashu token and adds it to the wallet.
```typescript
import { ReceiveToken } from "applesauce-wallet/actions";
import { getDecodedToken } from "@cashu/cashu-ts";
const token = getDecodedToken(tokenString);
await actions.run(ReceiveToken, token, { couch });
```
### ReceiveNutzaps
Claims one or more nutzap events.
```typescript
import { ReceiveNutzaps } from "applesauce-wallet/actions";
// Single nutzap
await actions.run(ReceiveNutzaps, nutzapEvent, couch);
// Multiple nutzaps
const nutzapEvents = nutzaps.map(n => n.event);
await actions.run(ReceiveNutzaps, nutzapEvents, couch);
```
### TokensOperation
Generic operation on wallet tokens. Used for sending, swapping, etc.
```typescript
import { TokensOperation } from "applesauce-wallet/actions";
await actions.run(
TokensOperation,
amount, // Amount to operate on
async ({ selectedProofs, mint, cashuWallet }) => {
// cashuWallet is a @cashu/cashu-ts Wallet instance
const { keep, send } = await cashuWallet.ops
.send(amount, selectedProofs)
.run();
return {
change: keep.length > 0 ? keep : undefined
};
},
{ mint: selectedMint, couch } // Options
);
```
### SetWalletMints
Updates the wallet's configured mints.
```typescript
import { SetWalletMints } from "applesauce-wallet/actions";
const newMints = ["https://mint1.com", "https://mint2.com"];
await actions.run(SetWalletMints, newMints);
```
### SetWalletRelays
Updates the wallet's relays.
```typescript
import { SetWalletRelays } from "applesauce-wallet/actions";
const newRelays = ["wss://relay1.com", "wss://relay2.com"];
await actions.run(SetWalletRelays, newRelays);
```
### AddNutzapInfoMint / RemoveNutzapInfoMint
Manages mints in the user's kind:10019 nutzap info.
```typescript
import { AddNutzapInfoMint, RemoveNutzapInfoMint } from "applesauce-wallet/actions";
// Add mint to nutzap config
await actions.run(AddNutzapInfoMint, {
url: "https://mint.example.com",
units: ["sat"]
});
// Remove mint from nutzap config
await actions.run(RemoveNutzapInfoMint, "https://mint.example.com");
```
### ConsolidateTokens
Merges multiple small tokens into fewer larger ones.
```typescript
import { ConsolidateTokens } from "applesauce-wallet/actions";
await actions.run(ConsolidateTokens, {
unlockTokens: true,
couch
});
```
### RecoverFromCouch
Recovers tokens stored in the couch during failed operations.
```typescript
import { RecoverFromCouch } from "applesauce-wallet/actions";
await actions.run(RecoverFromCouch, couch);
```
## IndexedDBCouch
The "couch" is temporary storage for proofs during operations that could fail.
```typescript
import { IndexedDBCouch } from "applesauce-wallet/helpers";
const couch = new IndexedDBCouch();
// Used in operations
await actions.run(ReceiveToken, token, { couch });
await actions.run(TokensOperation, amount, callback, { couch });
```
**Why use a couch?**
- Prevents losing proofs if app crashes mid-operation
- Enables recovery of stuck transactions
- Provides atomic operation semantics
## Subscribing to Wallet Events
```typescript
import { use$ } from "applesauce-react/hooks";
import { relaySet } from "applesauce-core/helpers";
// Subscribe to wallet-related events
use$(() => {
const relays = relaySet(walletRelays, userOutboxes);
if (relays.length === 0) return undefined;
return pool.subscription(
relays,
[
// Wallet events
{
kinds: [WALLET_KIND, WALLET_TOKEN_KIND, WALLET_HISTORY_KIND],
authors: [user.pubkey]
},
// Token deletions
{
kinds: [kinds.EventDeletion],
"#k": [String(WALLET_TOKEN_KIND)]
}
],
{ eventStore }
);
}, [walletRelays, userOutboxes, user.pubkey]);
// Subscribe to incoming nutzaps
use$(() => {
const relays = relaySet(nutzapRelays, userInboxes);
if (relays.length === 0) return undefined;
return pool.subscription(
relays,
{ kinds: [NUTZAP_KIND], "#p": [user.pubkey] },
{ eventStore }
);
}, [nutzapRelays, userInboxes, user.pubkey]);
```
## Complete Send Example
```typescript
import { TokensOperation } from "applesauce-wallet/actions";
import { getEncodedToken } from "@cashu/cashu-ts";
async function sendTokens(amount: number, selectedMint?: string) {
let createdToken: string | null = null;
await actions.run(
TokensOperation,
amount,
async ({ selectedProofs, mint, cashuWallet }) => {
const { keep, send } = await cashuWallet.ops
.send(amount, selectedProofs)
.run();
// Encode token for sharing
createdToken = getEncodedToken({
mint,
proofs: send,
unit: "sat"
});
return {
change: keep.length > 0 ? keep : undefined
};
},
{ mint: selectedMint, couch }
);
return createdToken;
}
```
## Complete Receive Example
```typescript
import { ReceiveToken } from "applesauce-wallet/actions";
import { getDecodedToken } from "@cashu/cashu-ts";
async function receiveToken(tokenString: string) {
const token = getDecodedToken(tokenString.trim());
if (!token) {
throw new Error("Failed to decode token");
}
await actions.run(ReceiveToken, token, { couch });
}
```
## Auto-Unlock Pattern
```typescript
const unlocking = useRef(false);
useEffect(() => {
if (unlocking.current || !autoUnlock) return;
let needsUnlock = false;
if (wallet && !wallet.unlocked) needsUnlock = true;
if (tokens?.some(t => !t.unlocked)) needsUnlock = true;
if (history?.some(h => !h.unlocked)) needsUnlock = true;
if (needsUnlock) {
unlocking.current = true;
actions
.run(UnlockWallet, { history: true, tokens: true })
.catch(console.error)
.finally(() => {
unlocking.current = false;
});
}
}, [wallet?.unlocked, tokens?.length, history?.length, autoUnlock]);
```
## Nutzap Timeline
```typescript
import { castTimelineStream } from "applesauce-common/observable";
import { Nutzap } from "applesauce-wallet/casts";
const nutzaps = use$(
() => eventStore
.timeline({ kinds: [NUTZAP_KIND], "#p": [user.pubkey] })
.pipe(castTimelineStream(Nutzap, eventStore)),
[user.pubkey]
);
// Filter unclaimed nutzaps
const unclaimed = useMemo(() => {
if (!nutzaps || !received) return nutzaps || [];
return nutzaps.filter(n => !received.includes(n.id));
}, [nutzaps, received]);
```
## Helper Functions
### getWalletRelays
Extract relay URLs from a wallet event.
```typescript
import { getWalletRelays } from "applesauce-wallet/helpers";
const wallet = await firstValueFrom(
eventStore.replaceable(WALLET_KIND, pubkey)
);
const relays = wallet ? getWalletRelays(wallet) : [];
```
## Integration with cashu-ts
The wallet actions use `@cashu/cashu-ts` internally. In `TokensOperation`:
```typescript
async ({ selectedProofs, mint, cashuWallet }) => {
// cashuWallet is a @cashu/cashu-ts Wallet instance
// Already initialized and connected to the mint
// Use cashu-ts WalletOps API
const { keep, send } = await cashuWallet.ops
.send(amount, selectedProofs)
.run();
// Return change proofs to be stored
return { change: keep };
}
```
## Error Handling
```typescript
try {
await actions.run(ReceiveNutzaps, nutzapEvents, couch);
} catch (err) {
if (err instanceof Error) {
console.error("Failed to receive nutzaps:", err.message);
}
// Tokens are safely stored in couch
// Can recover with RecoverFromCouch action
}
```
## Best Practices
1. **Always use couch**: Pass `couch` to operations that modify tokens
2. **Unlock before operations**: Check `wallet.unlocked` before actions
3. **Handle recovery**: Periodically run `RecoverFromCouch` on app start
4. **Subscribe to events**: Keep wallet data synced via subscriptions
5. **Check mint support**: Verify mint is in wallet config before operations
## Related Packages
- `applesauce-core` - Core event store and utilities
- `applesauce-actions` - ActionRunner for executing actions
- `applesauce-react` - React hooks like `use$`
- `applesauce-common` - Common helpers and casts
- `@cashu/cashu-ts` - Cashu protocol implementation
## Official Resources
- [Applesauce Documentation](https://hzrd149.github.io/applesauce/)
- [GitHub Repository](https://github.com/hzrd149/applesauce)
- [NIP-60 Specification](https://github.com/nostr-protocol/nips/blob/master/60.md)
- [NIP-61 Specification](https://github.com/nostr-protocol/nips/blob/master/61.md)

View File

@@ -0,0 +1,369 @@
---
name: nip-60-61
description: This skill should be used when implementing NIP-60 Cashu wallets or NIP-61 nutzaps on Nostr. Covers the wallet event structure (kind:17375), token events (kind:7375), spending history (kind:7376), nutzap events (kind:9321), nutzap info (kind:10019), P2PK locking, and wallet workflows.
---
# NIP-60 & NIP-61: Cashu Wallets and Nutzaps
## Purpose
This skill provides expert-level assistance with NIP-60 (Cashu Wallets) and NIP-61 (Nutzaps) - the Nostr protocols for storing ecash wallets on relays and sending P2PK-locked Cashu payments.
## When to Use
Activate this skill when:
- Implementing a NIP-60 compatible Cashu wallet
- Sending or receiving nutzaps (NIP-61)
- Working with encrypted wallet events
- Managing ecash proofs on Nostr relays
- Implementing P2PK-locked Cashu transfers
- Building nutzap-enabled Nostr clients
## NIP-60: Cashu Wallets
### Overview
NIP-60 enables Cashu-based wallets with information stored on Nostr relays, providing:
- **Ease of use**: Users can receive funds without external accounts
- **Interoperability**: Wallet follows users across applications
- **Privacy**: Content encrypted with NIP-44
### Event Kinds
| Kind | Name | Purpose |
|------|------|---------|
| **17375** | Wallet | Replaceable wallet configuration |
| **7375** | Token | Unspent proof storage |
| **7376** | History | Spending/receiving history |
| **7374** | Quote | Pending mint quotes |
| **10019** | Nutzap Info | Recipient preferences (NIP-61) |
### Wallet Event (kind:17375)
Replaceable event storing wallet configuration. Content is NIP-44 encrypted.
```json
{
"kind": 17375,
"content": "<NIP-44 encrypted JSON>",
"tags": [
["mint", "https://mint1.example.com", "sat"],
["mint", "https://mint2.example.com", "sat"],
["relay", "wss://relay1.example.com"],
["relay", "wss://relay2.example.com"]
]
}
```
**Encrypted content structure:**
```json
{
"privkey": "<hex private key for P2PK>"
}
```
**Important**: The `privkey` is a **separate key** exclusively for the wallet, NOT the user's Nostr private key. Used for:
- Receiving NIP-61 nutzaps (P2PK-locked tokens)
- Unlocking tokens locked to the wallet's pubkey
### Token Event (kind:7375)
Records unspent Cashu proofs. Multiple events can exist per mint.
```json
{
"kind": 7375,
"content": "<NIP-44 encrypted JSON>",
"tags": []
}
```
**Encrypted content structure:**
```json
{
"mint": "https://mint.example.com",
"unit": "sat",
"proofs": [
{
"id": "009a1f293253e41e",
"amount": 8,
"secret": "secret_string",
"C": "02abc..."
}
],
"del": ["<token_event_id_1>", "<token_event_id_2>"]
}
```
| Field | Description |
|-------|-------------|
| `mint` | Mint URL |
| `unit` | Currency unit (default: "sat") |
| `proofs` | Array of unspent Cashu proofs |
| `del` | IDs of deleted/spent token events |
### Spending History Event (kind:7376)
Optional events documenting transactions.
```json
{
"kind": 7376,
"content": "<NIP-44 encrypted JSON>",
"tags": [
["e", "<created_token_id>", "", "created"],
["e", "<destroyed_token_id>", "", "destroyed"],
["e", "<redeemed_nutzap_id>", "", "redeemed"]
]
}
```
**Encrypted content structure:**
```json
{
"direction": "in",
"amount": 100,
"unit": "sat"
}
```
**Direction values:**
- `"in"` - Received tokens
- `"out"` - Sent tokens
**Tag markers:**
- `created` - New token event created
- `destroyed` - Token event consumed/deleted
- `redeemed` - Nutzap event redeemed (leave unencrypted)
### Quote Event (kind:7374)
Tracks pending mint quotes for deposits.
```json
{
"kind": 7374,
"content": "<NIP-44 encrypted quote_id>",
"tags": [
["expiration", "<unix_timestamp>"],
["mint", "https://mint.example.com"]
]
}
```
Expiration typically ~2 weeks. Delete after quote is used or expired.
### Relay Discovery
Clients discover wallet relays from:
1. Kind 10019 event (preferred)
2. Kind 10002 NIP-65 relay list (fallback)
## NIP-61: Nutzaps
### Overview
Nutzaps are P2PK-locked Cashu tokens sent via Nostr. The payment itself serves as the receipt - no Lightning invoices needed.
Key principle: **"A Nutzap is a P2PK Cashu token in which the payment itself is the receipt."**
### Nutzap Info Event (kind:10019)
Recipients publish this to configure nutzap reception.
```json
{
"kind": 10019,
"content": "",
"tags": [
["relay", "wss://relay1.example.com"],
["relay", "wss://relay2.example.com"],
["mint", "https://mint1.example.com", "sat"],
["mint", "https://mint2.example.com", "sat", "usd"],
["pubkey", "02<compressed_pubkey_hex>"]
]
}
```
| Tag | Description |
|-----|-------------|
| `relay` | Where senders should publish nutzaps |
| `mint` | Trusted mints with supported units |
| `pubkey` | P2PK pubkey for locking tokens (NOT Nostr key) |
**Critical**: The pubkey MUST be prefixed with `02` for Nostr-Cashu compatibility.
### Nutzap Event (kind:9321)
The actual payment event sent by the payer.
```json
{
"kind": 9321,
"content": "Optional message/comment",
"pubkey": "<sender_pubkey>",
"tags": [
["proof", "{\"id\":\"...\",\"amount\":8,\"secret\":\"...\",\"C\":\"...\",\"dleq\":{...}}"],
["proof", "{\"id\":\"...\",\"amount\":4,\"secret\":\"...\",\"C\":\"...\",\"dleq\":{...}}"],
["u", "https://mint.example.com"],
["p", "<recipient_nostr_pubkey>"],
["e", "<nutzapped_event_id>", "<relay_hint>"]
]
}
```
| Tag | Description |
|-----|-------------|
| `proof` | JSON-stringified Cashu proof with DLEQ |
| `u` | Mint URL (must match recipient's config exactly) |
| `p` | Recipient's Nostr pubkey |
| `e` | Event being nutzapped (optional) |
**Proof structure (in proof tag):**
```json
{
"id": "009a1f293253e41e",
"amount": 8,
"secret": "[\"P2PK\",{\"nonce\":\"...\",\"data\":\"02pubkey\"}]",
"C": "02abc...",
"dleq": {
"e": "...",
"s": "...",
"r": "..."
}
}
```
### P2PK Locking Requirements
1. **Use recipient's wallet pubkey** from kind:10019, NOT their Nostr pubkey
2. **Prefix with '02'** for compatibility: `02<32-byte-hex>`
3. **Include DLEQ proofs** for offline verification (NUT-12)
4. Lock using NUT-11 P2PK spending condition
### Validation Rules
Observers verifying nutzaps must confirm:
1. Recipient has kind:10019 listing the mint
2. Token is locked to recipient's specified pubkey
3. Mint URL matches exactly (case-sensitive)
4. DLEQ proof validates offline
### Sending Workflow
```
1. Fetch recipient's kind:10019
2. Select a mint from their trusted list
3. Mint or swap tokens at that mint
4. P2PK-lock proofs to recipient's pubkey
5. Publish kind:9321 to recipient's relays
```
### Receiving Workflow
```
1. Subscribe to kind:9321 events tagged with your pubkey
2. Validate nutzap (mint in config, correct pubkey lock)
3. Swap proofs at the mint (prevents double-claim)
4. Create kind:7375 token event with new proofs
5. Create kind:7376 history event with "redeemed" marker
```
## Wallet Workflows
### Token Spending
When spending tokens:
1. **Select proofs** for the amount to spend
2. **Create new token event** with remaining (unspent) proofs
3. **Add spent token IDs** to the `del` field
4. **Delete original token event** via NIP-09
5. **Create history event** (optional) documenting the transaction
```typescript
// Pseudocode for spending
const { spend, keep } = await selectProofs(wallet.proofs, amount);
// Create new token event with change
if (keep.length > 0) {
await createTokenEvent({
proofs: keep,
del: [originalTokenEventId]
});
}
// Delete original token event
await deleteEvent(originalTokenEventId);
// Record history
await createHistoryEvent({
direction: 'out',
amount: spend.reduce((a, p) => a + p.amount, 0),
tags: [['e', originalTokenEventId, '', 'destroyed']]
});
```
### Token Receiving
When receiving tokens (from nutzap or direct transfer):
1. **Validate token** (correct mint, valid proofs)
2. **Swap proofs** at mint (prevents sender reuse)
3. **Create token event** with new proofs
4. **Create history event** with "redeemed" marker
### Couch Pattern (Safe Operations)
For atomic operations that could fail mid-way:
1. **Store proofs in "couch"** (temporary storage like IndexedDB)
2. **Perform mint operation** (swap, melt, etc.)
3. **On success**: Delete from couch, create new token event
4. **On failure**: Recover from couch
This prevents losing proofs if the app crashes during an operation.
## Security Considerations
1. **Separate wallet key**: Never use your Nostr private key for the wallet
2. **Encrypt everything**: All token data must be NIP-44 encrypted
3. **Verify DLEQ**: Always verify DLEQ proofs when receiving nutzaps
4. **Swap immediately**: Swap received tokens to prevent sender double-spending
5. **Trusted mints only**: Only accept nutzaps from mints you trust
6. **Relay redundancy**: Store wallet data on multiple relays
## Event Kind Reference
| Kind | Name | Type | Encryption |
|------|------|------|------------|
| 7374 | Quote | Regular | NIP-44 |
| 7375 | Token | Regular | NIP-44 |
| 7376 | History | Regular | NIP-44 (partial) |
| 9321 | Nutzap | Regular | None |
| 10019 | Nutzap Info | Replaceable | None |
| 17375 | Wallet | Replaceable | NIP-44 |
## Best Practices
1. **Multiple token events**: Don't put all proofs in one event
2. **Consolidate periodically**: Merge small tokens to reduce event count
3. **Track history**: Create kind:7376 events for audit trail
4. **Handle offline**: Use DLEQ for offline nutzap verification
5. **Clean up quotes**: Delete expired kind:7374 events
6. **Normalize URLs**: Use consistent mint URL formatting
## Official Resources
- [NIP-60 Specification](https://github.com/nostr-protocol/nips/blob/master/60.md)
- [NIP-61 Specification](https://github.com/nostr-protocol/nips/blob/master/61.md)
- [NIP-44 Encryption](https://github.com/nostr-protocol/nips/blob/master/44.md)
- [Cashu NUTs](https://cashubtc.github.io/nuts/)