Skip to main content

bitcoin-core-rpc

A standalone JSON-RPC client against Bitcoin Core.

PyPI version downloads development status license: MIT supported Python versions

test workflow status lint workflow status pre-commit.ci status documentation build

GitHub repository: btclib-org/bitcoin-core-rpc slack: btclib_dev

One source file with nothing but the standard library behind it, fully annotated and shipping py.typed. BitcoinCoreRpcClient invokes any one rpc method a node has, with positional or named parameters: one HTTP POST per call, basic authentication, the result or an exception.

Install it, or copy the file — Vendoring below is how, and it is a supported way to use this rather than a fallback.

pip install bitcoin-core-rpc

Talking to a node

from_chain is the local node of one of Core's chains: the loopback url, the port and the cookie file all come from Core's own defaults, so a node started with none of them overridden needs no arguments at all.

from bitcoin_core_rpc import BitcoinCoreRpcClient

client = BitcoinCoreRpcClient.from_chain("main")
print(client.call("getblockcount"))
print(client.call("getblockchaininfo")["chain"])

The credential is the cookie file bitcoind rewrites at every start, read at each call rather than held: a client built when the node was up still works an hour and a restart later. Where the datadir is somewhere else — macOS and Windows put it outside ~/.bitcoin — say so:

from pathlib import Path

client = BitcoinCoreRpcClient(
    "http://127.0.0.1:8332",
    cookie_path=Path.home() / "Library/Application Support/Bitcoin/.cookie",
)

rpcuser and rpcpassword are the other way. They are arguments and never part of the url: a url with user:password@ in it is refused, because that string ends up in configuration files, tracebacks and logs.

client = BitcoinCoreRpcClient(
    "http://127.0.0.1:8332", user="rpcuser", password="rpcpassword"
)

Calling

params is one value, shaped as JSON-RPC shapes it: a sequence for the positional form, a mapping for the named one. Core takes both, and which one a method wants is the method's business.

block_id = client.call("getblockhash", [700_000])
block = client.call("getblock", {"blockhash": block_id, "verbosity": 2})

Amounts do not travel as binary floating point in either direction: a number in the reply decodes as a Decimal, and a Decimal parameter is refused rather than rounded through float.

balance = client.for_wallet("hot").call("getbalance")   # Decimal, exact

for_wallet is the /wallet/<name> endpoint of a node with several wallets loaded, with the name percent-encoded — a wallet is a directory and may be called anything a filesystem accepts.

When it goes wrong

Three exceptions, because there are three different things to do about them. All three are FetchError, so one except FetchError covers the lot.

exception what happened what it carries
RpcError the node computed an error code, data
HttpError the exchange failed status
FetchError there was no answer to read
from bitcoin_core_rpc import FetchError, HttpError, RpcError

try:
    raw = client.call("getrawtransaction", [tx_id])
except RpcError as e:
    if e.code == -5:        # no such transaction; a node without -txindex
        ...                 # answers this for anything outside its wallet
except HttpError as e:
    if e.status == 503:     # the rpc work queue is full: try again later
        ...                 # a 401 never works again, so tell the two apart
except FetchError:
    ...                     # refused connection, expired timeout, no answer

There is no retry, and it is deliberate: call carries any method, so this client cannot know whether re-sending one is safe, and a timeout is not a deadline — a node that stopped answering may still be executing the call. HttpError.status is what makes a caller's own policy three lines rather than a match on the text of a message.

What it does not do

  • batches. One call is one HTTP request. A batch needs an api for correlating the answers and for partly failing, which is a question of its own; a loop over call is the replacement, an equivalent beside the node and not over a link where the round trip costs something.
  • notifications, a request sent with no id, which a node does not answer.
  • retries, per above.
  • redirects. A 30x arrives as an HttpError rather than as a second request: the first one already carries the Authorization for the host it names, and following the redirect would send that credential wherever the Location points.
  • proxies from the environment. HTTP_PROXY is set for a browser or a package manager and inherited by everything in the shell, which is the wrong source for the decision of where a wallet command is sent. A caller who does want one passes a transport.

Migrating from AuthServiceProxy

It is not python-bitcoinrpc's AuthServiceProxy and not a port of it. That class, and the copy of it Core's test framework maintains, carry the LGPL-2.1 of their python-jsonrpc ancestry, where this is MIT: this is an implementation of the protocol and shares no line with either. What a caller rewrites is below, an AuthServiceProxy line at a time with the same command under it.

Connecting. The credential is an argument, and a url carrying user:password@ is refused rather than accepted and stripped: that url is the string that ends up in a configuration file, a traceback and a log.

# AuthServiceProxy
rpc = AuthServiceProxy(f"http://{user}:{password}@127.0.0.1:8332")

# this client
client = BitcoinCoreRpcClient(
    "http://127.0.0.1:8332", user=user, password=password
)

A node left on its defaults needs neither argument: from_chain has the port and the cookie file, per Talking to a node.

Invoking a method. The method is an argument and not an attribute: any name a node has works without this class knowing it, and none of them can collide with a name of the client's own. params is then one argument as well, a sequence for the positional form and a mapping for the named one.

# AuthServiceProxy
block = rpc.getblock(block_id, 2)

# this client
block = client.call("getblock", [block_id, 2])
block = client.call("getblock", {"blockhash": block_id, "verbosity": 2})

A wallet command. /wallet/<name> is derived from the client rather than written into a second url, and the name is percent-encoded.

# AuthServiceProxy
hot = AuthServiceProxy(f"http://{user}:{password}@127.0.0.1:8332/wallet/hot")
balance = hot.getbalance()

# this client
balance = client.for_wallet("hot").call("getbalance")

A batch. There is none, per What it does not do, and a loop is what replaces it: the calls go one HTTP request each rather than several in one, and each answer is a value or an exception where a batch answered with a list to inspect.

# AuthServiceProxy
hashes = rpc.batch_([["getblockhash", height] for height in heights])

# this client
hashes = [client.call("getblockhash", [height]) for height in heights]

An error. JSONRPCException becomes three exceptions, and When it goes wrong above has the table: RpcError for an error the node computed, HttpError for an exchange that failed and FetchError for no answer to read. All three are FetchError, so except FetchError is the translation that catches what the one exception caught.

COMPARISON.md has the case for the switch beyond this rewrite guide, and why three features this client does not have are decisions rather than omissions.

Testing code that calls a node

transport is the seam, and it is public for this: a callable taking the request and a timeout, answering with the HTTP status and the body. The suite of this project opens no socket, and neither has yours to.

import json

def transport(request, timeout):
    request_id = json.loads(request.data)["id"]
    body = {"jsonrpc": "2.0", "id": request_id, "result": 481824}
    return 200, json.dumps(body).encode()

client = BitcoinCoreRpcClient(
    "http://127.0.0.1:8332", user="u", password="p", transport=transport
)
assert client.call("getblockcount") == 481824

What a transport of your own owes, none of which this module can check for it: its own bound on what it holds in memory while reading, its own bound on how long it holds the call — most client libraries spend timeout per socket operation, which a peer dripping a body resets forever — no redirect followed, and its own thread safety.

Type checking

The source is annotated throughout, mypy --strict runs over it here, and the distribution ships py.typed — so your own checker reads those annotations with no configuration of any kind:

$ mypy --strict your_code.py
your_code.py:4: error: Argument 1 to "call" of "BitcoinCoreRpcClient" has
incompatible type "int"; expected "str"

That marker is why the source is bitcoin_core_rpc/__init__.py and not a top-level module: PEP 561 puts it inside a package directory and nowhere else. pyproject.toml records what the alternatives were measured to do.

Vendoring

Copy bitcoin_core_rpc/__init__.py whole from a release tag, rename it to bitcoin_core_rpc.py, keep the license notice at the top of it — MIT, embedded rather than referenced, because a copy has no LICENSE beside it — and record the tag next to the copy. An update is a replacement of the whole file, and this shows every behavioral change first:

git diff OLD..NEW -- bitcoin_core_rpc/__init__.py

A vendored copy receives no security or compatibility fix automatically, so its recorded tag is what says whether it needs replacing. An installed one is a version an ordinary dependency bump moves, which is why installing is the default advice.

Security

Basic authentication is cleartext over plain HTTP, that being what Core's rpc speaks. On loopback that cleartext is between one process and the node beside it; for a node anywhere else it is on the wire, and rpc credentials authorise every wallet command that node has. SECURITY.md carries the rest, and how to report a vulnerability.

Contributing

CONTRIBUTING.md has the commands each CI job runs, verbatim. uv sync creates the environment; uv is the only tool that has to be installed.

Links

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

bitcoin_core_rpc-2026.8.8.tar.gz (329.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

bitcoin_core_rpc-2026.8.8-py3-none-any.whl (37.9 kB view details)

Uploaded Python 3

File details

Details for the file bitcoin_core_rpc-2026.8.8.tar.gz.

File metadata

  • Download URL: bitcoin_core_rpc-2026.8.8.tar.gz
  • Upload date:
  • Size: 329.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for bitcoin_core_rpc-2026.8.8.tar.gz
Algorithm Hash digest
SHA256 dd14d878a26bd66bc13c0501c04cfe1736b2e84636f93f2fb92265a797f18403
MD5 06e059415df762740192beac732f42b2
BLAKE2b-256 69353b2e4af1a6e14d2c6b55d727dddb38a4c16e7498a8545ff00e259b5389fe

See more details on using hashes here.

Provenance

The following attestation bundles were made for bitcoin_core_rpc-2026.8.8.tar.gz:

Publisher: release.yml on btclib-org/bitcoin-core-rpc

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file bitcoin_core_rpc-2026.8.8-py3-none-any.whl.

File metadata

File hashes

Hashes for bitcoin_core_rpc-2026.8.8-py3-none-any.whl
Algorithm Hash digest
SHA256 61dc813b3d7bf417eb02fc8742f27d4a1054801fde5e16de7396ee020992ea1b
MD5 78a5ab46c58286bdcf2bb678c5bfe362
BLAKE2b-256 72894cbaa77aaed3bd080f109054a3439cfecdd96a3d86029cbd598928320627

See more details on using hashes here.

Provenance

The following attestation bundles were made for bitcoin_core_rpc-2026.8.8-py3-none-any.whl:

Publisher: release.yml on btclib-org/bitcoin-core-rpc

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page