Chapter 47 - Payments
47
Nostr Implementation Possibility - Hex № 47

Nostr Wallet Connect (NWC)

Control a remote lightning wallet from any Nostr client through end-to-end encrypted messages over a relay.

1 Overview

Many Nostr clients want to pay lightning invoices. Few devices can run a lightning wallet all day. This NIP connects the two. A client sends payment commands to a remote wallet service. The wallet service executes them against a real wallet and reports back. A custodian may run the wallet service. A user may also run a bridge that connects their own wallet or node to the NWC protocol.

All traffic travels as end-to-end encrypted direct messages over a normal Nostr relay. The relay sees only event kinds and tags. It never sees the payloads. The user's Nostr identity key is never used. This avoids linking payment activity to the user. Ideally each connection uses its own unique key.

This NIP defines the core protocol: three event kinds, a connection URI, five commands, and an error model. More granular authorization models and advanced wallet features live in optional extension specifications in the NWC repository at https://github.com/nostr-wallet-connect/nwc.

Hint!

NWC powers one-tap zaps (NIP-57). The client fetches an invoice, then asks the wallet service to pay it. The user never leaves the app.

2 Actors and Theory of Operation

The protocol names three actors. Use these terms exactly:

Actors
TermMeaning
clientA Nostr app, on any platform, that wants to interact with a lightning wallet.
userThe person who uses the client and wants to connect their wallet to it.
wallet serviceA Nostr app that typically runs on an always-on computer, for example in the cloud or on a Raspberry Pi. It has access to the APIs of the wallets it serves.

A session follows five steps:

  1. The user gets a special connection URI from their NIP-47 compliant wallet application. The wallet may show it as a QR code, a pasteable string, or another form.
  2. The user copies the URI into their client by pasting or scanning. The client saves the URI for all later wallet interaction. The client then requests the info event (kind 13194) from the relays named in the URI. The wallet service has published that event there earlier, and the relays hold it as a replaceable event.
  3. When the user starts a payment, the client creates a pay_invoice request. It encrypts the request with the secret from the URI and sends it as a kind 23194 event to the relays from the URI. The wallet service listens on those relays, decrypts the request, and tells the user's wallet application to send the payment.
  4. When the payment completes, the wallet service sends an encrypted response (kind 23195) back to the client over the same relays.
  5. Optional extension specifications may define additional wallet-to-client events.
Tech Note

The source text of step 3 says the wallet service knows how to talk to the wallet "because the connection URI specified relay(s) that have access to the wallet app API". This contradicts the architecture: relays only carry encrypted events and have no wallet API access. The wallet service itself holds the API access, as the Terms section states. Read the sentence as: the URI names relays where the wallet service listens.

Client holds secret Relay sees kinds only Wallet service always on Lightning wallet node / custodian 23194 23195 API
Fig. 47-1 — NWC architecture. Encrypted requests and responses pass through the relay. Only the wallet service touches the wallet API.

3 The Connection URI

Encryption needs two keys. The connection URI carries the secret key of the client and only the public key of the wallet service. The client discovers the wallet service by scanning a QR code, handling a deeplink, or pasting the URI.

The wallet service generates the URI. The protocol is nostr+walletconnect://. The base path is the 32-byte hex-encoded pubkey of the wallet service. This pubkey SHOULD be unique per client connection.

Connection URI format
nostr+walletconnect://<wallet-service-pubkey, 32-byte hex>
  ?relay=<url-encoded relay URL>       &relay=… may repeat
  &secret=<32-byte random hex string>
  &lud16=<lightning address>
Query string parameters
ParameterPresenceRule
relayRequiredURL of the relay where the wallet service is connected and listens for events. May appear more than once.
secretRequiredA 32-byte randomly generated hex-encoded string. The client MUST use it to sign events and encrypt payloads when it talks to the wallet service. The wallet service MUST use the corresponding public key of this secret to talk to the client.
lud16RecommendedA lightning address. Clients can use it to set the lud16 field on the user's profile automatically if none is configured.

The per-connection secret exists for four reasons:

  • Authorization does not require passing keys back and forth.
  • The user can hold different keys for different applications. Keys can be revoked and created at will, with arbitrary constraints such as budgets.
  • The key is harder to leak. It is not shown to the user and not backed up.
  • It improves privacy. The user's main key is not linked to their payments.

Key usage is symmetric but strict:

  • When the client sends or receives a message, it uses the secret from the URI plus the wallet service's pubkey to encrypt or decrypt.
  • When the wallet service sends or receives a message, it uses its own secret plus the public key that corresponds to the client's secret. The wallet service SHOULD NOT store the secret it generated for the client. It MUST NOT rely on knowing the client secret for general operation.

The client stores the connection and reuses it for every wallet action. This NIP uses ephemeral events. Pick relays that do not close idle connections, so events do not drop. Ideally the relays retain the events until they are consumed or become stale.

Tech Note

Kinds 23194 and 23195 sit in the ephemeral range of NIP-01, so relays are not expected to store them at all. The spec still asks for relays that "retain the events until they are either consumed or become stale". These two goals conflict on a strict NIP-01 relay. In practice, NWC relays keep the events long enough for a listener that is briefly offline.

Example connection string
nostr+walletconnect://b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4
  ?relay=wss%3A%2F%2Frelay.damus.io
  &secret=71a8c14c1407c113601079c4302dab36460f0ccd0ad506f1f2dc73b5100e4f3c
Warning!

The URI contains a live spending secret. Whoever holds it can command the wallet within its budget. Treat the URI like a password. Never publish it in an event.

4 Events

The core protocol uses three event kinds:

Core event kinds
KindNameDirectionStorage class
13194NIP-47 info eventwallet service → relaysreplaceable
23194NIP-47 requestclient → wallet serviceephemeral
23195NIP-47 responsewallet service → clientephemeral

The info event (kind 13194)

The wallet service publishes the info event on the relay as a replaceable event. It advertises the capabilities the service supports. The content is a plaintext string of supported capabilities, space-separated, for example pay_invoice get_balance.

  • If the wallet service supports optional NWC extensions, the info event SHOULD carry an extensions tag with the supported extension identifiers, space-separated, for example 02 03 04.
  • Any additional methods that those extensions provide SHOULD also appear in the content capability list.
  • The info event should also carry an encryption tag with the supported encryption modes. See §7.
Info event - shape
{
  "kind": 13194,
  "tags": [
    ["encryption", "nip44_v2 nip04"],   // supported encryption schemes, see §7
    ["extensions", "02 03 04"]           // supported NWC extension ids, optional
  ],
  "content": "pay_invoice get_balance make_invoice lookup_invoice get_info"
}

Request and response events (kinds 23194 / 23195)

Both request and response events SHOULD carry one p tag. On a request it holds the public key of the wallet service. On a response it holds the public key of the client. A response SHOULD also carry an e tag with the id of the request event it answers.

A request may carry an optional expiration tag with a unix timestamp in seconds. If the wallet service receives the request after that timestamp, it should ignore the request.

The content of requests and responses is encrypted with NIP-44. Inside is a JSON-RPC-like object with a semi-fixed structure. For backwards compatibility: the initial protocol version used NIP-04. If a wallet service or client does not include the encryption tag in the info or request events, assume the connection uses NIP-04. See §7.

Request - kind 23194
{
  "kind": 23194,
  "tags": [
    ["encryption", "nip44_v2"],
    ["p", <wallet service pubkey>]
  ],
  "content": nip44_encrypt({          // scheme matches the encryption tag
    "method": "pay_invoice",          // method, string
    "params": {                       // params, object
      "invoice": "lnbc50n1..."        // command-related data
    }
  })
}
Response - kind 23195
{
  "kind": 23195,
  "tags": [
    ["p", <requesting client pubkey>],
    ["e", <id of the request event>]
  ],
  "content": nip44_encrypt({          // scheme requested by the client
    "result_type": "pay_invoice",     // names the structure of result
    "error": {                        // object, non-null on error
      "code": "UNAUTHORIZED",         // string error code, see §5
      "message": "human readable"
    },
    "result": {                       // object, null on error
      "preimage": "0123456789abcdef..."
    }
  })
}

Three rules bind the response body:

  • result_type MUST contain the name of the method that the event responds to.
  • On failure, error MUST contain a message field with human-readable text and a code field with the error code. result is null.
  • On success, error must be null.

5 Error Codes

The error.code field takes one of these values for any command:

General error codes
CodeMeaning
RATE_LIMITEDThe client sends commands too fast. It should retry in a few seconds.
NOT_IMPLEMENTEDThe command is not known, or is intentionally not implemented.
INSUFFICIENT_BALANCEThe wallet does not have enough funds to cover a fee reserve or the payment amount.
QUOTA_EXCEEDEDThe wallet has exceeded its spending quota.
RESTRICTEDThis public key is not allowed to do this operation.
UNAUTHORIZEDThis public key has no wallet connected.
INTERNALAn internal error.
UNSUPPORTED_ENCRYPTIONThe encryption type of the request is not supported by the wallet service.
OTHEROther error.

Two more codes belong to specific commands:

Command-specific error codes
CodeCommandMeaning
PAYMENT_FAILEDpay_invoiceThe payment failed. Possible causes: a timeout, all routes exhausted, insufficient capacity, or similar.
NOT_FOUNDlookup_invoiceThe invoice could not be found by the given parameters.

6 Commands

The core command set has five methods. All amounts are millisatoshis (msats). Each request names its method and passes a params object. Each response repeats the method name in result_type.

pay_invoice

Requests payment of an invoice.

pay_invoice - request / response
{
  "method": "pay_invoice",
  "params": {
    "invoice": "lnbc50n1...",  // bolt11 invoice
    "amount": 123,             // invoice amount in msats, optional
    "metadata": {}             // generic metadata: zap/boostagram details,
  }                            //   payer name/comment/etc, optional
}
---
{
  "result_type": "pay_invoice",
  "result": {
    "preimage": "0123456789abcdef...",  // preimage of the payment
    "fees_paid": 123                     // value in msats, optional
  }
}

Errors: PAYMENT_FAILED.

make_invoice

Creates a new invoice on the wallet.

make_invoice - request / response
{
  "method": "make_invoice",
  "params": {
    "amount": 123,                  // value in msats
    "description": "string",        // invoice description, optional
    "description_hash": "string",   // invoice description hash, optional
    "expiry": 213,                  // seconds from creation, optional
    "metadata": {}                  // generic metadata, optional
  }
}
---
{
  "result_type": "make_invoice",
  "result": {
    "type": "incoming",             // "incoming" invoices, "outgoing" payments
    "state": "pending",             // optional
    "invoice": "string",            // encoded invoice, optional
    "description": "string",        // invoice description, optional
    "description_hash": "string",   // invoice description hash, optional
    "preimage": "string",           // preimage, optional if unpaid
    "payment_hash": "string",       // payment hash for the payment
    "amount": 123,                  // value in msats
    "fees_paid": 123,               // value in msats
    "created_at": unixtimestamp,    // creation time
    "expires_at": unixtimestamp,    // expiration, optional if not applicable
    "metadata": {}                  // generic metadata
  }
}
Tech Note

The state field of make_invoice was marked optional in 2025 for backward compatibility with older wallet services. Do not treat its absence as an error.

lookup_invoice

Looks up the state of an invoice or payment. One of payment_hash or invoice is required.

lookup_invoice - request / response
{
  "method": "lookup_invoice",
  "params": {
    "payment_hash": "31afdf1..",  // one of payment_hash or invoice required
    "invoice": "lnbc50n1..."      // invoice to look up
  }
}
---
{
  "result_type": "lookup_invoice",
  "result": {
    "type": "incoming",             // "incoming" invoices, "outgoing" payments
    "state": "pending",             // "pending", "settled", "accepted" (hold
                                    //   invoices), "expired" (invoices) or
                                    //   "failed" (payments), optional
    "invoice": "string",            // encoded invoice, optional
    "description": "string",        // invoice description, optional
    "description_hash": "string",   // invoice description hash, optional
    "preimage": "string",           // preimage, optional if unpaid
    "payment_hash": "string",       // payment hash for the payment
    "amount": 123,                  // value in msats
    "fees_paid": 123,               // value in msats
    "created_at": unixtimestamp,    // creation time
    "expires_at": unixtimestamp,    // expiration, optional if not applicable
    "settled_at": unixtimestamp,    // settlement time, optional if unpaid
    "metadata": {}                  // generic metadata
  }
}

Errors: NOT_FOUND.

get_balance

Returns the wallet balance. The request takes an empty params object.

get_balance - request / response
{ "method": "get_balance", "params": {} }
---
{
  "result_type": "get_balance",
  "result": {
    "balance": 10000   // user's balance in msats
  }
}

get_info

Returns node and connection information. The request takes an empty params object.

get_info - request / response
{ "method": "get_info", "params": {} }
---
{
  "result_type": "get_info",
  "result": {
    "alias": "string",
    "color": "hex string",
    "pubkey": "hex string",
    "network": "string",        // mainnet, testnet, signet, or regtest
    "block_height": 1,
    "block_hash": "hex string",
    "methods": ["pay_invoice", "get_balance", "make_invoice",
                "lookup_invoice", "get_info"],  // methods for this connection
    "extensions": ["02", "03", "04", "06", "07", "08"]
                  // supported NWC extension specs for this connection, optional
  }
}

7 Encryption

The initial version of NWC used NIP-04 encryption. NIP-04 is deprecated and replaced by NIP-44. Always prefer NIP-44. Legacy wallet services or clients may not have migrated yet. Both sides negotiate the scheme through the encryption tag on the info event.

Encryption codes
Encryption codeUseNotes
nip44_v2NIP-44Required.
nip04NIP-04Deprecated. Present only for backward compatibility.
<not present>NIP-04Deprecated. Present only for backward compatibility.

Negotiation has two steps:

  1. The wallet service puts an encryption tag in the info event. The tag holds a space-separated list of schemes the service supports, for example nip44_v2 nip04.
  2. The client puts an encryption tag in each request event. The tag names the single scheme used for that request. The client should always prefer NIP-44 when the wallet service supports it.

When a client establishes a connection, it reads the info event and looks for the encryption tag. Absence of the tag means the wallet supports only NIP-04. If the tag is present, the client picks the best scheme that both sides support. That should always be NIP-44 when possible.

The scheme named in a request MUST be one the wallet service advertised. If the wallet service does not support the requested scheme, it returns an UNSUPPORTED_ENCRYPTION error. A request without an encryption tag means NIP-04.

Read info event kind 13194 encryption tag? space-separated list nip44_v2 preferred nip04 legacy fallback lists nip44_v2 absent / nip04 only
Fig. 47-2 — Encryption negotiation. The client reads the info event and picks the best shared scheme. No tag means NIP-04 only.

8 Extensions

This NIP defines the core protocol and a small common command set. Optional NWC extension specifications MAY define additional methods, metadata conventions, pairing flows, and more granular authorization behavior. The extensions live in the dedicated NWC repository at https://github.com/nostr-wallet-connect/nwc. That repository reserves 01.md for the NWC core specification, which corresponds to this simplified NIP-47. Optional features start at 02.md.

Tech Note

Earlier versions of this NIP defined notification events (kind 23196, later 23197 for NIP-44) and extra commands such as multi_pay_invoice, pay_keysend, and list_transactions. The 2026-08 simplification moved all of them out to the NWC extensions repository. The current core spec defines no notification kinds. Deployed wallet services may still emit the legacy notification kinds; consult the extension specs to handle them.

9 Relay Choice

This NIP sets no requirements on the type of relay. If the user relies on a custodial service, a relay hosted by that custodian can make sense. The relay may then enforce authentication and so prevent metadata leaks. Skipping the third-party relay also improves reliability in that setup.

10 Worked Example

The user scans the QR code from the wallet service, follows a nostr+walletconnect:// deeplink, or types the connection details by hand. Then a payment runs in three steps:

  1. The client sends a kind 23194 event to the wallet service. The content is a pay_invoice request. The signing key is the secret from the connection string.
  2. The wallet service verifies that the author's key may perform the payment, decrypts the payload, and sends the payment.
  3. The wallet service answers with a kind 23195 event. The content holds either an error or a preimage.
Client Relay + service ["REQ", "nwc", {"kinds":[13194], "authors":[svc]}] info event: capabilities + encryption modes ["EVENT", {kind:23194, pay_invoice, encrypted}] ["OK", <id>, true, ""] {kind:23195, result: preimage, encrypted} The service pays the invoice between OK and the 23195 response.
Fig. 47-3 — Pay-invoice flow. Info lookup, encrypted request, encrypted response with the preimage.
Session transcript - relay from the connection URI
-- 1. client discovers capabilities --
→ ["REQ", "nwc-info", {"kinds":[13194], "authors":["c04ccd5c…"]}]
← ["EVENT", "nwc-info", {"kind":13194,
     "tags":[["encryption","nip44_v2 nip04"],["extensions","02 03 04 06 07 08"]],
     "content":"pay_invoice get_balance get_info make_invoice lookup_invoice", …}]
← ["EOSE", "nwc-info"]

-- 2. client pays, signing with the URI secret --
→ ["EVENT", {"kind":23194, "pubkey":"b8e6bf46…",
     "tags":[["encryption","nip44_v2"],["p","c04ccd5c…"]],
     "content":"<nip44: {"method":"pay_invoice",
                "params":{"invoice":"lnbc50n1…"}}>", …}]
← ["OK", "df467db0…", true, ""]

-- 3. wallet service responds --
← ["EVENT", "nwc-sub", {"kind":23195, "pubkey":"c04ccd5c…",
     "tags":[["p","b8e6bf46…"],["e","df467db0…"]],
     "content":"<nip44: {"result_type":"pay_invoice", "error":null,
                "result":{"preimage":"0123456789abcdef…","fees_paid":4000}}>", …}]

-- a failure instead carries an error object --
   {"result_type":"pay_invoice", "result":null,
    "error":{"code":"INSUFFICIENT_BALANCE",
             "message":"not enough funds to cover fee reserve"}}

A full published info event, from the spec appendix:

Appendix - example info event
{
  "id": "df467db0a9f9ec77ffe6f561811714ccaa2e26051c20f58f33c3d66d6c2b4d1c",
  "pubkey": "c04ccd5c82fc1ea3499b9c6a5c0a7ab627fbe00a0116110d4c750faeaecba1e2",
  "created_at": 1713883677,
  "kind": 13194,
  "tags": [
    ["encryption", "nip44_v2 nip04"],
    ["extensions", "02 03 04 06 07 08"]
  ],
  "content": "pay_invoice get_balance get_info make_invoice lookup_invoice",
  "sig": "31f57b369459b5306a5353aa9e03be7fbde169bc881c3233625605dd12f53548179def16b9fe1137e6465d7e4d5bb27ce81fd6e75908c46b06269f4233c845d8"
}

11 Version History

NIP-47 started as a hackathon-era idea for one-tap zaps. It grew many commands, then slimmed back down to a small core plus extensions. Notable changes, newest first:

  • 20262026-08Core spec simplified to five commands. Notifications, keysend, multi-pay, and transaction listing moved to the optional NWC extensions repository. The extensions tag arrived.
  • 20262026-02Hold-invoice support added the accepted state to lookup_invoice; the spec text was restructured.
  • 20252025-10Deep-link pairing (nostr+walletconnect:// as a handled URI) documented.
  • 20252025-09metadata params added to make_invoice and payment commands; zap metadata usage clarified; make_invoice state marked optional.
  • 20252025-07The encryption tag arrived to negotiate the upgrade from NIP-04 to NIP-44; transactions gained a state field.
  • 20242024-06Notification events added (moved out again in 2026).
  • 20232023-12The extensions concept first proposed.
  • 20232023-05lud16 parameter added to the connection string.
  • 20232023-04PAYMENT_FAILED error code added; early feedback rounds and typo fixes.
  • Origin2023-03-30Semisol publishes "NIP-47 Wallet Connect".

Original Proposal - Credits

AuthorSemisol
First commit2023-03-30 — “NIP-47 Wallet Connect”
Statusdraft - optional — wallets and payment-capable clients implement it
Source47.md on GitHub - full change history

12 See Also

  • NIP-01 — base event format, replaceable and ephemeral storage classes used by kinds 13194, 23194, and 23195.
  • NIP-44 — the required encryption scheme for request and response payloads.
  • NIP-04 — the deprecated legacy encryption scheme kept for backward compatibility.
  • NIP-57 — lightning zaps; the main consumer of pay_invoice and its zap metadata.
  • NIP-46 — remote signing; the same remote-delegation pattern applied to keys instead of wallets.