archon

The did:cid Method Specification

Abstract

The did:cid method specification conforms to the requirements specified in DID Core 1.0, a W3C Recommendation. For more information about DIDs and DID method specifications, please see the DID Primer.

Introduction

The did:cid method is designed to support a P2P identity layer with secure decentralized verifiable credentials. DIDs created using this method are used for agents (e.g., users, issuers, verifiers, and nodes) and assets (e.g., verifiable credentials, verifiable presentations, schemas, challenges, and responses).

DID Format

A did:cid DID has the following format:

did-cid        = "did:cid:" cid-identifier
cid-identifier = CID v1 in standard base32 encoding

Example

did:cid:bafkreiawdmk6fmqc5p237vffyctazpzdgvgqfdj2i3hx2idtodxkwhyj5m

DID URL Syntax

A did:cid DID URL extends the DID with the standard URI components defined by the W3C DID Core 1.0 DID URL grammar:

did-url = did-cid path-abempty [ "?" query ] [ "#" fragment ]
Component Purpose in did:cid
path (/…) Selects a method-specific dereferenceable resource of the DID. This method defines /data and /registration (see DID URL Dereferencing).
query (?…) Selects a version to resolve or dereference. This method honors versionTime and versionSequence.
fragment (#…) Identifies a node within the DID document (e.g. #key-1), resolved client-side against the resolved document.

Note (DID Core 1.0): Earlier drafts of the DID specification allowed a ;-delimited service matrix parameter (did:cid:<cid>;service=…). DID Core 1.0 removed this form; service selection, where supported, uses the registered service and relativeRef query parameters. The did:cid method does not use the ;service matrix-parameter form.

DID Lifecycle

All did:cid DIDs begin life anchored to IPFS. Once created they can be used immediately by any application or service connected to a node. Subsequent updates to the DID (meaning that a document associated with the DID changes) are registered on a registry such as a blockchain (BTC, ETH, etc) or a decentralized database (e.g. hyperswarm). Creation selects the initial registry. An authorized registry-migration update changes the registry for subsequent operations; the migration itself is confirmed on the previous registry. Nodes derive their best current history from available evidence, as specified in DID state transitions.

The key concept of this design is that DID creation is decentralized through IPFS, and DID updates are decentralized through the registry selected by the accepted predecessor state. The DID is decentralized for its whole lifecycle, which is a hard requirement of DIDs.

DID State Transitions

This section defines the current version-1 transition contract shared by creation, update, deletion, and resolution. It consolidates existing behavior; it does not activate a new protocol version or the proposed key-permission policy in #1156. The detailed authorization cutoff rules and registration validation apply to every transition.

Transition table

An event envelope cannot choose the operation’s target. A creation targets the DID derived from the complete canonical creation operation and its method prefix; an update or deletion targets its signed operation.did. If event.did is present it MUST equal that target; otherwise Gatekeeper derives it. A creation’s extraneous operation.did, if present, does not override content-derived identity. Gatekeeper rejects mismatched envelopes before queue deduplication or candidate persistence. The same target check applies when replaying events. Retrieval-CID aliases remain content references and cannot override target identity.

Transition Predecessor and authority Result Registry for confirmation
Create agent No predecessor state. Verify the creation proof with publicJwk; the proof names #key-1. An explicit creation controller is invalid. Derive the DID from the complete canonical creation operation; initialize an agent document with its key. The agent controls itself. Initial registration.registry. Genesis is resolvable from its seed before registry publication.
Create asset No predecessor state. The named controller must resolve to an active, self-controlled agent at the authorization cutoff; its named key signs the creation. A local-only controller cannot create a non-local asset. Derive the DID from the complete canonical creation operation; initialize its owner and data. Initial registration.registry, with the same seed availability rule.
Update agent previd identifies the selected target predecessor. Verify with the named key in that predecessor’s agent document. Apply supplied components as whole replacements. The resulting document must keep its DID and may omit controller or name only itself. Registry in the predecessor’s registration.
Update asset / transfer ownership previd identifies the asset predecessor. Its previous owner signs, using the controller document selected at the authorization cutoff. A different new owner must also be an active, self-controlled agent at that cutoff; no new-owner countersignature is required. Apply supplied components as whole replacements. The resulting document must keep its DID and name one agent owner. Registry in the asset predecessor’s registration.
Migrate registry An update subject to the same agent/asset authorization and predecessor checks, supplying a complete replacement registration. Change the registry used by subsequent operations; preserve genesis version, kind, and prefix. The old registry confirms the migration; the new registry confirms its successors.
Delete agent or asset previd identifies an active predecessor; the same authority selection as an update applies. Set deactivated: true, reduce didDocument to { id }, clear data to {}, retain registration, and omit updated. No valid successor can extend this deletion. Registry in the predecessor’s registration.

previd identifies the previous operation, including its proof, not a hash of the resolved document set. The operation identity rules define canonical CIDs and content-backed legacy aliases. Direct updates/deletions must reference the current accepted head before storage or queue writes. Imports may consider an earlier predecessor and replace a competing branch under the existing registry/ordinal selection rules; an unavailable predecessor remains pending. A signature alone does not establish that its branch wins.

Component replacement and immutable fields

An update supplies one or more of didDocument, didDocumentData, and didDocumentRegistration. An omitted component retains its predecessor value; a supplied component replaces that entire component. There is no recursive or field-level merge. For example, replacing didDocument without its previous service member removes those services, whereas a data-only update preserves the entire DID document. Resolver-generated metadata is not a writable component.

The DID and genesis operation remain fixed. Every resulting DID document retains its id; every replacement registration retains genesis version, type, and prefix presence/value. A registration replacement must contain all required fields: omitting the whole component is allowed, but omitting type inside a supplied replacement is not. Registry, expiry, and extension fields can change through a valid complete registration replacement.

Within each DID document, verification-method IDs MUST be unique after resolving relative fragment IDs against that document’s DID. For example, #key-1 and <did>#key-1 denote the same method and cannot both be listed, even with the same public key. This applies to agent and asset documents. Distinct method names may share a key, and a later document version may replace the key under an existing name. Direct submission, import, and verified replay reject replacements that violate this rule; signed operation bytes are never rewritten.

Current validation accepts an owner-signed update that places public keys in an asset DID document’s verificationMethod field. Gatekeeper does not use those asset keys to authorize changes to the asset: it uses the owning agent’s document. This describes accepted document content, not a recommendation or a new asset signing capability. Assets cannot own assets, and agents cannot assign external controllers. “Assets do not have keys” is not a prohibition on publishing key material.

Version, validity, and local retention

Only registration version 1 is currently accepted. Replacement registration cannot opt an existing DID into another version. Stricter operation-key relationships remain a separate, paused compatibility decision (#1156): current operation verification selects the named key from verificationMethod and does not require membership in capabilityInvocation. Credential proof-purpose checks are separate. Likewise, the version-1 size rule remains the existing limit of 65,536 UTF-16 code units in the compact JSON serialization, not a newly imposed UTF-8 byte limit (#1159).

validUntil, when supplied, must have the accepted RFC 3339 string form. A valid registration replacement can change or omit it. Current Gatekeepers use expiry for node garbage collection; they do not add a wall-clock expiry check to each operation’s authorization or resolution. GC removal is local retention behavior, not a signed deletion or a consensus transition. Successful GC also clears the active deferred queue while preserving batch-ingress deduplication for that process. Candidate evidence for DIDs that are not removed remains available for replay. GC timing must not be interpreted as proof that an operation is invalid.

Deletion is terminal on the accepted branch. Newly available controller or anchor evidence can invalidate that branch, including its deletion, and replay may then resolve an active version. This is revalidation of evidence, not an operation that revives a validly deleted DID. confirmed records registry confirmation under the current accepted history, not irrevocable authorization.

DID Creation

DIDs are anchored to IPFS prior to any declaration on a registry. This allows DIDs to be created very quickly (less than 10 seconds) and at (virtually) no cost.

The did:cid method supports two main types of DID Subject: agents and assets. Agents have keys and control assets. Agents are self-controlled: an update cannot assign an external controller. Assets are controlled by a single agent (the owner of the asset); publishing keys on an asset does not grant it independent authority, and an asset cannot own another asset. The transition table defines both creation paths. The two types have slightly different creation methods.

Agents

To create an agent DID, the client must sign and submit a “create” operation to a node. This operation will be used to anchor the DID in IPFS.

  1. Generate a new private key
    1. We recommend deriving a new private key from an Hierarchical Deterministic (HD) wallet (BIP-32).
  2. Generate a public key from the private key
  3. Convert to JWK (JSON Web Key) format
  4. Create an operation object with these fields in any order:
    1. type must be “create”
    2. registration metadata includes:
      1. version number, e.g. 1
      2. type must be “agent”
      3. registry (a valid registry name, e.g. “BTC:mainnet” or “hyperswarm”; see registration validation)
    3. publicJwk is the public key in JWK format. It must be a secp256k1 key: kty is "EC", crv is "secp256k1", and x and y are each the base64url encoding of a 32-byte coordinate. A node rejects the operation if any of these do not hold.
    4. created time in ISO format
    5. blockid [optional] current block ID on registry (if registry is a blockchain)
  5. Sign the JSON with the private key corresponding to the public key (this enables the node to verify that the operation is coming from the owner of the public key)
    • The proof.verificationMethod must be set to #key-1 (a relative reference) since the DID does not yet exist
  6. Submit the operation to a node. For example, with a REST API, post the operation to the node’s endpoint to create new DIDs (e.g. /api/v1/did/)

The worked examples in this section and the two that follow were captured before archon-ecdsa-secp256k1-jcs-2026 was adopted for operations, so their proofs carry the legacy EcdsaSecp256k1Signature2019 label and the signature that goes with it — the payload is the operation alone. Both remain accepted, and the DIDs shown derive from these exact bytes. For the proof a node emits now, and the payload it signs, see the current-suite example.

Example

{
    "type": "create",
    "created": "2026-01-14T19:29:06.924Z",
    "registration": {
        "version": 1,
        "type": "agent",
        "registry": "hyperswarm"
    },
    "publicJwk": {
        "kty": "EC",
        "crv": "secp256k1",
        "x": "LRrQabMIkvGVTA2IRk0JdWCpu57MNGm89nugrBZHo24",
        "y": "KHsWAaidAIGCosDjRYDIk-94793e4xVEL4UwFxjWgB8"
    },
    "proof": {
        "type": "EcdsaSecp256k1Signature2019",
        "created": "2026-01-14T19:29:06.927Z",
        "verificationMethod": "#key-1",
        "proofPurpose": "authentication",
        "proofValue": "qNT0EhtojDxOJBh71pddmWnMharQZJxOelW71ehFfuZqrqPls32zSP4bD2CyYNEvAXSJRA-3X5DwR1vHVyTPHw"
    }
}

Upon receiving the operation the node must:

  1. Check publicJwk is a well-formed secp256k1 key (see above), rejecting the operation otherwise
  2. Verify the proof
  3. Apply JSON canonicalization scheme to the operation.
  4. Pin the seed object to IPFS.

The resulting content address (CID) in standard CID v1 base32 encoding is used as the DID suffix. For example the operation above corresponds to CID “bafkreig6rjxbv2aopv47dgxhnxepqpb4yrxf2nvzrhmhdqthojfdxuxjbe” yielding the DID did:cid:bafkreig6rjxbv2aopv47dgxhnxepqpb4yrxf2nvzrhmhdqthojfdxuxjbe.

Assets

To create an asset DID, the client must sign and submit a create operation to a node. This operation will be used to anchor the DID in IPFS.

  1. Create an operation object with these fields in any order:
    1. type must be “create”
    2. registration metadata includes:
      1. version number, e.g. 1
      2. type must be “asset”
      3. registry (a valid registry name, e.g. “BTC:mainnet” or “hyperswarm”; see registration validation)
    3. controller specifies the DID of the owner/controller of the new DID
    4. data can contain any data in JSON format, as long as it is not empty
    5. created time in ISO format
    6. blockid [optional] current block ID on registry (if registry is a blockchain)
  2. Sign the JSON with the private key of the controller
    • The proof.verificationMethod must be the full DID reference of the controller (e.g., did:cid:abc123#key-1)
  3. Submit the operation to a node. For example, with a REST API, post the operation to the node’s endpoint to create new DIDs (e.g. /api/v1/did/)

Example

{
    "type": "create",
    "created": "2026-01-14T19:32:24.354Z",
    "registration": {
        "version": 1,
        "type": "asset",
        "registry": "hyperswarm"
    },
    "controller": "did:cid:bagaaieradidcs4hohalzexldr5mdmbmt553tqq3ifqd56mvhifppvyfdc32q",
    "data": {
        "group": {
            "name": "testgroup",
            "members": []
        }
    },
    "proof": {
        "type": "EcdsaSecp256k1Signature2019",
        "created": "2026-01-14T19:32:24.375Z",
        "verificationMethod": "did:cid:bagaaieradidcs4hohalzexldr5mdmbmt553tqq3ifqd56mvhifppvyfdc32q#key-1",
        "proofPurpose": "authentication",
        "proofValue": "NGQMBq5venJ2i4F3-Uo0p_rEAlY0zr-YJeTTu7vUlZ0NfyqirIPISGGyy8KU-QrBvsCrfc0fsQm8sh-2BfAzqQ"
    }
}

Upon receiving the operation the node must:

  1. Verify the proof is valid for the specified controller — against the version of the controller’s document chosen as described in Authorizing an operation on a controlled DID.
  2. Apply JSON canonicalization scheme to the operation object.
  3. Pin the seed object to IPFS.

For example, the operation above that specifies an empty Credential asset corresponds to CID “z3v8AuahaEdEZrY9BGfu4vntYjQECBvDHqCG3mPAfEbn6No7AHh” yielding a DID of did:cid:z3v8AuahaEdEZrY9BGfu4vntYjQECBvDHqCG3mPAfEbn6No7AHh.

DID Update

A DID Update applies the transition table and component replacement rules to the documents associated with the DID. To initiate an update the client must sign an operation that includes the following fields:

  1. Create an operation object with these fields in any order:
    1. type must be set to “update”
    2. did specifies the DID
    3. doc is set to the new version of the document set, which must include any or all of:
      1. didDocument the main document
      2. didDocumentData the document’s data
      3. didDocumentRegistration the protocol spec
    4. previd the CID of the previous operation
    5. blockid [optional] current block ID on registry (if registry is a blockchain)
  2. Sign the JSON with the private key of the controller of the DID
  3. Submit the operation to a node. For example, with a REST API, post the operation to the node’s endpoint to update DIDs (e.g. /api/v1/did/)

It is recommended the client fetches the current version of the document and metadata, makes changes to it, then submit the new version in an update operation in order to preserve the fields that shouldn’t change.

Example update to rotate keys for an agent DID:

{
    "type": "update",
    "did": "did:cid:bagaaieradidcs4hohalzexldr5mdmbmt553tqq3ifqd56mvhifppvyfdc32q",
    "previd": "bagaaieradidcs4hohalzexldr5mdmbmt553tqq3ifqd56mvhifppvyfdc32q",
    "doc": {
        "didDocument": {
            "@context": [
                "https://www.w3.org/ns/did/v1"
            ],
            "id": "did:cid:bagaaieradidcs4hohalzexldr5mdmbmt553tqq3ifqd56mvhifppvyfdc32q",
            "verificationMethod": [
                {
                    "id": "#key-2",
                    "controller": "did:cid:bagaaieradidcs4hohalzexldr5mdmbmt553tqq3ifqd56mvhifppvyfdc32q",
                    "type": "EcdsaSecp256k1VerificationKey2019",
                    "publicKeyJwk": {
                        "kty": "EC",
                        "crv": "secp256k1",
                        "x": "hrpjLquejw7lOE2RVGr1LQ315k0JI1lwlI4WI3t983k",
                        "y": "G2_-Agy95QnIFzW5sa9Ik72vDPeqJ0rqqrxWs3CM49o"
                    }
                }
            ],
            "authentication": [
                "#key-2"
            ],
            "assertionMethod": [
                "#key-2"
            ]
        }
    },
    "proof": {
        "type": "EcdsaSecp256k1Signature2019",
        "created": "2026-01-14T19:29:16.117Z",
        "verificationMethod": "did:cid:bagaaieradidcs4hohalzexldr5mdmbmt553tqq3ifqd56mvhifppvyfdc32q#key-1",
        "proofPurpose": "authentication",
        "proofValue": "LEmM9NGL3b4WBzSUZVy0GOqzZ16KbGydBWfCwRNTmZV-ZRznm9g_09xIszITyB3y2A3DYYYaRp5E_tFegZgBgQ"
    }
}

Upon receiving the operation the node must:

  1. Verify the proof is valid for the controller of the DID — for an asset, against the version of the controller’s document chosen as described in Authorizing an operation on a controlled DID.
  2. Validate the resulting controller relationship: agents may omit didDocument.controller or name only their own DID; assets must name an existing, active, self-controlled agent. Transfers are signed by the previous owner. The DID kind is fixed by its creation operation. Updates cannot change type; omitting the registration component preserves it, while supplying a replacement requires its version, type, and registry.
  3. Validate previd against the current accepted head’s canonical operation CID, accepting a cached content-backed alias under the operation identity rules. Do this before storage or queue writes; imported competing branches use their selected predecessor instead.
  4. Queue the operation for the registry in the accepted predecessor state (or forward the submission to a trusted node that supports it). For a migration, this is the old registry; the replacement registry applies to successors.

For registries such as BTC with non-trivial transaction costs, it is expected that update operations will be placed in a queue, then registered periodically in a batch in order to balance costs and latency of updates. If the registry has trivial transaction costs, the update operation may be distributed individually and immediately. This method defers this tradeoff between cost, speed, and security to the node operators.

DID Revocation

Revoking a DID applies the delete row of the transition table. A valid deletion terminates its accepted branch: no update may extend it. Later evidence may invalidate that branch and its deletion, as described under validity and local retention. Revoked DIDs can be resolved without error, but resolvers will return a result with the didDocumentMetadata.deactivated property set to true. The didDocument is reduced to just its id, and the DID’s data resource (dereferenced at /data) is empty.

To revoke a DID, the client must sign and submit a delete operation to a node.

  1. Create an operation object with these fields in any order:
    1. type must be “delete”
    2. did specifies the DID to be deleted
    3. previd the CID of the previous operation
    4. blockid [optional] current block ID on registry (if registry is a blockchain)
  2. Sign the JSON with the private key of the controller of the DID
  3. Submit the operation to a node. For example, with a REST API, post the operation to the node’s DID endpoint (e.g. POST /api/v1/did/)

Example deletion operation:

{
    "type": "delete",
    "did": "did:cid:bagaaiera7vfnrxrmcvo7prrbmdhpvusroii4y2gir252nzk4jv5nxgkzldha",
    "previd": "bagaaiera7vfnrxrmcvo7prrbmdhpvusroii4y2gir252nzk4jv5nxgkzldha",
    "proof": {
        "type": "EcdsaSecp256k1Signature2019",
        "created": "2026-01-14T19:34:32.170Z",
        "verificationMethod": "did:cid:bagaaieradidcs4hohalzexldr5mdmbmt553tqq3ifqd56mvhifppvyfdc32q#key-1",
        "proofPurpose": "authentication",
        "proofValue": "YUTouPmhHDSudPSJ9iU44HdzBYDm7cqmDmanhgDLa4A3MBNiJpbWL2Db4BbzDYQ4NjJCRDWixYZOT2ojzzBHI3c"
    }
}

Upon receiving the operation the node must:

  1. Verify the proof is valid for the controller of the DID.
  2. Validate previd against the current accepted head’s canonical operation CID, accepting a cached content-backed alias under the operation identity rules. Do this before storage or queue writes; imported competing branches use their selected predecessor instead.
  3. Queue the operation for the registry in the accepted predecessor state (or forward the submission to a trusted node that supports it). For a migration, this is the old registry; the replacement registry applies to successors.

After revocation is confirmed on the DID’s registry, resolving the DID returns a result like this:

{
    "didDocument": {
        "id": "did:cid:bagaaiera7vfnrxrmcvo7prrbmdhpvusroii4y2gir252nzk4jv5nxgkzldha"
    },
    "didResolutionMetadata": {
        "contentType": "application/did+ld+json"
    },
    "didDocumentMetadata": {
        "deactivated": true,
        "created": "2026-01-14T19:32:24Z",
        "deleted": "2026-01-14T19:34:33Z",
        "versionId": "bagaaierats6ttxvpx2l3tat25ota7z7335akfd2iup5loajsdlqcwismkgpq",
        "versionSequence": "2"
    }
}

The metadata has a deactivated field set to true to conform to the W3C specification. The revoked DID’s data resource, dereferenced at /data, is empty.

DID Resolution

Resolution is the operation of returning a DID Document and its metadata for a given DID. It is distinct from dereferencing, which returns a resource identified by a DID URL (see DID URL Dereferencing).

Resolution reads the accepted predecessor-linked history reconstructed from available candidates. It generates the initial document from the creation seed, then applies the transition table in predecessor order. Registry migration is part of that history; resolution is not restricted to the registry named at creation, and ordinals from different registries are not a global ordering. The node’s supported-registry list limits local submission and queueing, not the registry names it may validate in imported history.

With verification requested, the resolver checks each selected operation’s predecessor, resulting controller/registration constraints, and signature against the selected authority. A failed verification returns an error; it does not silently skip an invalid operation and apply its successors. This is the shared event-authorization contract, not a claim of general DID-document schema validation.

versionSequence and versionTime select a prefix of accepted history. Historical updates use event time, with Hyperswarm and pin envelopes normalized to proof.created; controller authorization additionally uses the registry-local ordinal cutoff where applicable. Stop at the first excluded successor; do not sort by claimed proof times. Genesis created comes from the creation operation. Confirmed resolution stops before an update/deletion not confirmed on its predecessor’s registry. See the Gatekeeper resolution algorithm for exact cutoff and metadata rules.

Peer fallback, when configured, is an HTTP-layer resolution policy. It does not import the peer’s events or change local accepted history, and an unsupported local registry alone does not require delegating core resolution.

In pseudo-code:

function resolveDid(did, bounds, confirm, verify):
    wait for startup repair and active history replay
    read accepted history for did
    generate initial document from its creation seed
    if verify: authorize the creation
    for each successor in predecessor order:
        if outside bounds: break
        if confirm and not confirmed on predecessor registry: break
        if verify and event authorization fails: return an error
        apply the transition table and generate resolution metadata
    return the selected document and metadata

Resolution Result

A conformant resolution returns only the three members defined by the W3C DID Resolution data model:

The method-specific data and registration objects are not part of the resolution result; they are exposed as dereferenceable resources (see DID URL Dereferencing). Standard document metadata — created, updated, deleted, deactivated, versionId, versionSequence, canonicalId — is carried in didDocumentMetadata. Its timestamps are reported in UTC at second precision (2026-01-14T19:32:24Z), whatever precision the operation’s own created carried.

The method-specific confirmed and timestamp fields are not DID Core document metadata, so the conformant surface does not carry them in didDocumentMetadata; they are anchoring provenance, returned with the registration resource at /registration. The legacy /api/v1/did/<did> endpoint continues to include both inline in didDocumentMetadata.

didResolutionMetadata carries contentType — the media type of the returned representation. It does not carry the retrieved timestamp that the legacy endpoint returns, since that value changes on every call and is not part of the DID Core resolution metadata.

{
  "didDocument": { "id": "did:cid:<cid>", "...": "..." },
  "didResolutionMetadata": {
    "contentType": "application/did+ld+json"
  },
  "didDocumentMetadata": {
    "created": "2026-01-14T19:32:24Z",
    "versionId": "bagaaiera...",
    "versionSequence": "1"
  }
}

Representations

The resolver negotiates from the Accept request header:

Accept Content-Type didResolutionMetadata.contentType
absent, */*, application/* application/did+ld+json application/did+ld+json
application/did+ld+json application/did+ld+json application/did+ld+json
application/did+json application/did+json application/did+json
application/did-resolution application/did-resolution application/did+ld+json
anything else — 406 representationNotSupported

application/did+ld+json and application/did+json are DID document representation media types, and this endpoint returns the resolution result triple. A client that wants the result labelled for what it is asks for application/did-resolution; the document types remain the default because Universal Resolver drivers expect them. Note that didResolutionMetadata.contentType always reports a document representation — that field describes the representation of the document inside the envelope, not the envelope.

An Accept naming only media types this endpoint cannot produce is answered 406 with didResolutionMetadata.error set to representationNotSupported, rather than silently returning JSON-LD.

Responses set Vary: Accept. This applies to the resolution result only; the /data and /registration resources are plain application/json.

Endpoints

The conformant resolution and dereferencing surface follows the Universal Resolver driver convention:

DID URL HTTP Returns
did:cid:<cid> GET /1.0/identifiers/<did> DID Resolution result (the triple)
did:cid:<cid>/data GET /1.0/identifiers/<did>/data The data resource
did:cid:<cid>/registration GET /1.0/identifiers/<did>/registration The registration resource

This surface always returns confirmed, cryptographically verified state. The legacy /api/v1/did/<did> endpoint remains available for backwards compatibility; it returns the richer internal document set (with didDocumentData and didDocumentRegistration inline) and can return unconfirmed or unverified state.

DID URL Dereferencing

Dereferencing a did:cid DID URL returns a resource associated with the DID, as distinct from resolution (which returns the DID document and its metadata). Because did:cid is content-addressed, these resources are retrieved by content rather than from an external location.

Path resources

This method defines two dereferenceable resources, selected by the DID URL path:

{
  "registry": "BTC:mainnet",
  "type": "agent",
  "version": 1,
  "confirmed": true,
  "timestamp": {
    "chain": "BTC:mainnet",
    "opid": "bagaaiera...",
    "lowerBound": { "time": 1768424000, "timeISO": "2026-01-14T19:33:20Z", "blockid": "0000...", "height": 878123 },
    "upperBound": { "time": 1768425200, "timeISO": "2026-01-14T19:53:20Z", "blockid": "0000...", "height": 878125, "txid": "…", "txidx": 3, "opidx": 0 }
  }
}

timestamp is present only when the DID’s registry anchors to a blockchain and the anchoring block is known; registries such as hyperswarm return the registration fields and confirmed alone.

Neither resource is part of the DID resolution result. Both honor the versionTime and versionSequence version selectors, and both are returned as plain application/json.

Fragments

A fragment identifies a node within the DID document — for example did:cid:<cid>#key-1 dereferences to the verification method whose id matches. Per the DID Core processing model, the fragment is applied client-side to the resolved DID document (it is not transmitted to the resolver over HTTP); the node whose fully-qualified id matches the DID URL is returned.

Service dereferencing (future)

The registered service and relativeRef query parameters — used to construct external service-endpoint URLs — are not currently implemented. If added they would be additive, and would change neither the resolution result nor the path resources above.

Proof Verification

When verifying a proof on a credential or other signed object, the verifier must resolve the signer’s DID at the time the proof was created. This is essential for supporting key rotation - credentials signed with an old key must remain verifiable even after the signer rotates to a new key.

The proof.created timestamp serves two purposes:

  1. Records when the proof was made (audit trail)
  2. Anchors verification to the correct historical key state

In pseudocode:

function verifyProof(object):
    extract signerDid from proof.verificationMethod
    resolve signerDid at versionTime = proof.created
    get publicKey from resolved DID document
    verify signature using publicKey
    return valid or invalid

This temporal resolution ensures that a credential issued in 2020 can still be verified in 2030, even if the issuer has rotated keys multiple times since issuance.

Note: While the W3C Data Integrity specification makes proof.created optional, DID:CID requires it to support proper verification after key rotation.

Authorizing an operation on a controlled DID

An operation on an asset — create, update, or delete — is authorized by a key in its controller’s document, and the temporal rule above decides which version of that document: by default, the version as of the operation’s proof.created.

proof.created is the signer’s own claim. On its own it cannot distinguish an operation genuinely made in the past from one made later and dated back, so a key the controller has since rotated out could name a created from when it was current and be authorized by the document that still listed it. The only thing that can tell the two apart is a record of when the operation appeared that the signer did not write. A registry with a blockchain provides one: once the chain has committed the operation, the node knows its position — block and index — and resolves the controller at that position instead of at proof.created:

The anchoring check walks the controller’s confirmed prefix using each version’s predecessor registry, stopping at the first non-confirming successor. Only matching chain receipts in that prefix establish anchoring, and each must carry chain registration metadata. Genesis remains admitted separately; a wrong-registry genesis receipt does not establish anchoring. Pin receipts have no chain position; they neither establish anchoring nor disqualify a later chain migration when they lack registration metadata. An unconfirmed suffix must not switch controller selection from proof time to chain position simply because one node retained a wrong-registry chain receipt instead of a gossip hint.

Hyperswarm and pin historical resolution select a predecessor-linked prefix: stop at the first operation whose proof time exceeds the cutoff. Do not sort operations by proof time or skip an excluded predecessor to apply a successor with an earlier claimed time. The same proof time supplies Hyperswarm and pin updated and deleted metadata; genesis created continues to come from the creation operation. The Hyperswarm mediator assigns proof time to event.time. Gatekeeper normalizes older/imported Hyperswarm and pin envelopes and stored candidates before replay; the resolver uses event.time uniformly. This corrects envelope timestamps without rewriting signed operations, operation IDs, or ordinals.

This rule applies to both accepted proof formats, including legacy proofs whose created is unsigned. It defines reproducible time selection for the same accepted history, not consensus chronology or a new backdating restriction. Missing history can still revise authorization, and competing-branch selection is unchanged. An explicit controller-version field (#1185) is not required for this correction (#1149).

A proof whose key the controller had retired by the operation’s position is rejected. That also rejects an operation genuinely signed before a rotation but committed to the chain after it; the remedy is to sign it again with the current key. An agent’s updates to its own document are verified against the selected predecessor document, including when evaluating competing branches.

Only the registry’s own mediator may mark an event confirmed on that registry. An event received from a relaying peer or restored from an export is taken as an unconfirmed hint whatever it claims, and confirms when the node’s own mediator imports its chain record. Non-local operations and batch assets are distributed through gossip; a chain reference alone does not guarantee their availability.

Mediators visit discovered batches in chain order but skip unavailable batches or operations and continue with later entries. An unavailable reference cannot be distinguished from a nonexistent one, so it must not indefinitely block the registry. Failures remain in mediator storage across restarts and are retried after new batches; retry failures also do not block later retries. Successful retries clear the previous error. A late import retains its original chain time and ordinal, not the retry time.

Skipping does not declare a reference invalid or establish that controller history is complete. A sovereign node derives its best current state from the evidence it has. confirmed means the accepted event sequence is anchored on its expected registries; it does not promise that authorization can never change when previously unavailable history arrives.

Gatekeeper retains imported event candidates separately from accepted DID histories. When an import or direct submission changes controller history, it replays affected histories and their transitive dependents against the currently available evidence. A late rotation can remove an asset creation, update, or deletion authorized by a retired key, together with invalid successors. Conversely, a previously rejected operation can become accepted when its authorizing controller version arrives. Operations ordered before the rotation remain valid. Replay uses original chain positions, never recovery times. Removing controller evidence, explicitly or through garbage collection, also revalidates dependents.

Controller constraints are enforced at the shared authorization boundary for direct submissions, imports, and verified replay. Startup reconstruction rejects historical operations that assigned external agent controllers, assigned asset owners that were not agents, or changed DID type. Their candidate evidence remains retained, but those operations and dependent successors are not accepted. Agent histories authorize themselves, and asset histories depend only on agents. Replay therefore completes agent histories before asset histories, with predecessor retries within each DID. Startup rebuilds every DID once in that order; replay has no separate oscillation/quarantine policy.

Repeated anchors of the same canonical operation on its expected chain are reconsidered during replay: an earlier chain ordinal replaces a later accepted anchor only when it passes authorization at the earlier position. A late predecessor must not permanently select whichever anchor first became applicable. This can revise controller cutoffs and dependent histories. The rule does not change tied ordinal handling or first-observation behavior for local, Hyperswarm, and pin receipts.

Candidate journals persist across restart, including rejected candidates and replaced branches. Replay reconstructs accepted state before serving resolution after startup and refreshes dependent search and verification state. Existing databases adopt their stored histories into the journal; operations discarded before this upgrade must be recovered by rescanning their anchors. DID exports continue to contain accepted histories, so a DID export alone is not a backup of the candidate journal.

This is convergence on available anchored evidence, not proof of complete history. If a relevant rotation never becomes available, the node cannot take it into account. Competing unanchored successors use the canonical-CID rule below, independently of receipt order. Repeated observations of the same complete canonical operation in the same unanchored registry retain the first observation and do not trigger new authorization replay. Distinct blockchain anchors remain separate evidence; CID priority is a deterministic conflict rule, not evidence of real-world chronology.

Competing unanchored successors

Among valid competing successors of the same predecessor, when neither event confirms on the predecessor’s expected chain registry, the lexicographically smallest canonical operation CID wins. Compare the canonical lowercase base32 CID strings using ASCII ordering, including the complete proof in operation identity. This applies to hints for chain-registered DIDs as well as DIDs registered on Hyperswarm or local. The pin registry has no chain ordering and also uses CID priority, even when an event is labeled pin. An anchor on another registry is also provisional for this comparison. Receipt times, receipt ordinals, proof times, and batch delivery order do not choose between those siblings. previd still orders a successor after its predecessor; CID order is not a global sequence of all operations.

A later preferred sibling replaces the previously accepted branch, and dependent histories are replayed. Candidate evidence for displaced branches remains retained. The same preference applies during ordinary import and reconstruction, including when a successor arrives before its predecessor. A valid confirmation on the predecessor’s expected chain registry still takes precedence over hints; competition between chain events compares registry-local ordinals first. When both ordinals are present and equal, the lexicographically smaller canonical operation CID wins. This tie-breaker applies to distinct competing operations, not repeated anchors of the same operation.

Replay retains the existing hint traversal order to avoid repeatedly processing long predecessor chains in arbitrary CID order. The shared importer applies CID priority when siblings compete, including on subsequent replay passes. Thus traversal order is not the branch-selection rule. Per-operation/per-registry deduplication still retains the first observation, but its receipt ordinal no longer chooses a competing unanchored branch. Historical time selection and same-operation confirmation behavior remain unchanged.

This is the version-1 ordering decision in #1197. The convergence report records the production audit and tested boundaries. Both implementations must be upgraded together to apply this rule consistently; old nodes can still choose a different branch for a newly encountered fork. No operation bytes, signatures, genesis identifiers, or registry anchor positions are rewritten.

Equal chain positions are reachable: the Solana mediator currently records a transaction-local instruction index, so two transactions in one block can produce the same ordinal. The A2 audit in #1215 demonstrated persistent arrival-order divergence with real signed operations and ordinary imports in both Gatekeepers. The 2026-09-20 decision adds the CID tie-breaker to version 1; nodes must upgrade together for consistent selection of such forks. It does not rewrite operation identities or chain positions. The domain audit records the reproducer and remaining domain obligations.

Operation identity and retrieval references

An operation’s identity is the CID of its complete JCS-canonical JSON, including its proof. versionId, stored event opid, duplicate detection, and candidate replay use that identity. Equal signatures do not imply equal operations: legacy proof configuration can change without changing the signature. Different anchors of the same operation remain distinct occurrences.

A batch may reference another CID that retrieves the same JSON in a different key order. That CID is a retrieval reference, not a replacement operation identity. CID ingress retains the retrieved operation under that reference and derives its canonical identity. New operations should use the canonical versionId as previd. For compatibility, a signed predecessor reference already present in the node’s operation cache may resolve to the same canonical predecessor; neither the signed operation nor its previd is rewritten. An unknown reference remains unresolved. A peer’s claimed opid alone does not install a retrieval alias.

Both ports now use RFC 8785 canonicalization for operation identity and preserve those bytes through hashing and storage (#1180, merged in #1191). They also derive the historical TypeScript numeric-key reference from known operation content so legacy signed predecessors remain resolvable, including on fresh imports. This compatibility reference does not change new canonical IDs or rewrite signed operations. The canonicalization audit found no production genesis or signing-byte changes and three operation-ID changes on two assets; it does not establish compatibility for every hypothetical legacy seed.

Startup reconstruction canonicalizes stored event IDs and candidate IDs while retaining cached retrieval aliases. A complete database backup must include the operation cache as well as the candidate journal. Accepted-history exports alone may omit aliases needed by legacy signed predecessors; restore the full database or recover the original CID references from their anchors.

Registration validation

Version 1 requires registration objects with version: 1, type: "agent" or "asset", and a registry name of 1–128 characters matching [A-Za-z0-9][A-Za-z0-9:_-]*. On update, omitting didDocumentRegistration retains it; supplying it replaces the entire component and must include all required fields. Null, array, scalar, and partial replacements are invalid.

Every replacement preserves genesis version, kind, and prefix presence/value. A valid registry may change under existing migration rules. Optional validUntil must use the shared RFC 3339 string grammar; a complete replacement may change or omit expiry. Unknown extension fields remain allowed. These checks introduce no new expiry enforcement or creation-prefix grammar. Imports validate registry name shape independently of the node’s locally supported registries.

Both ports enforce this at shared event authorization, including startup recovery. The registration hardening record documents the production audit and approved version-1 compatibility decision. Malformed registrations previously accepted by older implementations are rejected on replay; none were found in the audited production history. Version 2 remains disabled.

Complete chain receipts

A chain event MUST contain a valid registry name, an RFC 3339 authoritative block time, and complete registration: height, index, txid, batch, opidx. Position fields are nonnegative safe integers. txid and batch are nonempty strings supplied by the mediator; batch identifies the anchored batch DID.

The batch DID identifies its signed create operation. Version-1 chain mediators MUST retrieve the genesis DID document through getGenesis(did) and interpret its ordered didDocumentData.batch.ops CID list. Genesis retrieval verifies the operation’s CID and create-operation structure, without requiring publisher/controller history or an accepted batch DID. The original signed operation remains retrievable by CID for proof inspection; retrieval does not assert publisher authorization. Every contained operation still undergoes normal Gatekeeper authorization. Later updates to the batch asset do not change which operations, or which opidx values, an existing chain anchor represents. If genesis content is unavailable, the mediator retries it; it does not use the latest asset state as a substitute.

The ordinal contract is [height, index, ...registryPosition, opidx]: its first two components MUST equal registration height and index, and its last MUST equal opidx. Additional registry-specific position components, if any, remain part of lexicographic ordering. Bundled Bitcoin, Zcash, Ethereum and Solana mediators use [height, index, opidx]. Registry names remain open; this rule does not introduce an allowlist.

CID batch ingress requires the corresponding prefix [height, index, ...registryPosition] and height, index, txid, batch metadata before fetching content. Gatekeeper appends each CID’s original list index, including gaps for unavailable entries, and derives opidx itself. Malformed batch metadata fails explicitly; malformed chain events are rejected at admission and per-event replay. There is no incomplete-chain-receipt category or richer-copy preference.

Local, Hyperswarm and pin remain unanchored. Peer relay converts chain claims to unconfirmed Hyperswarm hints before admission, so signed operations without complete anchor evidence can still propagate. This does not make controller history complete or authorization final; delayed evidence still triggers replay.

Chain reorganization invalidates the affected chain receipts, not the signed operation bytes. The trusted mediator withdraws the rescanned suffix before committing its new scan position, clears orphaned discovered batches, and imports replacement evidence. Gatekeeper retains withdrawn operations as unconfirmed hints and replays affected histories, including dependent asset authorization. An orphaned anchor must not continue to supply confirmation or ordering priority.

Implementation boundary: event authorization and proof verification

Import and verified replay share event authorization: select the target state the operation chains from, select the authorizing controller document under the rules above, then verify the operation against that document. Confirmation replacements and competing events use the predecessor state, not the latest document. Direct submissions have no trusted event position and retain the historical proof.created selection. Predecessor validation uses the target DID’s selected previous version, separately from the agent document selected to verify the signature. Direct updates and deletions must reference the current head before any operation storage or queue write. Import may select an earlier predecessor when considering a competing branch; an unavailable predecessor remains deferred and may become applicable when its history arrives. Verified resolution uses the same predecessor validation. No signed predecessor reference is rewritten.

Low-level operation verification checks structure, signer, and signature against the selected document; it does not resolve controller history or accept a chain anchor. Do not add a second verification against the proof.created document after selecting the chain-position document, since requiring both would change the authorization rule.

Dependent replay lives at this event authorization boundary and reuses the import algorithm. The availability policy is described in #1151, and revisable authorization in #1150.

Verification Method Format

The proof.verificationMethod field identifies which key was used to create the proof:

Cryptosuites

A DID operation and a credential are signed with the same key and under the same suite, but not over the same bytes: a credential’s proof configuration carries the document’s @context, and a credential may carry a proof set where an operation never does.

Operations carry a single proof — never a proof set, however many keys the signer has published. It is an archon-ecdsa-secp256k1-jcs-2026 proof, the same suite credentials use, and its proofPurpose is capabilityInvocation: an operation exercises control over a DID document, where authentication would claim the signer is proving they are the subject. Both gatekeeper implementations also accept the legacy EcdsaSecp256k1Signature2019 and authentication, and always will — every operation anchored before either was settled carries them, and each node replays its own history.

Credentials carry a DataIntegrityProof, and may carry more than one — see Proof sets below.

archon-ecdsa-secp256k1-jcs-2026

The secp256k1 suite, defined here. It is deliberately not registered in the W3C cryptosuite registry: no registered Data Integrity cryptosuite covers secp256k1 (ecdsa-jcs-2019 is defined for P-256 and P-384 only), so claiming a registered name for this algorithm would misstate what the proof is. An outside verifier that does not recognise the name skips the proof — which is the correct outcome, since it could not have checked it under any other name either.

The name is prefixed for the same reason. Registered suites are unprefixed, so an unprefixed name claims registry membership by convention, and a future registered suite taking that name would make every proof signed under it ambiguous. The curve is stated because it is the parameter that makes this suite necessary, and the date is the suite’s own version — a later change to the construction takes a later date, leaving proofs under this one unambiguous.

The proof configuration is the proof without proofValue: type, cryptosuite, created, verificationMethod, proofPurpose, and the secured document’s @context when it declares one. The unsecured document is the object with any proof member removed.

  1. Canonicalize the proof configuration and the unsecured document separately with JCS (RFC 8785).
  2. Digest each with SHA-256 and concatenate them, configuration first — 64 bytes.
  3. Digest that with SHA-256 again. ECDSA signs a 32-byte digest, where Ed25519 takes the message itself, so this step has no counterpart in eddsa-jcs-2022.
  4. Sign with secp256k1 ECDSA, producing a 64-byte compact r || s signature.
  5. proofValue is that signature, base64url-encoded (unpadded).

This create-agent operation uses the current suite. It was signed with the synthetic private key consisting of 32 bytes of 0x4a (for reproducibility, not for use as an identity). The proof configuration has no @context, because the operation has none. The checked example verifies the signature and the resulting DID against Gatekeeper.

{
    "type": "create",
    "created": "2026-09-22T00:00:00.000Z",
    "registration": {
        "version": 1,
        "type": "agent",
        "registry": "hyperswarm"
    },
    "publicJwk": {
        "kty": "EC",
        "crv": "secp256k1",
        "x": "OSd_CMNPrDPDsV5YoWajZol2ZUGeXD8hR3XubkcWcX4",
        "y": "yBLNwQllxyR5Omgo9b7LmJEfd1pGNiZZqzgqafZ4Qcs"
    },
    "proof": {
        "type": "DataIntegrityProof",
        "cryptosuite": "archon-ecdsa-secp256k1-jcs-2026",
        "created": "2026-09-22T00:00:00.001Z",
        "verificationMethod": "#key-1",
        "proofPurpose": "capabilityInvocation",
        "proofValue": "esafZoTsLCbrza6GTFuca4sFFHT4S4FJFT9KwBqNhMQXneC5gPgjdNraDML2UXZ0xHf4XScAFm-MZjV_jFsUFw"
    }
}

Its DID is did:cid:bagaaiera7apdjgpe7jleguoioddew7oqqxn2iqlsowktnbnqksf7bpzuntka.

Including the configuration in the signed payload is what makes created and proofPurpose unforgeable. A verifier must also reject a proof whose @context disagrees with the document’s: the configuration is canonicalized as given, so a proof signed over some other context would otherwise verify against a verifier that does not rebuild it.

eddsa-jcs-2022

The registered Ed25519 suite (W3C vc-di-eddsa), implemented as specified, so a conforming verifier can check an Archon credential without knowing anything about Archon. It requires a Multikey verification method with publicKeyMultibase; Archon publishes one at the #key-assertion-1 fragment, listed under both assertionMethod and authentication.

The signed payload is SHA-256(JCS(proof configuration)) || SHA-256(JCS(unsecured document)), where the proof configuration is the proof without proofValue and carrying the secured document’s @context. proofValue is the 64-byte Ed25519 signature in base58-btc multibase, so it begins with z.

Verification strictness. Ed25519 verification is delegated to each port’s library — @noble/curves in JS, OpenSSL (via cryptography) in Python — and RFC 8032 underspecifies how torsion (small-order) points are handled, so the two differ at the margin (see Taming the Many EdDSAs). The JS port pins noble’s cofactored ZIP-215 rule explicitly (zip215: true) so a library default change cannot move it; OpenSSL uses a stricter uncofactored equation and offers no switch. The two agree on every signature a standard signer produces, so all real credentials verify identically. They diverge only on a deliberately crafted torsion-bearing signature — a small-order key, or a nonzero torsion component added to R under an ordinary prime-order key — which noble’s cofactored equation accepts and OpenSSL’s uncofactored equation rejects. (Concretely: both accept the identity point, but the JS port also accepts an order-2 key OpenSSL rejects.) Such a signature can only come from a signer sabotaging their own material — a self-repudiation edge, not impersonation of anyone else — and reconciling it would mean reimplementing verification in one port, which is not warranted. Per-port test vectors record the current behaviour so a library change fails a test rather than a credential.

The legacy label

Credentials issued, and operations anchored, before this suite was named carry EcdsaSecp256k1Signature2019. Its payload is weaker: SHA-256(JCS(document)) alone, with the proof configuration outside the signature. On such a proof created and proofPurpose can be altered without breaking it — which is the defect archon-ecdsa-secp256k1-jcs-2026 was defined to fix, and the reason the two names are verified under different rules rather than treated as aliases.

Those credentials and operations are immutable, so verifiers accept the legacy type indefinitely rather than deprecating it. Nothing issues it any more, with one exception: the seed bank’s create operation, whose DID is the CID of that operation including its proof, so signing it any other way would compute a different DID and orphan every wallet holding one.

Because a verifier accepts a credential when any one proof verifies, a proof set is only as strong as its strongest surviving proof: an attacker may drop the others. A credential carrying a legacy proof therefore inherits that proof’s malleability no matter what accompanies it.

Proof sets

A credential’s proof member may be a single proof or an array of them. A verifier accepts the credential if any one proof verifies: a document may carry a suite the verifier does not implement alongside one it does, and refusing it for the former would reject a credential that is perfectly valid.

Each proof is computed independently over the document with proof removed — a set, not a chain — so each verifies alone.

A proof is only valid if the signer’s DID document authorizes its verificationMethod for the proof’s proofPurpose, under the relationship of that name. A key listed only for keyAgreement cannot produce a valid assertionMethod proof even though the signature itself checks out. Issuers apply the same test before attaching a proof, so a proof that would fail this check is not written in the first place.

DID Recovery

For security reasons, this method provides no support for storing private keys. We recommend that clients use BIP-39 to generate a master seed phrase consisting of at least 12 words, and that users safely store the recovery phrase.

If a user loses a device that contains their wallet, they should be able to install the wallet software on a new device, initialize it with their seed phrase and recover their DID along with all their credentials. This implies that a “vault” of the credentials should be stored with the agent DID document, though it should be encrypted with the DID’s current private key for privacy.

Required chain receipt positions

Authoritative chain events must include a nonempty registry-local ordinal array of nonnegative safe integers (at most 9007199254740991). Gatekeeper validates this before appending batch operation indices, queues, or candidate writes. Missing/null/empty chain positions are also excluded during reconstruction. Local, Hyperswarm, and pin events remain eligible without ordinals. Relayed chain envelopes are converted to Hyperswarm hints before validation; peers cannot confer chain confirmation. This rule constrains receipt envelopes, not signed operation bytes or canonical operation IDs. Equal positions still use the canonical-operation-CID rule for competing operations.