Introduction
The previous post examined where Nostr and Decentralized Identifiers overlap. It mentioned the did:nostr method only briefly. This post examines the draft in more detail.
Two points of context come first. The draft is a Draft Community Group Report from the W3C Nostr Community Group, not a W3C standard. Its own status section says that "It is inappropriate to cite this document as other than work in progress". The draft also changes over time, so record which version you read. This post uses version 0.1.1, which is the version in the subtitle of the document. The editor changed the version to 0.1.1 in the spec repository in July 2026, and 0.1.1 is still the current version as of September 2026.
The approach of this method is different from the anchored methods that this blog usually covers. The method does not commit operations to a ledger. Instead, it treats the DID document as, in the words of the draft, "a projection of signed Nostr events". This design gets real benefits, and it also gives up real properties. This post examines both, with the same standard that it applies to other methods.
How the Draft Derives the Identifier
The construction of the identifier is very simple. A Nostr identity is a secp256k1 public key, and the draft writes 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 the value of that string: "A conformant did:nostr identifier MUST be a valid secp256k1 x-only public key". Thus 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, and no fee. The Create operation of the draft ends with this note: "No on-chain registration is required as the security and uniqueness are derived from the key generation process". If you have a Nostr key pair, you already have a did:nostr identifier, even if you never resolved it.
This result matches the zero-cost creation property of did:btcr2 key-based identifiers, but it comes from a different direction. The did:btcr2 method derives a Bech32m identifier from the key and needs no on-chain footprint until the first update. In contrast, did:nostr never has an on-chain footprint.
The choice of format is important. The method specifies hex, not the npub Bech32 form from NIP-19. The draft is explicit about the difference: "Nostr DIDs use the raw 64-character hexadecimal public key, not the npub format". It also says that "npubs should be considered display-only identifiers to improve readability in user interfaces".
NIP-19 agrees about its own scope: its bech32 formats "are not meant to be used anywhere in the core protocol". NIP-01 uses lowercase hex for keys everywhere, and it requires that filter lists "MUST contain exact 64-character lowercase hex values". A DID is infrastructure for machines, so the hex format for machines is the correct choice for a DID. However, the string that a user knows as the Nostr identity is not the string in the DID. Implementers must handle this difference in the user interface.
Three Resolution Paths, Not One
This part of the method needs the most attention. It is also the part where the common one-line summary ("did:nostr resolves from relays") is 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 has the only MUST about location in the resolution section: "The HTTP representation of a did:nostr document MUST be located at: https://<domain>/.well-known/did/nostr/<pubkey>.json." The endpoint "SHOULD serve it with the DID-specific media type application/did+json". Also, servers "SHOULD include the following headers": ETag, Cache-Control, and Last-Modified.
The draft does not specify where <domain> comes from. The only discovery hint is a MAY: "Resolvers MAY check NIP-05 endpoints for a didResolver field to locate HTTP-hosted DID documents before querying relays". Thus a resolver that conforms to the draft can get a DID document from an ordinary web server, over an ordinary TLS connection. No signature covers the document as a whole. Think about this fact before you describe did:nostr as a relay-only method.
Offline second, and this part is good. The draft makes the path without network access normative, not incidental. A resolver "MUST be able to generate a valid DID document using only the public key from the DID identifier". It must do this "without requiring network access to Nostr relays". The draft states it again later: "Implementations MUST support minimal resolution without network access". The verification method comes directly from the identifier. Thus a resolver can generate authentication and assertionMethod deterministically, without a query to any party.
The key format is Multikey. The steps are:
- Prepend a parity byte to the x-only key to make a 33-byte compressed key.
- Prepend "the multicodec varint for secp256k1 compressed public keys (0xe7, 0x01)."
- Encode the result with multibase, 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 to named fields in the document:
- Kind 0 (user metadata): profile fields such as name, picture, and NIP-05 address, in the
profilefield. Acreated_atvalue holds "theevent.created_atof the latest Nostr profile (kind 0) event, in Unix seconds". - Kind 3 (contact list): the social graph, in the
followsfield. Each entry is adid:nostridentifier. For large accounts, the draft asks implementations to include only "a bounded subset" of the follows in the document. - Kind 10002: NIP-65 relay list metadata. It advertises "relays where the user generally writes to and relays where the user generally reads mentions". The draft maps it to
serviceentries of typeRelay.
One inconsistency needs attention. The draft lists steps for relay discovery. One step says 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. But elsewhere, the draft uses kind 10002 as the source of the service array and of the modified timestamp. If you implement the discovery steps literally, you will not get the relay list that the draft expects in the document.
Thus the security-critical part of resolution needs no network access. A resolver that cannot reach any relay can still produce a document with a correct verification method, because the identifier contains the key. The availability of relays changes the richness of the document, not its cryptographic core.
Why a Projection Is Not a History
This part needs care.
Anchored methods produce a document from an ordered sequence of operations. did:btcr2 resolution works as follows:
- The resolver starts from an initial DID document. For key-based identifiers, the resolver renders this document deterministically from the key. If the identifier commits to the hash of a genesis document, the resolver gets that document as sidecar data or from Content Addressable Storage (CAS).
- The resolver collects Beacon Signals from confirmed Bitcoin transactions.
- It orders the updates that the signals announce from the lowest to the highest
targetVersionId, with block height as a tiebreaker. - For each update, it checks that the current document hashes to the declared
sourceHash. Then it verifies the Data Integrity proof, applies the JSON Patch, and confirms that the result hashes to the declaredtargetHash.
The output is the end of one canonical chain. The resolver can also detect an attempt to insert a branch later. An update whose targetVersionId is more than one ahead of the current version causes a LATE_PUBLISHING error. A duplicate update with a different hash also causes a LATE_PUBLISHING error. This detection is the purpose of the late-publishing checks.
A did:nostr resolver produces a document differently. It asks relays for the current state of a few event kinds and renders the result. All three source kinds are replaceable in the Nostr model.
NIP-01 states this normatively. For kind n such that 10000 <= n < 20000 || n == 0 || n == 3, events are "replaceable". 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 are all inside that rule. A new event replaces the old one, and no relay must keep the old version.
Three consequences follow. Their effects add together.
Resolution is not reproducible over time. If you request the document now and next month, the answers can differ without a record of the change. The draft does define an Update operation, so the draft expects change, and change is not an anomaly. But the operation is unusual: "The did:nostr identifier is permanent: the key-to-DID binding cannot be changed," and only the projection changes.
The method has no auditable trail. Nothing in the method requires any party to keep the earlier state. Thus the method cannot answer the question "what did this document say last March?"
Two honest resolvers can disagree. The staleness signal of the draft solves only part of this problem. Nostr has no consensus layer. If a pubkey publishes an updated kind 0 to relays A and B but not to C, a resolver that reads C returns stale data. The draft gives consumers a value to compare: a modified field. Its value is "the most recent created_at across all signed parts composed into the document."
The draft says that the value comes "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."
This mechanism works against honest publishers and against relays that are behind, and the method deserves credit for it. It does not work against an adversary, because created_at is a self-asserted field inside the signed event. The holder of the key chooses the value, even a time far in the future. A resolver that compares two answers can tell which one claims to be newer. It cannot tell if the newer claim came from the legitimate controller or from an attacker with the key. Remember this difference, because the next section uses it again.
Deletion is a request, not a mechanism. NIP-09 defines kind 5 deletion request events. For relays, the text is a recommendation, not a requirement: "Relays SHOULD delete or stop publishing any referenced events that have an identical pubkey as the deletion request." The NIP itself admits the limit. It lets clients tell 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." NIP-62 also refers to legal obligations: "This procedure is legally binding in some jurisdictions". But nobody can enforce a MUST that applies to independent relay operators.
None of this makes did:nostr unsuitable. It makes the method suitable for a different job. Some systems need a resolvable, key-verifiable identifier for a social identity, where the current state matters and the history is not critical. For those systems, the projection model is a good fit and costs nothing to operate. Other systems must prove what a document said at a specific past moment, or show that no alternative history exists. The method does not offer that, and it does not claim to offer it.
The Rotation Gap
The draft is explicit about its own limits, and many specifications are less explicit. The full text of the "Deactivate" section is:
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.
This gap is significant. It is important to be precise about why the problem is hard, and not to treat it as an oversight.
The problem is structural. In did:nostr, the identifier is the key. No layer of indirection separates them, and this is what the draft means when it calls the "key-to-DID binding" permanent. A key rotation changes the pubkey, a new pubkey changes the identifier, and a new identifier is a different DID.
Compare this with the did:btcr2 model, where the identifier and the verification methods are separate. The identifier commits to an initial secp256k1 public key or to the SHA-256 hash of a genesis document. But updates can replace the key set of the resolved document, and the identifier stays the same. This indirection is what makes rotation possible.
Thus, to add rotation to did:nostr, a revision must add indirection that the Nostr identity model does not have today. Someone must define these rules:
- Which signed statement transfers authority from an old pubkey to a new one.
- How a resolver discovers that statement.
- How a resolver tells a legitimate rotation from an attack. In the attack, someone compromised the old key and published a rotation to a key that the attacker controls.
The last question is the hard one. Every method with rotation must answer the same question.
Anchored methods answer it partly with an order of events that 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. A signer cannot change later which block contains the signal. Nostr created_at gives neither property.
As the previous section shows, NIP-01 does supply a deterministic rule for replaceable events, so Nostr has some order. The latest timestamp wins, with a defined tiebreak. For 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 attacker controls the sort key. A compromised key and a legitimate owner can both publish plausible rotation events. The only tiebreak of the protocol is a timestamp that the attacker also chooses.
The earlier work in Nostr does not offer a solution. NIP-26 (Delegated Event Signing) was the closest attempt at indirection in the NIP set. It lets a separate key pair sign on behalf of a delegator. NIP-26 now has the status tags draft unrecommended optional relay, and the NIP index gives the reason: "adds unnecessary burden for little gain." As of September 2026, nothing in the index defines key rotation or key migration. This fact does not show that the problem has no solution, but it does suggest that other people explored this design space before.
Until a new revision of the draft defines rotation, the practical guidance is simple. A compromised did:nostr key means a lost identity, in the same way that a compromised Nostr key means a lost Nostr identity. The draft says this in its security considerations: "Loss of private keys will result in permanent loss of control over the identifier". If a deployment cannot accept that outcome, we recommend that it does not use the method as its root of trust.
How It Fits with Anchored Methods
The useful comparison is not did:nostr against did:btcr2. The two methods answer different questions, and one system can use both.
A possible division of work is as follows. Use an anchored DID as the durable root of trust, which signs credentials and survives a key compromise. Use did:nostr as the resolvable handle for the social layer and the message layer, where the current state is what matters.
The draft already supports half of this design. It defines an alsoKnownAs field to link identities across platforms. The draft also notes 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 that points to the Nostr identity. The Nostr profile can refer to the anchored DID. Each identifier then does the job that it does well.
The weak point is which identifier a relying party follows. If a verifier uses the Nostr pubkey as the key for its records, rotation on the anchored side does not help. The verifier does not look at the anchored identifier. The recovery path exists only for parties that treat the anchored DID as authoritative. If you get this wrong, the system appears to have a recovery path, but it does not have one.
Practical Notes for Implementers
The analysis above gives these notes. Each note covers a mistake that is easy to make.
Decide if you trust the HTTP path, and record the decision. The draft lists .well-known HTTP resolution first. But no signature covers the document at that location as a whole. Also, the draft does not say how a resolver learns <domain>, other than the optional NIP-05 didResolver hint. If you accept HTTP results, you trust the operator of that host to compose the projection correctly. As a default, derive the verification method yourself from the identifier. Treat all HTTP-served data with the same skepticism that you apply to relay data.
Query multiple relays and compare the results. Two relays can legitimately disagree, so a resolver that queries only one relay trusts that relay completely. If you query several relays and compare the results, you can at least see the disagreement. The method leaves most of the decision about a disagreement to you. NIP-01 supplies the base rule for replaceable events, and the modified field of the draft lets you compare two composed documents directly. Neither one supplies a reason to trust the timestamp, and "newest wins" is the rule that an attacker with a compromised key can use.
Do not treat relay silence as absence. If a relay returns nothing, the relay has nothing. This result does not mean that the event does not exist. This point is most important for negative checks.
Separate the deterministic part from the enriched part. The verification method that comes from the identifier is trustworthy without a network call. Profile data from relays, and any document from HTTP, is less trustworthy, because it depends on the party that answered. If code mixes these two parts, it will eventually give the view of a relay or a mirror the same trust as the cryptographic derivation.
Record which relays you queried. Record them together with the claims of the document. If someone audits or disputes a resolution result later, the relay set is part of the evidence. The draft gives you two values inside the data to log with it. The first is the modified value of the document. The second, for HTTP-served documents, is the pair of ETag and Last-Modified headers that servers "SHOULD include". Neither value records which relays answered, so keep your own log for that.
Conclusion
The did:nostr draft is a clean design for the problem that it addresses. These properties support that view:
- The identifier comes from a Nostr pubkey that the user already has, at no cost.
- The security-critical part of the document comes from a normative MUST that needs no network access.
- The method projects the rest of the document from events that the ecosystem already publishes. Thus it uses infrastructure that already works, and it does not require new infrastructure.
- The
modifiedfield is a careful detail: a staleness signal that every mirror computes in the same way from signed source events.
The draft does not yet offer lifecycle management. It has no rotation, no deactivation, and no auditable history. It also has no layer that orders events with a sort key outside the control of the key holder. The draft says this itself. An honest assessment is that these are hard problems, not simple omissions. The same design that makes creation free (the identifier is the key) also makes rotation difficult.
For deployments where the current state is what matters and a key loss is survivable, that tradeoff is acceptable. For other deployments, the next post examines the other side. It shows how a method that separates the identifier from its keys handles rotation, recovery, and the end of the life of an identifier.