Introduction
The previous post surveyed where Nostr and Decentralized Identifiers overlap and mentioned the did:nostr method in passing. The draft deserves a closer look.
Two framing points before the details. It is a Draft Community Group Report from the W3C Nostr Community Group, not a W3C standard, and its own status section says it "is inappropriate to cite this document as other than work in progress." It is also a moving target, so pin your reading: everything below is against version 0.1.1, the version the document's own subtitle declares, bumped in the spec repository in July 2026.
The method takes an approach genuinely different from the anchored methods this blog usually covers. Instead of committing operations to a ledger, it treats the DID document as, in the draft's own words, "a projection of signed Nostr events." That design buys real things. It also gives up real things. This post works through both without grading on a curve.
Deriving the Identifier
The identifier construction is about as simple as a DID method gets. A Nostr identity is a secp256k1 public key, and the draft renders it as hex: "The public key is represented as a 64-character, lowercase string."
did:nostr:<64-char-lowercase-hex-pubkey>
The draft is normative about what that string has to be: "A conformant did:nostr identifier MUST be a valid secp256k1 x-only public key," meaning its 32-byte value must be a field element and the x-coordinate of a point on the curve.
There is no registration step, no transaction, no fee. The draft's Create operation ends with the note: "No on-chain registration is required as the security and uniqueness are derived from the key generation process." If you have a Nostr keypair, you already have a did:nostr identifier, whether or not you have ever resolved it. This matches the zero-cost creation property that did:btcr2 achieves for key-based identifiers, arrived at from a different direction: btcr2 derives a Bech32m identifier from the key and defers any on-chain footprint until the first update, while did:nostr never has an on-chain footprint at all.
Note the encoding choice. The method specifies hex rather than the npub Bech32 form from NIP-19, and it is explicit about the distinction: "Nostr DIDs use the raw 64-character hexadecimal public key, not the npub format," and "npubs should be considered display-only identifiers to improve readability in user interfaces." NIP-19 agrees on its own scope, saying its bech32 formats "are not meant to be used anywhere in the core protocol." That is the right call for a DID. NIP-01 moves keys as lowercase hex throughout, requiring that filter lists "MUST contain exact 64-character lowercase hex values," and a DID is machine-facing infrastructure, so it should carry the machine-facing encoding. It does mean the string a user recognizes as their Nostr identity is not the string that appears in their DID, which is a UX seam implementers will need to paper over.
Three Resolution Paths, Not One
Here is where the method gets interesting, and where the common one-line summary ("did:nostr resolves from relays") is already wrong. The draft defines resolution as an ordered strategy of three paths:
DID resolution for did:nostr identifiers follows this strategy:
- HTTP Resolution - Check
https://<domain>/.well-known/did/nostr/<pubkey>.json- Offline-first Resolution - Generate minimal DID document from public key alone
- Enhanced Resolution - Optionally query Nostr relays for additional metadata
HTTP first. The HTTP path carries the only location-level MUST in the resolution section: "The HTTP representation of a did:nostr document MUST be located at: https://<domain>/.well-known/did/nostr/<pubkey>.json." Servers SHOULD serve it with application/did+json and SHOULD include ETag, Cache-Control, and Last-Modified headers. What the draft does not specify is where <domain> comes from. The only discovery hint on offer is a MAY: "Resolvers MAY check NIP-05 endpoints for a didResolver field to locate HTTP-hosted DID documents before querying relays." That leaves a conforming resolver fetching a DID document from an ordinary web server, over an ordinary TLS connection, with no signature over the document as a whole. Worth sitting with before you describe this as a relay-only method.
Offline second, and this is the good part. The draft makes the network-free path normative rather than incidental: "A conforming resolver MUST be able to generate a valid DID document using only the public key from the DID identifier, without requiring network access to Nostr relays," restated later as "Implementations MUST support minimal resolution without network access." The verification method falls out of the identifier, so authentication and assertionMethod can be generated deterministically without consulting anyone. The encoding is a Multikey: prepend a parity byte to the x-only key to form a 33-byte compressed key, prepend "the multicodec varint for secp256k1 compressed public keys (0xe7, 0x01)," and multibase-encode with base16-lower. The draft also requires that "All DID documents MUST include a type field with the value DIDNostr."
Relays third, and optional. "Note that relay queries and metadata integration are OPTIONAL enhancements to the basic resolution process." Enhanced resolution maps event kinds onto named document fields:
- Kind 0 (user metadata): profile fields such as name, picture, and NIP-05 address, surfaced as
profile, with acreated_atcarrying "theevent.created_atof the latest Nostr profile (kind 0) event, in Unix seconds" - Kind 3 (contact list): the social graph, surfaced as
follows, with each entry rendered as adid:nostridentifier, and the draft recommends bounding the inline list for large accounts - Kind 10002: NIP-65 relay list metadata, which advertises "relays where the user generally writes to and relays where the user generally reads mentions," surfaced as
serviceentries of typeRelay
One inconsistency to flag: the draft's relay-discovery steps say to query default relays with a filter for "kind 0 (metadata) and kind 3 (contact list) events by the target public key." Kind 10002 is not in that filter, even though the draft elsewhere treats it as the source of the service array and of the modified timestamp. If you implement the discovery steps literally, you will not fetch the relay list you are supposed to publish.
So the security-critical part of resolution requires no network access at all. A resolver that cannot reach a single relay can still produce a document with a correct verification method, because the key was in the identifier the whole time. Relay availability affects the richness of the document, not its cryptographic core.
Why a Projection Is Not a History
Now the part that requires care.
Anchored methods produce a document by replaying an ordered sequence of operations. did:btcr2 resolution starts from an initial DID document (rendered deterministically from the key for key-based identifiers, or supplied as sidecar data when the identifier commits to the hash of a genesis document), collects Beacon Signals from confirmed Bitcoin transactions, orders the updates they announce by targetVersionId ascending with block height as a tiebreaker, and for each update checks that the current document hashes to the declared sourceHash, verifies the Data Integrity proof, applies the JSON Patch, and confirms the result hashes to the declared targetHash. The output is the endpoint of a single canonical chain, and the resolver can detect when someone tries to insert a branch after the fact: an update whose targetVersionId is more than one ahead of the current version raises a LATE_PUBLISHING error. That detection is the whole point of the late-publishing machinery.
A did:nostr document is produced differently. The resolver asks relays for the current state of a handful of event kinds and renders what it gets. All three source kinds are replaceable in Nostr's model. NIP-01 puts it normatively: "for kind n such that 10000 <= n < 20000 || n == 0 || n == 3, events are replaceable, which means that, for each combination of pubkey and kind, only the latest event MUST be stored by relays, older versions MAY be discarded." Kind 0, kind 3, and kind 10002 all fall inside that rule. Publishing a new one supersedes the old one, and no relay is obliged to retain the superseded version.
Three consequences follow, and they compound.
Resolution is not reproducible over time. Ask for the document today and again next month and you may get different answers with no record of what changed. The draft does define an Update operation, so change is expected rather than anomalous, though it is an unusual one: "The did:nostr identifier is permanent: the key-to-DID binding cannot be changed," and only the projection moves. What is missing is the auditable trail. Nothing in the method obliges anyone to keep the prior state, so "what did this document say last March" is not a question the method can answer.
Two honest resolvers can disagree, and the draft's staleness signal only half closes the gap. Nostr has no consensus layer. If a pubkey publishes an updated kind 0 to relays A and B but not to C, a resolver reading C returns stale data. The draft does give consumers something to compare: a modified field whose value is "the most recent created_at across all signed parts composed into the document," which the draft says is derived "solely from the signed source events" so that "every mirror computes the same modified for the same state." Its stated purpose is exactly this problem: "This lets a consumer detect staleness and avoid overwriting newer state with older."
That works against honest publishers and lagging relays, and it is a real feature the method deserves credit for. It does not work against an adversary, because created_at is a self-asserted field inside the signed event: whoever holds the key picks the number. A resolver comparing two answers can tell which one claims to be newer. It cannot tell whether the newer claim came from the legitimate controller or from someone who compromised the key and stamped a distant future timestamp. Hold onto that distinction, because the next section runs into it again.
Deletion is a request, not a mechanism. NIP-09 defines kind 5 deletion request events, and the obligation on relays is a SHOULD: "Relays SHOULD delete or stop publishing any referenced events that have an identical pubkey as the deletion request." The NIP says the quiet part itself, permitting clients to inform users that "deletion does not guarantee deletion because it is impossible to delete events from all relays and clients." NIP-62 request-to-vanish is normatively stronger ("Relays MUST fully delete any events from the .pubkey if their service URL is tagged in the event") and is explicitly framed around legal obligations, but a MUST addressed to independent relay operators is still a MUST nobody can enforce.
None of this makes did:nostr unsuitable. It makes it suitable for a different job. If you need a resolvable, key-verifiable identifier for a social identity where the current state is what matters and history is not load-bearing, the projection model is a good fit and costs nothing to operate. If you need to prove what a document said at a specific past moment, or to demonstrate that no alternate history exists, the method does not offer that and does not claim to.
The Rotation Gap
The draft is explicit about its own limits, which is more than many specifications manage. Under the "Deactivate" heading, in full:
This version of the method defines no native mechanism for key rotation or deactivation: if the controller's key is lost or compromised, there is no in-method recovery. Defining rotation and deactivation semantics is an open item and is expected in a future revision.
That is a significant gap, and it is worth being precise about why it is hard rather than treating it as an oversight.
The problem is structural. In did:nostr, the identifier is the key. There is no layer of indirection between them, which is what the draft means when it calls the key-to-DID binding permanent. Rotating a key means changing the pubkey, which means changing the identifier, which means it is a different DID. Compare this to did:btcr2's model, where the identifier and the verification methods are distinct: the identifier commits either to an initial secp256k1 public key or to the SHA-256 hash of a genesis document, but the resolved document's key set can be replaced through updates while the identifier stays fixed. That indirection is exactly what makes rotation expressible.
Adding rotation to did:nostr therefore requires introducing indirection that the Nostr identity model does not currently have. Someone has to define which signed statement transfers authority from an old pubkey to a new one, how a resolver discovers that statement, and how it distinguishes a legitimate rotation from an attacker who compromised the old key and published a rotation to a key they control. That last question is the hard one, and it is the same question every method with rotation has to answer.
Anchored methods answer it partly with ordering, and specifically with ordering the key holder does not control. In did:btcr2 the version counter is inside the signed payload, the announcement is inside a confirmed Bitcoin transaction, and a signer cannot retroactively change which block their signal landed in. Nostr's created_at gives neither property. As shown above, NIP-01 does supply a deterministic rule for replaceable events, so this is not a total absence of ordering. Latest timestamp wins, with a defined tiebreak: "In case of replaceable events with the same timestamp, the event with the lowest id (first in lexical order) should be retained, and the other discarded." The problem is that the sort key is attacker-controlled. A compromised key and a legitimate owner can both publish plausible rotation events, and the tiebreak the protocol offers is a timestamp the attacker also gets to choose.
The prior art is not encouraging. NIP-26 delegated event signing is the closest the NIP set came to indirection, letting a separate keypair sign on a delegator's behalf, and it now carries the status tags draft unrecommended optional relay, with the NIP index giving the reason as "adds unnecessary burden for little gain." Nothing in the current index defines key rotation or migration. That is not a reason to conclude the problem is unsolvable, but it does suggest the design space has been probed before.
Until a revision lands, the practical guidance is straightforward: a compromised did:nostr key means a lost identity, the same as a compromised Nostr key means a lost Nostr identity. The draft says as much in its security considerations: "Loss of private keys will result in permanent loss of control over the identifier." Deployments that cannot tolerate that outcome should not use the method as their root of trust.
Where It Fits Alongside Anchored Methods
The useful framing is not did:nostr versus did:btcr2. They are answering different questions, and a system can reasonably use both.
A plausible split: use an anchored DID as the durable root of trust, the thing that signs credentials and survives key compromise, and use did:nostr as the resolvable handle for the social and messaging layer where the current state is what matters. The draft already anticipates half of this, defining an alsoKnownAs field for cross-platform identity linking, though it is careful to note that inclusion "represents a claim by the DID controller" and that applications "SHOULD verify these claims when possible." A DID document for the anchored identifier can carry a service endpoint pointing at the Nostr identity, and the Nostr profile can reference the anchored DID. Each identifier then does the job it is actually good at.
The seam to watch is which identifier a relying party follows. If a verifier keys its records on the Nostr pubkey, rotation on the anchored side does not help, because the verifier is not looking at the anchored identifier. The recovery path only exists for parties that treat the anchored DID as authoritative. Getting that wrong produces a system that appears to have a recovery story and does not.
Practical Notes for Implementers
A few things fall out of the above that are easy to get wrong:
Decide, and write down, whether you trust the HTTP path. The draft lists .well-known HTTP resolution first, but the document served there is not signed as a document, and the draft does not say how a resolver learns <domain> beyond the optional NIP-05 didResolver hint. If you accept HTTP results, you are trusting whoever operates that host to have composed the projection faithfully. A reasonable default is to derive the verification method yourself from the identifier and treat everything HTTP-served with the same skepticism you apply to relay data.
Query multiple relays and compare. Because two relays can legitimately disagree, a resolver that queries one relay is trusting it completely. Querying several and comparing at least surfaces the disagreement. What to do about a disagreement is largely, though not entirely, left to you: NIP-01 supplies the base rule for replaceable events and the draft's modified field lets you compare two composed documents directly. What neither supplies is a reason to trust the timestamp. Newest-wins is the obvious default and is also exactly what an attacker with a compromised key would exploit.
Do not treat relay silence as absence. A relay returning nothing means the relay has nothing, not that the event does not exist. This matters most for anything that looks like a negative check.
Separate the deterministic part from the enriched part. The verification method derived from the identifier is trustworthy without any network call. Profile data from relays, and any document fetched over HTTP, is not equally trustworthy, because it depends on who answered. Code that blurs these together will eventually treat a relay's or a mirror's view as if it had the same standing as the cryptographic derivation.
Record which relays you queried, alongside what the document claimed. If a resolution result is ever going to be audited or disputed, the relay set is part of the evidence. The draft gives you two in-band anchors to log with it: the document's modified value, and for HTTP-served documents the ETag and Last-Modified headers servers SHOULD supply. Neither one records which relays answered, so keep your own log for that.
Conclusion
The did:nostr draft is a clean piece of design for the problem it takes on. Deriving an identifier from an existing Nostr pubkey costs nothing, the security-critical part of the document is a normative MUST that needs no network access, and projecting the rest from events the ecosystem already publishes means the method inherits a working infrastructure instead of demanding a new one. The modified field is a thoughtful touch: a mirror-independent staleness signal computed from signed source events.
What it does not yet offer is lifecycle. There is no rotation, no deactivation, no auditable history, and no ordering layer whose sort key sits outside the key holder's control. The draft says as much, and the honest read is that these are hard problems rather than missing paperwork: the identifier-is-the-key design that makes creation free is the same design that makes rotation difficult.
For deployments where the current state is what matters and key loss is survivable, that tradeoff is fine. For deployments where it is not, the next post takes up the other side of it: how a method that separates the identifier from its keys handles rotation, recovery, and the end of an identifier's life.