The SDK
The five core primitives — Subtensor, Wallet, Balance, intents, and results — and their one-line constructors.
Everything in the SDK hangs off five primitives. Know these and the rest of the docs is reference.
| Primitive | What it is | Easy constructor |
|---|---|---|
Subtensor | the chain connection, blocking or async | bt.Subtensor() |
Wallet | your keys: coldkey (money) + hotkey (operations) | bt.Wallet("my_coldkey", "my_hotkey") |
Balance | unit-tagged money: TAO or a subnet's alpha | bt.tao(1.5), bt.alpha(2.5, 42) |
| intents | one mutation, as a dataclass | bt.Transfer(dest_ss58="5F...", amount_tao=1.5) |
ExtrinsicResult | the outcome, as data | returned by sub.execute(intent, wallet) |
And one optional guardrail: Policy bounds what any
execution may do.
Subtensor — the connection
import bittensor as bt
sub = bt.Subtensor() # mainnet (finney); "test" | "local" | "ws://..." tooOne class for both worlds: used directly it is a blocking client — connects
lazily on the first call, cleaned up automatically, no close() — and
awaited it is the async client:
sub.block() # blocking
client = await bt.Subtensor() # async, same surface awaited
async with bt.Subtensor() as client: ... # scoped asyncEvery read in the catalog is a typed method on a category namespace, also dispatchable by name; anything else on chain is reachable through generic accessors:
sub.balances.get("5F...") # typed namespace
sub.subnets.metagraph(netuid=1) # a whole subnet as one object
sub.read("delegate_take", hotkey_ss58="5F...") # same catalog, by name
sub.query(bt.storage.SubtensorModule.Tempo, [1]) # anything in the runtimeOne page per read under Queries; the full tour is The client.
Wallet — the keys
wallet = bt.Wallet("my_coldkey", "my_hotkey") # ~/.bittensor/wallets/<name>/
wallet = bt.Wallet("my_coldkey/my_hotkey") # same thing, one string
wallet = bt.Wallet() # name and hotkey both "default"The coldkey holds funds and signs financial operations; the hotkey
identifies you on subnets and signs operational calls (weights, serving). The
SDK picks the right signer per operation — you just pass the wallet. Anywhere
a wallet is expected, the name string alone also works:
sub.execute(intent, "my_coldkey/my_hotkey"). And anywhere an ss58 address
is expected (dest_ss58, hotkey_ss58, coldkey_ss58, ...), a Wallet or
keypair works too — the parameter takes the matching key's address:
sub.balances.get(wallet) # instead of wallet.coldkeypub.ss58_address
bt.AddStake(hotkey_ss58=wallet, netuid=1, amount_tao=10) # the wallet's hotkeyEncrypted coldkeys prompt for the password; set BT_WALLET_PASSWORD for
unattended use. Creation and recovery live in bt.wallets
(create, regen_coldkey, ...) or btcli wallet. See
Wallets and keys.
Balance — the money
bt.tao(1.5) # 1.5 TAO
bt.alpha(2.5, 42) # 2.5 alpha on subnet 42
bt.rao(1_500_000_000) # raw chain units (1 TAO = 1e9 rao)Every Balance is tagged with its unit — TAO or one subnet's alpha — and
refuses to mix them: adding alpha to TAO, or comparing across subnets, raises
bt.UnitMismatchError instead of silently producing a wrong number. Read the
amount back with .tao or .alpha (each raises on the other's unit).
Intent amount fields are unit-named (amount_tao, amount_alpha) and also
accept plain numbers and strings — amount_tao=1.5 means 1.5 TAO. Why the
strictness: Money.
Intents — the mutations
Every state-changing operation is a small dataclass, exported at the top
level — bt.Transfer, bt.AddStake, bt.SetWeights, bt.BurnedRegister,
... (one page each). Preview with plan, submit with execute:
intent = bt.AddStake(hotkey_ss58="5F...validator", netuid=1, amount_tao=10)
plan = sub.plan(intent, wallet) # fee, effects, warnings — nothing submitted
result = sub.execute(intent, wallet) # sign and submitIntents are plain data — build them by name from a dict too
(sub.execute_tool("add_stake", {...}, wallet)), which is how
agents and the CLI drive the same catalog. Semantics:
The transaction model.
The most common operations also have one-call helpers that bundle construct-connect-execute, e.g.:
bt.set_weights(1, {0: 0.1, 1: 0.7, 2: 0.2}, wallet="my_coldkey", hotkey="my_hotkey")ExtrinsicResult — the outcome
execute returns data, not an exception:
result = sub.execute(intent, wallet)
if not result.success:
print(result.error.code) # bt.ErrorCode.RATE_LIMITED, INSUFFICIENT_BALANCE, ...
print(result.error.remediation) # what to do about it
result.raise_for_failure() # or opt into raisingresult.error.code is a stable, machine-readable bt.ErrorCode — branch on
it, never on message strings. Success carries block_hash, extrinsic_id,
events, and explorer_url. The full taxonomy: Errors.
Policy — the guardrail
policy = bt.Policy(max_spend_tao=5.0, allowed_netuids=[1, 2])
sub = bt.Subtensor(policy=policy)A Policy on the connection hard-bounds every mutation that passes through
it: max_spend_tao, max_fee_tao, allowed_netuids (raw calls are refused
outright unless allow_raw_calls=True). Violations raise PolicyError at
execute time and show up in plan. Designed for handing a key to an agent or
a script you don't fully trust.
All five together
import bittensor as bt
sub = bt.Subtensor() # connection
wallet = bt.Wallet("my_coldkey/my_hotkey") # keys
balance = sub.balances.get(wallet)
print(balance) # a Balance, e.g. τ12.5
intent = bt.Transfer(dest_ss58="5F...", amount_tao=1.5) # mutation
result = sub.execute(intent, wallet) # outcome
if not result.success:
print(result.error.code, result.error.remediation)