Files
didactyl/plans/nip60_cashu_wallet.md

13 KiB

NIP-60 Cashu Wallet for Didactyl Agents

Overview

Add a NIP-60 Cashu wallet to every Didactyl agent. The wallet stores its state on Nostr relays (encrypted with the agent's keys) and interacts with Cashu mints over HTTP. The agent manages the wallet autonomously — it can check balances, mint tokens (fund via Lightning), melt tokens (pay Lightning invoices), swap tokens, and persist all state transitions as Nostr events per the NIP-60 spec.

Available nostr_core_lib Primitives

NIP-60 Event Layer (nip060.h)

Function Purpose
nostr_nip60_create_wallet_event() Create kind:17375 wallet event (privkey + mint URLs, NIP-44 encrypted)
nostr_nip60_parse_wallet_event() Decrypt and parse wallet event
nostr_nip60_free_wallet_data() Free wallet data
nostr_nip60_create_token_event() Create kind:7375 token event (proofs, NIP-44 encrypted)
nostr_nip60_parse_token_event() Decrypt and parse token event
nostr_nip60_free_token_data() Free token data
nostr_nip60_create_token_deletion() Create kind:5 deletion for spent tokens
nostr_nip60_create_rollover_token() Create new token with remaining proofs + del references
nostr_nip60_create_history_event() Create kind:7376 spending history event
nostr_nip60_parse_history_event() Decrypt and parse history event
nostr_nip60_free_history_data() Free history data
nostr_nip60_create_quote_event() Create kind:7374 quote tracking event
nostr_nip60_parse_quote_event() Decrypt and parse quote event
nostr_nip60_sum_proofs() Sum proof amounts
nostr_nip60_proofs_to_json() / nostr_nip60_proofs_from_json() Proof serialization
nostr_nip60_create_wallet_filter() Build REQ filter for kinds 17375+7375
nostr_nip60_create_history_filter() Build REQ filter for kind 7376

Cashu Mint HTTP Client (cashu_mint.h)

Function Purpose
cashu_mint_get_info() GET /v1/info — mint name, pubkey, keysets
cashu_mint_request_mint_quote() POST /v1/mint/quote/bolt11 — get Lightning invoice to fund wallet
cashu_mint_check_mint_quote() GET /v1/mint/quote/bolt11/{id} — check if invoice paid
cashu_mint_mint_tokens() POST /v1/mint/bolt11 — claim tokens after invoice paid
cashu_mint_request_melt_quote() POST /v1/melt/quote/bolt11 — get quote to pay Lightning invoice
cashu_mint_check_melt_quote() GET /v1/melt/quote/bolt11/{id} — check melt status
cashu_mint_melt_tokens() POST /v1/melt/bolt11 — pay Lightning invoice with tokens
cashu_mint_swap() POST /v1/swap — swap proofs (split/merge denominations)
cashu_mint_check_proofs_state() POST /v1/checkstate — verify proof spendability

Architecture

graph TD
    A[Agent Main Loop] --> B[Cashu Wallet Manager]
    B --> C[In-Memory Wallet State]
    C --> D[nostr_core_lib NIP-60 Events]
    C --> E[nostr_core_lib Cashu Mint HTTP]
    D --> F[Nostr Relays]
    E --> G[Cashu Mint Servers]
    
    H[LLM Tool Calls] --> I[cashu_wallet_balance]
    H --> J[cashu_wallet_mint_quote]
    H --> K[cashu_wallet_mint_claim]
    H --> L[cashu_wallet_melt_quote]
    H --> M[cashu_wallet_melt_pay]
    H --> N[cashu_wallet_info]
    H --> O[cashu_wallet_check_proofs]
    
    I --> B
    J --> B
    K --> B
    L --> B
    M --> B
    N --> B
    O --> B

Module Boundaries

1. src/cashu_wallet.h / src/cashu_wallet.c — Cashu Wallet Manager

The core wallet module that owns in-memory state and orchestrates all operations.

State:

  • Parsed nostr_nip60_wallet_data_t (privkey, mint URLs)
  • Array of nostr_nip60_token_data_t with their event IDs (unspent proofs)
  • Wallet initialized flag
  • Mutex for thread safety

Lifecycle:

  • cashu_wallet_init(config) — called during agent startup
  • cashu_wallet_load_from_relays() — fetch kind:17375 + kind:7375 events, decrypt, populate state
  • cashu_wallet_create_new(mint_urls, mint_count) — generate wallet privkey, publish kind:17375
  • cashu_wallet_cleanup() — free all state

Operations:

  • cashu_wallet_balance() — sum all unspent proofs, grouped by mint
  • cashu_wallet_balance_total() — total across all mints
  • cashu_wallet_mint_quote(mint_url, amount, unit) — request Lightning invoice from mint
  • cashu_wallet_check_mint_quote(mint_url, quote_id) — poll quote status
  • cashu_wallet_claim_tokens(mint_url, quote_id, amount) — mint tokens after invoice paid, publish kind:7375
  • cashu_wallet_melt_quote(mint_url, payment_request, unit) — get quote to pay LN invoice
  • cashu_wallet_melt_pay(mint_url, quote_id, proofs) — pay LN invoice, delete spent tokens, rollover change
  • cashu_wallet_swap(mint_url, proofs, target_amounts) — split/merge denominations
  • cashu_wallet_check_proofs(mint_url) — validate proof spendability, clean up spent
  • cashu_wallet_get_info(mint_url) — fetch mint info

State Persistence (Nostr events):

  • After every balance-changing operation, publish updated kind:7375 token events
  • Delete old kind:7375 events via kind:5 when proofs are spent
  • Publish kind:7376 history events for audit trail
  • Optionally publish kind:7374 quote events for cross-device state

2. src/tools/tool_cashu_wallet.c — LLM Tool Interface

Exposes wallet operations as tools the LLM can call.

3. Config additions in src/config.h / src/config.c

New cashu_wallet_config_t section.

Config Schema Changes

Add to didactyl_config_t:

typedef struct {
    int enabled;                          // master switch
    char** mint_urls;                     // default mint URLs
    int mint_count;
    char unit[16];                        // default unit, e.g. "sat"
    int auto_load;                        // load wallet from relays on startup
    int mint_timeout_seconds;             // HTTP timeout for mint operations
} cashu_wallet_config_t;

Example config.jsonc:

"cashu_wallet": {
    "enabled": true,
    "mints": [
        "https://mint.minibits.cash",
        "https://stablenut.umint.cash"
    ],
    "unit": "sat",
    "auto_load": true,
    "mint_timeout_seconds": 30
}

Tool Definitions

cashu_wallet_balance

  • Description: Check the agent wallet balance across all mints
  • Parameters: none (or optional mint_url to filter)
  • Returns: { "total_sats": 1234, "by_mint": [{ "mint": "...", "balance": 500 }] }

cashu_wallet_info

  • Description: Get information about a Cashu mint
  • Parameters: { "mint_url": "https://..." }
  • Returns: { "name": "...", "version": "...", "keysets": [...] }

cashu_wallet_mint_quote

  • Description: Request a Lightning invoice to fund the wallet
  • Parameters: { "mint_url": "https://...", "amount": 1000, "unit": "sat" }
  • Returns: { "quote_id": "...", "payment_request": "lnbc...", "expiry": ... }

cashu_wallet_mint_check

  • Description: Check if a mint quote invoice has been paid
  • Parameters: { "mint_url": "https://...", "quote_id": "..." }
  • Returns: { "paid": true/false, "amount": 1000 }

cashu_wallet_mint_claim

  • Description: Claim tokens after a mint quote invoice is paid
  • Parameters: { "mint_url": "https://...", "quote_id": "...", "amount": 1000 }
  • Returns: { "success": true, "new_balance": 2234 }

cashu_wallet_melt_quote

  • Description: Get a quote to pay a Lightning invoice from the wallet
  • Parameters: { "mint_url": "https://...", "payment_request": "lnbc...", "unit": "sat" }
  • Returns: { "quote_id": "...", "amount": 500, "fee_reserve": 2 }

cashu_wallet_melt_pay

  • Description: Pay a Lightning invoice using wallet tokens
  • Parameters: { "mint_url": "https://...", "quote_id": "..." }
  • Returns: { "success": true, "paid": true, "new_balance": 1732 }

cashu_wallet_check_proofs

  • Description: Validate that stored proofs are still spendable
  • Parameters: { "mint_url": "https://..." } (optional, checks all mints if omitted)
  • Returns: { "valid": 15, "spent": 2, "cleaned": true }

Phased Implementation Plan

Phase 1: Cashu Wallet Core Module

  • Create src/cashu_wallet.h with wallet state structs and function declarations
  • Create src/cashu_wallet.c with:
    • cashu_wallet_init() / cashu_wallet_cleanup() lifecycle
    • In-memory state: wallet data + token array with event IDs
    • Mutex protection for thread safety
    • cashu_wallet_create_new() — generate random privkey, build nostr_nip60_wallet_data_t, publish kind:17375 via nostr_handler_publish_kind_event()
    • cashu_wallet_load_from_relays() — use nostr_nip60_create_wallet_filter() + nostr_handler_query_json() to fetch and parse wallet + token events
    • cashu_wallet_balance() / cashu_wallet_balance_total() — iterate in-memory tokens, call nostr_nip60_sum_proofs()

Phase 2: Mint Interaction (Fund & Pay)

  • Add to src/cashu_wallet.c:
    • cashu_wallet_mint_quote() — call cashu_mint_request_mint_quote()
    • cashu_wallet_check_mint_quote() — call cashu_mint_check_mint_quote()
    • cashu_wallet_claim_tokens() — call cashu_mint_mint_tokens(), parse response proofs via nostr_nip60_proofs_from_json(), create and publish kind:7375 token event, publish kind:7376 history
    • cashu_wallet_melt_quote() — call cashu_mint_request_melt_quote()
    • cashu_wallet_melt_pay() — select proofs to spend, call cashu_mint_melt_tokens(), delete spent token events via nostr_nip60_create_token_deletion(), rollover change via nostr_nip60_create_rollover_token(), publish kind:7376 history
    • cashu_wallet_swap() — call cashu_mint_swap(), update token events
    • cashu_wallet_check_proofs() — call cashu_mint_check_proofs_state(), clean up spent proofs
    • cashu_wallet_get_info() — call cashu_mint_get_info()

Phase 3: Config & Startup Integration

  • Add cashu_wallet_config_t to src/config.h
  • Parse "cashu_wallet" section in src/config.c
  • Call cashu_wallet_init() from src/main.c after agent_init()
  • If auto_load is true, call cashu_wallet_load_from_relays() during startup
  • If no wallet event found and mints configured, call cashu_wallet_create_new()
  • Call cashu_wallet_cleanup() in shutdown path
  • Add src/cashu_wallet.c to Makefile SRCS

Phase 4: LLM Tools

  • Create src/tools/tool_cashu_wallet.c implementing all 8 tool execute functions
  • Add tool declarations to src/tools/tools_internal.h
  • Register tools in src/tools/tools_dispatch.c
  • Add JSON schemas in src/tools/tools_schema.c
  • Add src/tools/tool_cashu_wallet.c to Makefile SRCS

Phase 5: Testing & Validation

  • Unit test: create wallet, publish, reload from relay, verify state
  • Integration test: mint quote → claim → check balance → melt quote → pay
  • Test proof rollover: spend partial proofs, verify deletion + new token event
  • Test startup: fresh agent creates wallet; existing agent loads wallet
  • Test error handling: mint offline, invalid proofs, insufficient balance

Key Design Decisions

  1. Wallet privkey is NOT the agent's Nostr privkey — per NIP-60 spec, a separate privkey is generated and stored encrypted in the kind:17375 event. This privkey is used for P2PK ecash locking (future NIP-61 support).

  2. State lives on relays — the in-memory state is a cache. On startup the agent fetches its wallet state from relays. After every mutation, updated events are published back.

  3. Proof selection for spending — when melting, the wallet needs to select proofs that sum to at least the required amount. Implement a simple greedy algorithm (largest-first) with change output via swap.

  4. Thread safety — wallet operations are called from the LLM tool loop which may run concurrently with triggered skills. A mutex protects all state mutations.

  5. No NIP-61 (nutzaps) in this phase — receiving/sending nutzaps is a separate feature that builds on top of this wallet foundation.

File Changes Summary

File Change
src/cashu_wallet.h NEW — cashu wallet manager header
src/cashu_wallet.c NEW — cashu wallet manager implementation
src/tools/tool_cashu_wallet.c NEW — LLM tool implementations
src/config.h Add cashu_wallet_config_t to didactyl_config_t
src/config.c Parse "cashu_wallet" config section
src/main.c Call cashu_wallet_init() / cashu_wallet_cleanup()
src/tools/tools_internal.h Declare execute_cashu_wallet_* functions
src/tools/tools_dispatch.c Register cashu wallet tool dispatch entries
src/tools/tools_schema.c Add cashu wallet tool JSON schemas
Makefile Add cashu_wallet.c and tool_cashu_wallet.c to SRCS