| name | nostr-bunker-integration |
| description | Implement or debug NIP-46 remote-signer (bunker/Nostr Connect) integrations when the task involves client-signer communication, bunker URIs, auth challenges, or signer daemons. |
Nostr Bunker Integration
Overview
Implement NIP-46 remote signing correctly for both client applications and
bunker (remote signer) servers. NIP-46 keeps private keys on a dedicated signer
while clients communicate signing requests over Nostr relays using NIP-44
encrypted kind:24133 events. This skill covers connection establishment, the
JSON-RPC-like request/response protocol, auth challenges, relay switching,
permissions, signer discovery via NIP-89, and SDK-specific implementation
patterns.
When to Use
- The task involves NIP-46 remote signing between a Nostr client and a bunker/signer.
- The user needs bunker connection flows, request/response handling, auth challenges, permissions, or signer-daemon behavior.
- The request mentions
bunker://, nostrconnect://, remote signing, or NIP-89 signer discovery.
- The core problem is client-signer integration, not general event construction.
Do NOT use when:
- The task is direct NIP-44 or other crypto implementation without bunker flows.
- The request is about NIP-07 browser extension signing instead of NIP-46 remote signing.
- The problem is ordinary Nostr event creation with no signer-transport or permission model.
Response format
Always structure the final response with these top-level sections, in this order:
- Summary — state the task, scope, and main conclusion in 1-3 sentences.
- Decision / Approach — state the key classification, assumptions, or chosen path.
- Artifacts — provide the primary deliverable(s) for this skill. Use clear subheadings for multiple files, commands, JSON payloads, queries, or documents.
- Validation — state checks performed, important risks, caveats, or unresolved questions.
- Next steps — list concrete follow-up actions, or write
None if nothing remains.
Rules:
- Do not omit a section; write
None when a section does not apply.
- If files are produced, list each file path under Artifacts before its contents.
- If commands, JSON, SQL, YAML, or code are produced, put each artifact in fenced code blocks with the correct language tag when possible.
- Keep section names exactly as written above so output stays predictable across skills.
Workflow
1. Determine Your Role
| You are building... | Role | Start at |
|---|
| An app that needs signatures | Client | Step 2 (Client) |
| A signing service/daemon | Signer | Step 3 (Signer) |
| Both sides | Both | Steps 2 and 3 |
2. Client-Side Implementation
2a. Choose Connection Flow
There are two connection flows. Pick based on UX needs:
Flow A — Bunker-initiated (bunker://):
The signer provides a bunker:// URI. User pastes it into the client.
bunker://<remote-signer-pubkey>?relay=wss://relay1.example.com&relay=wss://relay2.example.com&secret=<optional-secret>
Steps:
- Parse the URI to extract
remote-signer-pubkey, relay URLs, and optional
secret
- Generate a disposable
client-keypair (persist for session, delete on
logout)
- Send a
connect request to the signer via the specified relays
- If secret was provided, signer validates it (single-use only)
- Call
get_public_key to learn the user's actual pubkey
Flow B — Client-initiated (nostrconnect://):
The client generates a URI. User scans it with the signer (e.g., QR code).
nostrconnect://<client-pubkey>?relay=wss://relay1.example.com&secret=<required>&name=My+App&perms=sign_event:1,nip44_encrypt
Steps:
- Generate a
client-keypair
- Build the
nostrconnect:// URI with client pubkey, relays, secret, name, and
optional permissions
- Display URI to user (QR code or copy/paste)
- Subscribe to kind:24133 events on specified relays, filtering for
p-tag =
client pubkey
- Signer sends a
connect response; client discovers remote-signer-pubkey
from the response author
- Validate the returned secret matches the one in the URI
- Call
get_public_key to learn the user's actual pubkey
2b. Send Requests
All requests are kind:24133 events with NIP-44 encrypted content:
{
"kind": 24133,
"pubkey": "<client_pubkey>",
"content": "<nip44_encrypt({id, method, params}, client_privkey, remote_signer_pubkey)>",
"tags": [["p", "<remote_signer_pubkey>"]]
}
The encrypted content is a JSON-RPC-like message:
{
"id": "<random_request_id>",
"method": "sign_event",
"params": ["<json_stringified_unsigned_event>"]
}
2c. Handle Responses
Subscribe for kind:24133 events where p-tag = client pubkey:
{
"id": "<matching_request_id>",
"result": "<result_string>",
"error": "<optional_error_string>"
}
Match responses to requests via the id field. If error is present, the
request failed.
2d. Handle Auth Challenges
If the signer needs additional user authentication, it returns:
{
"id": "<request_id>",
"result": "auth_url",
"error": "<URL_to_display_to_user>"
}
The client MUST:
- Open the URL from
error field in a popup or new tab
- Keep the subscription open with the same request ID
- Wait for a second response after the user authenticates
- The second response contains the actual result
2e. Implement Relay Switching
After connecting, immediately send a switch_relays request. The signer
controls which relays are used. If the signer returns an updated relay list, the
client MUST update its local state and send further requests on the new relays.
{ "id": "...", "method": "switch_relays", "params": [] }
Response: ["wss://new-relay1.com", "wss://new-relay2.com"] or null (no
change needed).
3. Signer (Bunker) Server Implementation
3a. Core Architecture
The signer holds private keys and responds to client requests:
Client App <--kind:24133 (NIP-44 encrypted)--> Remote Signer (holds privkeys)
via Nostr relays
Key responsibilities:
- Generate or hold
remote-signer-keypair (may differ from user keypair)
- Hold
user-keypair (the actual identity keys)
- Listen for kind:24133 events tagged to
remote-signer-pubkey
- Decrypt requests with NIP-44, execute methods, encrypt responses
3b. Connection Handling
For bunker-initiated connections:
- Generate a
bunker:// URI with your signer pubkey, relay URLs, and optional
single-use secret
- When you receive a
connect request, validate the secret if provided
- Respond with
"ack" (or the required secret for client-initiated flow)
- Track the
client-pubkey for future request routing
For client-initiated connections:
- Parse the
nostrconnect:// URI to extract client pubkey, relays, secret, and
permissions
- Send a
connect response event containing the secret as result
- The response author reveals your
remote-signer-pubkey to the client
3c. Implement Request Handlers
| Method | Params | Implementation |
|---|
connect | [remote_signer_pubkey, secret?, perms?] | Validate secret, store client session, return ack |
sign_event | [json_stringified({kind,content,tags,created_at})] | Sign with user privkey, return signed event JSON |
ping | [] | Return "pong" |
get_public_key | [] | Return user pubkey (NOT signer pubkey) |
nip04_encrypt | [third_party_pubkey, plaintext] | NIP-04 encrypt with user privkey |
nip04_decrypt | [third_party_pubkey, ciphertext] | NIP-04 decrypt with user privkey |
nip44_encrypt | [third_party_pubkey, plaintext] | NIP-44 encrypt with user privkey |
nip44_decrypt | [third_party_pubkey, ciphertext] | NIP-44 decrypt with user privkey |
switch_relays | [] | Return updated relay list or null |
3d. Permission Enforcement
Parse requested permissions from connect params or nostrconnect:// URI.
Format: comma-separated method[:params].
Examples:
sign_event — allow signing any kind
sign_event:1 — only allow signing kind:1
nip44_encrypt,nip44_decrypt — allow NIP-44 operations
sign_event:1,sign_event:4,nip44_encrypt — specific subset
The signer SHOULD:
- Store granted permissions per client session
- Reject requests outside granted permissions with an error response
- Optionally prompt the user for approval of ungranated permissions
3e. Auth Challenge Flow
When user approval is needed (e.g., for ungranted permissions):
- Generate a one-time auth URL
- Return
{ "id": "<req_id>", "result": "auth_url", "error": "<auth_url>" }
- Wait for the user to authenticate at the URL
- After authentication, send the actual response with the same request ID
3f. Announce via NIP-89
Publish signer metadata for client discovery:
NIP-05 (optional):
{
"names": { "_": "<remote-signer-app-pubkey>" },
"nip46": {
"relays": ["wss://relay1.com", "wss://relay2.com"],
"nostrconnect_url": "https://signer-domain.example/<nostrconnect>"
}
}
NIP-89 kind:31990 event:
{
"kind": 31990,
"tags": [
["d", "<random-id>"],
["k", "24133"],
["relay", "wss://relay1.com"],
["relay", "wss://relay2.com"],
["nostrconnect_url", "https://signer-domain.example/<nostrconnect>"]
],
"content": "{\"name\":\"My Bunker\",\"picture\":\"...\",\"about\":\"...\"}"
}
4. SDK Selection
Choose based on your runtime. See
references/sdk-reference.md for complete code
examples.
| Runtime | SDK | Client Support | Signer Support | Notes |
|---|
| TypeScript/JS | @nostr/tools | Full | Partial | BunkerSigner class, both connection flows |
| TypeScript/JS | @nostr-dev-kit/ndk | Full | No | NDKNip46Signer, higher-level abstraction |
| Rust | nostr-connect | Full | Full | Part of rust-nostr, alpha but functional |
| Dart/Flutter | dart-nostr-sdk | Full | No | NDK port with NIP-46 support |
5. Test the Connection
Use the bundled connection tester to validate bunker URLs and generate
nostrconnect URIs:
bun run scripts/bunker-url-tools.ts parse "bunker://ab12...?relay=wss://relay.example.com&secret=mysecret"
bun run scripts/bunker-url-tools.ts generate-nostrconnect --relays wss://relay.example.com --name "My App"
bun run scripts/bunker-url-tools.ts --help
Checklist
Example
Complete client connection using @nostr/tools:
Input: User pastes a bunker:// URI into the app.
import { generateSecretKey, getPublicKey } from "@nostr/tools/pure";
import { BunkerSigner, parseBunkerInput } from "@nostr/tools/nip46";
import { SimplePool } from "@nostr/tools/pool";
async function connectToBunker(bunkerUri: string) {
const bp = await parseBunkerInput(bunkerUri);
if (!bp) throw new Error("Invalid bunker URI");
const clientSk = generateSecretKey();
const pool = new SimplePool();
const bunker = BunkerSigner.fromBunker(clientSk, bp, {
pool,
onauth: (url: string) => {
window.open(url, "_blank", "width=600,height=400");
},
});
await bunker.connect();
const userPubkey = await bunker.getPublicKey();
console.log("Connected as:", userPubkey);
await bunker.switchRelays();
const signedEvent = await bunker.signEvent({
kind: 1,
created_at: Math.floor(Date.now() / 1000),
tags: [],
content: "Hello from my bunker-connected app!",
});
await bunker.close();
pool.close([]);
return signedEvent;
}
Complete client-initiated connection (nostrconnect://):
import { generateSecretKey, getPublicKey } from "@nostr/tools/pure";
import { BunkerSigner, createNostrConnectURI } from "@nostr/tools/nip46";
import { SimplePool } from "@nostr/tools/pool";
async function initiateConnection() {
const clientSk = generateSecretKey();
const clientPubkey = getPublicKey(clientSk);
const secret = crypto.randomUUID();
const uri = createNostrConnectURI({
clientPubkey,
relays: ["wss://relay.damus.io", "wss://relay.primal.net"],
secret,
name: "My Nostr App",
perms: ["sign_event:1", "sign_event:0", "nip44_encrypt", "nip44_decrypt"],
});
console.log("Scan this with your signer:", uri);
const pool = new SimplePool();
const signer = await BunkerSigner.fromURI(clientSk, uri, { pool });
const userPubkey = await signer.getPublicKey();
return { signer, userPubkey };
}
Common Mistakes
| Mistake | Fix |
|---|
Using remote-signer-pubkey as the user's identity | Always call get_public_key after connecting. The signer pubkey is for transport encryption only; the user's pubkey may be different. |
| Using NIP-04 for kind:24133 content encryption | NIP-46 requires NIP-44 encryption. NIP-04 is deprecated and insecure. |
| Not handling auth challenges | Implement the auth_url flow: open URL, keep subscription, wait for second response with same request ID. |
| Reusing the bunker secret after successful connection | Secrets are single-use. The signer SHOULD reject reuse. Generate fresh secrets for each connection attempt. |
Not calling switch_relays after connection | The signer controls relay selection. Call switch_relays immediately after connecting and update local relay state. |
| Skipping secret validation in nostrconnect:// flow | Client MUST validate that the secret in the connect response matches the one it sent. Without this, connections can be spoofed. |
| Not persisting client keypair across sessions | Store the client secret key locally for reconnection. Generating a new one each time forces re-authorization. |
Confusing connect request vs response | In bunker:// flow, client sends connect request. In nostrconnect:// flow, signer sends connect response. |
| Not implementing permission scoping on signer | Parse and enforce permissions from connect params. Reject unauthorized method calls with error responses. |
| Signing events with signer keypair instead of user keypair | The signer signs with the user's private key, not the signer's transport key. |
Quick Reference
| Component | Value |
|---|
| Event kind | 24133 |
| Content encryption | NIP-44 (version 2) |
| Connection URI (signer-initiated) | bunker://<remote-signer-pubkey>?relay=<url>&secret=<optional> |
| Connection URI (client-initiated) | nostrconnect://<client-pubkey>?relay=<url>&secret=<required>&name=<app>&perms=<csv> |
| Request format | { "id": "<random>", "method": "<name>", "params": ["<arg>", ...] } |
| Response format | { "id": "<request_id>", "result": "<value>", "error": "<optional>" } |
| Auth challenge result | "auth_url" (URL in error field) |
| Signer discovery | NIP-89 kind:31990 with k tag of 24133 |
| Permission format | method[:param] comma-separated (e.g., sign_event:1,nip44_encrypt) |
Key Principles
-
Keys are separate — remote-signer-pubkey (transport encryption) and
user-pubkey (identity/signing) may be different keys. The signer controls
both. Always call get_public_key to learn the user's identity.
-
Signer controls relays — The remote signer decides which relays to use.
Clients must implement switch_relays and comply with relay changes. This
ensures the signer can migrate away from unreliable relays.
-
Secrets are single-use — Connection secrets prevent spoofing and replay.
In bunker:// flow they are optional but recommended. In nostrconnect://
flow they are required. Never accept a connection with a previously used
secret.
-
NIP-44 only — All kind:24133 content MUST be encrypted with NIP-44
(version 2). NIP-04 is deprecated and must not be used for remote signing
transport.
-
Auth challenges are async — When a signer needs user approval, it returns
an auth_url response. The client must handle this asynchronously: display
the URL, keep the subscription open, and wait for the real response on the
same request ID.
Optimization Notes
- Preserve the user's requested output shape exactly and do not substitute generic advice for concrete artifacts.
- Include exact commands, code structures, protocol fields, tags, parameters, file paths, or deliverable sections when the task asks for them.
- Make safety gates explicit before irreversible, destructive, externally visible, or compliance-sensitive actions.
- For multi-step work, present steps in execution order and include validation or rollback checks where relevant.
- Avoid overfitting to a single eval example: express lessons as reusable rules, not as task-specific answers.