NIP-05: DNS-Based Verification
Map human-readable identifiers to Nostr pubkeys via DNS. Verification through .well-known/nostr.json.
| Type | Nostr Implementation Possibility |
| Number | nip-05 |
| Status | Final |
| Original | https://github.com/nostr-protocol/nips/blob/master/05.md |
NIP-05: DNS-Based Verification
Status: Final
NIP-05 maps human-readable identifiers to Nostr pubkeys using DNS. Instead of remembering npub1qqqsyq..., you can find someone at alice@example.com.
Overview
alice@example.com
│
└── Resolves to ──► npub1abc...
The domain owner hosts a JSON file that maps names to pubkeys, proving the association.
How It Works
1. User Claims Identifier
User adds NIP-05 to their profile (kind 0):
{
"kind": 0,
"content": "{\"name\":\"Alice\",\"nip05\":\"alice@example.com\"}"
}
2. Domain Hosts Verification File
Server at example.com/.well-known/nostr.json:
{
"names": {
"alice": "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"
},
"relays": {
"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d": [
"wss://relay.example.com",
"wss://relay.damus.io"
]
}
}
3. Clients Verify
When displaying alice@example.com:
- Fetch
https://example.com/.well-known/nostr.json?name=alice - Check if
names.alicematches the pubkey in the event - Display verification badge if match
Verification Implementation
import httpx
async def verify_nip05(identifier: str, expected_pubkey: str) -> bool:
"""Verify a NIP-05 identifier against a pubkey."""
if "@" not in identifier:
return False
name, domain = identifier.split("@", 1)
# Fetch verification file
url = f"https://{domain}/.well-known/nostr.json?name={name}"
try:
async with httpx.AsyncClient() as client:
response = await client.get(url, timeout=10)
response.raise_for_status()
data = response.json()
# Check pubkey match
claimed_pubkey = data.get("names", {}).get(name)
return claimed_pubkey == expected_pubkey
except Exception:
return False
import { nip05 } from 'nostr-tools';
async function verifyNip05(identifier, expectedPubkey) {
try {
const profile = await nip05.queryProfile(identifier);
return profile && profile.pubkey === expectedPubkey;
} catch {
return false;
}
}
Looking Up Users
Find a user’s pubkey from their identifier:
async def lookup_nip05(identifier: str) -> dict | None:
"""Look up pubkey and relays from NIP-05 identifier."""
if "@" not in identifier:
return None
name, domain = identifier.split("@", 1)
url = f"https://{domain}/.well-known/nostr.json?name={name}"
try:
async with httpx.AsyncClient() as client:
response = await client.get(url, timeout=10)
response.raise_for_status()
data = response.json()
pubkey = data.get("names", {}).get(name)
if not pubkey:
return None
relays = data.get("relays", {}).get(pubkey, [])
return {
"identifier": identifier,
"pubkey": pubkey,
"relays": relays
}
except Exception:
return None
const profile = await nip05.queryProfile('alice@example.com');
if (profile) {
console.log('Pubkey:', profile.pubkey);
console.log('Relays:', profile.relays);
}
Setting Up NIP-05
Static File (Simple)
Create /.well-known/nostr.json:
{
"names": {
"alice": "pubkey-hex-alice",
"bob": "pubkey-hex-bob",
"_": "default-pubkey"
}
}
The _ entry handles @domain.com (no name part).
Server Requirements
- HTTPS required — HTTP won’t work
- CORS headers — Allow cross-origin requests:
Access-Control-Allow-Origin: * - Content-Type —
application/json - Query parameter — Support
?name=parameter
Dynamic Endpoint (Advanced)
from fastapi import FastAPI, Query
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["GET"],
)
@app.get("/.well-known/nostr.json")
async def nostr_json(name: str = Query(None)):
# Look up in database
if name:
user = await db.get_user_by_name(name)
if user:
return {
"names": {name: user.pubkey},
"relays": {user.pubkey: user.relays}
}
return {"names": {}}
Nginx Configuration
location /.well-known/nostr.json {
add_header Access-Control-Allow-Origin *;
add_header Content-Type application/json;
# Static file
try_files $uri =404;
# Or proxy to backend
# proxy_pass http://backend/.well-known/nostr.json;
}
Agent Identity
Set up NIP-05 for your agent:
1. Create Agent Profile
profile_content = json.dumps({
"name": "MyAgent",
"about": "An AI agent on Nostr",
"nip05": "agent@yourdomain.com",
"lud16": "agent@getalby.com" # Optional: receive zaps
})
profile_event = create_event(
kind=0,
content=profile_content,
private_key=AGENT_PRIVATE_KEY
)
await publish(profile_event)
2. Host Verification File
At yourdomain.com/.well-known/nostr.json:
{
"names": {
"agent": "your-agent-pubkey-hex"
},
"relays": {
"your-agent-pubkey-hex": [
"wss://relay.damus.io",
"wss://nos.lol"
]
}
}
3. Verify It Works
result = await lookup_nip05("agent@yourdomain.com")
assert result["pubkey"] == AGENT_PUBLIC_KEY
print("NIP-05 verified!")
Relay Hints
The relays field helps clients find users:
{
"names": {"alice": "pubkey-hex"},
"relays": {
"pubkey-hex": [
"wss://relay.damus.io",
"wss://personal-relay.alice.com"
]
}
}
Clients can:
- Connect to listed relays
- Subscribe to that pubkey
- Find the user’s content
Common Patterns
Subdomain as Username
alice.example.com → _@alice.example.com
nostr.json at alice.example.com/.well-known/nostr.json:
{"names": {"_": "alice-pubkey"}}
Multiple Users
{
"names": {
"alice": "pubkey-alice",
"bob": "pubkey-bob",
"charlie": "pubkey-charlie"
}
}
Case Sensitivity
Names SHOULD be treated as case-insensitive. Normalize to lowercase:
name = name.lower()
Security Considerations
Domain Control
NIP-05 only proves someone controls the domain. It doesn’t verify real-world identity.
HTTPS Required
Always use HTTPS. HTTP is not secure and should fail verification.
Caching
Clients may cache results. Updates may take time to propagate.
Impersonation
Anyone can claim any NIP-05 in their profile. Only verification proves it.
# Profile claims alice@example.com
# But verification fails...
is_valid = await verify_nip05("alice@example.com", event["pubkey"])
if not is_valid:
# Don't trust the claim!
pass
Machine-Readable Summary
{
"nip": 5,
"title": "DNS-Based Verification",
"status": "final",
"defines": [
"nip05-identifier-format",
"well-known-json-structure",
"verification-flow",
"relay-hints"
],
"identifier_format": "name@domain",
"endpoint": "/.well-known/nostr.json",
"query_param": "name",
"requirements": [
"https",
"cors-headers",
"json-content-type"
],
"related": [
"/learn/nostr/identifiers",
"/learn/nostr/keys",
"/learn/nostr/specs/nip-01"
]
}