# Using Nostr Identities On XMTP This guide explains how developers can integrate **Nostr identities** (npub pubkeys or NIP-05 handles) with **XMTP messaging**. This enables secure messaging in Nostr apps or adding Nostr identity resolution to XMTP-based messengers. The goal is to **link Nostr pubkeys** with **XMTP Inboxes** securely through a mutual verification process. ## Key Concepts ### Identity Systems to Link - **Nostr Identity**: Represented by a **npub** (bech32-encoded pubkey). - **XMTP Identity**: Represented by an **Inbox ID**. - **Objective**: Bind these so: - A Nostr npub can be mapped to an XMTP Inbox. - An XMTP message sender (Inbox ID) can be resolved to a npub and profile. ### Verification Mechanism To securely associate the two identities: - The **XMTP account signs a message** containing the **Nostr npub**. - This signature (along with the Inbox ID) is stored in the user's **kind 0 metadata event**, which is the standard replaceable profile event on Nostr. - This enables **two-way confirmation**: - XMTP Inbox proves it wants to link to the npub (via the signature). - npub owner approves the connection by publishing the record in their signed kind 0 event. We extend the standard kind 0 JSON metadata with a top-level `"xmtp"` field: ```json { "name": "Your Name", "picture": "https://...", "about": "Your bio", "nip05": "_@yourdomain.com", "lud16": "you@getalby.com", "xmtp": { "inboxId": "inbox-abc123...", "verificationSignature": "base64sig...", // Signed by XMTP installation key on the npub string "createdAt": "2026-02-01T12:00:00Z" } } ``` This aligns with existing Nostr practices for adding custom fields (e.g., lud16, website). ## Step-by-Step Flow ### Step 1: Create XMTP Client and Sign npub - Generate an XMTP client. - Sign the target **Nostr npub** with the XMTP client’s **installation key**. ```javascript import { Client } from "@xmtp/node-sdk"; import { createSigner, createUser } from "@xmtp/agent-sdk"; // Generate random keypair for XMTP (or load existing) const user = createUser(); const client = await Client.create(createSigner(user), { env: "dev", // Use "production" for mainnet dbPath: "xmtp-db.db3" }); const inboxId = client.inboxId; // Your Nostr npub (bech32 format) const nostrNpub = "npub1yournpubhere..."; // Sign npub with XMTP installation key const signatureBytes = client.signWithInstallationKey(nostrNpub); const verificationSignature = Buffer.from(signatureBytes).toString("base64"); console.log("Inbox ID:", inboxId); console.log("Verification Signature:", verificationSignature); ``` ### Step 2: Publish Association in Kind 0 Metadata - Fetch the user's current kind 0 event (if any). - Parse and merge the new `"xmtp"` object. - Publish the updated kind 0 event, signed with your Nostr private key. ```javascript import { getPublicKey, nip19, finalizeEvent, SimplePool } from "nostr-tools"; // Your Nostr private key (nsec) — keep secure! const privateKey = "nsec1yourprivatekeyhere..."; const pubkey = getPublicKey(privateKey); const npub = nip19.npubEncode(pubkey); const pool = new SimplePool(); const relays = ["wss://relay.damus.io", "wss://nos.lol"]; // Fetch existing kind 0 const events = await pool.list(relays, [{ kinds: [0], authors: [pubkey] }]); let metadata = { name: "", about: "", picture: "" }; // Minimal defaults if (events.length > 0) { const latest = events.sort((a, b) => b.created_at - a.created_at)[0]; try { metadata = JSON.parse(latest.content); } catch (e) { console.warn("Invalid existing metadata JSON"); } } // Merge XMTP data metadata.xmtp = { inboxId, verificationSignature, createdAt: new Date().toISOString() }; // Create and publish kind 0 const event = { kind: 0, content: JSON.stringify(metadata), created_at: Math.floor(Date.now() / 1000), pubkey, tags: [], // Kind 0 typically has no tags }; const signedEvent = finalizeEvent(event, privateKey); await Promise.all(pool.publish(relays, signedEvent)); pool.close(relays); console.log("Published updated kind 0 with XMTP linking"); ``` ### Step 3: Resolution (npub to Inbox ID and Vice Versa) #### npub to Inbox ID - Fetch the latest kind 0 event for the pubkey. - Parse `content.xmtp`. - Verify the signature using XMTP SDK. ```javascript async function lookupInboxForNpub(npub) { const { type, data: pubkey } = nip19.decode(npub); if (type !== "npub") throw new Error("Invalid npub"); const pool = new SimplePool(); const relays = ["wss://relay.damus.io", "wss://nos.lol"]; const events = await pool.list(relays, [{ kinds: [0], authors: [pubkey] }]); pool.close(relays); if (events.length === 0) return null; const latest = events.sort((a, b) => b.created_at - a.created_at)[0]; let metadata; try { metadata = JSON.parse(latest.content); } catch (e) { return null; } const xmtp = metadata.xmtp; if (!xmtp || !xmtp.inboxId) return null; const isValid = await verifySignature(xmtp.inboxId, npub, xmtp.verificationSignature); return isValid ? xmtp.inboxId : null; } const verifySignature = async (inboxId, npub, verificationSignature) => { if (!verificationSignature) return false; // Optional: allow without sig if trusting publish const [inboxState] = await Client.fetchInboxStates([inboxId], { env: "dev" }); const signatureBytes = Buffer.from(verificationSignature, "base64"); for (const installation of inboxState.installations) { if (Client.verifySignedWithPublicKey(npub, signatureBytes, installation.bytes)) { return true; } } return false; }; ``` #### Inbox ID to npub - When receiving a message from an unknown XMTP Inbox: - Need to resolve Inbox ID to npub and profile. - **Solutions**: 1. **App-specific indexer**: Subscribe to kind 0 events and maintain a mapping (filter for those containing `"xmtp"`). 2. **Global infrastructure**: Use public Nostr search engines or relays that index kind 0 metadata. ## Complete Example Script Here's a full Node.js script that links a Nostr identity to a new XMTP inbox using kind 0: ```javascript import { Client } from "@xmtp/node-sdk"; import { createSigner, createUser } from "@xmtp/agent-sdk"; import { generatePrivateKey, getPublicKey, nip19, finalizeEvent, SimplePool } from "nostr-tools"; // Generate or load Nostr keys (demo: generate new — use secure storage in production) const privateKey = generatePrivateKey(); const pubkey = getPublicKey(privateKey); const npub = nip19.npubEncode(pubkey); console.log("Your Nostr npub:", npub); // Create XMTP client const user = createUser(); const client = await Client.create(createSigner(user), { env: "dev", dbPath: "xmtp-db.db3" }); const inboxId = client.inboxId; // Sign npub const signatureBytes = client.signWithInstallationKey(npub); const verificationSignature = Buffer.from(signatureBytes).toString("base64"); // Publish to Nostr kind 0 const pool = new SimplePool(); const relays = ["wss://relay.damus.io", "wss://nos.lol"]; // Fetch existing kind 0 const events = await pool.list(relays, [{ kinds: [0], authors: [pubkey] }]); let metadata = { name: "", about: "", picture: "" }; if (events.length > 0) { const latest = events.sort((a, b) => b.created_at - a.created_at)[0]; try { metadata = JSON.parse(latest.content); } catch (e) {} } // Merge metadata.xmtp = { inboxId, verificationSignature, createdAt: new Date().toISOString() }; const event = { kind: 0, content: JSON.stringify(metadata), created_at: Math.floor(Date.now() / 1000), pubkey, }; const signedEvent = finalizeEvent(event, privateKey); await Promise.all(pool.publish(relays, signedEvent)); pool.close(relays); console.log("Linked XMTP Inbox:", inboxId, "to Nostr npub:", npub); ``` ## Prerequisites - Node.js environment - Packages: `@xmtp/node-sdk`, `@xmtp/agent-sdk`, `nostr-tools` - Access to Nostr relays - XMTP dev environment (switch to production when ready) ## Notes - **Standard-compliant**: Uses only kind 0 (no new kinds or NIPs needed). Mirrors how Lightning addresses and other extensions are added. - **Security**: The cryptographic signature ensures the inbox claims the npub; publishing in signed kind 0 proves npub ownership. - **Discoverability**: Relies on fetching kind 0 events — robust since kind 0 is universally supported and cached by clients/relays. - **Extensions**: Could support NIP-05 by adding the domain as a separate field or signing it additionally. - **Future-proof**: If XMTP or the community formalizes a standard later, migration is straightforward. This method enables verifiable, standard-compliant linking between Nostr and XMTP identities, supporting cross-protocol messaging and identity enrichment. Inspired by [Using ATProto Identities on XMTP](https://xmtplabs.notion.site/Using-ATProto-Identities-On-XMTP-2f830823ce928049bdc5c664f803f733)