Chapter 05 - Identity
05
Nostr Implementation Possibility - Hex № 05

Mapping Nostr Keys to DNS-Based Internet Identifiers

A human-readable name@domain label for a public key, checked against a small JSON file the domain owner controls.

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.

Hint!

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>.

Wire format - kind 0 content, with nip05
{
  "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:

Identifier format rules
PartRule
<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.
Warning!

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:

Well-known URL template
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.

Response shape - /.well-known/nostr.json
{
  "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.

Client example.com (HTTPS) GET /.well-known/nostr.json?name=bob 200 OK {"names":{"bob":"b0635d6a…"}} Client compares names.bob to the kind 0 event's pubkey MATCH → nip05 identifier is valid No match, or key missing → identifier is not valid
Fig. 5-1 — Verification flow. The client, not the domain, decides whether the identifier is valid. It only compares two hex strings it already trusts.

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.

Response shape - with relays
{
  "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.

  1. Split the typed identifier into <local-part> and <domain>.
  2. Fetch the well-known URL and read the public key from names.
  3. Fetch the newest kind 0 event for that public key.
  4. Check that the event's own nip05 field 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.

Typed ID bob@example.com well-known → pubkey kind 0 fetch by pubkey Check nip05 matches?
Fig. 5-2 — Reverse discovery. The same two checks as Fig. 5-1, run starting from a typed name instead of an existing event.

6 Security Rules and Client Behavior

Warning!

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 names mapping must use lowercase hex public keys. NIP-19 npub values are for display in a client's user interface only. Never put an npub in a NIP-05 response.
  • Follow the pubkey, not the address. Suppose a client resolves bob@bob.com to the key abc...def and a user chooses to follow that profile. The client stores abc...def as the follow target. If bob.com later starts returning a different key, 1d2...e3f, the client must not silently switch the followed key. It should instead stop showing bob@bob.com as 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.com may register the local part _ instead, giving _@bob.com. Clients should show and treat _@domain as just domain.
Tech Note

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.

Hint!

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.

Kind 0 event (fields relevant to this NIP)
{
  "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:

HTTP session - https://example.com/.well-known/nostr.json
→ 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:

Checking CORS headers - 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 relays attribute 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-Origin recommendation for JavaScript clients.
  • Origin2022-05Migrated into the nostr-protocol/nips repository from the original fiatjaf/nostr repository.
Tech Note

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

Authorfiatjaf
First commit2022-05-01 — "migrate nips from main nostr repo"
Statusfinal - optional
Source05.md on GitHub - full change history

9 See Also

  • NIP-01 — defines the kind 0 user metadata event that carries the nip05 field.
  • NIP-19 — bech32 npub encoding, for display only; never used inside a NIP-05 response.
  • NIP-65 — relay list metadata; a more complete alternative to the optional relays attribute here.
  • NIP-39 — linking a profile to other platforms; a related but separate identity-claim mechanism.