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.
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:
| Term | Meaning |
|---|---|
| client | A Nostr app, on any platform, that wants to interact with a lightning wallet. |
| user | The person who uses the client and wants to connect their wallet to it. |
| wallet service | A 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:
- 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.
- 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
infoevent (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. - When the user starts a payment, the client creates a
pay_invoicerequest. 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. - When the payment completes, the wallet service sends an encrypted response (kind 23195) back to the client over the same relays.
- Optional extension specifications may define additional wallet-to-client events.
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.
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.
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>
| Parameter | Presence | Rule |
|---|---|---|
| relay | Required | URL of the relay where the wallet service is connected and listens for events. May appear more than once. |
| secret | Required | A 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. |
| lud16 | Recommended | A 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
secretfrom the URI plus the wallet service'spubkeyto 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.
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.
nostr+walletconnect://b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4
?relay=wss%3A%2F%2Frelay.damus.io
&secret=71a8c14c1407c113601079c4302dab36460f0ccd0ad506f1f2dc73b5100e4f3c
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:
| Kind | Name | Direction | Storage class |
|---|---|---|---|
| 13194 | NIP-47 info event | wallet service → relays | replaceable |
| 23194 | NIP-47 request | client → wallet service | ephemeral |
| 23195 | NIP-47 response | wallet service → client | ephemeral |
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
extensionstag with the supported extension identifiers, space-separated, for example02 03 04. - Any additional methods that those extensions provide SHOULD also appear in the
contentcapability list. - The info event should also carry an
encryptiontag with the supported encryption modes. See §7.
{
"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.
{
"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
}
})
}
{
"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_typeMUST contain the name of the method that the event responds to.- On failure,
errorMUST contain amessagefield with human-readable text and acodefield with the error code.resultis null. - On success,
errormust be null.
5 Error Codes
The error.code field takes one of these values for any command:
| Code | Meaning |
|---|---|
| RATE_LIMITED | The client sends commands too fast. It should retry in a few seconds. |
| NOT_IMPLEMENTED | The command is not known, or is intentionally not implemented. |
| INSUFFICIENT_BALANCE | The wallet does not have enough funds to cover a fee reserve or the payment amount. |
| QUOTA_EXCEEDED | The wallet has exceeded its spending quota. |
| RESTRICTED | This public key is not allowed to do this operation. |
| UNAUTHORIZED | This public key has no wallet connected. |
| INTERNAL | An internal error. |
| UNSUPPORTED_ENCRYPTION | The encryption type of the request is not supported by the wallet service. |
| OTHER | Other error. |
Two more codes belong to specific commands:
| Code | Command | Meaning |
|---|---|---|
| PAYMENT_FAILED | pay_invoice | The payment failed. Possible causes: a timeout, all routes exhausted, insufficient capacity, or similar. |
| NOT_FOUND | lookup_invoice | The 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.
{
"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.
{
"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
}
}
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.
{
"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.
{ "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.
{ "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 code | Use | Notes |
|---|---|---|
| nip44_v2 | NIP-44 | Required. |
| nip04 | NIP-04 | Deprecated. Present only for backward compatibility. |
| <not present> | NIP-04 | Deprecated. Present only for backward compatibility. |
Negotiation has two steps:
- The wallet service puts an
encryptiontag in the info event. The tag holds a space-separated list of schemes the service supports, for examplenip44_v2 nip04. - The client puts an
encryptiontag 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.
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.
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:
- The client sends a kind 23194 event to the wallet service. The content is a
pay_invoicerequest. The signing key is thesecretfrom the connection string. - The wallet service verifies that the author's key may perform the payment, decrypts the payload, and sends the payment.
- The wallet service answers with a kind 23195 event. The content holds either an error or a preimage.
-- 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:
{
"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
extensionstag arrived. - 20262026-02Hold-invoice support added the
acceptedstate tolookup_invoice; the spec text was restructured. - 20252025-10Deep-link pairing (
nostr+walletconnect://as a handled URI) documented. - 20252025-09
metadataparams added tomake_invoiceand payment commands; zap metadata usage clarified;make_invoicestatemarked optional. - 20252025-07The
encryptiontag arrived to negotiate the upgrade from NIP-04 to NIP-44; transactions gained astatefield. - 20242024-06Notification events added (moved out again in 2026).
- 20232023-12The extensions concept first proposed.
- 20232023-05
lud16parameter added to the connection string. - 20232023-04
PAYMENT_FAILEDerror code added; early feedback rounds and typo fixes. - Origin2023-03-30Semisol publishes "NIP-47 Wallet Connect".
Original Proposal - Credits
| Author | Semisol |
| First commit | 2023-03-30 — “NIP-47 Wallet Connect” |
| Status | draft - optional — wallets and payment-capable clients implement it |
| Source | 47.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_invoiceand its zap metadata. - NIP-46 — remote signing; the same remote-delegation pattern applied to keys instead of wallets.