SDK: @did-btcr2/api
@did-btcr2/api is the SDK of the TypeScript implementation. createApi()
returns one facade for the DID operations, the Bitcoin connection, key
management, and the CAS (content-addressed store). If you want to customize the
protocol, use @did-btcr2/method
directly.
Install
Section titled “Install”npm install @did-btcr2/apiThe package exports only its facade
(ADR 132).
Each call on this page starts at the object that createApi() returns: its
methods and its sub-facades (api.crypto, api.kms, api.did, api.btcr2,
api.btc, api.cas, and api.smt). The package needs Node.js 22 or newer, or
a browser.
Configure
Section titled “Configure”createApi(config) takes explicit config objects. It reads no environment
variables.
import { createApi } from '@did-btcr2/api';
const api = createApi({ // The Bitcoin network. Each network has a default REST (Esplora) host. btc: { network: 'mutinynet' }, // Optional: the CAS. The default is the read-only gateway // https://trustless-gateway.link, with a timeout of 30 seconds. cas: { gateway: 'https://trustless-gateway.link', timeoutMs: 10_000 },});| Network | Default REST host |
|---|---|
bitcoin |
https://mempool.space/api |
testnet3 |
https://mempool.space/testnet/api |
testnet4 |
https://mempool.space/testnet4/api |
signet |
https://mempool.space/signet/api |
mutinynet |
https://mutinynet.com/api |
regtest |
http://localhost:3000 (REST) and http://localhost:18443 (RPC) |
Other btc options:
rest: { host, headers }sets your own Esplora host. The host must meet the Esplora server requirements: for example, a mempool instance needsMEMPOOL_BACKEND=esplora.rpcsets a Bitcoin Core RPC client.executorsets your own HTTP client.timeoutMssets a request timeout.
The connection must be on the network of the DID: the api refuses to resolve or update a DID from a different network. A new DID takes the network of the connection.
cas: { rpcUrl } connects a writable CAS (the RPC endpoint of an IPFS node).
Create
Section titled “Create”Creation is offline: no chain read and no fee. There are two identifier types:
k1(deterministic): the identifier encodes a compressed secp256k1 public key. The initial DID document follows from the key.x1(external): the identifier encodes the SHA-256 hash of a Genesis Document.api.btcr2.buildGenesisDocument()builds one from keys, beacons, and services. Keep the document: a resolver needs it as sidecar data.
// Create a deterministic `did:btcr2:k1…` identifier from a compressed// secp256k1 public key. Creation is offline: no chain read and no fee.import { createApi } from '@did-btcr2/api';
// A new DID takes the network of the Bitcoin connection.const api = createApi({ btc: { network: 'mutinynet' } });
const keys = api.crypto.keypair.generate(); // or api.crypto.keypair.fromSecret(secretKey)const did = api.createDid('deterministic', keys.publicKey.compressed);
// The initial DID document has three Singleton beacons: P2PKH, P2WPKH, and// P2TR. Fund one of these addresses before the first update.const beacons = api.btcr2.getBeacons(api.btcr2.getInitialDocument(did));
console.log({ did, beacons });// Create an external `did:btcr2:x1…` identifier from a genesis document.// The identifier encodes the SHA-256 hash of the canonical document.import { createApi } from '@did-btcr2/api';
const api = createApi({ btc: { network: 'mutinynet' } });const keys = api.crypto.keypair.generate();
// One key with all four verification relationships, and one Singleton// beacon at the P2WPKH address of that key. The builder uses the// placeholder id `did:btcr2:_` and the two required contexts.const genesisDocument = api.btcr2.buildGenesisDocument({ verificationMethods: [{ publicKey: keys.publicKey.compressed }],});
// Hash exactly the JSON that you keep: one changed byte gives another DID.const json = JSON.stringify(genesisDocument);const { did, didDocument } = api.btcr2.createExternalFromDocument(JSON.parse(json));const [beacon] = api.btcr2.getBeacons(didDocument);
// Keep `json`. Resolution of an x1 DID needs the genesis document as// sidecar data: api.resolveDid(did, { sidecar: { genesisDocument } })const report = api.did.validate(did, { genesisDocument: JSON.parse(json) });
console.log({ did, beacon: beacon.address, valid: report.valid });api.generateDid() makes a key, keeps it in the in-process key manager, and
returns { did, keyId }. api.kms.signer(keyId) then gives the signer for
updates.
Resolve
Section titled “Resolve”Resolution drives the Resolver
state machine. The api reads the beacon signals from Bitcoin and applies each
update that the sidecar data or the CAS supplies.
// Resolve a `did:btcr2` identifier. The api reads the beacon signals from// the Bitcoin connection, which must be on the network of the DID.import { createApi, type Sidecar } from '@did-btcr2/api';
const api = createApi({ btc: { network: 'mutinynet' } });const did = 'did:btcr2:k1q5p...'; // your DID
// Without sidecar data, resolution works for a k1 DID with no updates, and// for a DID whose updates are in a CAS. tryResolveDid gives a DID Resolution// error code instead of a throw.const attempt = await api.tryResolveDid(did);if (attempt.ok) console.log(attempt.document, attempt.metadata);else console.warn(attempt.error, attempt.errorMessage); // e.g. MISSING_UPDATE_DATA
// Sidecar data comes from the DID controller:// - An x1 DID needs its genesis document.// - A DID with updates needs every signed update, unless a CAS holds them.const sidecar: Sidecar = { updates: [/* the signed updates of the DID, in order */],};
// Resolution applies a beacon signal after it has 6 confirmations.const result = await api.resolveDid(did, { sidecar });// didDocumentMetadata: { versionId, confirmations, deactivated, updated? }console.log(result.didDocument, result.didDocumentMetadata);versionId and versionTime in the resolution options select an earlier
version of the document.
Update
Section titled “Update”An update is a JSON Patch to the
DID document. The api signs the update and broadcasts a beacon signal. The
signal is a Bitcoin transaction with the hash of the update in its OP_RETURN
output, so the beacon address must hold a confirmed UTXO. The initial document
of a k1 DID has three beacons, and api.btcr2.getBeacons() gives their
addresses. Fund one of them, and wait for one confirmation.
The signer comes from the key manager of the api. api.kms.import(keyPair)
keeps a key pair and returns its key id, and api.kms.signer(keyId) gives the
signer.
// Apply a JSON Patch to a DID document, sign the update, and broadcast a// beacon signal. The beacon address must hold a confirmed UTXO.import { createApi } from '@did-btcr2/api';
const api = createApi({ btc: { network: 'mutinynet' } });
const did = 'did:btcr2:k1q5p...'; // your DIDconst secretKey = new Uint8Array(32); // your 32-byte secp256k1 secret key
// The key manager of the api holds the key and gives the signer.const signer = api.kms.signer(api.kms.import(api.crypto.keypair.fromSecret(secretKey)));
// Link a website to the DID. The api resolves the current document first.// verificationMethodId and beaconId are optional: the api uses the method// that publishes the signer's key and the only beacon that can fund the// signal.const result = await api.updateDid(did, [{ op: 'add', path: '/service/-', value: { id: `${did}#website`, type: 'LinkedDomains', serviceEndpoint: 'https://example.com' },}], signer);
// Keep result.signedUpdate. Bitcoin holds only its hash. A resolver needs// the signed update as sidecar data, unless you publish it to a CAS.// result: { signedUpdate, txid, announcement?, proof?, publishedToCas }console.log(result.txid, result.signedUpdate);Every update failure that the specification names is an UpdateError of type
INVALID_DID_UPDATE. Examples: a patch that fails to apply, a key that the
document does not list in capabilityInvocation, and a deactivated DID. A
signing key that is not a Multikey with a zQ3s public key is an
UpdateError of type INVALID_DID_DOCUMENT.
Before the next update, wait until the beacon signal of the last update has
minConf confirmations (default 6). The api compares the resolved versionId
with the signed updates in resolutionOptions.sidecar. If the resolution does
not apply all of them, the api refuses the update with INVALID_DID_UPDATE.
Publish to a CAS
Section titled “Publish to a CAS”By default, the api publishes nothing (publishToCas: 'never'). The controller
then gives each signed update to the relying parties as sidecar data, and only
they can see the change. With a writable CAS, publishToCas: 'auto' publishes
the signed update before the broadcast. Any resolver can then get the update
from the CAS with no sidecar data. 'always' refuses the update if no writable
CAS is configured.
const api = createApi({ btc: { network: 'mutinynet' }, cas: { rpcUrl: 'http://127.0.0.1:5001' }, // the RPC endpoint of an IPFS node});const result = await api.updateDid(did, patches, signer, { announce: { publishToCas: 'auto' } });console.log(result.publishedToCas); // { update: true, announcement: false }Deactivate
Section titled “Deactivate”Deactivation is an update with the patch
[{ "op": "add", "path": "/deactivated", "value": true }].
api.deactivateDid() adds the patch for you. Deactivation is permanent.
// Deactivate a DID. This is permanent: the api refuses a later update.// Deactivation is an update with the patch// [{ op: 'add', path: '/deactivated', value: true }]import { createApi, type SignedBTCR2Update } from '@did-btcr2/api';
const api = createApi({ btc: { network: 'mutinynet' } });
const did = 'did:btcr2:k1q5p...'; // your DIDconst secretKey = new Uint8Array(32); // your 32-byte secp256k1 secret keyconst updates: SignedBTCR2Update[] = [/* every signed update of the DID, in order */];const signer = api.kms.signer(api.kms.import(api.crypto.keypair.fromSecret(secretKey)));
const { txid, signedUpdate } = await api.deactivateDid(did, signer, { resolutionOptions: { sidecar: { updates } },});
// Add signedUpdate to the sidecar data: a resolver needs it to see the// deactivation.console.log(txid, signedUpdate);Sign a message
Section titled “Sign a message”A key of the DID can sign a text message. api.btcr2.signMessage() returns
the text and a bip340-jcs-2025 Data Integrity proof. The proof purpose is
always assertionMethod, so a message signature is never valid as an update
proof or as a transaction signature
(ADR 137).
The specification does not define message signatures: the format is a choice
of this implementation.
api.btcr2.verifyMessage() checks a signed message against a DID document and
returns a report. Neither function reads the network, so give each function the
current DID document from a resolution.
// Sign a text message with a key of a DID. The proof purpose is always// assertionMethod, so the signature is never valid as an update proof.import { createApi } from '@did-btcr2/api';
const api = createApi({ btc: { network: 'mutinynet' } });
const did = 'did:btcr2:k1q5p...'; // your DIDconst secretKey = new Uint8Array(32); // your 32-byte secp256k1 secret keyconst signer = api.kms.signer(api.kms.import(api.crypto.keypair.fromSecret(secretKey)));
// signMessage does no I/O. Resolve the current DID document first: the key// must be in its assertionMethod. Add the sidecar data of the DID:// api.tryResolveDid(did, { sidecar })const resolution = await api.tryResolveDid(did);if (!resolution.ok) throw new Error(`${resolution.error}: ${resolution.errorMessage}`);
// The format has no time and no replay protection. Put the date, a nonce,// and the audience in the text.const text = 'I control this DID. 2026-10-08 nonce 7f3a';const signed = api.btcr2.signMessage(resolution.document, text, signer);
// signed: { type: 'BTCR2Message', message, proof }. Send it as JSON.console.log(JSON.stringify(signed));// Verify a signed message against the current DID document of its signer.import { createApi } from '@did-btcr2/api';
const api = createApi({ btc: { network: 'mutinynet' } });
const did = 'did:btcr2:k1q5p...'; // the DID that must have signedconst json = '{ "type": "BTCR2Message", ... }'; // the signed message
// Resolve the current DID document: after a key rotation or a deactivation,// an old message fails. Add the sidecar data of the DID:// api.tryResolveDid(did, { sidecar })const resolution = await api.tryResolveDid(did);if (!resolution.ok) throw new Error(`${resolution.error}: ${resolution.errorMessage}`);
// verifyMessage does no I/O and does not throw for a bad message. The checks// run in order (structure, signer, active, assertionMethod, signature) and// stop at the first failure.const report = api.btcr2.verifyMessage(resolution.document, JSON.parse(json));
if (report.verified) console.log(report.message);else console.warn(report.checks.find((check) => !check.ok)); // { name, ok, detail }Use in a browser
Section titled “Use in a browser”The default configuration works in a browser
(ADR 124).
A GET request has no Content-Type header, so the browser sends no CORS
preflight. Chain data skips the HTTP cache, and the default CAS gateway allows
CORS. The Demo uses the default configuration and sets only the
timeouts.
Reference
Section titled “Reference”@did-btcr2/apiREADME: the exports, an example for each operation, and the architecture principles.@did-btcr2/apion npm and its changelog.