Command-line interface for the did:btcr2 DID method.
Part of the did-btcr2-js monorepo.
This package provides the btcr2 CLI for creating, resolving, updating, and deactivating did:btcr2 decentralized identifiers. It also manages an encrypted keystore of keypairs, reads and writes CLI configuration and profiles, and prints shell completion scripts. It wraps the @did-btcr2/api SDK via dependency injection, using commander.js for argument parsing.
Out of the box, btcr2 resolve works with zero configuration. The Bitcoin network is derived from the DID itself, and public endpoints (mempool.space, ipfs.io) are used as defaults. Override endpoints via CLI flags, environment variables, or a config file.
Signing operations (update, deactivate, and generated create keys) read secret keys from an encrypted on-disk keystore. Choose a key with --signing-key <ref> or set an active key with btcr2 key use <ref>.
npm install -g @did-btcr2/cli
Or with pnpm:
pnpm add -g @did-btcr2/cli
Requires Node.js >= 22.
Without installing globally, run directly via npx:
npx @did-btcr2/cli resolve -i did:btcr2:k1qq...
| Command | Alias | Description |
|---|---|---|
init |
- | Set up the btcr2 home: create the directory, a default config, and establish the keystore |
quickstart |
- | One-command onboarding: init + record the network + (optionally) cache the session and probe endpoints |
create |
- | Create an identifier and initial DID document |
resolve |
read |
Resolve a DID document |
update |
- | Update a DID document (signs via the keystore) |
deactivate |
delete |
Deactivate a DID permanently (signs via the keystore) |
key |
- | Manage keypairs in the keystore |
keystore |
- | Establish, inspect, and re-key the keystore |
config |
- | Read and write CLI configuration |
profile |
- | Manage configuration profiles |
completion |
- | Print a shell completion script |
Creates an identifier and initial DID document. Two identifier types, selected by -t/--type:
k (deterministic): a 33-byte compressed secp256k1 public key. Three mutually-exclusive input modes:
--bytes nor --signing-key): mint a fresh key, persist it to the keystore, set it active, and print the identifier. Sealing the secret prompts for the keystore passphrase.--signing-key <ref>): use a stored key’s public key as the genesis bytes. Reading a public key never decrypts, so this never prompts.--bytes <hex>): a 33-byte public key as hex. Offline and keystore-free.x (external): raw-bytes only, the 32-byte SHA-256 hash of a genesis document via --bytes.| Flag | Description |
|---|---|
-t, --type <type> |
Identifier type: k (deterministic) or x (external). Default: k |
-n, --network <network> |
Bitcoin network: bitcoin, testnet3, testnet4, signet, mutinynet, or regtest. Default: config defaults.network, else the active profile’s network, else regtest |
-b, --bytes <bytes> |
Genesis bytes as a hex string. For type=k, a 33-byte public key (omit to generate a key); for type=x, the 32-byte genesis-document hash |
--signing-key <ref> (global) selects a stored key for the existing-key mode; it applies only to -t k.
On a testnet with a public faucet, text-mode create (for a -t k identifier) also prints a funding hint on stderr: the initial P2WPKH beacon address next to its faucet and explorer links, from the per-network preset. It is suppressed under --quiet and -o json, and absent on regtest and mainnet.
Required flag: -i/--identifier. At most one of -r or -p may be given.
| Flag | Description |
|---|---|
-i, --identifier <identifier> |
did:btcr2 identifier to resolve (required) |
-r, --resolution-options <json> |
Resolution options as an inline JSON string |
-p, --resolution-options-path <path> |
Path to a JSON file containing resolution options |
Signs and broadcasts an update to a DID document. The signing key comes from the encrypted keystore (choose one with --signing-key <ref> or set an active key with btcr2 key use).
Required flags: -s/--source-document, --source-version-id, -p/--patches, -m/--verification-method-id, -b/--beacon-id.
| Flag | Description |
|---|---|
-s, --source-document <json> |
Source DID document as a JSON string |
--source-version-id <number> |
Source version ID as a non-negative integer |
-p, --patches <json> |
JSON Patch operations as a JSON array string |
-m, --verification-method-id <id> |
DID document verification method ID |
-b, --beacon-id <json> |
Beacon ID as a JSON string |
--publish-to-cas <mode> |
Publish update artifacts to a writable CAS before broadcast: auto, always, or never (default: never). See Publishing updates to CAS |
--fee-rate <satsPerVByte> |
Fee rate in sats/vByte for the beacon transaction (default: 5). Raise it under congestion so the transaction confirms (also BTCR2_FEE_RATE, profile btc.feeRate) |
--change-address <address> |
Send transaction change to this address instead of the beacon address, so a DID’s announcements are not linked on-chain (profile btc.changeAddress) |
On a network with a block explorer, text-mode update (and deactivate) also prints a Watch: link on stderr for the broadcast txid, suppressed under --quiet and -o json.
Permanently deactivates a DID. This is irreversible. Deactivation applies the { "op": "add", "path": "/deactivated", "value": true } patch and routes through the same signed-update path as update, so it also signs via the keystore.
Required flags: -s/--source-document, --source-version-id, -m/--verification-method-id, -b/--beacon-id. Optional: --publish-to-cas <mode>, --fee-rate <satsPerVByte>, --change-address <address> (same as update).
btcr2 init is the one-command setup: it creates the btcr2 home directory, writes a default config if none exists, and establishes the keystore if none exists. It is idempotent (existing files are left untouched unless --force). For a single command that also records the network and (optionally) caches the session and probes the endpoints, see quickstart.
| Flag | Description |
|---|---|
-n, --network <network> |
Bitcoin network to record as defaults.network so later commands can drop -n. Written idempotently: it never clobbers a network you set earlier |
--dev |
Establish an unencrypted dev keystore (plaintext keys, no passphrase). Testnet/regtest only; mainnet operations are refused |
--force |
Re-create the config even if it exists. Never re-creates the keystore (re-establishing one is the explicit keystore init --force) |
By default init establishes an encrypted keystore and prompts (with confirmation) for a passphrase up front, so the first key generate never seals the keystore under an unconfirmed, mistyped passphrase. Supply the passphrase non-interactively with BTCR2_KEYSTORE_PASSPHRASE or --passphrase-file for scripted setup. The output envelope reports the resolved network alongside the paths and protection.
btcr2 quickstart collapses onboarding into one step: it runs init’s scaffold, records the network (default mutinynet), and optionally caches the session and probes the endpoints. It reimplements nothing - it composes init, keystore unlock, and config doctor - so the keystore and session guarantees hold unchanged.
| Flag | Description |
|---|---|
-n, --network <network> |
Bitcoin network to set up. Default: mutinynet |
--dev |
Establish an unencrypted dev keystore (testnet only) |
--unlock |
Cache the passphrase for the session so later commands do not re-prompt. Opt-in; on a fresh encrypted keystore it reuses the establish-time passphrase with no second prompt |
--ttl <duration> |
Session lifetime with --unlock: bare seconds or an s/m/h suffix (default 1h, max 24h; also BTCR2_KEYSTORE_TTL) |
--no-doctor |
Skip the endpoint reachability probe (which is on by default and advisory: a failed probe warns but quickstart still exits 0) |
--allow-mainnet |
Permit -n bitcoin (records mainnet as the default; dev keystores are still refused). Guarded before any writes |
--force |
Re-create the config even if it exists (never the keystore) |
The workshop happy path:
btcr2 quickstart -n mutinynet --unlock --ttl 2h # (or: --dev for an unencrypted dev keystore)
btcr2 key generate --set-active
btcr2 create # network comes from defaults.network
# ...fund the beacon (create prints the faucet + explorer links), resolve, update, deactivate...
Manage keypairs in the keystore. All subcommands operate offline (no Bitcoin connection).
| Subcommand | Alias | Description |
|---|---|---|
key generate |
- | Generate a new keypair and store it. Flags: --name <name>, --set-active |
key list |
ls |
List stored keys (id, fingerprint, name, active) |
key show <ref> |
- | Show a key’s public material and tags (never prints the secret) |
key import |
- | Import a secret from a hex file (--secret-file) or a public key as watch-only (--public). Flags: --name, --set-active |
key export <ref> |
- | Export public material by default; --secret --out <path> writes the secret to a new 0600 file |
key delete <ref> |
rm |
Delete a key. --force deletes even the active key |
key use <ref> |
- | Set the active key, persisted across invocations |
A key reference is a full URN, a unique name tag, or a unique fingerprint prefix.
Establish, inspect, and re-key the keystore. These operate on the keystore file directly (no Bitcoin connection).
| Subcommand | Alias | Description |
|---|---|---|
keystore init |
- | Establish the keystore (encrypted by default; prompts and confirms the passphrase). --dev creates an unencrypted dev keystore; --force re-establishes an existing one (discards its keys) |
keystore status |
- | Show the resolved path, protection mode (encrypted/dev/absent), whether a passphrase is established, the key count, and the session state (whether one is live and its remaining lifetime). Never decrypts or prompts |
keystore change-passphrase |
passwd |
Re-seal every key under a new passphrase (encrypted keystores only). Prompts for the current passphrase, then a new one (with confirmation). Clears any cached session |
keystore unlock |
- | Cache the verified passphrase for the session so later commands do not re-prompt. --ttl <duration> sets the lifetime (default 1h, max 24h; also BTCR2_KEYSTORE_TTL); --allow-mainnet permits unlocking a bitcoin-default context |
keystore lock |
- | Revoke the cached session so later commands prompt again. Idempotent; needs no passphrase |
Encrypted vs dev keystores. An encrypted keystore seals each secret with argon2id + XChaCha20-Poly1305 under one passphrase, and records a verifier so a mistyped passphrase fails loudly (with Incorrect passphrase) instead of sealing a key under an unknown or divergent passphrase. A dev keystore (--dev) stores secrets in plaintext and never prompts: it is for disposable testnet/regtest keys only, and the CLI hard-refuses to sign or generate a mainnet (bitcoin) key with one.
Session unlock. keystore unlock caches the verified passphrase in <home>/session.json (0600) so a returning operator authenticates once instead of on every signing command. A cached session sits below BTCR2_KEYSTORE_PASSPHRASE / --passphrase-file and above the interactive prompt, so unattended and CI paths still win. Mainnet keeps per-use authentication: a bitcoin operation is withheld from a session that was not unlocked with --allow-mainnet. The cached passphrase is base64url-encoded, not encrypted; its only protection at rest is the 0600 file mode.
Read and write CLI configuration.
| Subcommand | Alias | Description |
|---|---|---|
config init |
- | Create a default config file with one profile per network. --force overwrites |
config get [path] |
- | Print a value at a dotted path, or the whole config. Secret values are redacted; --show-secrets reveals them |
config set <path> <value> |
- | Set a value at a dotted path (value parsed as JSON when valid, else a string). An invalid enum for a known key is rejected; an unknown path warns but writes |
config unset <path> |
- | Delete a value at a dotted path |
config list |
ls |
Print the entire config file. Secret values are redacted; --show-secrets reveals them |
config validate |
- | Report unknown keys, invalid enum values, and an unsupported schema version |
config effective |
- | Print the resolved connection config with per-value provenance (flag/env/file/default). -n, --network <n> selects the network; --show-secrets reveals the RPC password |
config path |
- | Print the resolved home directory, config-file, and keystore paths |
config doctor |
- | Probe reachability of the resolved endpoints (read-only; touches the network). -n, --network <n> selects the network |
Manage configuration profiles.
| Subcommand | Alias | Description |
|---|---|---|
profile add <name> |
- | Add an empty profile |
profile use <name> |
- | Set the active profile (writes defaults.profile) |
profile show [name] |
- | Show a profile (defaults to the active profile) |
profile remove <name> |
rm |
Remove a profile |
btcr2 completion [shell] prints a shell completion script (bash, zsh, or fish) to stdout. Defaults to bash. For example: eval "$(btcr2 completion bash)".
# Generate a fresh key (type=k), store it in the keystore, and print the identifier
btcr2 create -n regtest
# Deterministic (type=k): from an explicit compressed secp256k1 public key (33 bytes hex)
btcr2 create -t k -n regtest -b 02aa...
# Deterministic (type=k): from a stored key's public key
btcr2 create -t k -n regtest --signing-key mykey
# External (type=x): from a SHA-256 hash of a genesis document (32 bytes hex)
btcr2 create -t x -n bitcoin -b bb...
# Zero-config: network and endpoints are derived from the DID
btcr2 resolve -i did:btcr2:k1qq...
# Alias: read
btcr2 read -i did:btcr2:k1qq...
# With resolution options as inline JSON
btcr2 resolve -i did:btcr2:k1qq... -r '{"versionId":"1"}'
# With resolution options from a JSON file
btcr2 resolve -i did:btcr2:k1qq... -p resolution-options.json
# JSON output
btcr2 -o json resolve -i did:btcr2:k1qq...
# Signs with the active keystore key (or one chosen via --signing-key)
btcr2 update \
-s "$(cat did.json)" \
--source-version-id 1 \
-p '[{"op":"add","path":"/service/-","value":{"id":"#svc","type":"X","serviceEndpoint":"https://x"}}]' \
-m 'did:btcr2:k1qq...#key-0' \
-b '{"id":"#beacon-0","type":"SingletonBeacon","serviceEndpoint":"bitcoin:bc1..."}'
# Irreversible. Applies the deactivation patch and signs via the keystore.
btcr2 deactivate \
-s "$(cat did.json)" \
--source-version-id 1 \
-m 'did:btcr2:k1qq...#key-0' \
-b '{"id":"#beacon-0","type":"SingletonBeacon","serviceEndpoint":"bitcoin:bc1..."}'
btcr2 key generate --name mykey --set-active
btcr2 key list
btcr2 key use mykey
Override precedence, highest wins: CLI flags, then environment variables, then config file, then network defaults.
| Flag | Description |
|---|---|
-v, --version |
Output the current version |
-o, --output <format> |
Output format: json or text (default: config defaults.output, else text) |
--verbose |
Verbose output |
--quiet |
Suppress non-essential output |
--home <dir> |
btcr2 home directory holding config.json + keystore.json (default: ~/.btcr2, %LOCALAPPDATA%\btcr2 on Windows; overrides $BTCR2_HOME) |
-c, --config <path> |
Path to config file (default: <home>/config.json) |
--profile <name> |
Config profile name (default: auto-detected from network) |
--btc-rest <url> |
Override Bitcoin REST endpoint (Esplora API) |
--btc-rpc-url <url> |
Override Bitcoin Core RPC endpoint |
--btc-rpc-user <user> |
Bitcoin Core RPC username |
--btc-rpc-pass <pass> |
Bitcoin Core RPC password (accepts an env:<VAR> or file:<path> secret reference) |
--btc-rpc-wallet <name> |
Bitcoin Core wallet name for wallet-scoped RPCs (/wallet/<name>) |
--btc-rpc-header <header> |
Extra Bitcoin Core RPC header "Key: Value" (repeatable) |
--btc-rest-header <header> |
Extra Bitcoin REST header "Key: Value" (repeatable), e.g. an API key |
--btc-timeout <ms> |
Bitcoin REST/RPC request timeout in milliseconds (default: unbounded) |
--cas-gateway <url> |
IPFS HTTP gateway for CAS reads (read-only) |
--cas-rpc-url <url> |
IPFS HTTP RPC endpoint for a writable CAS (reads + writes; enables --publish-to-cas) |
--cas-timeout <ms> |
CAS request timeout in milliseconds (default: 30000; 0 disables) |
--keystore <path> |
Path to the keystore file (default: <home>/keystore.json) |
--passphrase-file <path> |
Read the keystore passphrase from a file (unattended use) |
--signing-key <ref> |
Key for create/update/deactivate signing: a URN, fingerprint prefix, or name |
| Variable | Equivalent flag |
|---|---|
BTCR2_BTC_REST |
--btc-rest |
BTCR2_BTC_RPC_URL |
--btc-rpc-url |
BTCR2_BTC_RPC_USER |
--btc-rpc-user |
BTCR2_BTC_RPC_PASS |
--btc-rpc-pass |
BTCR2_BTC_RPC_PASS_FILE |
file whose contents are the RPC password |
BTCR2_CAS_GATEWAY |
--cas-gateway |
BTCR2_CAS_RPC_URL |
--cas-rpc-url |
BTCR2_BTC_TIMEOUT |
--btc-timeout |
BTCR2_CAS_TIMEOUT |
--cas-timeout |
BTCR2_FEE_RATE |
--fee-rate |
BTCR2_OUTPUT |
-o, --output |
BTCR2_HOME |
--home |
BTCR2_KEYSTORE_PASSPHRASE |
keystore passphrase (unattended use) |
BTCR2_KEYSTORE_TTL |
session lifetime for keystore unlock / quickstart --unlock (default 1h, max 24h) |
The CLI keeps its config and keystore side by side in one home directory, resolved as --home <dir>, then $BTCR2_HOME, then the platform default: ~/.btcr2 on Linux/macOS and %LOCALAPPDATA%\btcr2 on Windows. Both files live directly under it (<home>/config.json, <home>/keystore.json); btcr2 config path prints the resolved locations. --config and --keystore still override each file individually, so the historical XDG split can be reproduced explicitly.
Default location: <home>/config.json. A malformed config file fails loudly (the CLI never silently falls back to public endpoints, and never overwrites an unparseable file).
Profiles are matched by network name when --profile is not specified. For example, resolving a regtest DID automatically selects the "regtest" profile. A profile that is not named after a network can declare its network with a network field.
{
"schemaVersion": 1,
"defaults": {
"profile": "production",
"network": "bitcoin",
"output": "text"
},
"profiles": {
"regtest": {
"btc": {
"rest": "http://localhost:3000",
"rpcUrl": "http://localhost:18443",
"rpcUser": "polaruser",
"rpcPass": "polarpass",
"wallet": "primary",
"feeRate": 5,
"timeoutMs": 30000
}
},
"production": {
"network": "bitcoin",
"btc": {
"rest": "https://my-mempool/api",
"headers": { "Authorization": "Bearer <api-key>" },
"feeRate": 20
},
"cas": { "gateway": "https://ipfs.io", "rpcUrl": "http://127.0.0.1:5001", "timeoutMs": 30000 },
"identity": { "keystore": "/secure/prod-keystore.json", "default": "did:btcr2:...#key-0" }
}
}
}
Field notes:
defaults: profile selects the active profile; network fixes the network create encodes when -n is absent; output sets the default output format. schemaVersion is stamped on every write; a file written by a newer CLI is refused.btc: rest/rpcUrl/rpcUser/rpcPass are endpoints and credentials; wallet targets a Bitcoin Core wallet (/wallet/<name>); headers/rpcHeaders add REST/RPC headers; feeRate (sats/vByte), changeAddress, and timeoutMs set broadcast and request behavior. The RPC url, user, and pass are resolved as one atomic unit, so a URL from a higher-precedence layer never inherits credentials from a lower one.cas: gateway is a read-only IPFS HTTP gateway; cas.rpcUrl is a writable IPFS HTTP RPC endpoint (enables --publish-to-cas; rpcUrl wins over gateway); timeoutMs bounds CAS operations (0 disables).identity: keystore points the profile at its own keystore file, and default is the profile’s default signing key. Both fall below the corresponding --keystore / --signing-key flags.Use config validate to check a file, and config effective to see the resolved values with their provenance.
config get and config list redact secret-looking values (RPC password and any pass/secret/token key) by default; pass --show-secrets to reveal them.
An rpcPass written directly into config.json is stored in cleartext (the file is mode 0600 but not encrypted). For anything sensitive, keep the secret out of the file with a reference or an RPC-URL-embedded credential:
"rpcPass": "env:MY_RPC_PASS" reads the password from the MY_RPC_PASS environment variable."rpcPass": "file:/run/secrets/rpc-pass" reads it from a file (a single trailing newline is trimmed).BTCR2_BTC_RPC_PASS_FILE=/run/secrets/rpc-pass names a file to read when no other RPC password source applies.When no overrides are configured:
bitcoin, testnet3, testnet4, and signet; mutinynet.com for mutinynet; http://localhost:3000 for regtesthttp://localhost:18443 for regtest (credentials required), not configured for public networks--cas-rpc-url (or cas.rpcUrl) to publish with --publish-to-casCAS publication is optional and never required. Every update and deactivate can be completed and shared entirely via sidecar: the command always prints the artifacts a resolver needs (the signed update, the transaction id, the CAS announcement for CAS beacons, and the SMT proof for SMT beacons) for you to distribute yourself.
Optionally, the signed update (and, for CAS beacons, the announcement) can be published to a content-addressed store before the on-chain broadcast, so any OP_RETURN update hash is fetchable from CAS at resolution time without sidecar data. This is opt-in via --publish-to-cas:
| Mode | Behavior |
|---|---|
never (default) |
Publish nothing. Distribute the printed artifacts via sidecar. |
auto |
Best-effort. Publish when a writable CAS is configured; otherwise skip publication silently for every beacon type and proceed. Never blocks an update. |
always |
Require a writable CAS; error up-front for every beacon type when none is configured. |
A writable CAS is configured with --cas-rpc-url <url> (an IPFS HTTP RPC endpoint, e.g. a local Kubo node at http://127.0.0.1:5001), the BTCR2_CAS_RPC_URL environment variable, or a profile’s cas.rpcUrl. The default IPFS gateway is read-only, so without a configured --cas-rpc-url, --publish-to-cas auto publishes nothing and completes the update sidecar-only, while --publish-to-cas always errors up-front (naming the fix) for every beacon type.
Privacy: under auto/always, canonical signed updates (and announcements) are published to the configured, possibly public, CAS before the on-chain anchor. Keep never (the default) to distribute update data privately via sidecar.
# Opt into CAS publication against a local IPFS (Kubo) node
btcr2 update \
--cas-rpc-url http://127.0.0.1:5001 \
--publish-to-cas auto \
-s "$(cat did.json)" \
--source-version-id 1 \
-p '[{"op":"add","path":"/service/-","value":{"id":"#svc","type":"X","serviceEndpoint":"https://x"}}]' \
-m 'did:btcr2:k1qq...#key-0' \
-b '{"id":"#beacon-0","type":"SingletonBeacon","serviceEndpoint":"bitcoin:bc1..."}'