Install
openclaw skills install bitkit-cliBitcoin Lightning payment CLI for agents. Lowest LSP fees. Self-custody wallet with LNURL, typed exit codes, JSON envelope output, encrypted messaging, and d...
openclaw skills install bitkit-cliBitcoin Lightning payment CLI for agents. Lowest LSP fees. Self-custody wallet with LNURL/Lightning Address support, typed exit codes, JSON envelope output, encrypted Pubky messaging, and daemon mode.
Install: curl -sSL https://raw.githubusercontent.com/synonymdev/bitkit-cli/main/install.sh | sh
Binary names: bitkit or bk (identical alias)
Always use: --json flag on every invocation for parseable output.
All --json output uses a consistent envelope:
Success (stdout):
{ "ok": true, "data": { ... } }
Error (stderr):
{ "ok": false, "error": "message", "code": 1 }
Parse with: jq -r '.data.field' for success data, check .ok first.
# 1. Create wallet (no encryption for agent use)
bk init --no-password --json
# 2. Start daemon for instant command execution
bk start --json
# 3. Get on-chain address and fund it
ADDRESS=$(bk address --json | jq -r '.data.address')
# 4. Order inbound Lightning liquidity via LSP
ORDER=$(bk lsp create-order 500000 --json)
ORDER_ID=$(echo "$ORDER" | jq -r '.data.order_id')
# 5. Pay the order (on-chain to payment_address), then open channel
bk lsp open-channel "$ORDER_ID" --listen 9735 --json
# 6. Create an invoice and receive payment
bk invoice 5000 --description "agent service" --wait --listen 9735 --json
# 7. Pay someone else's invoice
bk pay lnbc50u1p... --json
# 8. Check balance and history
bk balance --json
bk history --json
# 9. Stop daemon when done
bk stop --json
By default each command cold-starts the LDK node (slow). Start a persistent daemon for instant execution:
bk start --json # start daemon (default port 3457)
bk status --json # check if running
bk balance --json # instant -- proxied through daemon
bk stop --json # stop daemon
When the daemon is running, all commands automatically proxy through its HTTP API. When stopped, commands fall back to per-command cold-start. No code changes needed.
startStart the background daemon. Idempotent -- returns current PID if already running.
bk start --json
bk start --port 8080 --json
{
"ok": true,
"data": {
"status": "started",
"pid": 12345,
"port": 3457
}
}
| Arg | Default | Description |
|---|---|---|
--port <port> | 3457 | HTTP API port |
status is "started" or "already_running".
stopStop the running daemon.
bk stop --json
{
"ok": true,
"data": {
"status": "stopped",
"pid": 12345
}
}
Errors if no daemon is running (exit code 1).
statusCheck daemon status.
bk status --json
{
"ok": true,
"data": {
"running": true,
"pid": 12345,
"port": 3457,
"started_at": "2026-02-19T10:00:00+00:00",
"version": "0.1.0"
}
}
When stopped: running is false, all other fields are null.
| Flag | Env Variable | Default | Description |
|---|---|---|---|
--json | -- | off | Machine-readable JSON to stdout |
--dir <path> | BITKIT_DIR | ~/.bitkit/ | Wallet data directory |
--network <net> | BITKIT_NETWORK | mainnet | mainnet or regtest |
--listen <port> | BITKIT_LISTEN | off | P2P listen port on 0.0.0.0:<port> |
--password <pw> | BITKIT_PASSWORD | -- | Wallet password (for encrypted seeds) |
initCreate a new wallet. Idempotent -- re-running prints existing wallet info.
bk init --no-password --json
{
"ok": true,
"data": {
"node_id": "02abc123...",
"seed_phrase": "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
"wallet_dir": "/root/.bitkit",
"network": "mainnet",
"pubky_id": "8pinxrz9tuxfz3qo5gkhdebuhtq6mrimh3matdncsrsno7kg45mo"
}
}
| Arg | Required | Description |
|---|---|---|
--no-password | one of | Store seed as plaintext (for agents) |
--password <pw> | one of | Encrypt seed with AES-256-GCM + Argon2id |
infoShow node status, channel counts, sync state.
bk info --json
{
"ok": true,
"data": {
"node_id": "02abc123...",
"network": "mainnet",
"channels_active": 1,
"channels_pending": 0,
"block_height": 880000,
"synced": true,
"wallet_dir": "/root/.bitkit"
}
}
balanceShow Lightning and on-chain balances in satoshis.
bk balance --json
{
"ok": true,
"data": {
"lightning_sats": 450000,
"onchain_sats": 50000,
"total_sats": 500000,
"total_onchain_sats": 55000,
"anchor_reserve_sats": 5000,
"pending_sweep_sats": 0,
"pending_sweeps": []
}
}
onchain_sats -- spendable on-chain balancetotal_onchain_sats -- all on-chain funds including reservedanchor_reserve_sats -- sats reserved for anchor channel feespending_sweep_sats -- funds sweeping from closed channels (not yet spendable)pending_sweeps -- array of sweep entries with amount_sats, status, spending_txid, confirmation_height| Arg | Description |
|---|---|
--btc | Display in BTC instead of sats (human output only) |
configShow resolved configuration with source tracking. Does not start the node.
bk config --json
{
"ok": true,
"data": {
"network": { "value": "mainnet", "source": "default" },
"wallet_dir": { "value": "/root/.bitkit", "source": "default" },
"chain_source": { "value": "esplora", "source": "file" },
"esplora_url": { "value": "https://blockstream.info/api", "source": "file" },
"electrum_url": { "value": "", "source": "file" },
"rgs_url": { "value": "https://rapidsync.lightningdevkit.org/snapshot", "source": "file" },
"blocktank_url": { "value": "https://api1.blocktank.to/api", "source": "file" },
"listen_port": { "value": "off", "source": "default" }
}
}
Each entry has value (resolved value) and source (cli, env, file, or default).
addressGet a new on-chain receive address. Generates a fresh address each call. Optionally validate an existing address.
bk address --json
bk address --type taproot --json
bk address --validate bc1q... --json
Generate address:
{
"ok": true,
"data": { "address": "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4", "address_type": "native-segwit" }
}
Validate address (--validate):
{
"ok": true,
"data": { "address": "bc1q...", "valid": true, "network": "mainnet", "reason": null }
}
On invalid address: valid is false and reason explains why.
| Arg | Default | Description |
|---|---|---|
--type <type> | native-segwit | Address type: legacy, nested-segwit, native-segwit, taproot |
--validate <addr> | -- | Validate <addr> for the current network; no node start needed |
--qr | off | Display address as QR code in terminal (human output only) |
sendSend bitcoin on-chain to an address.
bk send bc1q... 50000 --json
bk send bc1q... 50000 --fee-rate 5 --json
bk send bc1q... --drain --json
bk send bc1q... 50000 --utxo <txid>:<vout> --json
{
"ok": true,
"data": {
"txid": "abc123...",
"address": "bc1q...",
"amount_sats": 50000,
"drain": false,
"fee_rate_sat_per_vb": 5,
"utxos_used": ["abc123...:0"]
}
}
When --drain is used, amount_sats is null. When no --fee-rate is set, fee_rate_sat_per_vb is null.
| Arg | Default | Description |
|---|---|---|
<address> | required | Destination Bitcoin address |
<amount_sats> | -- | Amount in satoshis (omit with --drain) |
--drain | off | Send all spendable funds (mutually exclusive with --utxo) |
--fee-rate <sat/vb> | auto | Fee rate in sat/vb |
--utxo <txid:vout> | -- | Spend a specific UTXO (repeatable; incompatible with --drain) |
list-utxosList all spendable UTXOs in the on-chain wallet.
bk list-utxos --json
{
"ok": true,
"data": {
"utxos": [
{
"txid": "abc123...",
"vout": 0,
"value_sats": 1000000,
"outpoint": "abc123...:0"
}
],
"total_sats": 1000000,
"count": 1
}
}
Use outpoint values (e.g. abc123...:0) as --utxo arguments for send and estimate-fee.
estimate-feePreview the fee for an on-chain send without broadcasting.
bk estimate-fee bc1q... 50000 --json
bk estimate-fee bc1q... 50000 --fee-rate 5 --json
{
"ok": true,
"data": {
"fee_sats": 250,
"address": "bc1q...",
"amount_sats": 50000,
"fee_rate_sat_per_vb": 5
}
}
fee_rate_sat_per_vb is null when not specified (node chooses automatically).
| Arg | Default | Description |
|---|---|---|
<address> | required | Destination Bitcoin address |
<amount_sats> | required | Amount to send in satoshis |
--fee-rate <sat/vb> | auto | Override fee rate in sat/vb |
--utxo <txid:vout> | -- | Restrict coin selection to specific UTXOs |
bump-feeReplace an unconfirmed transaction with a higher-fee version (RBF). The original transaction must be in the mempool and RBF-enabled.
bk bump-fee <txid> <fee_rate> --json
{
"ok": true,
"data": {
"original_txid": "abc123...",
"new_txid": "def456...",
"fee_rate_sat_per_vb": 20
}
}
| Arg | Description |
|---|---|
<txid> | TXID of the unconfirmed transaction to replace |
<fee_rate> | New fee rate in sat/vb (must be higher than the original) |
On failure (invalid txid, not in mempool, RBF not signalled): exits with code 1.
cpfpAccelerate a stuck transaction by spending one of its outputs at a higher fee rate (Child-Pays-For-Parent).
bk cpfp <txid> --json
bk cpfp <txid> --fee-rate 30 --json
bk cpfp <txid> --urgent --json
{
"ok": true,
"data": {
"parent_txid": "abc123...",
"child_txid": "def456...",
"fee_rate_sat_per_vb": 30,
"urgent": false
}
}
fee_rate_sat_per_vb is null when neither --fee-rate nor --urgent is set.
| Arg | Default | Description |
|---|---|---|
<txid> | required | TXID of the parent transaction to accelerate |
--fee-rate <sat/vb> | auto | Explicit fee rate for the child transaction |
--urgent | off | Automatically compute a high fee rate to confirm next block |
On failure (invalid txid, no spendable output found): exits with code 1.
historyList recent transactions, newest first.
bk history --limit 5 --type send --json
{
"ok": true,
"data": [
{
"type": "send",
"amount_sat": 5000,
"fee_sat": 3,
"status": "complete",
"timestamp": "2026-02-15 12:00:00",
"description": "coffee"
}
]
}
| Arg | Default | Description |
|---|---|---|
--limit <n> | 20 | Number of entries |
--type <send|recv> | all | Filter by direction |
Status values: complete, pending, failed
Type values: send, recv
invoiceGenerate a BOLT 11 invoice to receive payment.
bk invoice 5000 --description "agent service" --json
{
"ok": true,
"data": {
"bolt11": "lnbc50u1p...",
"payment_hash": "a1b2c3d4e5f6...",
"amount_sat": 5000,
"description": "agent service",
"expires_at": "2026-02-15T14:30:00+00:00"
}
}
| Arg | Default | Description |
|---|---|---|
<amount_sats> | required | Amount in satoshis |
--description <text> | "" | Invoice memo |
--expiry <secs> | 3600 | Invoice expiry in seconds |
--wait | off | Block until payment received |
--timeout <secs> | 300 | Timeout when using --wait |
--qr | off | Display invoice as QR code in terminal (human output only) |
Two-phase output with --wait: First prints the invoice envelope above, then blocks. When payment arrives, prints a second envelope to stdout:
{
"ok": true,
"data": {
"status": "received",
"amount_sat": 5000,
"payment_hash": "a1b2c3d4e5f6..."
}
}
If timeout expires, exits with code 2 (network error). Always use --listen <port> with --wait so the payer can connect.
payPay a BOLT 11 invoice, LNURL, or Lightning Address. Polls until success, failure, or timeout.
bk pay lnbc50u1p... --json
bk pay lnurl1dp68gurn... --json
{
"ok": true,
"data": {
"status": "success",
"amount_sat": 5000,
"fee_sat": 3,
"payment_hash": "a1b2c3d4e5f6...",
"preimage": "1a2b3c4d5e6f..."
}
}
| Arg | Default | Description |
|---|---|---|
<bolt11> | required | BOLT 11 invoice, LNURL, or Lightning Address |
--max-fee <sats> | unlimited | Maximum routing fee in satoshis |
--timeout <secs> | 60 | Payment timeout |
LNURL/Lightning Address support: pay accepts LNURL (lnurl1...) and resolves to a BOLT11 invoice. For LNURL-pay with variable amounts (where min_sendable != max_sendable), the CLI currently requires a fixed-amount LNURL. Lightning Address (user@domain.tld) requires a --amount flag which is not yet implemented — use a BOLT11 invoice instead.
On failure: exits with code 1 ("Payment failed"). On timeout: exits with code 2.
withdrawWithdraw from an LNURL-withdraw endpoint. Creates an invoice and submits it to the LNURL service.
bk withdraw lnurl1dp68gurn... --json
bk withdraw lnurl1dp68gurn... --amount 10000 --json
{
"ok": true,
"data": {
"status": "submitted",
"amount_sat": 10000,
"bolt11": "lnbc100u1p..."
}
}
| Arg | Default | Description |
|---|---|---|
<lnurl> | required | LNURL-withdraw bech32 string |
--amount <sats> | max withdrawable | Amount in satoshis |
open-channelOpen a Lightning channel to a peer. Requires on-chain funds.
bk open-channel 02ab...@127.0.0.1:9735 100000 --json
{
"ok": true,
"data": {
"channel_id": "12345678901234567890",
"peer": "02ab...",
"amount_sats": 100000
}
}
| Arg | Description |
|---|---|
<peer> | Peer in pubkey@host:port format |
<amount_sats> | Channel capacity in satoshis |
--push <sats> | Give the peer initial balance |
close-channelClose a Lightning channel. Get channel_id and peer from list-channels.
bk close-channel 12345678901234567890 02ab... --json
{
"ok": true,
"data": {
"channel_id": "12345678901234567890",
"status": "closing"
}
}
| Arg | Description |
|---|---|
<channel_id> | Channel ID (decimal, from list-channels) |
<peer> | Counterparty node public key |
--force | Force close (use when peer is unresponsive) |
Status values: closing (cooperative), force_closing
list-channelsList all Lightning channels with capacity, balances, and status.
bk list-channels --json
{
"ok": true,
"data": [
{
"channel_id": "12345678901234567890",
"peer": "02ab...",
"capacity_sats": 500000,
"local_balance_msat": 250000000,
"remote_balance_msat": 250000000,
"is_usable": true,
"is_channel_ready": true,
"is_outbound": false
}
]
}
Note: balances are in millisatoshis. Divide by 1000 for satoshis.
Use the Blocktank LSP to get inbound Lightning liquidity without manually finding peers.
lsp infoGet LSP service info: available nodes, channel limits, fee rates.
bk lsp info --json
{
"ok": true,
"data": {
"nodes": [
{
"alias": "blocktank-lsp",
"pubkey": "02abc...",
"connection_strings": ["02abc...@1.2.3.4:9735"]
}
],
"min_channel_size_sats": 20000,
"max_channel_size_sats": 5000000,
"min_expiry_weeks": 2,
"max_expiry_weeks": 52,
"network": "Mainnet",
"fee_rates_fast": 10,
"fee_rates_mid": 5,
"fee_rates_slow": 1
}
}
lsp estimate-feeEstimate the cost of a channel order before creating it.
bk lsp estimate-fee 500000 --json
{
"ok": true,
"data": {
"fee_sats": 500,
"network_fee_sats": 300,
"service_fee_sats": 200
}
}
| Arg | Default | Description |
|---|---|---|
<lsp_balance_sats> | required | Inbound liquidity amount |
--expiry <weeks> | 6 | Channel expiry in weeks |
--client-balance-sats <sats> | 0 | Outbound liquidity (your side) |
lsp create-orderCreate a channel order. Returns payment details.
bk lsp create-order 500000 --json
{
"ok": true,
"data": {
"order_id": "ord-abc123...",
"state": "Created",
"fee_sats": 500,
"lsp_balance_sats": 500000,
"client_balance_sats": 0,
"payment_address": "bc1q...",
"payment_bolt11": "lnbc...",
"order_expires_at": "2026-03-01T00:00:00Z"
}
}
| Arg | Default | Description |
|---|---|---|
<lsp_balance_sats> | required | Inbound liquidity amount |
--expiry <weeks> | 6 | Channel expiry in weeks |
--client-balance-sats <sats> | 0 | Your outbound balance |
--zero-conf | off | Enable zero-conf channel |
After creating, pay to payment_address (on-chain) or payment_bolt11 (Lightning). Then poll get-order until state becomes Paid.
lsp get-orderCheck the status of an order.
bk lsp get-order ord-abc123 --json
{
"ok": true,
"data": {
"order_id": "ord-abc123...",
"state": "Open",
"state2": "Paid",
"fee_sats": 500,
"lsp_balance_sats": 500000,
"client_balance_sats": 0,
"channel_expiry_weeks": 6,
"channel_state": "Opening",
"funding_tx": "abc123def...",
"payment_state": "Paid",
"order_expires_at": "2026-03-01T00:00:00Z",
"created_at": "2026-02-17T00:00:00Z"
}
}
channel_state and funding_tx are null until the channel is being opened.
State enum values for lsp get-order:
| Field | Values | Description |
|---|---|---|
state | Created, Open, Expired, Closed | Legacy order state |
state2 | Created, Paid, Executed, Expired | Current order state (prefer this) |
channel_state | Opening, Open, Closed | Channel lifecycle state |
payment_state | Created, Paid, Refunded, RefundAvailable, Canceled | Payment lifecycle state |
Poll state2 for order progress: Created → Paid (after on-chain payment) → Executed (channel opened).
lsp open-channelTell Blocktank to open a channel to your node. The order must be in Paid state. Requires --listen so Blocktank can connect.
bk lsp open-channel ord-abc123 --listen 9735 --json
{
"ok": true,
"data": {
"order_id": "ord-abc123...",
"state": "Open",
"channel_state": "Opening",
"funding_tx": "abc123def..."
}
}
End-to-end encrypted messaging via the Pubky network. Identity is created during init.
message whoamiPrint your Pubky messaging ID. Does not require network access.
bk message whoami --json
{
"ok": true,
"data": {
"pubky_id": "8pinxrz9tuxfz3qo5gkhdebuhtq6mrimh3matdncsrsno7kg45mo"
}
}
message sendSend an encrypted message to a peer.
bk message send 8pin...xyz '{"type":"invoice","bolt11":"lnbc...","amount_sats":5000,"description":"service"}' --json
{
"ok": true,
"data": {
"message_id": "abc-123-def",
"recipient": "8pin...xyz"
}
}
| Arg | Description |
|---|---|
<pubky> | Recipient's Pubky ID |
<text> | Message content (plain text or JSON string) |
message readRead the full conversation with a peer, sorted by timestamp.
bk message read 8pin...xyz --json
{
"ok": true,
"data": {
"messages": [
{
"sender": "8pin...abc",
"content": "{\"type\":\"invoice\",\"bolt11\":\"lnbc...\",\"amount_sats\":5000,\"description\":\"service\"}",
"timestamp": 1708000000,
"verified": true
}
]
}
}
| Field | Description |
|---|---|
sender | Pubky ID of the message author |
content | Message text (may contain JSON -- parse it) |
timestamp | Unix timestamp in seconds |
verified | true if cryptographic signature verified |
message listenPoll for new messages from a peer. Streams one envelope per line to stdout as messages arrive.
bk message listen 8pin...xyz --interval 3 --timeout 120 --json
Each new message is printed as a single-line envelope:
{"ok":true,"data":{"sender":"8pin...abc","content":"hello","timestamp":1708000001,"verified":true}}
| Arg | Default | Description |
|---|---|---|
<pubky> | required | Peer's Pubky ID |
--interval <secs> | 3 | Poll interval |
--timeout <secs> | 0 (forever) | Stop after this many seconds |
Status messages go to stderr. Only message JSON goes to stdout.
When the daemon is running (bk start), all commands proxy through an HTTP API on localhost:3457 (configurable via --port). The API uses HTTP Basic Auth with auto-generated credentials.
Authentication: HTTP Basic Auth (bitkit:<password>). Password is auto-generated on first bk start and stored in <wallet_dir>/api-password.
Endpoints:
| Method | Path | Description |
|---|---|---|
| GET | /health | Health check (no auth) |
| GET | /balance | Wallet balance |
| GET | /info | Node info |
| GET | /history?limit=20&type=send | Payment history |
| GET | /channels | List channels |
| GET | /utxos | List UTXOs |
| POST | /address | Generate receive address |
| POST | /send | Send on-chain |
| POST | /invoice | Create Lightning invoice |
| POST | /pay | Pay Lightning invoice |
| POST | /estimate-fee | Estimate on-chain fee |
| POST | /bump-fee | RBF fee bump |
| POST | /cpfp | CPFP acceleration |
| POST | /channels/open | Open channel |
| POST | /channels/close | Close channel |
| GET | /events | WebSocket event stream |
All responses use the same JSON envelope as CLI --json output: {"ok": true, "data": {...}}.
POST Request Body Schemas:
POST /address:
{ "address_type": "native-segwit" }
address_type is optional. Values: legacy, nested-segwit, native-segwit, taproot.
POST /send:
{ "address": "bc1q...", "amount_sats": 50000, "drain": false, "fee_rate": 5, "utxos": ["txid:0"] }
amount_sats is required unless drain is true. fee_rate, drain, utxos are optional.
POST /estimate-fee:
{ "address": "bc1q...", "amount_sats": 50000, "fee_rate": 5, "utxos": ["txid:0"] }
fee_rate and utxos are optional.
POST /bump-fee:
{ "txid": "abc123...", "fee_rate": 20 }
Both fields required.
POST /cpfp:
{ "txid": "abc123...", "fee_rate": 30, "urgent": false }
txid is required. fee_rate and urgent are optional (mutually exclusive).
POST /invoice:
{ "amount_sats": 5000, "description": "agent service", "expiry": 3600 }
amount_sats is required. description and expiry are optional.
POST /pay:
{ "bolt11": "lnbc50u1p...", "max_fee": 100, "timeout": 60 }
bolt11 is required. max_fee and timeout are optional.
POST /channels/open:
{ "peer": "02ab...@127.0.0.1:9735", "amount_sats": 100000, "push_sats": 0 }
peer and amount_sats are required. push_sats is optional.
POST /channels/close:
{ "channel_id": "12345678901234567890", "peer": "02ab...", "force": false }
channel_id and peer are required. force is optional (default false).
Configure in ~/.bitkit/<network>/config.toml:
webhook_url = "https://your-agent.example.com/webhook"
webhook_secret = "your-hmac-secret"
Events and Payloads:
Each webhook POST body contains event, data, and timestamp (Unix ms):
payment_received — Lightning payment received:
{
"event": "payment_received",
"data": { "payment_hash": "abc...", "amount_sat": 1000 },
"timestamp": 1708444800000
}
payment_sent — Outbound Lightning payment succeeded:
{
"event": "payment_sent",
"data": { "payment_hash": "abc...", "amount_sat": 5000, "fee_sat": 3 },
"timestamp": 1708444800000
}
payment_failed — Outbound Lightning payment failed:
{
"event": "payment_failed",
"data": { "payment_hash": "abc...", "reason": "RouteNotFound" },
"timestamp": 1708444800000
}
payment_hash and reason are nullable (may be null).
channel_ready — Channel fully open and usable:
{
"event": "channel_ready",
"data": { "channel_id": "4242...", "counterparty_node_id": "02ab..." },
"timestamp": 1708444800000
}
counterparty_node_id is nullable.
channel_closed — Channel closed:
{
"event": "channel_closed",
"data": { "channel_id": "4242...", "counterparty_node_id": "02ab...", "reason": "CooperativeClosure" },
"timestamp": 1708444800000
}
counterparty_node_id and reason are nullable.
Signature: X-Bitkit-Signature: hmac-sha256=<hex> header (HMAC-SHA256 of body using webhook_secret).
Retry: 3 attempts with exponential backoff (1s, 4s, 16s).
Connect to ws://localhost:3457/events with Basic Auth. Receives the same events as webhooks in real-time as JSON text frames.
# Example with websocat
websocat ws://localhost:3457/events -H "Authorization: Basic $(echo -n 'bitkit:PASSWORD' | base64)"
Frame format: Each event is a JSON text frame with the same structure as webhook payloads:
{"event":"payment_received","data":{"payment_hash":"abc...","amount_sat":1000},"timestamp":1708444800000}
Behavior:
{"warning":"missed 3 events"}Config file: ~/.bitkit/<network>/config.toml (created by bk init). All fields are under [node]:
[node]
network = "mainnet" # "mainnet" or "regtest"
chain_source = "esplora" # "esplora" or "electrum"
esplora_url = "https://blockstream.info/api"
electrum_url = "" # used when chain_source = "electrum"
rgs_url = "https://rapidsync.lightningdevkit.org/snapshot"
blocktank_url = "https://api1.blocktank.to/api"
webhook_url = "" # POST target for events
webhook_secret = "" # HMAC-SHA256 signing key
auto_liquidity = false # enable background inbound capacity monitor
auto_liquidity_threshold_sats = 100000 # order channel when inbound < this
auto_liquidity_channel_size_sats = 500000 # channel size to order
| Field | Type | Default | Description |
|---|---|---|---|
network | string | "mainnet" | Bitcoin network |
chain_source | string | "esplora" | Chain sync backend: esplora or electrum |
esplora_url | string | "https://blockstream.info/api" | Esplora API URL |
electrum_url | string | "" | Electrum server URL (regtest: tcp://127.0.0.1:60001) |
rgs_url | string | (LDK default) | Rapid Gossip Sync URL for Lightning routing graph |
blocktank_url | string | "https://api1.blocktank.to/api" | Blocktank LSP API URL |
webhook_url | string | "" | Webhook POST target (empty = disabled) |
webhook_secret | string | "" | HMAC-SHA256 key for X-Bitkit-Signature header |
auto_liquidity | bool | false | Enable background inbound capacity monitor |
auto_liquidity_threshold_sats | u64 | 100000 | Order channel when inbound capacity drops below this |
auto_liquidity_channel_size_sats | u64 | 500000 | Size of auto-ordered channels |
When auto_liquidity = true in config.toml, the daemon monitors inbound channel capacity every 5 minutes. If total inbound capacity drops below auto_liquidity_threshold_sats, it automatically creates a Blocktank channel order for auto_liquidity_channel_size_sats.
Important: The auto-created order still requires on-chain payment to payment_address from the order. The daemon does not pay automatically — an agent or human must fund the order.
Workflow:
auto_liquidity = truebk start --jsonchannel_ready events via webhook or WebSocketbk lsp get-order <id> for the payment_addressThe daemon HTTP API uses HTTP Basic Auth. The password is auto-generated on first bk start.
Location: <wallet_dir>/api-password (e.g., ~/.bitkit/api-password)
Format: 32 random bytes encoded as 64 hex characters
Permissions: File mode 0600 (owner read/write only)
# Read the password
cat ~/.bitkit/api-password
# Use with curl
curl -u "bitkit:$(cat ~/.bitkit/api-password)" http://localhost:3457/balance
# Use with WebSocket
websocat ws://localhost:3457/events -H "Authorization: Basic $(echo -n "bitkit:$(cat ~/.bitkit/api-password)" | base64)"
The password persists across daemon restarts. Delete the file to force regeneration on next bk start.
| Code | Name | Common Causes | Recovery |
|---|---|---|---|
| 0 | Success | -- | -- |
| 1 | User error | Bad input, missing wallet, invalid invoice, expired invoice | Fix input and retry |
| 2 | Network error | Connection failed, sync timeout, payment timeout | Retry after delay |
| 3 | Insufficient funds | Not enough on-chain or Lightning balance | Fund wallet or open channel |
When --json is used and an error occurs, a structured error envelope is written to stderr:
{
"ok": false,
"error": "Invalid invoice: ...",
"code": 1
}
OUTPUT=$(bk pay "$BOLT11" --json 2>/dev/null)
EXIT_CODE=$?
case $EXIT_CODE in
0) echo "Success: $(echo "$OUTPUT" | jq -r '.data.payment_hash')" ;;
1) echo "Bad input -- check the invoice" >&2; exit 1 ;;
2) echo "Network issue -- retrying..." >&2; sleep 5; bk pay "$BOLT11" --json ;;
3) echo "Insufficient funds" >&2; exit 3 ;;
esac
Important: Errors print to stderr (as JSON envelope when --json is used). Success output prints to stdout. Always capture them separately.
Run multiple independent agent wallets by using separate data directories.
export BITKIT_DIR="/tmp/agent-${SESSION_ID}"
bk init --no-password --json
bk address --json
# ... each agent operates independently
Or use the --dir flag:
bk --dir /tmp/agent-alice init --no-password --json
bk --dir /tmp/agent-bob init --no-password --json
Each directory gets its own seed, node identity, Pubky ID, and channel state. Agents sharing a machine can run concurrently with different --dir values.
| Format | Pattern | Example |
|---|---|---|
| Node pubkey | 66-char hex (compressed secp256k1) | 02abc123... |
| Peer address | pubkey@host:port | 02abc...@127.0.0.1:9735 |
| Transaction ID | 64-char hex | 4a5e1e4baab89f3a... |
| Outpoint | txid:vout (decimal vout) | 4a5e1e4b...:0 |
| Channel ID | Decimal u128 | 12345678901234567890 |
| Bitcoin address | Network-validated bech32/base58 | bc1q..., bcrt1q... |
| BOLT11 invoice | lnbc... or lnbcrt... prefix | lnbc50u1p... |
| LNURL | Bech32 with lnurl HRP | lnurl1dp68gurn... |
| Lightning Address | user@domain.tld | agent@pay.bitkit.to |
| Pubky ID | 52-char z-base-32 | 8pinxrz9tux...45mo |
| Fee rate | Positive integer (sat/vb) | 5 |
| Amount | Non-negative integer (satoshis) | 50000 |
All validation errors exit with code 1 and include a descriptive error message.
Agents communicate structured data by sending JSON strings as message content. The convention uses a type field to distinguish message kinds:
Invoice request:
{
"type": "invoice",
"bolt11": "lnbc50u1p...",
"amount_sats": 5000,
"description": "data analysis service"
}
Payment confirmation:
{
"type": "payment_confirmation",
"payment_hash": "a1b2c3d4e5f6...",
"amount_sats": 5000
}
# 1. Get your own Pubky ID to share with other agents
MY_ID=$(bk message whoami --json | jq -r '.data.pubky_id')
# 2. Send a message to another agent
bk message send "$PEER_PUBKY" "Hello, I need data analysis" --json
# 3. Listen for their reply (they'll send an invoice)
bk message listen "$PEER_PUBKY" --timeout 120 --json | while IFS= read -r MSG; do
TYPE=$(echo "$MSG" | jq -r '.data.content' | jq -r '.type // empty' 2>/dev/null)
if [ "$TYPE" = "invoice" ]; then
BOLT11=$(echo "$MSG" | jq -r '.data.content' | jq -r '.bolt11')
echo "Received invoice: $BOLT11"
break
fi
done
# 4. Pay the invoice
bk pay "$BOLT11" --json
Complete flow where Agent A requests a service, Agent B invoices for it, Agent A pays, and Agent B confirms.
#!/usr/bin/env bash
set -euo pipefail
export BITKIT_DIR=/tmp/agent-b
# Initialize and share identity
bk init --no-password --json >/dev/null
MY_ID=$(bk message whoami --json | jq -r '.data.pubky_id')
echo "Agent B Pubky ID: $MY_ID"
# Listen for requests from Agent A
bk message listen "$AGENT_A_PUBKY" --timeout 300 --json | while IFS= read -r MSG; do
CONTENT=$(echo "$MSG" | jq -r '.data.content')
# Create invoice for the requested service
INVOICE_JSON=$(bk invoice 5000 --description "data analysis" --json)
BOLT11=$(echo "$INVOICE_JSON" | jq -r '.data.bolt11')
# Send invoice to Agent A
PAYLOAD=$(jq -n --arg b "$BOLT11" '{type:"invoice",bolt11:$b,amount_sats:5000,description:"data analysis"}')
bk message send "$AGENT_A_PUBKY" "$PAYLOAD" --json
# Wait for payment
RECEIVED=$(bk invoice 5000 --description "data analysis" --wait --listen 9735 --json | tail -1)
HASH=$(echo "$RECEIVED" | jq -r '.data.payment_hash')
# Confirm payment
CONFIRM=$(jq -n --arg h "$HASH" '{type:"payment_confirmation",payment_hash:$h,amount_sats:5000}')
bk message send "$AGENT_A_PUBKY" "$CONFIRM" --json
break
done
#!/usr/bin/env bash
set -euo pipefail
export BITKIT_DIR=/tmp/agent-a
# Initialize
bk init --no-password --json >/dev/null
MY_ID=$(bk message whoami --json | jq -r '.data.pubky_id')
# Request service from Agent B
bk message send "$AGENT_B_PUBKY" "Please analyze dataset X" --json
# Wait for invoice
BOLT11=""
bk message listen "$AGENT_B_PUBKY" --timeout 120 --json | while IFS= read -r MSG; do
TYPE=$(echo "$MSG" | jq -r '.data.content' | jq -r '.type // empty' 2>/dev/null)
if [ "$TYPE" = "invoice" ]; then
BOLT11=$(echo "$MSG" | jq -r '.data.content' | jq -r '.bolt11')
break
fi
done
# Pay the invoice
PAY_RESULT=$(bk pay "$BOLT11" --json)
echo "Payment status: $(echo "$PAY_RESULT" | jq -r '.data.status')"
echo "Fee paid: $(echo "$PAY_RESULT" | jq -r '.data.fee_sat') sats"
# Wait for confirmation
bk message listen "$AGENT_B_PUBKY" --timeout 60 --json | while IFS= read -r MSG; do
TYPE=$(echo "$MSG" | jq -r '.data.content' | jq -r '.type // empty' 2>/dev/null)
if [ "$TYPE" = "payment_confirmation" ]; then
echo "Payment confirmed by Agent B"
break
fi
done
Trust model: Fully self-custodial. The BIP39 seed never leaves the local machine. No telemetry, analytics, or tracking of any kind.
External endpoints contacted:
| Endpoint | Purpose | Data Sent | Configurable |
|---|---|---|---|
| Esplora server | Chain sync (mainnet) | Transaction queries | Yes (esplora_url) |
| Electrum server | Chain sync (regtest) | Transaction queries | Yes (electrum_url) |
| RGS server | Lightning gossip sync | None (download only) | Yes (rgs_url) |
| Blocktank API | LSP channel orders | Node ID, order params | Yes (blocktank_url) |
| LNURL service | Pay/withdraw resolution | Amount, callback URL | User-initiated only |
| Pubky DHT | Encrypted messaging | E2E encrypted payloads | Built-in |
| Webhook URL | Event notifications | Payment/channel events | Yes (webhook_url) |
Seed storage: Custom binary format with magic bytes (BKIT), version byte, and encrypted (AES-256-GCM + Argon2id KDF) or plaintext flag. File: ~/.bitkit/<network>/seed.enc.
API password: Auto-generated 32-byte random hex, stored with 0600 permissions. Only used for local daemon HTTP API access.
From a brand new wallet to being able to send and receive Lightning payments:
# 1. Create wallet
bk init --no-password --json
# 2. Get on-chain address
ADDRESS=$(bk address --json | jq -r '.data.address')
echo "Fund this address: $ADDRESS"
# 3. (Fund the address externally, wait for confirmation)
# 4. Estimate channel fees
bk lsp estimate-fee 500000 --json
# 5. Create channel order
ORDER_ID=$(bk lsp create-order 500000 --json | jq -r '.data.order_id')
# 6. Pay the order on-chain (the payment_address from create-order)
# ... or wait for on-chain funding to cover it
# 7. Poll until order is paid
while true; do
STATE=$(bk lsp get-order "$ORDER_ID" --json | jq -r '.data.state2')
[ "$STATE" = "Paid" ] && break
sleep 10
done
# 8. Open the channel
bk lsp open-channel "$ORDER_ID" --listen 9735 --json
# 9. Verify channel is active
bk list-channels --json | jq '.data[0].is_usable'
# Now ready to send and receive Lightning payments
bitcoin, lightning, payment, invoice, self-custody, wallet, CLI, agent, LNURL, lightning-address, BOLT11, LSP, lowest-fees, daemon, webhooks, websocket, encrypted-messaging, on-chain, channels, liquidity