1 Overview
A Nostr public key is a 64-character hex string. It is precise, but nobody can read it at a glance or remember it. NIP-01 defines the kind 0 user metadata event, where a user publishes a display name, an avatar, and other profile fields.
NIP-05 adds one more optional metadata field: nip05. Its value looks like an email address, for example bob@example.com. A client that sees this field can ask the domain example.com, over plain HTTPS, which public key the name bob belongs to. If the answer matches the event's own pubkey, the client treats the identifier as valid and may display it.
This check needs no new cryptography and no new server type. The domain owner edits or serves one JSON file. Any web host that can serve a static file, or run a small script, can support NIP-05.
NIP-05 does not replace the public key. It only attaches a friendly label to it, and lets anyone confirm that the label and the key currently agree. The label can change or disappear; the public key stays the same identity forever.
Think of nip05 like a business card, not a passport. It helps people find and recognize a key. It does not, by itself, prove who controls that key.
2 The nip05 Field and Identifier Format
A client that wants to publish an identifier adds a nip05 key to the stringified JSON in the content field of its kind 0 event. The value is an internet identifier: a string shaped like <local-part>@<domain>.
{
"name": "bob",
"about": <string, optional>,
"nip05": "bob@example.com"
}
The internet-identifier specification linked above is very liberal about what a local part may contain. NIP-05 is stricter. Follow this rule exactly:
| Part | Rule |
|---|---|
| <local-part> | MUST use only the characters a-z, 0-9, -, _, and .. No uppercase letters, no spaces, no other symbols. |
| <domain> | A normal DNS domain name. The client sends its verification request here. |
The local part is case-sensitive in the lookup, and both it and the returned public keys must already be lowercase. A client must not lowercase or otherwise normalize the local part before building the request URL.
3 The Well-Known Endpoint and Verification Flow
To verify a nip05 value, the client splits it into <local-part> and <domain> at the @ character. It then makes an HTTPS GET request to a fixed, well-known path on that domain:
https://<domain>/.well-known/nostr.json?name=<local-part>
The server answers with a JSON object. It must contain a names key: a mapping from plain names to hex-formatted public keys, in lowercase.
{
"names": { "<name>": <lowercase hex pubkey>, ... }
}
The client reads the value at names[<local-part>]. If that hex string equals the pubkey of the kind 0 event that carried the nip05 field, the identifier is valid. The client may now display it. If the key is missing, or does not match, the identifier is not valid and the client should not display it as verified.
4 The relays Attribute
The response object may also carry a recommended relays key. It maps each public key to a list of relay URLs where that user's events may be found.
{
"names": {
"bob": "b0635d6a9851d3aed0cd6c495b282167acf761729078d975fc341b22650b07b9"
},
"relays": {
"b0635d6a9851d3aed0cd6c495b282167acf761729078d975fc341b22650b07b9": [
"wss://relay.example.com", "wss://relay2.example.com"
]
}
}
A client may use this list the same way it uses a NIP-65 relay list, to find where a newly-discovered user actually publishes. A web server that builds this file dynamically, per query, should also include the matching relays entry for any name it serves, whenever that data is available.
5 Finding Users by Identifier
A client may also run the flow in reverse: start from an identifier typed by a user, such as a search box entry, and end with a full profile.
- Split the typed identifier into
<local-part>and<domain>. - Fetch the well-known URL and read the public key from
names. - Fetch the newest kind 0 event for that public key.
- Check that the event's own
nip05field matches the identifier the user typed.
This lets a client offer a search box where a user types bob@example.com and the client resolves it straight to a pubkey and a profile.
6 Security Rules and Client Behavior
The /.well-known/nostr.json endpoint MUST NOT return an HTTP redirect. A fetcher MUST ignore any redirect this endpoint sends. Do not follow it, even automatically at the HTTP-library level.
A few more rules protect users from small mistakes turning into identity confusion:
- Keys are hex, always. The
namesmapping must use lowercase hex public keys. NIP-19npubvalues are for display in a client's user interface only. Never put annpubin a NIP-05 response. - Follow the pubkey, not the address. Suppose a client resolves
bob@bob.comto the keyabc...defand a user chooses to follow that profile. The client storesabc...defas the follow target. Ifbob.comlater starts returning a different key,1d2...e3f, the client must not silently switch the followed key. It should instead stop showingbob@bob.comas a valid label for the old key, because the identifier no longer matches. - The root identifier. A domain owner who does not want a redundant-looking address like
bob@bob.commay register the local part_instead, giving_@bob.com. Clients should show and treat_@domainas justdomain.
Why a query string, ?name=<local-part>, instead of a path segment? A query string lets one /.well-known/nostr.json address serve both a dynamic server, which computes the answer per request, and a static file that simply lists every registered name at once.
Browser CORS policy can block a JavaScript client from reading /.well-known/nostr.json on a domain it does not own. A blocked CORS request looks exactly like a missing file: pure-JavaScript code cannot tell the two apart. If a JS client cannot verify a NIP-05 identifier, it may suggest the user check their server's CORS headers with a command like the one in the worked example below. Domain owners should serve /.well-known/nostr.json with the header Access-Control-Allow-Origin: * so that browser-based clients can read it.
NIP-05 identifies, it does not verify trust by itself. Anyone can register any domain and claim any name on it. The exception: a person or project already known to control a well-known domain — a company, an established project — gains real trust value from a NIP-05 match on that domain, because the domain itself is the trust anchor people already recognize.
7 Worked Example
Client sees the kind 0 event below and wants to verify the nip05 field.
{
"pubkey": "b0635d6a9851d3aed0cd6c495b282167acf761729078d975fc341b22650b07b9",
"kind": 0,
"content": "{\"name\": \"bob\", \"nip05\": \"bob@example.com\"}"
// id, created_at, tags, sig omitted
}
The identifier splits into local part bob and domain example.com. The client makes this HTTPS request:
→ GET /.well-known/nostr.json?name=bob HTTP/1.1 → Host: example.com → Accept: application/json ← HTTP/1.1 200 OK ← Content-Type: application/json ← Access-Control-Allow-Origin: * ← ← { ← "names": { ← "bob": "b0635d6a9851d3aed0cd6c495b282167acf761729078d975fc341b22650b07b9" ← }, ← "relays": { ← "b0635d6a9851d3aed0cd6c495b282167acf761729078d975fc341b22650b07b9": [ ← "wss://relay.example.com", "wss://relay2.example.com" ← ] ← } ← } -- names.bob equals event.pubkey, so the client marks bob@example.com valid --
If a JavaScript client instead sees a network failure at this step, it cannot tell whether the file is missing or CORS blocked it. The domain owner can check their own server from a terminal:
$ curl -sI https://example.com/.well-known/nostr.json?name=bob | grep -i ^Access-Control Access-Control-Allow-Origin: *
8 Version History
Notable changes to NIP-05, newest first:
- 20262026-02Made explicit that both hex public keys and local-part names must be lowercase (#2208).
- 20242024-08Added the identification-versus-verification distinction: NIP-05 identifies a key, it does not by itself prove trust.
- 20242024-06Renamed the informal "set_metadata" wording to "user metadata," matching NIP-01 terminology.
- 20242024-03Upgraded the
relaysattribute from a plain suggestion to recommended. - 20232023-02A proposed security extension (per-account allowed paths) was merged, then reverted days later. No trace remains in the current spec.
- 20232023-01A short-lived NIP-35 on DNS-identifier reflectivity was merged into NIP-05; the number 35 was later reused for an unrelated proposal (Torrents).
- 20222022-12Clarified that clients must keep following the pubkey, not the nip05 address, if the two ever disagree later; required hex-only keys in responses.
- 20222022-05Added the CORS warning and the
Access-Control-Allow-Originrecommendation for JavaScript clients. - Origin2022-05Migrated into the
nostr-protocol/nipsrepository from the originalfiatjaf/nostrrepository.
The source history is confusing on one point: an early commit message says "merge NIP-35 into NIP-05," but the current manifest lists NIP-35 as an unrelated, active proposal (Torrents). The number was evidently freed and reused later. Nothing in the current 05.md depends on that old NIP-35 content, so this page describes only the present spec.
Original Proposal - Credits
| Author | fiatjaf |
| First commit | 2022-05-01 — "migrate nips from main nostr repo" |
| Status | final - optional |
| Source | 05.md on GitHub - full change history |
9 See Also
- NIP-01 — defines the kind 0 user metadata event that carries the
nip05field. - NIP-19 — bech32
npubencoding, for display only; never used inside a NIP-05 response. - NIP-65 — relay list metadata; a more complete alternative to the optional
relaysattribute here. - NIP-39 — linking a profile to other platforms; a related but separate identity-claim mechanism.