Guides

Proxy accounts

Keep the coldkey offline and do daily work through scoped, revocable, optionally delayed delegate keys.

View as Markdown

Every coldkey that sits on a working machine is one compromise away from a total loss — and the loss is irreversible: nobody can claw back a drained wallet. The proxy pallet exists so the coldkey holding your funds never has to be that key. You register a delegate key that may sign on the real account's behalf, scoped to a class of operations (proxy type) and optionally forced through a time delay you can veto. The valuable key goes to cold storage; the key that lives on your laptop or server is a limited, revocable stand-in.

What this buys you:

  • Blast-radius control. A leaked Staking proxy can shuffle your stake; it cannot transfer a single TAO out. Revoke it, rotate, move on — your principal never moved.
  • A veto window. A delegation with delay > 0 must announce each call and wait; the real account can reject anything it doesn't recognize before it executes.
  • An offline primary. After a one-time setup, day-to-day operations never need the primary coldkey on any networked machine.

The related pure proxy is an account nobody has a key for — the chain derives a fresh address controlled only through proxy relationships. Useful as disposable role accounts and as swappable members of a multisig.

How it works

The real account signs add-proxy once, naming a delegate, a proxy type, and a delay. From then on the delegate submits ordinary calls wrapped in Proxy.proxy(real, ...) — the CLI flag for this is --proxy-for, the SDK keyword is proxy_for= — and the runtime checks each call against the delegation's type (proxy_type_filter) before dispatching it as the real account. Bookkeeping is paid with reserved deposits (returned on removal), not fees.

Proxy types

Prefer the narrowest type that covers the job:

  • Any — everything, including transfers. Avoid: leaking an Any proxy is exactly as bad as leaking the coldkey.
  • NonTransfer — everything except balance transfers, stake transfers, and coldkey swaps. Notably it can manage the account's other proxies, which makes it the natural "manager" proxy.
  • Transfer — balance transfers and stake transfers (transfer_stake / transfer_stake_and_hotkey) only. SmallTransfer is the same but caps each transfer below 0.5 TAO (0.5 alpha for stake transfers).
  • Staking — stake add / remove / move / swap / unstake-all and their limit variants, plus set_root_claim_type. Its allowlist does not include Utility.batch_all, so batched staking through a Staking proxy fails with CallFiltered — use NonTransfer for batched staking.
  • Registration — neuron registration (burned_register, register, register_limit).
  • ChildKeysset_children and set_childkey_take; RootClaimclaim_root only.
  • Owner — subnet-owner admin calls; NonCritical — everything except registrations, coldkey swaps, and subnet dissolution; SubnetLeaseBeneficiary — the lease-scoped subset of owner calls.

Triumvirate, Senate, Governance, and RootWeights are deprecated on the current runtime — they deny all calls.

Prerequisites

uv venv && source .venv/bin/activate
uv pip install bittensor

One install gives you both btcli and the Python SDK. Practice on testnet (-n test on every command, or btcli config set network test) before mainnet. Two wallets for the walkthrough:

  • safe — the real account holding the funds. Its coldkey is the crown jewel; on mainnet it should live in a hardware wallet or on an air-gapped machine.
  • ops — the delegate that will do daily work. Its coldkey lives on your working machine and needs a little TAO for transaction fees.
btcli wallet create -w safe
btcli wallet create -w ops

Step 1 — grant the delegation

The real account signs the grant — this is the one step that needs the safe coldkey. Give ops staking powers with no delay:

btcli proxy add --delegate ops --proxy-type Staking --delay 0 -w safe

--delegate accepts a wallet name, address-book name, or raw ss58. The same operation exists as the add-proxy intent (btcli tx add-proxy), with --dry-run to preview the fee and reserved deposit first. Multiple delegations can exist between the same pair of accounts as long as each uses a different proxy type.

Keep the coldkey off this machine anyway. The CLI can sign this grant on a Ledger (--ledger) or in a browser extension (--signer extension with Talisman, Polkadot.js, SubWallet, ...), so even the initial add-proxy never puts the safe mnemonic on a networked box:

btcli proxy add --delegate ops --proxy-type Staking -w safe --ledger

After the first grant, a NonTransfer proxy can add and remove the account's other proxies — so the primary coldkey is only ever needed once (see the recommended architecture below).

Step 2 — view delegations

Straight from chain state — who may sign for the account, with what scope, delay, and reserved deposit:

btcli proxy list --coldkey safe
btcli query proxies --coldkey safe --json     # same read, machine-readable
import bittensor as bt

async with bt.Subtensor("test") as client:
    delegations = await client.balances.proxies(coldkey_ss58="5F...safe")

For proxies you use often — especially ones on other machines or pure proxies — the local proxy book gives them names that every --proxy-for and address argument resolves:

btcli proxy book add --name safe-staking --address 5F...ops \
  --proxy-type Staking --delay 0 --note "daily staking ops"
btcli proxy book list

(update, remove, and clear complete the group.)

Step 3 — act through the proxy

Every btcli tx command takes --proxy-for. The wallet you sign with (-w) is the delegate; --proxy-for names the real account being acted for:

btcli tx add-stake --hotkey 5F...validator --netuid 1 --amount-tao 25 \
  -w ops --proxy-for safe --dry-run

btcli tx add-stake --hotkey 5F...validator --netuid 1 --amount-tao 25 \
  -w ops --proxy-for safe

The stake moves from the safe account's balance; the safe coldkey was never touched. --force-proxy-type Staking additionally requires the delegation used to be exactly that type. In Python it's one keyword on execute:

from bittensor.wallet import Wallet

ops = Wallet(name="ops")

async with bt.Subtensor("test") as client:
    intent = bt.AddStake(hotkey_ss58="5F...validator", netuid=1, amount_tao=25)
    result = await client.execute(intent, ops, proxy_for="5F...safe")
    if not result.success:
        print(result.error.code, result.error.remediation)

A call outside the delegation's scope fails with Unproxyable / CallFiltered — the runtime filter, not the CLI, is what enforces the scope.

Step 4 — delayed proxies: announce, wait, execute, veto

For higher-risk powers, add a delay (in blocks; one block ≈ 12 seconds, so 300 blocks ≈ 1 hour). The delegate must then announce each call's hash and wait out the delay before executing — and during that window the real account (or its NonTransfer proxy) can veto:

btcli proxy add --delegate ops --proxy-type Transfer --delay 300 -w safe

Announce. Announcing publishes the hash of the intended call (blake2-256 of its SCALE encoding). Compose the call to get its hash, then submit Proxy.announce as the delegate — via the raw-call escape hatch on the CLI, or submit_call in Python:

ops = Wallet(name="ops")

async with bt.Subtensor("test") as client:
    call = bt.calls.Balances.transfer_keep_alive(
        dest="5DevPayee...",
        value=10 * 10**9,               # raw calls take rao (1 TAO = 1e9 rao)
    )
    composed = await client.compose(call)
    call_hash = "0x" + bytes(composed.call_hash).hex()

    await client.submit_call(
        bt.calls.Proxy.announce(real="5F...safe", call_hash=call_hash), ops
    )

Execute after the delay. The announced call is rebuilt with byte-identical parameters — anything else hashes differently and matches no announcement. The convenient form takes an intent op and its args:

btcli proxy execute \
  --delegate ops --real safe \
  --inner-op transfer \
  --inner-args '{"dest_ss58": "5DevPayee...", "amount_tao": 10}' \
  -w ops

(the same operation as the execute-proxy-announced intent, also reachable as btcli tx execute-proxy-announced or bt.ExecuteProxyAnnounced in Python). Executing before the delay elapses fails with Unannounced.

Monitor and veto. A delay only protects you if you watch — check pending announcements on a schedule shorter than the delay, otherwise the window is only forensic. Announcements are keyed by delegate in the Proxy.Announcements storage map:

async with bt.Subtensor("test") as client:
    for delegate, value in await client.query_map(bt.storage.Proxy.Announcements):
        announcements, _deposit = value
        for ann in announcements:
            if str(ann["real"]) == "5F...safe":
                print(delegate, ann["call_hash"], "announced at", ann["height"])

Reject anything you don't recognize — rejection cancels the announcement on-chain, signed by the real account or a NonTransfer proxy acting for it:

btcli call Proxy.reject_announcement \
  --args '{"delegate": "5F...ops", "call_hash": "0x..."}' \
  -w safe

A delegate withdrawing its own mistaken announcement uses Proxy.remove_announcement instead, signed by the delegate and passing the real account: --args '{"real": "5F...safe", "call_hash": "0x..."}'.

Step 5 — pure proxies

create-pure-proxy spawns a fresh account with no private key anywhere — the spawner controls it purely through the proxy relationship. That makes it a disposable, role-scoped account (a staking-only treasury, a per-project operational account), and the standard building block for multisigs with swappable members: make each multisig member a pure proxy, and a departing teammate is handled by re-pointing one pure proxy's delegate — the multisig address never changes.

btcli proxy create --proxy-type Any --name treasury -w safe

--name saves the spawned address and its creation block height / extrinsic index to the proxy book — record these; killing the account later requires them. Fund the pure proxy, then act as it with the same --proxy-for flow, signed by the spawner:

btcli tx transfer --dest treasury --amount-tao 50 -w safe
btcli tx add-stake --hotkey 5F...validator --netuid 1 --amount-tao 20 \
  -w safe --proxy-for treasury

btcli proxy list --coldkey treasury shows the spawner as its delegate. Additional delegates can be registered on the pure proxy itself by dispatching add-proxy through it (btcli tx add-proxy --delegate ... -w safe --proxy-for treasury).

When it's no longer needed, empty it first, then destroy it. kill must be dispatched through the pure proxy itself and must reproduce the exact creation coordinates — which the proxy book entry supplies automatically:

btcli tx transfer --dest safe --amount-tao all -w safe --proxy-for treasury
btcli proxy kill treasury -w safe

Irreversible: any funds left in a killed pure proxy are gone permanently. And since the spawner relationship is the only control path, losing the spawner key — or removing its delegation — strands the pure proxy and everything it holds. See kill-pure-proxy.

Step 6 — revoke

Removal must match the original grant's (delegate, type, delay) triple exactly, and takes effect immediately regardless of the delegation's delay. The deposit comes back:

btcli proxy remove --delegate ops --proxy-type Staking --delay 0 -w safe

The panic switch drops every delegation at once (remove-proxies):

btcli proxy remove --all -w safe

Careful with --all if the account spawned pure proxies — they are controlled through delegations, so removing everything can strand them permanently. Treat any leaked zero-delay proxy key as already spent: revoke first, investigate second.

The pattern the pieces are designed for, from a compromised-wallet perspective:

  1. Primary coldkey in a hardware wallet or air-gapped machine. It signs exactly one thing, once: add-proxy for a delayed NonTransfer manager proxy (--ledger / --signer extension keep it off the CLI machine even for that).
  2. Manager proxy (NonTransfer, delay > 0) creates, removes, and monitors all other proxies, and rejects hostile announcements. It cannot move funds even if leaked, and its own actions are announced and delayed — visible before they bite.
  3. Scoped daily proxies (Staking, Registration, ChildKeys, ..., delay 0) on working machines, one per job, revoked the moment they're not actively needed.
  4. A monitor — a cron job or agent polling Proxy.Announcements (and your delegations via btcli query proxies --json) on a schedule shorter than your shortest delay.

With this in place, the key that can drain you touches a signer once in its life, every routine key is individually expendable, and an attacker's move announces itself before it can execute. For shared custody on top — subnet-owner treasuries especially — put a multisig behind the real account.

Subnet owners specifically

A subnet owner's coldkey controls the subnet's identity and owner-settable hyperparameters — exactly the kind of authority that should never live in a single key on an operational machine. The layering:

  • Make the owner account a multisig from registration (or migrate an existing owner to one via the announced coldkey swap — see that guide; a plain transfer moves TAO but not subnet ownership).
  • From the owner, grant a dedicated operations key the narrow Owner proxy type. It can manage subnet identity and hyperparameters but cannot transfer funds or rotate the owner. Use it for all routine changes; the owner account itself acts only to replace or revoke that proxy.
  • Add an announcement delay on the Owner proxy when your operational cadence permits, and monitor announcements more often than the delay.

Crowd-funded subnets get a chain-native version of this model for free: a crowdloan lease creates the lease-owner account itself and grants the beneficiary a scoped SubnetLeaseBeneficiary proxy.

Deposits and limits

  • Adding a proxy reserves 0.06 TAO base + 0.033 TAO per delegation from the granting account, returned on removal. At most 20 proxies per account.
  • Announcements reserve 0.036 TAO base + 0.068 TAO each from the delegate; at most 75 pending per delegate.
  • The delegate pays ordinary transaction fees on proxied calls, so keep a little TAO on it.

Troubleshooting

  • NotProxy — no delegation matches; check btcli proxy list and that you're signing with the delegate (-w) and naming the real account (--proxy-for), not the reverse.
  • Unproxyable / CallFiltered — the call is outside the delegation's proxy type (remember Staking excludes batch_all).
  • Unannounced — a delayed delegation executed without announcing, before the delay elapsed, or with parameters that don't hash to the announcement.
  • Duplicate — a delegation with the same (delegate, type, delay) already exists.
  • TooMany — over the 20-proxy or 75-announcement cap; remove unused entries.