Compatibility boundary
zecd targets generic Bitcoin-RPC compatibility, not bug-for-bug bitcoind emulation. This page defines what that boundary covers and the edges where a shielded-first light wallet necessarily behaves differently from bitcoind. Intentional per-method divergences are in the method index.
What compatibility means
Any integration that drives a coin purely through Bitcoin Core RPC works: request a deposit
address with getnewaddress, hand it to the payer, poll listtransactions /
gettransaction / getbalance for the payment and its confirmations. Method names, response
field names and types, the JSON-RPC 1.0 envelope, HTTP Basic/cookie auth, decimal 8-place
amounts, and error codes all match Bitcoin Core (see conventions and wire
format). The conformance suite drives a live daemon with the same client logic
python-bitcoinrpc uses, so an unmodified AuthServiceProxy client works out of the box (see
testing and conformance).
Edges
Behaviors an integrator should design around. Each follows from being a shielded-first light wallet.
Spending needs confirmations
An incoming mempool payment is visible immediately: getunconfirmedbalance,
listtransactions, and listunspent with minconf=0 all show it at 0 confirmations, fed by
zecd's getrawmempool poller. But a received note must mine and reach the confirmation
minimum before it is spendable. The default policy is ZIP
315's: 3 confirmations for the wallet's own change, 10 for
third-party payments (roughly 12.5 minutes at 75-second blocks). [spend] trusted_confirmations / untrusted_confirmations tune it wallet-wide (see
configuration).
A parameterless getbalance reports what is spendable under that policy; funds below the
threshold show in getunconfirmedbalance and getbalances.mine.untrusted_pending meanwhile.
An explicit minconf (getbalance "*" 1) overrides the policy symmetrically and counts
everything at that depth, as in Bitcoin Core. minconf 0 is served as 1: a shielded note is
never spendable unmined. See wallet balances.
Fees are never client-settable
Fees follow ZIP 317: a deterministic formula (5,000 zatoshis
times max(2, logical actions); a typical send pays 0.0001 ZEC) computed at build time. There
is no fee market to outbid, so client fee instructions are meaningless. zecd rejects them
with -8 rather than silently ignoring them:
subtractfeefromamount(sendtoaddress) andsubtractfeefrom(sendmany): would change who pays the fee.fee_rateonsendtoaddress/sendmany: an explicit fee instruction.settxfee: always-8.
Estimation hints are safely ignored: conf_target and estimate_mode on sends, and
maxfeerate on sendrawtransaction (the conventional fee already buys next-block
inclusion). estimatesmartfee/estimatefee remain as inert probe-compat stubs returning a
stable conventional rate (feerate 0.00001). The exact fee actually paid is reported after
the fact in gettransaction.fee. See sending and utility and
control.
Addresses are Unified Addresses
getnewaddress returns a shielded Unified Address (u1... on mainnet, utest1... on
testnet). Clients that treat addresses as opaque strings are fine; clients that parse the
address as a transparent Bitcoin address (base58 checks, script construction) are not.
validateaddress validates every Zcash address kind and reports what a given address can
receive via its receiver_types array. See addresses and shielded
pools.
Sends that leave a single shielded pool reveal information
A transparent recipient reveals the recipient and the amount on-chain; crossing the
Sapling to Orchard turnstile (spending one pool, paying the other) reveals the crossed amount
via valueBalance. Both are permitted under the default policy, AllowRevealedRecipients.
The [spend] privacy_policy setting (and z_sendmany's per-call privacyPolicy) is a
four-rung ladder that lets you forbid either leak, or additionally opt in to fully
transparent spends. See privacy policy for the rungs and where each is
enforced.
Memos are extensions
Shielded memos (ZIP 302) sit beyond Bitcoin Core's surface, so zecd exposes them as extensions that dialect-pure clients never trip over:
sendtoaddresstakes a hex-encoded memo as an extra trailing parameter, afterverbose. At most 512 bytes; non-hex or oversized memos are-8(zcashd's messages); a memo paired with a transparent recipient is-8.- History entries (
listtransactions,gettransaction.details,z_listtransactions) carrymemo(hex) andmemoStr(decoded text) fields when an output has one; entries without a memo omit the fields entirely. z_sendmanypermits a zero-valued output, zcashd's memo-only-send pattern (a shielded recipient,amount: 0, and amemo). The Bitcoin-Core-dialectsendtoaddress/sendmanykeep rejecting a zero amount with-3 Invalid amount, as Core does.
See sending and async operations.
Partial reads during initial sync
During initial sync or a post-restore rescan, read RPCs serve whatever has been scanned so
far: getbalance on a half-synced wallet is a partial number, not an error. (Bitcoin Core
rejects every RPC with a warm-up error, -28, while it loads at startup.) Gate automation on
GET /readyz with [health] readiness = "synced", or on getwalletinfo.scanning / getblockchaininfo.initialblockdownload,
before trusting balances.
These signals stay busy until the wallet can serve full history, not just until the block
scan reaches the tip. Compact blocks carry no memos, so after the scan catches up a
per-transaction enhancement pass fetches each transaction's full data from Zebra to backfill
memos; on a from-birthday restore that backlog can take hours after scan_progress hits 1.0.
The backlog is surfaced as pending_enhancements on GET /status, scanning and
initialblockdownload stay truthy, and "synced" readiness holds /readyz at 503 with
reason="enhancing" until it drains to zero. See the operations
runbook.
sendmany collapses duplicate recipients
sendmany recipients arrive as a JSON object, and JSON parsing collapses duplicate keys
(last one wins) before zecd sees them, so Bitcoin Core's -8 Invalid parameter, duplicated address cannot be reproduced. Do not list the same address twice; combine the amounts into
one entry. z_sendmany takes an array of recipient objects instead, so it does detect and
reject duplicates with -8.
listsinceblock cursors do not survive reorgs
zecd keeps only the current chain's scanned block hashes (a light wallet has no stale-header
index), so if a listsinceblock cursor block is reorged away, or is below the wallet
birthday, listsinceblock <hash> returns -5 Block not found. Bitcoin Core instead walks
back to the common ancestor and includes transactions from the fork point onward. Treat -5
as "cursor invalid": re-baseline with a parameterless listsinceblock and dedupe by txid
(idempotent payment processing is required for reorg safety anyway). See wallet
history.