1 Overview
A private key should touch as few systems as possible. Every app, operating system, and device that holds the key adds attack surface. This NIP moves the key out of the client. The key lives inside a remote signer. The remote signer can be a daemon, a server, or a dedicated hardware device.
The client and the remote signer talk in two directions over normal Nostr relays. Each request and each response is a kind 24133 event. The payload inside is encrypted with NIP-44. The client asks: sign this event, encrypt this text, decrypt this text. The remote signer answers. The user's key stays in the bunker.
The flow has five steps. First, the client generates a disposable client-keypair. Second, a connection is established; the remote signer learns the client-pubkey and the client learns the remote-signer-pubkey. Third, the client sends requests: it encrypts to the remote-signer-pubkey and adds a p tag with that pubkey. Fourth, the remote signer responds the same way toward the client-pubkey. Fifth, the client calls get_public_key to learn the user-pubkey.
Kind 24133 sits in the ephemeral range (20000–29999) from NIP-01. Relays forward these events but do not store them. Both sides must be online, or use relays that briefly buffer.
2 Roles and Keys
This NIP uses three keypairs. Keep them apart. All pubkeys in this NIP are in hex format.
| Term | Meaning |
|---|---|
| user | A person that is trying to use Nostr. |
| client | The user-facing application. It sends requests to the remote signer. |
| remote-signer | A daemon or server that answers requests from the client. Also called the “bunker”. |
| client-keypair | Keys the client generates. Used to encrypt content and talk to the remote signer. |
| remote-signer-keypair | Keys the remote signer uses to encrypt content and talk to the client. May equal the user-keypair, but not necessarily. |
| user-keypair | The keys that represent the user. They sign events for sign_event requests. The remote signer controls them. |
The client-keypair is largely disposable. The client does not need to show it to the user. The client may store it locally. The client must delete it on logout. Because the remote-signer-pubkey and the user-pubkey can differ, the client must not assume they are equal. The client must call get_public_key after connect to learn the user-pubkey.
3 Initiating a Connection
There are two ways to connect. Either side can hand the other a token.
Flow A — the remote signer starts: bunker://
The remote signer provides a connection token in this form:
bunker://<remote-signer-pubkey>?relay=<wss://relay-to-connect-on> &relay=<wss://another-relay-to-connect-on> &secret=<optional-secret-value>
The user passes this token to the client. The client then sends a connect request to the remote signer on the listed relays. A bunker:// token carries no information about the client. So in this flow the client should include client metadata in the connect request (see §4). The optional secret is valid for one successfully established connection only. The remote signer should ignore new connection attempts that reuse an old secret.
Flow B — the client starts: nostrconnect://
The client provides a token with nostrconnect:// as the protocol and the client-pubkey as the origin. Query parameters carry the rest:
| Parameter | Required | Meaning |
|---|---|---|
| relay | required | One or more relay URLs on which the client listens for responses from the remote signer. |
| secret | required | A short random string. The remote signer must return it as the result of its response. |
| perms | optional | A comma-separated list of permissions the client asks the remote signer to approve. |
| name | optional | The name of the client application. |
| url | optional | The canonical URL of the client application. |
| image | optional | A small image that represents the client application. |
nostrconnect://83f3b2ae6aa368e8275397b9c26cf550101d63ebaab900d19dd4a4429f5ad8f5 ?relay=wss%3A%2F%2Frelay1.example.com &perms=nip44_encrypt%2Cnip44_decrypt%2Csign_event%3A13%2Csign_event%3A14%2Csign_event%3A1059 &name=My+Client &secret=0s8j2djs &relay=wss%3A%2F%2Frelay2.example2.com
The user passes this token to the remote signer. The remote signer then sends a connect response event to the client-pubkey on the listed relays. The client discovers the remote-signer-pubkey from the author of that response. The secret must be provided to avoid connection spoofing. The client must validate the secret that the connect response returns.
4 Request Events — kind: 24133
A request is a normal NIP-01 event with kind 24133. Its author is the client-pubkey. It carries one p tag with the remote-signer-pubkey:
{
"kind": 24133,
"pubkey": <client-pubkey>,
"content": <nip44(<request>)>,
"tags": [["p", <remote-signer-pubkey>]]
}
The content field is a JSON-RPC-like message. It is encrypted with NIP-44. Its plaintext structure is:
{
"id": <random_string>,
"method": <method_name>,
"params": [array_of_strings]
}
idis a random string that identifies the request. The response carries back the same ID.methodis the name of the method or command (see the table below).paramsis a positional array of string parameters.
Methods
The client can send these ten methods to the remote signer:
| Command | Params | Result |
|---|---|---|
| connect | [<remote-signer-pubkey>, <optional_secret>, <optional_requested_perms>, <optional_client_metadata>] | "ack" or <required-secret-value> |
| sign_event | [<{kind, content, tags, created_at}>] | json_stringified(<signed_event>) |
| ping | [] | "pong" |
| get_public_key | [] | <user-pubkey> |
| nip04_encrypt | [<third_party_pubkey>, <plaintext_to_encrypt>] | <nip04_ciphertext> (NIP-04) |
| nip04_decrypt | [<third_party_pubkey>, <nip04_ciphertext_to_decrypt>] | <plaintext> |
| nip44_encrypt | [<third_party_pubkey>, <plaintext_to_encrypt>] | <nip44_ciphertext> (NIP-44) |
| nip44_decrypt | [<third_party_pubkey>, <nip44_ciphertext_to_decrypt>] | <plaintext> |
| switch_relays | [] | ["<relay-url>", "<relay-url>", ...] or null |
| logout | [] | "ack" |
Requested permissions
The connect method may carry optional_requested_perms for user convenience. The permissions are a comma-separated list of method[:params]. Example: nip44_encrypt,sign_event:4 asks for permission to call nip44_encrypt and to call sign_event with kind:4. For sign_event the optional parameter is the kind number. Parameters for the other methods are to be defined later. The perms parameter in a nostrconnect:// string may use the same format.
The source text says the permission format “may be used for the perms field of metadata” in the nostrconnect:// string. But perms is defined as a query parameter, and the metadata object lists only name, url, and image. The spec never resolves this wording. Treat perms as a query parameter.
Client metadata
The connect method may carry optional_client_metadata. This lets the remote signer identify and label the connection. It is a JSON-stringified object. Its fields mirror the nostrconnect:// query parameters, and all are optional: name, url, and image. This helps most in the bunker:// flow, where the remote signer has no other source for this information. To send metadata without requesting permissions, pass an empty string for optional_requested_perms. The metadata must then sit in the fourth position.
Client metadata is client-supplied and unauthenticated. The remote signer should treat it as a display hint only. The remote signer must not use it for authorization decisions.
Switching relays
The remote signer stays in control of the relays used for the connection at all times. Its relay set can evolve as old relays shut down and new ones appear. In the client-initiated flow the client may even pick relays foreign to the signer's preferences. The signer must be able to switch those at once.
So compliant clients should send a switch_relays request immediately after the connection is established — always, or at reasonable intervals. The remote signer replies with its updated relay list, or null when nothing changes. As soon as the client receives an updated list, it updates its local state and sends all further requests on the new relays. The remote signer is then free to disconnect from the previous relays.
Ending a session
When the user logs out of the client, the client may send a logout request. This tells the remote signer that the session is no longer needed. On receipt, the remote signer should remove the session tied to the requesting client-pubkey and reject any further requests from it. A new connect is then required for a new session. The remote signer should reply with "ack" before it removes the session.
logout is a courtesy hint, not a security boundary. The remote signer must not rely on it being sent. The client must still delete its locally stored client-keypair on logout, whether or not the request was acknowledged.
5 Response Events — kind: 24133
A response is also a kind 24133 event. Its author is the remote-signer-pubkey. It carries one p tag with the client-pubkey:
{
"id": <id>,
"kind": 24133,
"pubkey": <remote-signer-pubkey>,
"content": <nip44(<response>)>,
"tags": [["p", <client-pubkey>]],
"created_at": <unix timestamp in seconds>
}
The content field is again a NIP-44 encrypted JSON-RPC-like message:
{
"id": <request_id>,
"result": <results_string>,
"error": <optional_error_string>
}
idis the ID of the request that this response answers.resultis a string with the result of the call. It can be a plain string or a JSON-stringified object.erroris optional. It is an error in string form. Its presence indicates an error with the request.
The remote signer must reply with an error to any request whose method is unknown or unsupported. Silent timeouts are not allowed for such requests.
Two rough edges in the source. First, the field is named result in the JSON but results in the prose; the wire field is result. Second, result is defined as a string, yet switch_relays returns an array or null; the spec does not say whether that value is JSON-stringified. Implementations should accept both forms.
6 Auth Challenges
An auth challenge is a special response. The remote signer sends it when it needs the user to authenticate by other means. The response content object takes this form:
{
"id": <request_id>,
"result": "auth_url",
"error": <URL_to_display_to_end_user>
}
The client should display the URL from the error field in a popup or a new tab. Then the client subscribes and listens for another response from the remote signer with the same request ID. The remote signer sends that final response once the user authenticates in the other window. If the user never authenticates, the final response never arrives.
The auth challenge overloads the error field: it carries the auth URL, not an error. This contradicts §5, where the presence of error “indicates an error with the request”. Clients must check for result == "auth_url" before they treat error as a failure.
7 Announcing and Discovering Signers
A remote signer may publish its metadata with NIP-05 and NIP-89. With NIP-05, a request to <remote-signer>/.well-known/nostr.json?name=_ may return:
{
"names": {
"_": <remote-signer-app-pubkey>
},
"nip46": {
"relays": ["wss://relay1", "wss://relay2", ...],
"nostrconnect_url": "https://remote-signer-domain.example/<nostrconnect>"
}
}
- The
<remote-signer-app-pubkey>may be used to verify the domain from the signer's NIP-89 event. relaysshould be used to build a more precisenostrconnect://string for this signer.- The
nostrconnect_urltemplate may redirect users into the signer's connection flow. Replace the<nostrconnect>placeholder with an actualnostrconnect://string.
A remote signer may also publish a NIP-89 kind: 31990 event with a k tag of 24133. That event may include one or more relay tags and may include a nostrconnect_url tag. These tags carry the same meaning as the fields above.
A client may improve UX by discovering remote signers through their kind: 31990 events. The client may pre-generate nostrconnect:// strings for them. In that case the client should verify that the kind: 31990 event's author appears as the <remote-signer-app-pubkey> in the signer's nostr.json?name=_ file.
8 Worked Example
Three keys are in play. The remote-signer-pubkey is fa984bd7dbb2…8018f52. The user-pubkey is the same value in this example — the signer holds the user key directly. The client-pubkey is eff37350d839…531d86. The client asks the bunker to sign a kind-1 note.
{
"kind": 24133,
"pubkey": "eff37350d839ce3707332348af4549a96051bd695d3223af4aabce4993531d86",
"content": nip44({
"id": <random_string>,
"method": "sign_event",
"params": [json_stringified(<{
content: "Hello, I'm signing remotely",
kind: 1,
tags: [],
created_at: 1714078911
}>)]
}),
"tags": [["p", "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"]]
// p-tags the remote-signer-pubkey
}
The remote signer fills in pubkey, id, and sig of the requested event, then answers:
{
"kind": 24133,
"pubkey": "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52",
"content": nip44({
"id": <random_string>,
"result": json_stringified(<signed-event>)
}),
"tags": [["p", "eff37350d839ce3707332348af4549a96051bd695d3223af4aabce4993531d86"]]
// p-tags the client-pubkey
}
A full session over the bunker's relay looks like this. Orange lines are decrypted client requests. Blue lines are decrypted signer responses.
-- user pasted bunker://fa984bd7…?relay=…&secret=zx81 into the client -- → {"id":"r1","method":"connect","params":["fa984bd7…","zx81","", "{\"name\":\"My Client\",\"url\":\"https://myclient.example\"}"]} ← {"id":"r1","result":"ack"} → {"id":"r2","method":"get_public_key","params":[]} ← {"id":"r2","result":"fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"} → {"id":"r3","method":"switch_relays","params":[]} ← {"id":"r3","result":null} → {"id":"r4","method":"sign_event","params":["{\"kind\":1,\"content\":\"Hello, I'm signing remotely\",…}"]} ← {"id":"r4","result":"{\"id\":\"…\",\"pubkey\":\"fa984bd7…\",\"sig\":\"…\",…}"} -- an unsupported method gets an error, never silence -- → {"id":"r5","method":"make_coffee","params":[]} ← {"id":"r5","result":"","error":"unsupported method"} -- user logs out -- → {"id":"r6","method":"logout","params":[]} ← {"id":"r6","result":"ack"}
9 Version History
NIP-46 began life as “Nostr Connect” and was rewritten twice. The current shape — bunker tokens, split signer and user keys, NIP-44 only — settled between 2024 and 2026. Notable changes, newest first:
- 20262026-07Required an error reply for unknown or unsupported methods; no more silent timeouts.
- 20262026-06Added optional client metadata to
connectand thelogoutmethod for ending sessions. - 20262026-01Added
switch_relaysso the signer controls the relay set. - 20252025-02Removed the old
get_relaysmethod;relaytags joined the NIP-89 discovery event. - 20242024-12Dropped NIP-04 for the transport; request and response payloads use NIP-44 only.
- 20242024-10Introduced the
remote-signer-keyas distinct from the user key; clients must callget_public_keyafterconnect. NIP-05 login was removed andcreate_accountmoved to another NIP. - 20242024-02Full rewrite (“new version of NIP-46”);
bunker://tokens replaced the oldnpub1…#?notation. - 20232023-10fiatjaf rewrote the flow around request and response events; NIP-44 calls were added soon after.
- Origin2023-02Marco Argentieri proposed “Nostr Connect”: connect your Nostr app with remote signing devices.
Original Proposal - Credits
| Author | Marco Argentieri |
| First commit | 2023-02-20 — “NIP-46: Nostr Connect 🔌 connect your Nostr app with remote signing devices” (PR #153) |
| Status | active — widely implemented by signer apps and clients |
| Source | 46.md on GitHub - full change history |
10 See Also
- NIP-01 — the base event format,
ptags, and the ephemeral kind range that 24133 lives in. - NIP-44 — the encryption that wraps every request and response payload.
- NIP-04 — legacy encryption still exposed to apps via
nip04_encrypt/nip04_decrypt. - NIP-05 — the
nostr.jsondocument a signer uses to announce its relays. - NIP-89 —
kind: 31990handler events used for signer discovery. - NIP-07 — the browser-extension signer; the local cousin of this remote flow.