Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

JSON-RPC Extensions

satd preserves Bitcoin Core's JSON-RPC contract by default: the same method names, response field names, and types, so existing clients work unchanged. On top of that, satd adds opt-in extensions for developers and integrators. Each extension is either enabled by a server flag (and is therefore live-reloadable over SIGHUP) or exposed as an additional method or parameter. None of them alters the default Core-compatible wire shape. All are governed by the stability policy. The authoritative catalogue of where satd differs from Core is CORE_DIFFERENCES.md.

For the push-based event firehose and cursor-resumable watch subscriptions (gRPC, WebSocket, SSE, ZMQ), see the Streaming Consumption API chapter. That is a distinct surface from the extensions described here.

Note. JSON-RPC keeps Bitcoin Core's cookie / rpcuser / rpcauth credentials by default. Capability-scoped bearer tokens (-rpcauthbearer, rpc:read / rpc:submit / rpc:write) are an opt-in addition. See Authentication & Authorization.

Satoshis-as-integers

Bitcoin Core emits every amount as an IEEE-754 double in whole BTC (0.00001000), which loses precision near dust and at the supply boundary. This is Core's long-standing #3249, open since 2013. satd can instead emit exact integer satoshis.

This is a server-wide default, --rpc-default-units=sats|btc (rpcdefaultunits as a config key), not a per-request flag. The default is btc, where output is byte-identical to Core: a fixed 8-decimal number, formatted from the integer satoshi value so it is exact. Set it to sats and amounts serialize as JSON integers everywhere. In that mode responses also carry a _units: "sats" tag so a client can confirm the shape it received. The tag is absent in the default btc mode, which stays byte-for-byte compatible. The option is live-reloadable. A per-request HTTP-header override is a planned follow-up.

Structured RPC errors

By default, error responses are byte-identical to Core's {code, message}. --rpc-extended-errors (rpcextendederrors; default off, live-reloadable) is a server-wide option. With it enabled, satd additionally populates the JSON-RPC data object with machine-actionable fields:

  • category: a stable taxonomy string, for example mempool.policy.feerate, validation.consensus, storage.not_found.
  • suggestion: a concrete remediation hint, when one applies.
  • debug: arbitrary structured detail (field positions, computed values), when present.

Category names are stable once shipped in a release: new names can be added, and existing ones never change meaning. As with the units default, this is a server-wide option, since the common deployment pattern is satd driven only by satd-aware tooling. A per-request X-Satd-Extended-Errors header is a planned follow-up.

Fee estimation

Core's estimatesmartfee conf_target [estimate_mode] is kept with its exact response shape ({feerate, blocks, errors}) and is Core-compatible by default. The optional mode argument accepts Core's economical / conservative / unset vocabulary, all treated as the historical estimator. It also accepts satd's own historical / mempool / blend values.

satd also adds an estimatefees [targets] [mode] method (default mode blend, default targets [1, 3, 6, 12, 24]). It simulates the next N block templates from the current mempool, with ancestor-feerate (CPFP-aware) package sorting. It never hard-errors; it always returns a result. The response maps each target to a {feerate, confidence} pair, where confidence is high | medium | low, and includes a feerate histogram. This is the basis for Core's #11500.

Mempool subscription stream

subscribemempool is a JSON-RPC WebSocket subscription, paired with unsubscribemempool, that emits structured lifecycle events. Each event is tagged by a kind field:

  • enter: a transaction was admitted to the mempool.
  • leave_confirmed: it was confirmed in a block.
  • leave_evicted: it was dropped, with an explicit reason (full_pool | expiry | block_conflict | policy | reorg).
  • leave_replaced: it was RBF-replaced, carrying the replacing_txid.

Bitcoin Core requires polling getrawmempool or rebuilding this state from per-tx ZMQ frames. This stream carries explicit eviction reasons and RBF replacement linkage directly. For the richer firehose with cursor replay, see the Streaming Consumption API; subscribemempool is the lightweight JSON-RPC option.

Bounded mempool aggregate

getmempoolsummary ( top_n ) returns the view a dashboard renders — a vsize histogram over the whole acting mempool, plus the top_n transactions by ancestor feerate (descending, ties broken by smaller vsize).

{
  "size": 71594,
  "bytes": 117777467,
  "vsize_histogram": [
    { "min_vsize": 0, "max_vsize": 100, "count": 4127 },
    { "min_vsize": 50000, "max_vsize": null, "count": 6 }
  ],
  "top": [
    { "txid": "…", "vsize": 141, "time": 1789255111,
      "ancestorcount": 1, "ancestorsize": 141, "ancestorfees": 282,
      "descendantcount": 1 }
  ]
}

The reply is bounded by top_n (default 50, ceiling 1000; above the ceiling is an -8 error rather than a silent clamp) instead of by mempool size. The same view built from getrawmempool verbose costs a reply of tens of MiB and seconds of node CPU at a mainnet-sized mempool, essentially all of it discarded by a caller drawing an aggregate — the per-entry depends and spentby arrays alone are most of that payload.

size and bytes match getmempoolinfo. Row field names and semantics match the getrawmempool verbose entry they derive from: ancestorcount and descendantcount include the transaction itself, ancestorsize and ancestorfees include its own vsize and fee. Bucket edges are half-open [min_vsize, max_vsize) and travel with the counts, so a client labels its axis from the response rather than hardcoding edges; the top bucket's max_vsize is null. The method is read-only.

UTXO age distribution

gettxoutsetinfo adds utxo_age_distribution, the UTXO set counted by age in blocks relative to the reply's height. On a regtest chain of 206 blocks, each with one spendable coinbase output:

"utxo_age_distribution": {
  "labels": ["<1h", "<1d", "<1w", "<1mo", "<6mo", "<1y", "<3y", "3y+"],
  "counts": [6, 138, 62, 0, 0, 0, 0, 0],
  "exact": true
}

The bucket edges are 6, 144, 1008, 4320, 25920, 51840 and 155520 blocks. The counts sum to txouts. When exact is true the first four buckets are exact counts. The last four are estimated from a per-1000-block histogram, to within one 1000-block chunk at each edge. exact is false while the node is still building its per-height window, a one-time background scan of the coins after upgrading an existing datadir. Until then the first four buckets are estimated the same way.

Silent-payment block data

getsilentpaymentblockdata "blockhash" ( verbosity dust_limit ) returns the public BIP 352 tweak data for one block, from the tweak index (-silentpaymentindex=1, default off). It is the JSON-RPC fallback for the streaming tweaks category — the same bytes, for scripts, the reference-implementation differential, and integrators not yet on an SDK.

  • verbosity 0 (default) → { "block_hash", "height", "tweaks": ["<33-byte hex>", …] }.
  • verbosity 1 → each entry becomes { "txid", "tweak", "max_value" }.
  • dust_limit (sats, default 0) drops entries whose largest taproot output value is below the floor.

Errors: -5 for an unknown or non-active block, -8 when the index is disabled, and -1 when the block is not yet indexed at that height (the row is absent — a height-by-height scanner cannot proceed past a gap, but unlike BIP 157 it cannot silently miss its own outputs either). The method is read-only. A light client runs one ECDH per returned tweak locally, so the scan key never reaches the node; for the streaming firehose with cursor replay, and the integrator guide to every silent-payment consumption mode, see Silent Payments (BIP 352).

OP_RETURN-pruned blocks

On a node running with pruneopreturn (see OP_RETURN data pruning), old blocks can hold transactions whose large OP_RETURN payloads were stripped from disk. The RPCs read such a block through its strip record:

  • getblock at verbosity 1 is unchanged, txids and sizes included, and adds "opreturn_stripped": {"transactions": k, "outputs": m, "bytes": b} on a stripped block. At verbosity 0 a stripped block is refused with Core's -1 Block not available (pruned data).
  • A stripped transaction, in getblock 2 and 3 and verbose getrawtransaction, has its real txid and hash, no hex, and "opreturn_stripped": true. Each stripped output's scriptPubKey reads as the bare OP_RETURN it keeps (asm OP_RETURN, hex 6a, type nulldata), plus "stripped_size", the original length. Fees and prevouts come from undo data and are unchanged.
  • getrawtransaction hex, and gettxspendingprevout's spendingtx, refuse a stripped transaction with -1 Transaction data not available (OP_RETURN data stripped).
  • gettxoutproof, verifytxoutproof, getblockstats and verifychain work as on an intact block.
  • A reorg or invalidateblock that would disconnect a stripped block fails naming it; invalidateblock checks first and changes nothing (-8).
  • getblockfrompeer restores a stripped block in place.

getopreturnpruneinfo (no arguments) reports the feature:

{
  "enabled": true, "depth": 4320, "min_size": 4096,
  "examined_below_height": 912345, "examine_target_height": 912345,
  "blocks": 123, "transactions": 140, "outputs": 141,
  "bytes_zeroed": 9876543, "bytes_freed_estimate": 9400000,
  "peers": "limited", "worker": "running",
  "filesystem_block_size": 4096, "probe": "ok",
  "blocks_dir_apparent_bytes": 812345678901, "blocks_dir_allocated_bytes": 812336278901
}

Under restoreopreturns the response also carries restore, the download's counts, and worker is one of restoring, waiting_for_peers, waiting_for_disk, waiting_for_tip or restored:

"restore": {
  "total_blocks": 123, "remaining_blocks": 40, "restored_blocks": 83,
  "bytes_downloaded": 96000000, "remaining_bytes": 47000000,
  "in_flight": 32, "peers": 2,
  "progress_ratio": 0.675, "estimated_remaining_seconds": 240
}

progress_ratio is over the current pass. estimated_remaining_seconds is null until the first block is restored, then the blocks left at the block rate so far: a rough guide, since block sizes change along the chain. Once the last block is restored, restore keeps the last pass's counts until the node finishes.

getblockchaininfo carries the first nine of these, worker and restore under opreturn_prune, only on a node that strips or still holds stripped blocks.

Node attestation (NAP)

createnapattestation, submitnapattestation, listnapattestations and removenapattestation publish coin-weighted statements signed by holders, with -nap=1, and listnapaggregate lists those collected from publishers, with -napaggregator=1. getnapreport reports the weight behind each value of each key in the collected set, and replaynapsnapshot the same for a snapshot that exportnapsnapshot wrote. They have a chapter of their own: Node Attestation (NAP).

BIP 322 message verification

verifybip322 "address" "signature" "message" verifies a BIP 322 generic signed message: a proof that whoever can spend address signed message. Unlike verifymessage, which is Core's and covers P2PKH only, it works for any address type, multisig and Taproot script paths included.

sat-cli verifybip322 "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l" "smpAkcwRAIgZRfIY…" "Hello World"
{ "result": "valid", "format": "simple", "version": 0, "locktime": 0, "sequence": 0 }
  • Signature formats. smp… (simple: a witness stack) and ful… (full: the signed to_sign transaction) as the BIP defines them. An unprefixed string, which most wallets made before the BIP was finalized in 2026 still emit, is read as Core's legacy format for a P2PKH address and as simple for anything else. Proof-of-funds (pof…) is refused with -8.
  • result is valid, invalid, or inconclusive: the proof is valid only under rules BIP 322 treats as upgradeable, such as an unknown witness version, an OP_SUCCESS leaf or a reserved NOP. Not valid comes with a reason. A signature that does not decode is invalid, not an error.
  • version, locktime, sequence are the BIP's "valid at time T and age S": a full proof for a timelocked script sets to_sign's locktime or input sequence, and the proof holds only from that time or age. verifybip322 reports them and does not judge them against the chain. A legacy result has none.
  • Rules. Verification runs satd's script interpreter under the BIP's required rules (SIGHASH_ALL only, or SIGHASH_DEFAULT for Taproot; no OP_CODESEPARATOR; LOW_S, STRICTENC, NULLFAIL, MINIMALDATA, CLEANSTACK and MINIMALIF, all for every script type), then its upgradeable ones. It is checked against the BIP's published test vectors and btcd's negative vectors.

Errors: -5 for an address that is not valid on this network. The method is read-only and needs no chain state.

Stratum

getstratuminfo reports the Stratum mining server. It takes no arguments, is read-only, and answers on a node with the server off too — with enabled: false, null listeners and zero counters — so a monitor can poll it unconditionally.

{
  "enabled": true,
  "listeners": { "v1": "127.0.0.1:3333", "v1_tls": null, "v2": "0.0.0.0:3336" },
  "authority_pubkey": "<32-byte x-only key, hex>",
  "job_declaration": false,
  "connections": 2,
  "channels": 3,
  "current_job": {
    "height": 912345,
    "job_id": "1a",
    "prev_hash": "<hex>",
    "template_txs": 3210,
    "template_fees": 12345678
  },
  "shares": { "accepted": 412, "rejected": 3, "stale": 1 },
  "blocks_found": 0,
  "last_block": null,
  "hashrate": 1210000000000.0,
  "miners": [
    {
      "protocol": "v1",
      "peer": "192.0.2.10:51234",
      "channel_id": null,
      "address": "bc1q…",
      "worker": "rig1",
      "device": "<user agent the firmware sent>",
      "difficulty": 10000,
      "connected_time": 1760000000,
      "shares": { "accepted": 208, "rejected": 1, "stale": 0 },
      "best_share_difficulty": 4812337.6,
      "last_share_time": 1760006280,
      "hashrate": 1210000000000.0
    }
  ]
}
  • listeners are the bound addresses, with the real port when a bind used :0. authority_pubkey is null without a Stratum V2 listener.
  • channels counts authorized Stratum V1 connections and open Stratum V2 channels.
  • current_job is the work being handed out, or null while none is (during initial block download). job_id identifies the work within this server; the job ids a miner sees are per connection.
  • shares count since startup. stale is a share for a job that is no longer current; rejected is every other refusal.
  • blocks_found counts blocks found through the server that joined the active chain; last_block is { "height", "hash", "time" } for the most recent, or null.
  • miners lists every miner connected now, oldest first: an authorized Stratum V1 connection, or a Stratum V2 channel (channel_id set). A miner leaves the list when it disconnects.
    • address is the payout address, or null when --stratumaddress pays; worker is the part of the username after the first ., reduced to printable ASCII and at most 64 characters.
    • device is the Stratum V1 user agent, or the Stratum V2 vendor, hardware version and firmware, reduced to printable ASCII; null if the miner sent none.
    • difficulty is the share difficulty the miner is set to now. connected_time and last_share_time (null before the first accepted share) are Unix times.
    • shares count this miner's shares, as the node-wide shares do. best_share_difficulty is the highest difficulty an accepted share's header achieved.
    • hashrate is an estimate in hashes per second, from the difficulty of the shares accepted over the last ten minutes (see Verifying a miner).
  • The top-level hashrate is the sum over miners.

PSBT version 2 (BIP 370) and BIP 375

Every PSBT method — decodepsbt, analyzepsbt, combinepsbt, joinpsbts, utxoupdatepsbt, finalizepsbt — accepts a version 2 PSBT and answers with a version 2 PSBT. Bitcoin Core does not: it rejects any PSBT whose PSBT_GLOBAL_VERSION is above zero. Version 0 behaviour is unchanged, so nothing you already do moves.

Version 2 matters because BIP 375, which is how a wallet sends to a silent payment address through a PSBT, is version 2 only. Its fields are carried losslessly: anything satd does not understand comes back with the same bytes in the same place, which is what lets a PSBT pass through this node on its way between two wallets that do understand it.

BIP 375 is a draft. satd is refereed against the vectors the BIP publishes, and its field set has not changed since 0.1.0, but a draft can still change.

What decodepsbt returns

A version 2 PSBT decodes to a different shape from a version 0 one. Field names follow Bitcoin Core's own version 2 pull request wherever it has one.

{
  "psbt_version": 2,
  "tx_version": 2,
  "fallback_locktime": 0,
  "locktime": 0,
  "input_count": 2,
  "output_count": 2,
  "inputs_modifiable": false,
  "outputs_modifiable": false,
  "has_sighash_single": false,
  "silent_payments": {                 // only when a BIP 375 global field is present
    "global_shares": [
      { "scan_key": "02…", "ecdh_share": "03…", "dleq_proof": "…" }
    ]
  },
  "unknown": { },                      // global pairs satd does not recognise
  "inputs": [
    {
      "previous_txid": "…",
      "previous_vout": 0,
      "sequence": 4294967293,
      "witness_utxo": { "amount": 0.001, "scriptPubKey": { "hex": "0014…" } },
      "has_utxo": true,
      "is_final": false,
      "sp_shares": [
        { "scan_key": "02…", "ecdh_share": "03…", "dleq_proof": "…" }
      ],
      "unknown": { }
    }
  ],
  "outputs": [
    {
      "amount": 0.0009,
      "script": { "hex": "5120…" },    // absent while it has not been computed
      "silent_payment": { "scan_key": "02…", "spend_key": "03…", "label": 3 },
      "unknown": { }
    }
  ],
  "tx": { }, "tx_hex": "…",            // only when every output has a script
  "unique_id": "…",                    // BIP 370's identifier for this PSBT
  "fee": 0.0001                        // as in version 0: null unless every input's UTXO is known
}

An output's script is absent, not empty, while a silent payment script is still being computed. The two mean different things, and a PSBT library that renders the second as the first describes an output paying nobody. For the same reason tx and tx_hex appear only once every output has a script.

amount honours the per-request amounts=sats|btc switch, as elsewhere. A share whose DLEQ proof is missing keeps its row with a null dleq_proof rather than disappearing: a missing proof is the thing a reader most needs to see.

Creating one: createpsbt psbt_version=2

createpsbt takes a trailing optional psbt_version. It defaults to 0, so every existing call is unaffected, and it sits after Core's own replaceable and version arguments so a positional call written against Core keeps meaning what it meant.

sat-cli createpsbt \
  '[{"txid":"…","vout":0}]' \
  '[{"tsp1q…":0.25},{"bcrt1q…":0.2499}]' \
  0 null null 2

An sp1… (or tsp1…) output key is only accepted with psbt_version=2. Without it the node refuses by name — silent payment recipients need a version 2 PSBT; pass psbt_version=2 — rather than building a PSBT that cannot carry the recipient, so a caller whose signer speaks only version 0 learns at creation instead of at signing. createrawtransaction refuses an sp1… key outright and says why: a raw transaction has nowhere to put the information a Signer needs.

Two details worth knowing.

  • Use the array form of outputs when order matters. A JSON object's key order is the serialiser's business, and the output order decides both the transaction and each silent payment output's k.
  • A repeated sp1… key is legal, unlike a repeated ordinary address. Two payments to one recipient get k = 0 and k = 1, and each keeps its own amount.

The PSBT comes back with PSBT_GLOBAL_TX_MODIFIABLE saying both inputs and outputs may still be added, and the silent payment output carrying its two keys and no script. Computing that script is the Signer's job, because computing it is what freezes the transaction.

decodepsbt and analyzepsbt spell the recipient back as the sp1… address it came from. The PSBT itself carries only the two keys, so this is the only way an operator can check the recipient against what they were given.

Where satd is stricter than Core's version 0 rules

  • combinepsbt refuses a conflict. Where two PSBTs give the same key different values, satd refuses and names the field. Core keeps whichever it saw first. For a BIP 375 ECDH share that value decides where the money goes.
  • joinpsbts refuses a committed silent payment PSBT. Joining adds inputs, and every silent payment output script derives from the input set. A PSBT whose scripts are already computed, whose global ECDH share is set, or which does not allow inputs and outputs to be added is refused by name.
  • finalizepsbt is the Transaction Extractor. It recomputes every silent payment output script and refuses unless each one verifies and matches. There is no override flag, and extract=false is gated too.

analyzepsbt reports a silent_payments object with a per-output verdict; see Sending in the Silent Payments chapter for the statuses and what satd checks to reach them. A verified: false object carries a reason and means satd could not run the checks — it never means they passed.

Client-side PSBT signing (no signing RPC)

There is no signing method: satd never handles private keys. Signing is a client-side sat-cli command.

sat-cli signpsbtwithkey reads a WIF private key or a BIP-32 xpriv from stdin, prompting without echo when stdin is a terminal. It signs the PSBT entirely locally, using only the prevout data already carried in the PSBT. It covers the common single-sig script types (Legacy, SegWit v0, nested SegWit, and Taproot key-path) and writes partial_sigs / tap_key_sig for the node's finalizepsbt to assemble, rather than finalizing itself. An xpriv is expanded over the standard BIP 44/49/84/86 paths, so it can sign PSBTs that carry no derivation metadata, including satd's own createpsbt output. The key never crosses the JSON-RPC boundary, so satd stays strictly keyless.

A version 2 PSBT is signed too, and signing one means doing BIP 375's Signer work first. See Sending in the Silent Payments chapter.

sat-cli signpsbtwithsigner passes a version 2 PSBT to an external signer untouched, because for a silent payment the only party that can compute an ECDH share is the one holding the input's private key — a device is exactly who should be asked, and sat-cli cannot know which devices have learned BIP 375. What comes back is checked with the same code the node runs: a reply describing a different transaction, one whose silent payment script is not what its own shares derive to, or one carrying a signature while an output still has no script, is refused rather than emitted. A signer that simply does not understand the PSBT has its own message relayed, with one line saying the device must support BIP 375.