Nostr Wallet Connect (NIP-47)
- Nostr Wallet Connect (NIP-47)
Nostr Wallet Connect (NIP-47)
A Nostr-native protocol for connecting applications to Lightning wallets.
Architecture
┌──────────────────┐ ┌──────────────────┐
│ APP (Client) │ │ WALLET SERVICE │
│ │◄────────▶│ │
│ Amethyst │ Nostr │ Minibits │
│ Damus │ Relays │ Alby │
│ Primal │ │ Mutiny │
│ Snort │ │ Cashu Mint │
└──────────────────┘ └──────────────────┘
│ │
│ Kind 23194 (Request) │
│ ────────────────────────▶ │
│ │
│ Kind 23195 (Response) │
│ ◄──────────────────────── │
│ │
│ All NIP-44 encrypted │
│ Ephemeral (not stored) │
Connection String
The NWC URI connects everything:
nostr+walletconnect://<pubkey>?relay=<relay-url>&secret=<secret>&lud16=<lnaddress>
Example:
nostr+walletconnect://b889ff5b1513...?relay=wss://relay.minibits.cash&secret=715ac01f...&lud16=user@minibits.cash
Components:
| Part | Description |
|---|---|
pubkey |
Wallet service’s Nostr pubkey (hex) |
relay |
Relay URL for communication |
secret |
Shared secret for NIP-44 encryption |
lud16 |
Lightning address for the wallet |
Event Types
Kind 13194 — Wallet Info (Replaceable)
The wallet service publishes its capabilities:
{
"kind": 13194,
"content": "",
"tags": [
["capabilities", "pay_invoice", "get_balance", "make_invoice",
"lookup_invoice", "list_transactions", "multi_pay_invoice",
"multi_pay_keysend"]
]
}
Kind 23194 — Wallet Request (Ephemeral)
App sends encrypted commands to the wallet:
{
"kind": 23194,
"pubkey": "<app-pubkey>",
"content": "<NIP-44 encrypted JSON request>",
"created_at": <now>
}
Kind 23195 — Wallet Response (Ephemeral)
Wallet responds with encrypted results:
{
"kind": 23195,
"pubkey": "<wallet-pubkey>",
"content": "<NIP-44 encrypted JSON response>",
"created_at": <now>
}
Commands
All encoded as JSON inside NIP-44 encrypted content.
pay_invoice — Pay a BOLT 11 invoice
// Request
{
"method": "pay_invoice",
"params": {
"invoice": "lnbc10n1p...",
"amount": 1000 // optional, for zero-amount invoices
}
}
// Response
{
"result_type": "pay_invoice",
"result": {
"preimage": "a1b2c3...",
"fees_paid": 1
}
}
get_balance — Query wallet balance
// Request
{
"method": "get_balance"
}
// Response
{
"result_type": "get_balance",
"result": {
"balance": 500000 // in millisatoshis
}
}
make_invoice — Create a new invoice
// Request
{
"method": "make_invoice",
"params": {
"amount": 100000,
"description": "For pizza",
"expiry": 3600
}
}
// Response
{
"result_type": "make_invoice",
"result": {
"invoice": "lnbc1u1p...",
"payment_hash": "a1b2c3...",
"amount": 100000
}
}
lookup_invoice — Check invoice status
// Request
{
"method": "lookup_invoice",
"params": {
"payment_hash": "a1b2c3..."
}
}
// Response
{
"result_type": "lookup_invoice",
"result": {
"type": "paid",
"preimage": "d4e5f6...",
"settled_at": 1700000000
}
}
list_transactions — Get payment history
// Request
{
"method": "list_transactions",
"params": {
"limit": 10,
"offset": 0,
"type": "incoming"
}
}
// Response
{
"result_type": "list_transactions",
"result": {
"transactions": [{
"type": "incoming",
"invoice": "lnbc...",
"amount": 50000,
"fees_paid": 0,
"preimage": "...",
"settled_at": 1700000000,
"payment_hash": "..."
}]
}
}
multi_pay_invoice — Pay multiple invoices
{
"method": "multi_pay_invoice",
"params": {
"invoices": [
{"id": "1", "invoice": "lnbc...", "amount": 1000},
{"id": "2", "invoice": "lnbc...", "amount": 2000}
]
}
}
multi_pay_keysend — Keysend to multiple recipients
{
"method": "multi_pay_keysend",
"params": {
"keysends": [
{"id": "1", "pubkey": "02a1b2...", "amount": 1000},
{"id": "2", "pubkey": "03c4d5...", "amount": 500}
]
}
}
Error Responses
{
"result_type": "pay_invoice",
"error": {
"code": "INSUFFICIENT_BALANCE",
"message": "Not enough sats in the wallet"
}
}
Common error codes:
| Code | Meaning |
|---|---|
INSUFFICIENT_BALANCE |
Not enough funds |
QUOTA_EXCEEDED |
Daily/monthly limit hit |
INVOICE_EXPIRED |
BOLT 11 expired |
INTERNAL |
Wallet internal error |
UNAUTHORIZED |
Connection not authorized |
Security Model
- Ephemeral events (kind 23xxx): relays don’t store, disappears quickly
- NIP-44 encryption: ChaCha20-Poly1305 authenticated encryption
- Shared secret: Only app and wallet can decrypt
- No relay trust: relays see encrypted blobs, can’t read or tamper
- Connection string: treat the
secretlike a private key
Connection Lifecycle
1. Wallet generates NWC URI (with secret)
2. User copies URI into app
3. App parses URI, extracts pubkey, relay, secret
4. App subscribes to kind 23195 from wallet pubkey
5. App sends commands via kind 23194
6. Wallet reads kind 23194, processes, responds via 23195
7. Either party can revoke by rotating the secret
NWC on Nexus
My setup:
# Check balance
python3 /root/wallet.py balance
# Pay invoice
python3 /root/wallet.py pay "lnbc..."
# List transactions
python3 /root/wallet.py transactions
NWC URI stored in memory with the nostr+walletconnect:// protocol.
Multiple NWC Connections
NIP-47 supports multiple wallet services. Each connection is identified by a different pubkey and relay. Apps can connect to:
- Primary wallet (Minibits)
- Savings wallet (Alby)
- Mint wallet (Cashu via NIP-60)
Future: NIP-47 v2
Planned improvements:
- Better error taxonomy
- Batch operations
- Subscription-based balance updates
- Amount limits / spending controls
- Keysend with messages
Write a comment