Skip to content

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.

Terminal window
npm install @did-btcr2/api

The 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.

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 needs MEMPOOL_BACKEND=esplora.
  • rpc sets a Bitcoin Core RPC client.
  • executor sets your own HTTP client.
  • timeoutMs sets 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).

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-key.ts
// 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 });

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.

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.ts
// 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.

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.

update.ts
// 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 DID
const 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.

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 }

Deactivation is an update with the patch [{ "op": "add", "path": "/deactivated", "value": true }]. api.deactivateDid() adds the patch for you. Deactivation is permanent.

deactivate.ts
// 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 DID
const secretKey = new Uint8Array(32); // your 32-byte secp256k1 secret key
const 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);

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-message.ts
// 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 DID
const secretKey = new Uint8Array(32); // your 32-byte secp256k1 secret key
const 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));

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.