Skip to main content

DID:Nostr

The did:nostr method enables Decentralized Identifiers (DIDs) on the Nostr network, bridging Nostr with the W3C DID ecosystem.

Specification: nostrcg.github.io/did-nostr

Overview​

A Nostr DID is simply a Nostr public key formatted as a DID:

did:nostr:<64-character-hex-pubkey>

Example:

did:nostr:124c0fa99407182ece5a24fad9b7f6674902fc422843d3128d38a0afbee0fdd2

Key Points​

  • Uses the raw 64-character hex public key (not npub)
  • No on-chain registration required
  • Uniqueness derived from cryptographic key generation
  • Compatible with W3C Verifiable Credentials

DID Document Structure​

A minimal did:nostr document:

{
"@context": [
"https://www.w3.org/ns/did/v1",
"https://w3id.org/security/multikey/v1"
],
"id": "did:nostr:124c0fa99407182ece5a24fad9b7f6674902fc422843d3128d38a0afbee0fdd2",
"type": "DIDNostr",
"verificationMethod": [{
"id": "did:nostr:124c0fa9...#key-0",
"type": "Multikey",
"controller": "did:nostr:124c0fa9...",
"publicKeyMultibase": "fe70102124c0fa99407182ece5a24fad9b7f6674902fc422843d3128d38a0afbee0fdd2"
}],
"authentication": ["did:nostr:124c0fa9...#key-0"],
"assertionMethod": ["did:nostr:124c0fa9...#key-0"]
}

Optional Fields​

Profile Information (from kind 0):

{
"profile": {
"name": "Alice",
"about": "Building the decentralized web",
"picture": "https://example.com/photo.jpg",
"nip05": "alice@example.com",
"lud16": "alice@getalby.com",
"timestamp": 1737906600
}
}

Social Graph (from kind 3):

{
"follows": [
"did:nostr:32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245",
"did:nostr:46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184147700e3a8"
]
}

Relay Service Endpoints:

{
"service": [{
"id": "did:nostr:124c0fa9...#relay1",
"type": "Relay",
"serviceEndpoint": "wss://relay.example.com/"
}]
}

Alternative Identities:

{
"alsoKnownAs": [
"https://alice.example.com/#me",
"at://alice.bsky.social"
]
}

Resolution Strategies​

1. Offline-First (Minimal)​

Generate a valid DID document from the public key alone, without network access:

function resolveOffline(did: string): DIDDocument {
const pubkey = did.replace('did:nostr:', '');

// Convert to Multikey format
const multikey = toMultikey(pubkey);

return {
'@context': [
'https://www.w3.org/ns/did/v1',
'https://w3id.org/security/multikey/v1'
],
id: did,
type: 'DIDNostr',
verificationMethod: [{
id: `${did}#key-0`,
type: 'Multikey',
controller: did,
publicKeyMultibase: multikey
}],
authentication: [`${did}#key-0`],
assertionMethod: [`${did}#key-0`]
};
}

2. HTTP Resolution (Fastest)​

Query the .well-known path:

async function resolveHTTP(
did: string,
domain: string
): Promise<DIDDocument | null> {
const pubkey = did.replace('did:nostr:', '');
const url = `https://${domain}/.well-known/did/nostr/${pubkey}.json`;

try {
const response = await fetch(url);
if (!response.ok) return null;
return response.json();
} catch {
return null;
}
}

3. Enhanced Resolution (Relay Queries)​

Augment with Nostr relay data:

async function resolveEnhanced(
did: string,
pool: SimplePool,
relays: string[]
): Promise<DIDDocument> {
const pubkey = did.replace('did:nostr:', '');

// Start with minimal document
const doc = resolveOffline(did);

// Fetch profile (kind 0)
const profiles = await pool.querySync(relays, {
kinds: [0],
authors: [pubkey],
limit: 1
});

if (profiles.length > 0) {
const profile = JSON.parse(profiles[0].content);
doc.profile = {
name: profile.name,
about: profile.about,
picture: profile.picture,
nip05: profile.nip05,
lud16: profile.lud16,
timestamp: profiles[0].created_at
};
}

// Fetch follows (kind 3)
const contactLists = await pool.querySync(relays, {
kinds: [3],
authors: [pubkey],
limit: 1
});

if (contactLists.length > 0) {
doc.follows = contactLists[0].tags
.filter(t => t[0] === 'p')
.map(t => `did:nostr:${t[1]}`);
}

// Fetch relay list (kind 10002)
const relayLists = await pool.querySync(relays, {
kinds: [10002],
authors: [pubkey],
limit: 1
});

if (relayLists.length > 0) {
doc.service = relayLists[0].tags
.filter(t => t[0] === 'r')
.map((t, i) => ({
id: `${did}#relay${i}`,
type: 'Relay',
serviceEndpoint: t[1]
}));
}

return doc;
}

Multikey Encoding​

Transform Nostr's BIP-340 x-only public key to W3C Multikey format:

function toMultikey(pubkeyHex: string): string {
// 1. Parse the 32-byte x-only public key
const xOnly = hexToBytes(pubkeyHex);

// 2. Determine parity and add prefix
// (0x02 for even y, 0x03 for odd)
const parityByte = determineParityByte(xOnly);
const compressed = new Uint8Array([parityByte, ...xOnly]);

// 3. Add secp256k1 multicodec prefix (0xe7, 0x01)
const withCodec = new Uint8Array([0xe7, 0x01, ...compressed]);

// 4. Encode with multibase base16-lower (prefix 'f')
return 'f' + bytesToHex(withCodec);
}

// Example:
// Input: 124c0fa99407182ece5a24fad9b7f6674902fc422843d3128d38a0afbee0fdd2
// Output: fe70102124c0fa99407182ece5a24fad9b7f6674902fc422843d3128d38a0afbee0fdd2

CRUD Operations​

Create​

Generate a DID by creating a Nostr keypair:

import { generateSecretKey, getPublicKey } from 'nostr-tools';

function createNostrDID(): { did: string; secretKey: Uint8Array } {
const secretKey = generateSecretKey();
const pubkey = getPublicKey(secretKey);

return {
did: `did:nostr:${pubkey}`,
secretKey
};
}

Read (Resolve)​

Use any of the three resolution strategies above.

Update​

Not supported. The DID document is derived from Nostr events. To "update" the document, publish new Nostr events (kind 0, 3, 10002) and re-resolve.

Delete​

Not supported. Nostr DIDs cannot be deactivated. Loss of the private key means loss of control.

Converting Between Formats​

import { nip19 } from 'nostr-tools';

// npub to DID
function npubToDid(npub: string): string {
const { data } = nip19.decode(npub);
return `did:nostr:${data}`;
}

// DID to npub
function didToNpub(did: string): string {
const pubkey = did.replace('did:nostr:', '');
return nip19.npubEncode(pubkey);
}

// hex to DID
function hexToDid(pubkey: string): string {
return `did:nostr:${pubkey.toLowerCase()}`;
}

Hosting DID Documents​

Serve DID documents via .well-known:

https://example.com/.well-known/did/nostr/<pubkey>.json

Nginx configuration:

location ~ ^/.well-known/did/nostr/([a-f0-9]{64})\.json$ {
alias /var/www/did-documents/$1.json;
add_header Content-Type application/json;
add_header Access-Control-Allow-Origin *;
}

Dynamic resolution (Node.js):

app.get('/.well-known/did/nostr/:pubkey.json', async (req, res) => {
const { pubkey } = req.params;

if (!/^[a-f0-9]{64}$/.test(pubkey)) {
return res.status(400).json({ error: 'Invalid pubkey' });
}

const did = `did:nostr:${pubkey}`;
const document = await resolveEnhanced(did, pool, relays);

res.setHeader('Content-Type', 'application/json');
res.setHeader('Nostr-Timestamp', Math.floor(Date.now() / 1000));
res.json(document);
});

Use Cases​

Verifiable Credentials​

Issue credentials signed with Nostr keys:

{
"@context": ["https://www.w3.org/2018/credentials/v1"],
"type": ["VerifiableCredential"],
"issuer": "did:nostr:124c0fa99407182ece5a24fad9b7f6674902fc422843d3128d38a0afbee0fdd2",
"credentialSubject": {
"id": "did:nostr:32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245",
"achievement": "Nostr Developer"
}
}

Cross-Platform Identity​

Link Nostr identity to other systems:

{
"alsoKnownAs": [
"did:web:alice.example.com",
"did:key:z6Mkf...",
"at://alice.bsky.social"
]
}

Security Considerations​

ConcernMitigation
Key lossNo recovery - maintain secure backups
CorrelationUse separate DIDs for different contexts
Public dataDon't include sensitive info in documents
Relay trustUse multiple relays, verify signatures

Reference Implementations​

See Also​