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_twith their event IDs (unspent proofs) - Wallet initialized flag
- Mutex for thread safety
Lifecycle:
cashu_wallet_init(config)— called during agent startupcashu_wallet_load_from_relays()— fetch kind:17375 + kind:7375 events, decrypt, populate statecashu_wallet_create_new(mint_urls, mint_count)— generate wallet privkey, publish kind:17375cashu_wallet_cleanup()— free all state
Operations:
cashu_wallet_balance()— sum all unspent proofs, grouped by mintcashu_wallet_balance_total()— total across all mintscashu_wallet_mint_quote(mint_url, amount, unit)— request Lightning invoice from mintcashu_wallet_check_mint_quote(mint_url, quote_id)— poll quote statuscashu_wallet_claim_tokens(mint_url, quote_id, amount)— mint tokens after invoice paid, publish kind:7375cashu_wallet_melt_quote(mint_url, payment_request, unit)— get quote to pay LN invoicecashu_wallet_melt_pay(mint_url, quote_id, proofs)— pay LN invoice, delete spent tokens, rollover changecashu_wallet_swap(mint_url, proofs, target_amounts)— split/merge denominationscashu_wallet_check_proofs(mint_url)— validate proof spendability, clean up spentcashu_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_urlto 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.hwith wallet state structs and function declarations - Create
src/cashu_wallet.cwith: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, buildnostr_nip60_wallet_data_t, publish kind:17375 vianostr_handler_publish_kind_event()cashu_wallet_load_from_relays()— usenostr_nip60_create_wallet_filter()+nostr_handler_query_json()to fetch and parse wallet + token eventscashu_wallet_balance()/cashu_wallet_balance_total()— iterate in-memory tokens, callnostr_nip60_sum_proofs()
Phase 2: Mint Interaction (Fund & Pay)
- Add to
src/cashu_wallet.c:cashu_wallet_mint_quote()— callcashu_mint_request_mint_quote()cashu_wallet_check_mint_quote()— callcashu_mint_check_mint_quote()cashu_wallet_claim_tokens()— callcashu_mint_mint_tokens(), parse response proofs vianostr_nip60_proofs_from_json(), create and publish kind:7375 token event, publish kind:7376 historycashu_wallet_melt_quote()— callcashu_mint_request_melt_quote()cashu_wallet_melt_pay()— select proofs to spend, callcashu_mint_melt_tokens(), delete spent token events vianostr_nip60_create_token_deletion(), rollover change vianostr_nip60_create_rollover_token(), publish kind:7376 historycashu_wallet_swap()— callcashu_mint_swap(), update token eventscashu_wallet_check_proofs()— callcashu_mint_check_proofs_state(), clean up spent proofscashu_wallet_get_info()— callcashu_mint_get_info()
Phase 3: Config & Startup Integration
- Add
cashu_wallet_config_ttosrc/config.h - Parse
"cashu_wallet"section insrc/config.c - Call
cashu_wallet_init()fromsrc/main.cafteragent_init() - If
auto_loadis true, callcashu_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.ctoMakefileSRCS
Phase 4: LLM Tools
- Create
src/tools/tool_cashu_wallet.cimplementing 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.ctoMakefileSRCS
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
-
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).
-
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.
-
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.
-
Thread safety — wallet operations are called from the LLM tool loop which may run concurrently with triggered skills. A mutex protects all state mutations.
-
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 |