Learn-skills.dev nostr-nip05-setup
Set up NIP-05 DNS-based identity verification for Nostr, including the /.well-known/nostr.json endpoint, CORS headers, and kind:0 profile updates. Use when configuring NIP-05, setting up nostr.json, debugging NIP-05 verification failures, adding DNS identity to a Nostr profile, or working with /.well-known/nostr.json files and CORS for Nostr clients.
git clone https://github.com/NeverSight/learn-skills.dev
T=$(mktemp -d) && git clone --depth=1 https://github.com/NeverSight/learn-skills.dev "$T" && mkdir -p ~/.claude/skills && cp -r "$T/data/skills-md/accolver/skill-maker/nostr-nip05-setup" ~/.claude/skills/neversight-learn-skills-dev-nostr-nip05-setup && rm -rf "$T"
data/skills-md/accolver/skill-maker/nostr-nip05-setup/SKILL.mdNIP-05 Identity Setup
Overview
Set up NIP-05 DNS-based identity verification so Nostr clients can map human-readable identifiers (like
bob@example.com) to hex public keys. This
skill covers creating the /.well-known/nostr.json endpoint, configuring CORS
headers, updating kind:0 profiles, and debugging verification failures.
When to Use
- Setting up NIP-05 verification for a domain
- Creating or editing a
file/.well-known/nostr.json - Configuring web server CORS headers for Nostr clients
- Updating a kind:0 profile with a
fieldnip05 - Debugging why NIP-05 verification is failing
- Setting up the
root identifier_@domain
Do NOT use when:
- Building a Nostr relay (that's relay protocol)
- Working with NIP-19 bech32 encoding (npub/nsec display format)
- Implementing Nostr event signing or key management
Workflow
1. Determine the Setup Type
Ask: "Static file or dynamic server?"
| Scenario | Approach | Best For |
|---|---|---|
| Few users, rarely changes | Static file | Personal sites, small communities |
| Many users, frequent changes | Dynamic server with query handling | NIP-05 providers, large communities |
| Existing web server | Add location block + CORS headers | Nginx, Apache, Caddy setups |
2. Create the nostr.json File
The file maps human-readable names to hex public keys.
Format:
{ "names": { "bob": "b0635d6a9851d3aed0cd6c495b282167acf761729078d975fc341b22650b07b9" }, "relays": { "b0635d6a9851d3aed0cd6c495b282167acf761729078d975fc341b22650b07b9": [ "wss://relay.example.com", "wss://relay2.example.com" ] } }
Critical rules:
- Public keys MUST be 64-character lowercase hex (NOT npub bech32 format)
- The
object is REQUIREDnames - The
object is RECOMMENDED — it helps clients discover where to find the userrelays - Local-part (the name) MUST only contain
(lowercase, no uppercase)a-z0-9-_.
is the special "root" identifier —_
displays as just_@example.com
in clientsexample.com
Root identifier example:
{ "names": { "_": "b0635d6a9851d3aed0cd6c495b282167acf761729078d975fc341b22650b07b9" } }
This lets the user be identified as just
example.com instead of
bob@example.com.
3. Serve the File Correctly
The file MUST be served at exactly:
https://<domain>/.well-known/nostr.json?name=<local-part>
Three non-negotiable requirements:
- HTTPS only — HTTP will not work. Clients only fetch over HTTPS.
- CORS header — Response MUST include
because JavaScript Nostr clients run in browsers and are blocked by CORS policies without this header.Access-Control-Allow-Origin: * - No redirects — The endpoint MUST NOT return HTTP redirects (301, 302, etc.). Fetchers MUST ignore redirects per the NIP-05 spec. This is a security constraint.
The
?name=<local-part> query string format exists so both static files and
dynamic servers work. A static file ignores the query string and returns all
names; a dynamic server can filter by name.
See references/server-configs.md for Nginx, Apache, Caddy, and Node.js configuration examples.
4. Update the Kind:0 Profile
The user's kind:0 metadata event must include the
nip05 field:
{ "kind": 0, "content": "{\"name\":\"bob\",\"nip05\":\"bob@example.com\",\"about\":\"A Nostr user\",\"picture\":\"https://example.com/avatar.jpg\"}" }
The
nip05 value format is <local-part>@<domain> — it looks like an email
address but is NOT an email address.
Verification flow (what clients do):
- Client sees
in a kind:0 event"nip05": "bob@example.com" - Client splits into local-part
and domainbobexample.com - Client fetches
https://example.com/.well-known/nostr.json?name=bob - Client checks if
matches the event'sresponse.names.bobpubkey - If match → identity is verified and displayed
5. Test the Setup
Verify the endpoint returns correct JSON:
curl -s https://example.com/.well-known/nostr.json?name=bob | jq .
Verify CORS headers are present:
curl -sI https://example.com/.well-known/nostr.json?name=bob | grep -i ^access-control # Expected: Access-Control-Allow-Origin: *
Verify no redirects:
curl -sI -o /dev/null -w "%{http_code}" https://example.com/.well-known/nostr.json?name=bob # Expected: 200 (NOT 301, 302, 307, 308)
Verify the pubkey is hex (not npub):
curl -s https://example.com/.well-known/nostr.json?name=bob | jq -r '.names.bob' | grep -E '^[0-9a-f]{64}$' # Should match — 64 lowercase hex characters
Checklist
- nostr.json file created with correct
mappingnames - Public keys are 64-char lowercase hex (not npub)
- Local-parts use only
charactersa-z0-9-_. - File served over HTTPS
-
header presentAccess-Control-Allow-Origin: * - No HTTP redirects on the endpoint
-
object included (recommended)relays - Kind:0 profile updated with
fieldnip05 - Endpoint tested with curl and returns 200 with correct JSON
Common Mistakes
| Mistake | Why It Breaks | Fix |
|---|---|---|
| Using npub instead of hex pubkey | NIP-05 requires hex format; npub is only for UI display | Convert npub to hex using a tool like |
| Missing CORS header | JavaScript Nostr clients in browsers can't fetch the file | Add to server config |
| HTTP redirects (301/302) | NIP-05 spec says fetchers MUST ignore redirects | Serve directly, no redirects — check trailing slash redirects |
| Uppercase in local-part | Spec requires only | Normalize to lowercase before storing |
| Serving over HTTP instead of HTTPS | Clients only fetch NIP-05 over HTTPS | Set up TLS (Let's Encrypt, Cloudflare, etc.) |
Trailing slash redirect on | Nginx/Apache may redirect to | Ensure exact match location, not directory |
| Wrong Content-Type | Some servers don't set | Add header |
| Query string breaks static file | Some servers reject or misroute on static files | Ensure server passes query strings through to static files |
Quick Reference
| Item | Value |
|---|---|
| Endpoint | |
| Required header | |
| Pubkey format | 64-char lowercase hex |
| Local-part chars | |
| Root identifier | (displays as just the domain) |
| Profile field | in kind:0 content |
| Protocol | HTTPS only (no HTTP) |
| Redirects | MUST NOT redirect |
Key Principles
-
Hex keys, never npub — The nostr.json file uses raw 64-character lowercase hex public keys. npub is a display format (NIP-19) and must never appear in nostr.json. This is the single most common mistake.
-
CORS is mandatory — Without
, every browser-based Nostr client silently fails to verify the identity. The user sees no error — verification just doesn't work.Access-Control-Allow-Origin: * -
No redirects, ever — HTTP redirects are a security risk in this context. The spec explicitly requires fetchers to ignore them. Common culprits: trailing-slash redirects, HTTP→HTTPS redirects on the endpoint itself, and www→non-www redirects.
-
Identification, not verification — NIP-05 identifies users (maps human-readable names to keys), it does not verify trust. The exception is well-known domains (companies, projects) where domain ownership itself implies trust.
-
Clients follow pubkeys, not NIP-05 addresses — If a user follows
, the client stores the pubkey, not the address. If the NIP-05 mapping changes later, the client unfollows the address display but keeps following the pubkey.bob@example.com