---
# System prepended metadata

title: Using Nostr Identities On XMTP

---

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