How to Build a DID Resolver: A Developer Guide to Decentralized Identity Infrastructure

May 22, 2026

How to Build a DID Resolver: A Developer Guide to Decentralized Identity Infrastructure

Introduction

A DID resolver connects a decentralized identifier to the DID document that describes it. The document contains the public keys, service endpoints, and authentication methods of that identity. The W3C DID Resolution specification defines the resolution process (a Candidate Recommendation Draft as of September 2026). Some methods need almost no infrastructure. did:key expands a public key from the DID itself, and did:web needs one HTTPS GET. Bitcoin-anchored methods such as did:ion and did:btcr2 need real resolvers that watch the chain, get off-chain data, and rebuild the state.

This post shows what a resolver does, how to build one for these Bitcoin-anchored methods, and which architecture decisions you must make.

What a DID Resolver Does

For a DID such as did:ion:EiD3abc123..., a W3C-conformant resolver returns three values (DID Resolution):

  • didDocument
  • didDocumentMetadata (version data, created and updated timestamps, and the deactivated flag)
  • didResolutionMetadata (content type and errors)

A DID Resolution Result holds these three values. Most applications use the didDocument. The metadata is important when you must detect deactivation or show resolution errors. This is a typical didDocument. It uses JsonWebKey2020 for the verification method, with the related https://w3id.org/security/suites/jws-2020/v1 context:

{
  "@context": [
    "https://www.w3.org/ns/did/v1",
    "https://w3id.org/security/suites/jws-2020/v1"
  ],
  "id": "did:ion:EiD3abc123...",
  "verificationMethod": [
    {
      "id": "did:ion:EiD3abc123...#key-1",
      "type": "JsonWebKey2020",
      "controller": "did:ion:EiD3abc123...",
      "publicKeyJwk": { "kty": "EC", "crv": "secp256k1", "x": "...", "y": "..." }
    }
  ],
  "authentication": ["#key-1"],
  "service": [
    {
      "id": "#dwn",
      "type": "DecentralizedWebNode",
      "serviceEndpoint": "https://dwn.example.com"
    }
  ]
}

The document tells you how to interact with the owner of the DID: which keys verify signatures, and where the services of the owner are. (Note: many older examples use "type": "IdentityHub". DIF renamed its Identity Hub work to Decentralized Web Node, and the old identity-hub repository now redirects to the decentralized-web-node repository. Thus, DecentralizedWebNode is the current service type in the DWN draft spec. The DWN spec also requires enc and sig properties in this service entry, and the example does not show them.)

Resolution Architecture for Bitcoin-Anchored DIDs

Two Bitcoin-anchored methods show very different designs:

  • did:ion: Short-form DIDs (and all DIDs with updates) need on-chain Sidetree anchors to resolve. Thus, the resolver scans all of Bitcoin for OP_RETURN outputs with the ion: prefix (ION mainnet config) and processes each batch. Long-form ION DIDs contain their genesis state in the URI, so you can use them before the anchor (Long-Form DID URIs).
  • did:btcr2: The update data arrives by sidecar or from CAS, and only the 32-byte Signal Bytes go on chain. The resolver finds only the spends of the Beacon Addresses in the DID document. Thus, it does not parse each block for a method prefix, as ION does.

A btcr2 resolver must still query the chain to find these spends, and the query method is a trust decision. The btcr2 resolve spec RECOMMENDS an indexed Bitcoin RPC service such as electrs or Esplora. Bitcoin Core has no index by address (optional indexes). The spec also permits a resolver to traverse blocks from the genesis block.

If you index the chain with your own node, you trust no third party for chain state. Non-Repudiation needs the complete set of Beacon Signals. If you delegate the lookup, you accept a tradeoff (see below).

For ION, resolution has about three steps:

1. Scan the Bitcoin Blockchain

The resolver monitors Bitcoin transactions for Sidetree anchor payloads (ION uses OP_RETURN outputs with the ion: prefix). Each anchor string contains the operation count and the IPFS CID of the Core Index File for a batch of DID operations.

2. Get Off-Chain Data

The resolver uses the CID from the anchor transaction to get the index files and chunk files. The files are on IPFS, or on the content-addressable system that the method specifies. These files contain the individual DID operations (create, update, recover, deactivate).

3. Rebuild the State

The resolver processes the operations for a DID in a deterministic order. It applies each valid operation to build the current DID document. The resolver validates operations against the commitment chain. An update must reveal the value that the updateCommitment of the previous operation committed to. A recovery must do the same for the recoveryCommitment.

Resolution for btcr2

For btcr2, the flow is similar, but the scope is the opposite. The resolver does not scan each Bitcoin block for ion: payloads. Instead, it builds the current DID document in a loop (Resolve):

  1. Start from the initial document. For a k-form DID, the resolver derives it deterministically from the DID. For an x-form DID, the resolver gets the Genesis Document by sidecar or from CAS by its hash.
  2. Read the service entries that list the Beacon Addresses of that version.
  3. Find the transactions that spend from those addresses and have at least 6 confirmations (the default minConf).
  4. Process the OP_RETURN Signal Bytes of each spend against the update data from sidecar or CAS. The Signal Bytes are a Singleton update hash, a CAS Map hash, or an SMT root.
  5. Apply the next update in targetVersionId order.

An update can change the list of beacon services. Thus, the loop repeats (read beacons, process signals, apply updates) until no more updates remain.

Implementation Approaches

Full Node

Run your own resolver node. The node syncs the Bitcoin blockchain and indexes the applicable operations (all ion: anchors for ION, or the Beacon Addresses that you monitor for btcr2). This gives you:

  • No external dependency for chain data
  • Fast resolution (local lookups)
  • The ability to verify the full operation history

The cost is infrastructure. In July 2026, the block files of a full node used 802 GiB, per the electrs usage guide. Bitcoin Core also publishes a disk guideline for a full mainnet node: the size of the data directory plus a 5-10% margin (release process). This guideline increased from 810 GiB in September 2025 to 897 GiB in September 2026. Thus, we recommend about 2 TB of SSD for an archival node with an address index. You also need space for IPFS and for method-specific indexes.

The option to get back some of this space depends on how you index:

  • Own index of Beacon Addresses: Your resolver keeps an index of only the Beacon Addresses that it monitors, and it follows the chain tip. After the back-scan, you can switch the Bitcoin node to pruned mode. The prune target has a minimum of 550 MB (Bitcoin Core 0.11.0 release notes). The chainstate database is in addition to this target, and the Bitcoin Core guideline for it is 14 GiB (chainparams). But a pruned node cannot scan old blocks for the Beacon Addresses of a new DID.
  • General address indexer: This is the option that the spec recommends. With this option, the node must stay unpruned. The indexer reads each historical block, and a pruned node deletes old blocks. The electrs quickstart starts bitcoind with -prune=0.

In both cases, the first back-scan must read all historical blocks to find the applicable OP_RETURN outputs. Thus, plan for the full chain size at startup.

Lightweight Resolver

Delegate the chain scan to an indexer that another party operates (for example, a third-party electrs or Esplora endpoint). Then get and verify operations only for the DIDs that your application needs. This decreases the infrastructure, but it adds a trust dependency on the view of the chain that the indexer gives. The indexer decides which Beacon Address spends, and thus which Beacon Signals, you see. Cryptographic validation of the update data does not remove this dependency. A signature check on the data that you received cannot detect a signal that a faulty indexer, or an indexer that censors, did not send.

To remove this dependency, get the chain view yourself. The spec permits a resolver to "instead traverse blocks from the genesis block" as an alternative to an indexed RPC service. Your own indexed node gives the same view with lower latency. Neither option is mandatory. We recommend them for high-value verifications, where you cannot accept the report of an external party about the chain.

Universal Resolver Gateway

The Decentralized Identity Foundation maintains a Universal Resolver. As of September 2026, its README lists 82 method drivers behind one HTTP API. The list includes a driver for the older did:btcr method, but not for did:btcr2.

GET /1.0/identifiers/did:ion:EiD3abc123...

DIF runs a public test instance at dev.uniresolver.io. The Universal Resolver README calls it a "DIF-hosted instance of the Universal Resolver that can be used for testing purposes". Use it for experiments, not for production. For production support of many methods, run the Universal Resolver yourself, or put method-specific resolvers behind your own router.

A note on the status of did:ion

This post uses did:ion as an example because ION is the DIF implementation of Sidetree on Bitcoin. But Microsoft removed did:ion as a trust system in Entra Verified ID in December 2023. After that change, did:web is the only trust system in Entra Verified ID. The ION repository is still under DIF, but its last commit is from August 2023 (as of September 2026). If you select a Bitcoin-anchored method today, evaluate did:btcr2 too.

Key Design Decisions

Cache strategy

Many DID documents do not change frequently. Use a cache with long lifetimes, but add a method to detect updates. For example, poll for new anchor transactions, or subscribe to the event stream of a node.

Multi-method support

If your application must verify credentials from many DID methods (for example, did:ion, did:key, did:web), build a method router. The router sends each DID to a method-specific resolver.

Errors

Resolution can fail, or it can return a partial result, for these reasons:

  • The controller deactivated the DID.
  • Off-chain data is temporarily not available.
  • The operation chain contains invalid signatures.

Return structured responses that show the difference between these cases. The W3C DID Resolution spec defines an error property in didResolutionMetadata. Its value is an RFC 9457 error object, with a type URL such as https://www.w3.org/ns/did#NOT_FOUND. The spec defines codes such as INVALID_DID, INVALID_DID_DOCUMENT, NOT_FOUND, METHOD_NOT_SUPPORTED, INVALID_OPTIONS, FEATURE_NOT_SUPPORTED, and INTERNAL_ERROR. A DID method can add its own errors. For example, did:btcr2 defines MISSING_UPDATE_DATA and LATE_PUBLISHING (btcr2 errors).

(Note: a deactivated DID is not an error. The resolver returns no error, and the didDocumentMetadata contains deactivated: true (DID Resolution algorithm). The generic algorithm returns didDocument: null for a deactivated DID, and the HTTP(S) binding uses status code 410. Use the flag as a signal for your application logic, not as a resolver failure.)

Verification depth

Select how far back you verify. A full resolver validates each operation from creation. A pragmatic resolver can accept the current state from a trusted node and verify the cryptographic chain only for high-value operations.

Integration Patterns

For most applications, the resolver is a service that your backend calls during credential verification:

1. Receive Verifiable Presentation from user
2. Extract issuer DID from credential
3. Resolve issuer DID → get public keys
4. Verify credential signature against resolved keys
5. Make trust decision

Keep the resolver as a separate service or library. Then many applications can share it, and you can update it independently.

Conclusion

A DID resolver is basic infrastructure for applications that work with decentralized identity. For Bitcoin-anchored DIDs, the resolver gets the trust properties of Bitcoin only if you understand the architecture. You must also make deliberate decisions about sovereignty, caches, and verification depth.

You can start with a lightweight resolver if necessary. But the move to your own fully indexed node is a trust decision, not only a scale decision. A chain view that you verify yourself lets you confirm each Beacon Signal without trust in the view of a third party. This is the condition for the non-repudiation guarantee of btcr2 where this guarantee is important.

Jintek LLC