Guides
Proxy accounts
Keep the coldkey offline and do daily work through scoped, revocable, optionally delayed delegate keys.
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
Stakingproxy 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 > 0must 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
Anyproxy 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 includeUtility.batch_all, so batched staking through a Staking proxy fails withCallFiltered— use NonTransfer for batched staking. - Registration — neuron registration (
burned_register,register,register_limit). - ChildKeys —
set_childrenandset_childkey_take; RootClaim —claim_rootonly. - 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 bittensorOne 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 opsStep 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 --ledgerAfter 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-readableimport 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 safeThe 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 safeAnnounce. 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 safeA 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 treasurybtcli 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 safeIrreversible: 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 safeThe panic switch drops every delegation at once
(remove-proxies):
btcli proxy remove --all -w safeCareful 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.
A recommended architecture
The pattern the pieces are designed for, from a compromised-wallet perspective:
- Primary coldkey in a hardware wallet or air-gapped machine. It signs
exactly one thing, once:
add-proxyfor a delayed NonTransfer manager proxy (--ledger/--signer extensionkeep it off the CLI machine even for that). - 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. - Scoped daily proxies (
Staking,Registration,ChildKeys, ...,delay 0) on working machines, one per job, revoked the moment they're not actively needed. - A monitor — a cron job or agent polling
Proxy.Announcements(and your delegations viabtcli 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; checkbtcli proxy listand 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 (rememberStakingexcludesbatch_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.