Building a DID Resolver: A Developer's Guide to Decentralized Identity Infrastructure

May 22, 2026

Building a DID Resolver: A Developer's Guide to Decentralized Identity Infrastructure

Introduction

A DID resolver is the bridge between a decentralized identifier and the DID document that describes it: the public keys, service endpoints, and authentication methods associated with that identity. The resolution process itself is specified by the W3C DID Resolution draft. Some methods need next-to-no infrastructure to resolve (did:key decodes a public key directly from the DID URI, did:web is a single HTTPS GET), but Bitcoin-anchored methods like did:ion and did:btcr2 need real resolvers that watch the chain, fetch off-chain data, and reconstruct state. This post walks through what a resolver does, how to build one for those Bitcoin-anchored methods, and the architectural decisions involved.

What Does a DID Resolver Do?

Given a DID like did:ion:EiD3abc123..., a W3C-conformant resolver returns a DID Resolution Result, a structure containing three parts: the didDocument, didDocumentMetadata (versioning, created/updated timestamps, deactivation flag), and didResolutionMetadata (content type, error codes). For most everyday use the didDocument is what you'll work with; metadata becomes important when you need to detect deactivation or surface resolution errors. A representative didDocument shape (using JsonWebKey2020 for the verification method, with the matching 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 DID's owner: which keys to use for signature verification, and where to find their services. (Note: many older examples list "type": "IdentityHub". DIF's Identity Hub work was renamed to Decentralized Web Node, so DecentralizedWebNode is the current service type.)

Resolution Architecture for Bitcoin-Anchored DIDs

The two production-relevant Bitcoin-anchored methods take noticeably different shapes:

  • did:ion: short-form DIDs (and any DID that has been updated) need on-chain Sidetree anchors to resolve, so the resolver scans Bitcoin globally for ion: OP_RETURN outputs and processes every batch. (Long-form ION DIDs carry their genesis state in the URI itself and can be used pre-anchor.)
  • did:btcr2: most update data is delivered sidecar or via CAS, and the resolver only needs to find spends of the Beacon Addresses listed in each DID's document, so it never has to parse every block for a method prefix the way ION does. Finding those spends still means querying the chain, and how is a trust decision: the btcr2 resolve spec recommends an indexed Bitcoin RPC such as electrs or Esplora (Bitcoin Core has no address-history query of its own), but a resolver may instead traverse blocks from genesis. Index the chain against your own node and you trust no third party for chain state (the basis of btcr2's non-repudiation guarantee), or delegate that lookup and accept the trade-off (more below)

For ION, resolution is roughly three steps:

1. Scan the Bitcoin Blockchain

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

2. Fetch Off-Chain Data

Using the CID from the anchor transaction, the resolver retrieves the index/chunk files from IPFS (or whichever content-addressable system the method specifies). These files contain the individual DID operations (create, update, recover, deactivate).

3. Reconstruct State

The resolver processes operations in deterministic order for a given DID, applying each valid operation to build the current DID document. Operations are validated against the commitment chain: Sidetree updates must reveal the value committed to in the previous operation's updateCommitment, and similarly for recovery via recoveryCommitment.

For btcr2, the flow is broadly similar but inverted in scope: instead of scanning every Bitcoin block for ion: payloads, a resolver iteratively builds the current DID document. It starts from the initial document (deterministically derived from a k-form DID, or, for an x-form DID, supplied via sidecar or fetched from CAS by its hash), reads the service entries listing that version's Beacon Addresses, watches those for spends, and processes each spend's OP_RETURN Signal Bytes (a singleton update hash, a CAS Map hash, or an SMT root) against sidecar or CAS-delivered update data to apply the next state transition. Because each update can change the beacon service list, the loop repeats (read beacons → process signals → apply updates) until no further updates remain.

Implementation Approaches

Full Node

Run your own resolver node that syncs the Bitcoin blockchain and indexes the relevant operations (all ion: anchors for ION; the Beacon Addresses you care about for btcr2). This gives you:

  • Complete sovereignty: no external dependency for resolution
  • Fastest resolution times (local lookups)
  • Ability to verify the entire operation history

The cost is infrastructure. As of mid-2026 the Bitcoin chain is roughly 740 GB and growing ~60-80 GB/year (see blockchainsize.org or ycharts for live numbers); allocate 1-2 TB of SSD for an archival node, plus IPFS storage and method-specific indexing. Once your resolver has back-scanned all historical operations and built its own index, you can switch the underlying Bitcoin node to pruned mode (the prune target has a 550 MB minimum, plus a ~7-10 GB UTXO/chainstate database stored separately, per Bitcoin Core's block-file pruning notes). But the initial bootstrap needs full historical block bodies to find the relevant OP_RETURNs, so plan for the full chain at startup.

Lightweight Resolver

Delegate Bitcoin chain scanning to an existing indexer (e.g. electrs or Esplora, the spec's recommended option) and only fetch/verify operations for DIDs your application cares about. This reduces infrastructure requirements but introduces a trust dependency on that indexer's view of the chain: it decides which Beacon-Address spends, and therefore which Beacon Signals, you get to see. Cryptographically validating the returned update data does not remove that dependency, because signature-checking the payloads you did receive can't detect a signal a faulty or censoring indexer simply withheld. To close that gap, run and query your own fully-indexed node: the spec explicitly lets a resolver "instead traverse blocks from the genesis block" rather than trust a third-party RPC. That self-sourced chain view is encouraged but not required; reserve it for high-stakes verifications where you can't accept an external party's account of what's on-chain.

Universal Resolver Gateway

The Decentralized Identity Foundation maintains a Universal Resolver. Its driver registry currently lists 80+ DID methods behind a single HTTP API:

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

DIF runs a public test instance at dev.uniresolver.io, described in the Universal Resolver README as a "DIF-hosted instance of the Universal Resolver that can be used for testing purposes," fine for experimentation but not intended for production. For production multi-method support, run the Universal Resolver yourself or compose method-specific resolvers behind your own router.

A note on did:ion's status

did:ion is the most-cited example here because it's the canonical Sidetree-on-Bitcoin implementation. Just be aware that Microsoft removed did:ion as a trust system in Entra Verified ID in December 2023, pivoting to did:web. The open-source ION project still operates under DIF, but the commercial weight has moved elsewhere; if you're picking a Bitcoin-anchored method today, evaluate did:btcr2 alongside it.

Key Design Decisions

Caching strategy

DID documents change infrequently. Cache aggressively, but implement a mechanism to detect updates (poll for new anchor transactions or subscribe to a node's event stream).

Multi-method support

If your application needs to verify credentials from multiple DID methods (e.g., did:ion, did:key, did:web), build a method router that dispatches to method-specific resolvers.

Error handling

Resolution can fail or return a degraded result for several reasons:

  • The DID has been deactivated
  • Off-chain data is temporarily unavailable
  • The operation chain contains invalid signatures

Return structured responses that distinguish between these. The W3C DID Resolution spec defines a didResolutionMetadata.error field with codes like INVALID_DID, INVALID_DID_DOCUMENT, NOT_FOUND, METHOD_NOT_SUPPORTED, INVALID_OPTIONS, FEATURE_NOT_SUPPORTED, and INTERNAL_ERROR. (Note: a deactivated DID is not an error per se: it resolves successfully, but the didDocumentMetadata carries deactivated: true. Treat that as a signal to your application logic, not a resolver failure.)

Verification depth

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

Integration Patterns

For most applications, the resolver is a service 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 so it can be shared across applications and updated independently.

Conclusion

A DID resolver is fundamental infrastructure for any application working with decentralized identity. For Bitcoin-anchored DIDs, the resolver inherits Bitcoin's trust guarantees, but only if you understand the architecture and make deliberate choices about sovereignty, caching, and verification depth. Start lightweight if you must, but treat the move to your own fully-indexed node as a trust decision rather than a mere scaling one: only verifying chain state yourself lets you confirm every Beacon Signal without trusting a third party's view of the chain, which is what unlocks btcr2's non-repudiation guarantee where it matters.

Jintek LLC