A lean Python SDK (import bittensor) and CLI (btcli) for the Bittensor chain.
Project description
bittensor
A lean Python SDK and CLI for the Bittensor chain. One install gives you both a
library (import bittensor) and a command line (btcli).
Documentation: bittensor.com/docs
This package supersedes the separate
bittensor-cli and
bittensor-wallet packages:
btcli and the wallet ship here.
The design goal is a thin, unopinionated wrapper: easy, safe, and fast to do the core chain operations, with nothing hidden. It does not implement the neuron networking layer (axon/dendrite/synapse) — it is the layer that talks to the chain.
Why it's built this way
Almost everything is a projection of the chain's own runtime metadata:
- The generated layer (
bittensor.storage,runtime_api,constants,calls) is emitted from metadata, so it can't drift from the chain. - Reads (81 of them) and intents (72 of them) are the small hand-written semantic layer on top — the part metadata can't express (units, safety, aggregation).
- The CLI is generated from those registries: every read becomes a
btcli query <name>, every intent becomes abtcli tx <name>. Adding an SDK operation adds its command for free, and the two can't diverge. - A CI gate proves every chain call is either wrapped by an intent or explicitly marked raw-only, so nothing is silently forgotten.
Install
Requires Python 3.10–3.13. Using uv:
uv venv && source .venv/bin/activate
uv pip install -e .
This installs the btcli command and the bittensor Python package. The
CLI is a first-class part of the package: its terminal-UI dependencies
(typer, rich) and EVM signing (eth-account) are always installed.
CLI
Networks and configuration
Every command accepts the connection and identity options. Set them per-command, via environment variables, or persist them once:
btcli config set network test
btcli config set wallet my_coldkey
btcli config get # show the whole config
Precedence, highest first: CLI flag > environment variable > config file >
built-in default. So after the config above, btcli query tx-rate-limit
talks to testnet, but btcli -n finney query tx-rate-limit overrides it for
that one call.
Global options appear on every command's --help:
| Option | Env | Meaning |
|---|---|---|
--network, -n |
BT_NETWORK |
finney / test / local, or a ws:// endpoint |
--wallet, -w |
BT_WALLET |
coldkey wallet name |
--wallet-hotkey, -H |
BT_WALLET_HOTKEY |
hotkey name within the wallet |
--wallet-path |
BT_WALLET_PATH |
wallet directory |
--json |
machine-readable JSON output | |
--yes, -y |
skip confirmation prompts | |
--dry-run |
preview a mutation without submitting | |
--quiet, -q |
suppress informational output |
Shell completion
Ready-made completion scripts ship in completions/ (and are installed into the
standard system locations — share/bash-completion/completions,
share/zsh/site-functions, share/fish/vendor_completions.d). On most system or
Homebrew-style installs shells pick them up automatically, so there's nothing to
run.
If your shell doesn't auto-load them (e.g. a venv/uv install), source the
script once from your rc file:
# bash (~/.bashrc)
source /path/to/completions/btcli.bash
# zsh (~/.zshrc) — or drop `_btcli` on your $fpath
source /path/to/completions/_btcli
# fish — copy into a dir fish auto-loads
cp /path/to/completions/btcli.fish ~/.config/fish/completions/
btcli --install-completion still works if you'd rather have it edit your
rc file for you.
Wallets
btcli wallet create -w my_coldkey
btcli wallet list
btcli wallet regen-coldkey -w my_coldkey # prompts for the mnemonic securely
btcli wallet sign --message "hello" --use-hotkey -w my_coldkey
btcli wallet verify --message "hello" --signature 0x... --ss58 5F...
Reading state
btcli wallet balance 5F...coldkey
btcli subnets list
btcli subnets show 1
btcli stake show --hotkey 5F...validator --netuid 1
# Generated read commands (one per SDK read):
btcli query metagraph --netuid 1
btcli query delegate-take --hotkey 5F...
btcli query crowdloan --crowdloan-id 0
btcli query --help # full list
Submitting transactions
Every intent is a btcli tx <name>; run btcli tx --help for the list.
# Preview first (fee, effects, policy) — nothing is submitted:
btcli tx add-stake --hotkey 5F...validator --netuid 1 --amount-tao 10 --dry-run
# Submit (prompts to confirm unless --yes):
btcli tx add-stake --hotkey 5F...validator --netuid 1 --amount-tao 10 -w my_coldkey
btcli tx transfer --dest 5F...dest --amount-tao 1.5 -w my_coldkey --yes
Address arguments accept local names
Any address option (--dest, --hotkey, --coldkey, and the wallet balance address) takes three forms:
- a raw ss58 address;
- a local key name — hotkey options take
HOTKEYorWALLET/HOTKEY; coldkey options take a wallet name; - omitted, in which case
--hotkey/--coldkeyfall back to your configured wallet's key. Destination-style options never default.
btcli query hotkey-owner --hotkey my_coldkey/my_hotkey
btcli wallet balance my_coldkey # resolves the wallet's coldkey
SDK
Reading
import asyncio
import bittensor as sub
async def main():
async with sub.Client("finney") as client:
# Typed namespaces — every read in the catalog, one namespace per
# category, with autocomplete and typed returns
bal = await client.balances.get("5F...coldkey")
subnets = await client.subnets.all()
neurons = await client.neurons.all(netuid=1)
cost = await client.subnets.subnet_registration_cost()
take = await client.delegation.delegate_take(hotkey_ss58="5F...")
# The same reads dispatched by name (the form agents and `btcli query` use)
take = await client.read("delegate_take", hotkey_ss58="5F...")
# Generic accessors over the generated descriptors — anything on chain
tempo = await client.query(sub.storage.SubtensorModule.Tempo, [1])
ed = await client.constant(sub.constants.Balances.ExistentialDeposit)
asyncio.run(main())
There is a synchronous facade too:
client = sub.SyncClient("finney")
print(client.balances.get("5F...coldkey"))
client.close()
Writing: intents, plan, execute
A mutation is an intent — a serializable dataclass. plan previews it
(fee, effects, warnings, policy) without submitting; execute signs and submits
through a single policy-gated choke point.
from bittensor.wallet import Wallet
wallet = Wallet(name="my_coldkey", hotkey="my_hotkey")
async with sub.Client("finney") as client:
intent = sub.AddStake(hotkey_ss58="5F...validator", netuid=1, amount_tao=10)
plan = await client.plan(intent, wallet)
print(plan.fee, plan.effects, plan.ok)
result = await client.execute(intent, wallet)
print(result.success, result.block_hash)
Build intents by name (handy for agents and tools):
await client.execute_tool("transfer", {"dest_ss58": "5F...", "amount_tao": 1.0}, wallet)
Safety: Policy
Attach a Policy to bound what any mutation may do; violations raise
PolicyError at execute time (and show up in plan).
policy = sub.Policy(max_spend_tao=5.0, allowed_netuids=[1, 2])
async with sub.Client("finney", policy=policy) as client:
...
Typed money
Balance is unit-tagged (TAO vs. a subnet's alpha) and refuses to mix units, so
you can't accidentally pass alpha where TAO is expected. Construction and
readback are unit-named: from_tao / .tao for TAO, from_alpha / .alpha
for a subnet's alpha (.tao on an alpha balance raises). Use strings/Decimal
for exact large amounts.
sub.Balance.from_tao("10000000.123456789") # exact TAO
sub.Balance.from_alpha(2.5, netuid=42) # subnet-42 alpha, prints with the
# subnet's on-chain token symbol
# (α₄₂ before a client connects)
sub.tao(1.5); sub.alpha(2.5, 42); sub.rao(1_500_000_000)
Alpha is never summed across subnets or silently treated as TAO. To value stake
in TAO, use the stake_value_for_coldkey read — a spot-price mark
(alpha × price, excludes slippage/fees), pinned to one block.
Typed errors
Failures come back as ExtrinsicResult with a machine-readable ErrorCode and a
remediation hint, derived from the exact chain error name:
result = await client.execute(sub.BurnedRegister(netuid=999), wallet)
if not result.success:
print(result.error.code, result.error.remediation) # e.g. subnet_not_exists
Advanced submission modes
These compose with any intent:
-
Proxy — sign as a registered proxy so the real coldkey stays offline:
await client.execute(intent, delegate_wallet, proxy_for="5F...real_coldkey")
On the CLI:
--proxy-for <ss58|wallet>on anybtcli txcommand. Manage delegations with theadd-proxy/remove-proxyintents and theproxiesread. -
Atomic batch — several intents in one all-or-nothing extrinsic:
await client.execute(sub.Batch(intents=[ {"op": "transfer", "dest_ss58": "5F...", "amount_tao": 1.0}, {"op": "add_stake", "hotkey_ss58": "5F...", "netuid": 1, "amount_tao": 2.0}, ]), wallet)
-
MEV-shielded — encrypt the call so the mempool can't front-run it (a mainnet feature; needs validator-side reveal):
await client.submit_shielded(sub.Transfer(dest_ss58="5F...", amount_tao=1.0), wallet)
The CLI shields stake-trading commands (
stake add/remove/move/swap/transfer,unstake-all, and the limit variants) by default. Opt out per command with--no-mev-shield, persistently withbtcli config set mev_shield false, or opt any other mutation in with--mev-shield.
EVM
Subtensor runs a full EVM; its accounts (h160, MetaMask-style) and native
accounts (ss58) are disjoint signing domains on the same chain. btcli evm
owns the seam — keys, address math, money movement, association, and
precompiles — so none of it needs JS scripts or MetaMask.
Full walkthrough: EVM guide (quick start, diagrams, command reference, precompiles).
btcli evm key new -w my_coldkey # encrypted keystore V3, next to your hotkeys
btcli evm fund --amount-tao 5 # coldkey -> EVM key (via its ss58 mirror)
btcli evm balance # in TAO and wei (EVM uses 18 decimals, not 9)
btcli evm send --to 0x... --amount-tao 1 # h160 -> h160
btcli evm send-to-ss58 --to my_coldkey --amount-tao 1 # stored EVM key -> ss58 (precompile)
btcli evm claim-deposit --amount-tao 1 # MetaMask deposit -> coldkey (substrate extrinsic)
btcli evm associate --netuid 1 # link the EVM key to your hotkey, proof included
btcli evm stake add --netuid 1 --hotkey 5F... --amount-tao 2 # stake from the EVM key
btcli evm call metagraph getUidCount 1 # any precompile; view calls are free, no key
btcli evm precompiles # the catalog: names, addresses, descriptions
btcli evm abi staking-v2 # address + ABI JSON for Hardhat/ethers/viem
btcli evm config --format metamask # ready-to-paste tool config (also: hardhat, remix)
btcli evm doctor # probe RPC, chain ID, gas price, key balance
Keystore files import directly into MetaMask/geth/ethers (btcli evm key export), and --evm-key accepts stored key names everywhere. In the SDK the
same layer is bittensor.evm: h160_to_ss58 (the mirror mapping, verified
against the chain's own address-mapping precompile), ss58_to_pubkey (the
bytes32 form precompiles take), keystore management, the precompile catalog
with call encoding, and wei/TAO conversions. Substrate-side EVM intents
(fund_evm_key, evm_withdraw via claim-deposit or tx evm-withdraw,
associate_evm_key) ride the normal
plan/execute/policy flow.
Escape hatch: raw calls
Every chain call the metadata exposes is available under bittensor.calls, even
the ones no intent wraps. Submit one directly (an active Policy refuses this
unless it sets allow_raw_calls=True):
call = sub.calls.Commitments.set_commitment(netuid=1, info={...})
await client.submit_call(call, wallet, signer="hotkey")
For agents
The full catalog of executable operations and their JSON schemas is available programmatically and on the CLI, so an agent can discover and call everything without hard-coded knowledge:
btcli tools # machine-readable JSON of every intent + params
sub.intents.list_tools()
Combined with --json on every command, --dry-run to preview, and
Policy to bound spend/netuids, the CLI is safe for automated use (it refuses to
hang on a prompt: a non-interactive session without --yes is declined, not
blocked).
What's covered
The intent layer wraps every user-facing state transition across the runtime —
staking, transfers, registration, weights, children/take, proxies, multisig,
crowdloans, coldkey swap, identity, leasing, auto-staking, serving, and
subnet-owner hyperparameters. Calls left as raw-only are deprecated, root/admin
only, or off-chain-signed; each is recorded with a reason and reachable through
submit_call.
Development
Dev environment and gates run through uv and just:
just sync # install the locked dev environment
just check # lint + typecheck + unit tests + codegen gates (same as CI)
just test # offline unit tests only
just fmt # auto-fix lint findings and reformat
The generated layer is emitted from a node's metadata and committed. Regenerate and check it against a running node:
python -m codegen <ws-endpoint> # regenerate bittensor/_generated
python -m codegen.check --drift <ws> # committed files match chain metadata
python -m codegen.check --coverage # every chain call has a deliberate status
python -m codegen.check --names # every classified error name still exists
The migrated chain-facing SDK coverage lives in the Rust core e2e suite:
E2E_ENDPOINT=ws://127.0.0.1:9944 cargo test -p bittensor-core --test e2e -- --nocapture
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file bittensor-11.0.0rc6-py3-none-any.whl.
File metadata
- Download URL: bittensor-11.0.0rc6-py3-none-any.whl
- Upload date:
- Size: 483.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d5e46462f8a530d3dfed08fba3d479835b1a16607394e5ba34247fb2c1049101
|
|
| MD5 |
663fbe6a2083c947fa4fdc6d22971b9f
|
|
| BLAKE2b-256 |
a9e3a1794c1cdfeeaa25bc5761129790c2b4a4d4710693f0ef3328ed94f7e52f
|