# 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)