For agents

How an agent discovers, previews, and safely executes any Bittensor operation.

View as Markdown

The SDK and CLI are built to be driven by agents. Every operation is discoverable at runtime with a JSON schema, every mutation can be previewed before it spends anything, every failure returns a machine-readable code with a remediation hint, and a Policy can hard-bound what a session is allowed to do. Nothing here requires parsing human prose.

Discover the operations

btcli tools        # JSON: every transaction op + summary, description, signer, input schema
import bittensor as sub
sub.intents.list_tools()   # same catalog, as Python dicts
sub.reads.list_reads()     # every read: name, params, docs, category

The same catalogs are published statically by these docs: /catalog/intents.json, /catalog/reads.json, /catalog/errors.json. Each entry includes a docs_url pointing at its reference page, and every docs page is fetchable as raw markdown (see the copy-markdown link on any page, or /llms-full.txt for everything at once).

Execute by name

An agent never needs to import intent classes — build and execute by op name with a plain dict, validated against the schema:

async with sub.Client("finney") as client:
    result = await client.execute_tool(
        "transfer", {"dest_ss58": "5F...", "amount_tao": 1.0}, wallet
    )

On the CLI, every op in the catalog is btcli tx <op-name> (underscores become dashes) and every read is btcli query <name>.

Preview before you spend

plan (SDK) and --dry-run (CLI) run the full pipeline — fee estimation, predicted effects, warnings, policy check — without submitting:

plan = await client.plan(intent, wallet)
plan.fee        # estimated fee
plan.effects    # list[str]: what this will do
plan.warnings   # non-fatal cautions (e.g. dust amounts)
plan.ok         # would policy allow it?

Bound the blast radius with Policy

Attach a Policy to the client and every execution — by class, by name, or raw — passes through one choke point:

policy = sub.Policy(max_spend_tao=5.0, allowed_netuids=[1, 2])
async with sub.Client("finney", policy=policy) as client:
    ...  # anything exceeding the caps raises PolicyError at execute time

Operations whose cost cannot be bounded ahead of time (e.g. subnet registration, whose price floats) are blocked by a spend cap until it is raised — the safe default. Raw calls are refused unless the policy sets allow_raw_calls=True.

Branch on failures, don't parse them

Every write returns an ExtrinsicResult; failures carry a semantic ErrorCode and a remediation hint:

result = await client.execute(intent, wallet)
if not result.success:
    match result.error.code:
        case sub.ErrorCode.RATE_LIMITED:   ...  # wait and retry
        case sub.ErrorCode.INSUFFICIENT_BALANCE: ...  # reduce amount
        case _:
            log(result.error.remediation)   # actionable next step, always present

CLI rules for non-interactive use

  • --json on any command produces machine-readable output.
  • --yes skips confirmation prompts. Without it, a non-interactive session is declined, not blocked — the CLI never hangs waiting for input it can't get.
  • --dry-run previews any tx command.
  • Configuration comes from flags, BT_* environment variables, or btcli config — highest to lowest precedence.

The escape hatch

Anything on chain that no intent wraps (deprecated, root/admin-only, or off-chain-signed calls) is still reachable:

call = sub.calls.Commitments.set_commitment(netuid=1, info={...})
await client.submit_call(call, wallet, signer="hotkey")

And generic accessors cover any storage item, constant, or runtime API in the chain metadata:

tempo = await client.query(sub.storage.SubtensorModule.Tempo, [1])
ed = await client.constant(sub.constants.Balances.ExistentialDeposit)