Skip to main content
SUBMIT A PRSUBMIT AN ISSUElast edit: Apr 01, 2026

Working with Proxies

This page covers each step in the use of proxy wallets as a security feature for Bittensor operations:

  • Creating proxy relationships between existing wallets
  • Executing transactions with 0-day proxy wallets.
  • Announcing and then executing transactions with a non-zero delay period.
  • Removing proxy relationships
  • Monitoring pending announcements on your proxy accounts
  • Rejecting unauthorized announcements

See:

Introduction

Security Considerations

Proxy wallets are a powerful security feature, but to get the full benefits, it is critical to observe good key security. When one wallet serves as proxy for another (the 'safe wallet'), both the safe wallet and the proxy wallet have their own full coldkey keypair (the public key which goes into the wallet's address, and the private key, which is recoverable using the seed phrase), and must be handled with proper care.

Generally, the safe wallet should be given the maximum security possible, whereas the proxy wallet (if it is carefully limited in its permissions), can be handled in a more convenient, less secure way. For example, a proxy might be loaded into a less trusted compute runtime, whereas the safe wallet's coldkey private key/seed phrase should never be loaded into any but the most absolutely secure device). However, depending on the proxy's configuration, compromise of a proxy wallet's coldkey can still be disastrous. For example, a proxy with ProxyType:any and delay:0 can immediately perform any operation on behalf of the safe wallet, so leaking such a proxy key is just as bad as leaking the safe wallet key.

mind your proxies!

A non-zero delay creates a window to cancel transactions that implement attacks, but if you are not checking for announcements regularly, an attacker who has stolen a proxy key can announce a call and wait for the delay to expire without any intervention. The delay protects you only if you are watching.

Two rules follow from this:

  1. Revoke any proxy relationship you are not actively monitoring. A dormant delayed proxy with no one watching it is little safer than a zero-delay proxy.
  2. Check for pending announcements on a schedule shorter than your configured delay. If your delay is 100 blocks (~20 minutes), you must check more frequently than that to have any realistic veto window.

See also: Coldkey and Hotkey Workstation Security: Monitor proxy announcements.

Before executing any operations with any coldkeys holding TAO on Bittensor main network, carefully think through the desired end result and the steps required to achieve it.

See: Coldkey and Hotkey Workstation Security.

Hardware wallet requirements for initial proxy setup

The first proxy relationship on a coldkey must be created by the primary coldkey itself. This is the one operation where you cannot use a proxy, since none exists yet. To do this without exposing your primary coldkey to a hot machine, you need a hardware wallet solution that supports arbitrary Subtensor extrinsics:

  • Polkadot Vault + Polkadot.js Apps: Polkadot Vault loads full chain metadata and can sign any extrinsic, including proxy.addProxy. Transactions are passed between the air-gapped device and polkadot.js/apps via QR code. This is the most flexible option.
  • Ledger + Talisman/SubWallet: Ledger hardware wallets support proxy creation through compatible wallet apps like Talisman and SubWallet.

After creating the initial NonTransfer proxy from your hardware wallet, all subsequent proxy management (creating scoped proxies, revoking proxies, rejecting announcements) can be done through that proxy. The primary coldkey never needs to leave cold storage again.

If you need btcli or the SDK for any operation, use a proxy key, not your primary coldkey. And remember to use scope limitations, delays, and prompt revokation to limit the risk exposure of your proxies.

See: Coldkey and Hotkey Workstation Security.

Prerequisites

Practice/Dev

To follow along with the below examples for practice, you have two options:

Main Network

Once you have practiced on a local or test chain, and you are ready to execute these operations on Bittensor main network (finney), you will need two wallets and enough TAO to cover some small fees:

  • The safe wallet or 'real account' that will be protected by the proxy.
  • The proxy wallet, which will act on behalf of the safe wallet.
fee

The delegate account must hold enough funds to cover transaction fees, which are approximately 25 Rao (0.000025 TAO).

See: Fees

Add a Proxy Relationship

Add a proxy record on the blockchain to designate a proxy wallet for your safe wallet.

coldkey security!

Note that this operation requires the safe wallet's coldkey private key, which is a maximally sensitive and valuable cryptographic secret.

See: Coldkey and Hotkey Workstation Security.

info

Multiple proxy relationships can exist between a pair of wallets, as long as each proxy entry uses a different ProxyType. Attempting to register a duplicate entry with the same delegate and ProxyType will result in a proxy.Duplicate error.

Add the on-chain proxy relationship

Run btcli proxy add to create a proxy relationship between existing wallets on-chain.

Note that --wallet.name specifies the safe wallet, since the private key must be loaded in for the safe wallet, not the proxy. This makes sense because it is the safe wallet that is delegating the authority to order transactions to the proxy wallet, so it must be authenticated with the private key using its encryption password.

btcli proxy add \
--wallet.name SAFE_WALLET_NAME \
--delegate PROXY_WALLET_COLDKEY_ss58 \ # Proxy wallet's coldkey
--proxy-type PROXY_TYPE \

Parameters:

  • --wallet.name: Your wallet name (the real account that will authorize the proxy)
  • --delegate: The SS58 address of the proxy (i.e. the delegate of transaction power)
  • --proxy-type: The type of proxy relationship (e.g., Staking, Transfer, Any, etc.)
  • --delay: Optional delay in blocks (0 for immediate execution)

For our example, we'll use two wallets called PracticeSafeWallet and PracticeProxy. To follow along, create two new wallets with these names and substitute their coldkey ss58 addresses:

  • PracticeSafeWallet: 5CS9x5NsPHpb2THeS92zBYCSSk4MFoQjjx76DB8bEzeJTTSt
  • PracticeProxy: 5CZmB94iEG4Ld7JkejAWToAw7NKEfV3YZHX7FYaqPGh7isXe

To give the PracticeProxy account the ability to order small transfers from the PracticeSafeWallet wallet's balance immediately (with 0 delay), we'll use the following comand:

btcli proxy add \
--wallet.name PracticeSafeWallet \
--delegate 5CZmB94iEG4Ld7JkejAWToAw7NKEfV3YZHX7FYaqPGh7isXe \
--proxy-type SmallTransfer \
--network test
✅ Your extrinsic has been included as 5951841-6
Added proxy delegatee '5CZmB94iEG4Ld7JkejAWToAw7NKEfV3YZHX7FYaqPGh7isXe' from delegator
'5CS9x5NsPHpb2THeS92zBYCSSk4MFoQjjx76DB8bEzeJTTSt' with proxy type 'SmallTransfer' with delay 0.
Would you like to add this to your address book? [y/n]: y

BTCLI's proxy address book

Use btcli config add-proxy to configure your local BTCLI with a proxy relationship. Make sure to follow the instructions carefully depending on whether:

  1. You are using a proxy relationship between pre-existing wallets, as described on this page. This covers most use cases for proxies.
  2. You are using a pure proxy. See pure proxies.

View all saved proxies with:

btcli config proxies
 Name                    Address                 Spawner/Delegator       Proxy Type      Delay   Note
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
practice-proxying 5CngkPSSnhK7ot6zFv3Q… 5GrwvaEF5zXb26Fz9rcQ… Any 0 always be awesome
practice-small-transfers 5GrwvaEF5zXb26Fz9rcQ… 5FLSigC9HGRKVhB9FiEo… SmallTransfer 0 small transfers only

Manage proxies through a NonTransfer proxy

After initial setup, you should never need your primary coldkey to manage proxies. A NonTransfer proxy can create and remove other proxy relationships on behalf of the real account (is_superset in runtime/src/lib.rs:815-828 defines which proxy types a NonTransfer proxy can manage: everything except Transfer and SmallTransfer).

This means your primary coldkey stays in cold storage. Use it once to create the initial NonTransfer proxy from your hardware wallet, then use that proxy for all subsequent proxy management.

import asyncio
import bittensor as bt
from bittensor.core.chain_data.proxy import ProxyType
from bittensor.core.extrinsics.pallets import Proxy

nontransfer_proxy_wallet = bt.Wallet(name="YOUR_NONTRANSFER_PROXY") # replace with your NonTransfer proxy wallet name
real_account_ss58 = "YOUR_REAL_ACCOUNT_SS58" # replace with your real account SS58
new_delegate_ss58 = "NEW_DELEGATE_SS58" # replace with the SS58 of the new proxy to add

async def main():
async with bt.AsyncSubtensor(network="test") as subtensor:
# Create a Staking proxy via the NonTransfer proxy
add_proxy_call = await Proxy(subtensor).add_proxy(
delegate=new_delegate_ss58,
proxy_type="Staking",
delay=0,
)
response = await subtensor.proxy(
wallet=nontransfer_proxy_wallet,
real_account_ss58=real_account_ss58,
force_proxy_type=ProxyType.NonTransfer,
call=add_proxy_call,
)
print(response)

asyncio.run(main())

To remove a proxy through the same path, use Proxy(subtensor).remove_proxy() with the same parameters.

Check an Account’s Proxies

You can check which proxies are associated with an account to see their delegate addresses, proxy types, and any configured delays. To do this:

To check proxies in BTCLI, you can view your local address book:

btcli config proxies

This displays all proxies you've saved to your local address book.

No BTCLI Coverage

BTCLI does not currently provide a command to query on-chain proxy state directly. To view all proxies registered on-chain for an account, use the SDK's get_proxies_for_real_account() method or query via Polkadot.js Apps.

Execute a 0-Delay Proxy Call

A proxy wallet that is set up with a delay of 0 can execute transactions allowed by its proxy type simply by declaring which real account they are acting as proxy for.

consider security!

This operation will be run in a coldkey workstation that is set up for the proxy wallet, not the safe wallet/real account. For main network (finney) wallets, the safe wallet's coldkey private key should never be loaded onto the proxy workstation, otherwise we undermine the security advantage of the proxy relationship. The safe wallet's coldkey private key/seed phrase should be kept in cold storage as much as possible, and should only be loaded into dedicated, highly secure, code environments provisioned specifically for that purpose.

However, 0-delay proxies are high-risk keys, since their compromise allows a would-be-attacker to act immediately, repeatedly and opportunistically.

Many btcli commands support the --proxy flag to proxy an operation on behalf of another wallet.

terminology and parameter names

The language here may be counter-intuitive, in that the --proxy flag specifies the wallet being proxied.

The wallet specified by --wallet.name is actually the wallet we normally call "the proxy", and --proxy specifies the safe wallet or 'real account'. It makes more sense if you think of the --proxy flag as specifying that the operation is being called by proxy for the wallet that follows, i.e., the safe wallet.

More to the point, we can logically infer that it must be the case that --wallet.name refers to the proxy, and the ss58 supplied (in the --proxy field) must refer to the safe wallet, since this command is meant to be run by the proxy, protecting the safe wallet. Therefore, the proxy's private key must be present and unlocked, not the safe wallet's, which should remain in cold storage.

This command will transfer 18 TAO from PracticeSafeWallet to a third wallet, Miner.


btcli wallet transfer \
--wallet.name PracticeProxy \
--proxy 5CS9x5NsPHpb2THeS92zBYCSSk4MFoQjjx76DB8bEzeJTTSt \
--destination 5DA7UsaYbk1UnhhtTxqpwdqjuxhQ2rW7D6GTN1S1S5tC2NRV \
--amount 0.333 \
--network test

Proxy params:

  • --wallet.name: The proxy wallet that signs the transaction on behalf of the real account.
  • --proxy: The real account's SS58 address (or proxy name from address book)
Initiating transfer on network: test
Do you want to transfer:
amount: 0.3330 τ
from: PracticeProxy : 5CZmB94iEG4Ld7JkejAWToAw7NKEfV3YZHX7FYaqPGh7isXe
to: 5DA7UsaYbk1UnhhtTxqpwdqjuxhQ2rW7D6GTN1S1S5tC2NRV
for fee: 0.0000 τ
Transferring is not the same as staking. To instead stake, use btcli stake add instead.
Proceed with transfer? [y/n]: y
Enter your password:
Decrypting...
✅ Finalized
Block Hash: 0xc8f8cba3395cd34d6dd2e2bc3b8e1b5e6b6eeb60754dac398b08bca735a6a32d
Balance:
98.7739 τ ➡ 98.4409 τ
Using saved proxies

If you haved a proxy relationship saved to your BTCLI address book, you can reference it by name as shown:

btcli wallet transfer \
--wallet.name PracticeProxy \
--proxy PracticeSafeWallet \
--destination 5DA7UsaYbk1UnhhtTxqpwdqjuxhQ2rW7D6GTN1S1S5tC2NRV \
--amount 0.333 \
--network test

Remove a Proxy

Removing a proxy revokes the delegate’s permission to act on behalf of the primary account, effectively ending the proxy relationship on-chain. To remove a proxy:

coldkey security!

Note that this operation requires the safe wallet's coldkey private key, which is a maximally sensitive and valuable cryptographic secret.

See: Coldkey and Hotkey Workstation Security.

btcli proxy remove \
--wallet.name WALLET_NAME \
--delegate DELEGATE_ADDRESS \
--proxy-type Staking \
--delay 0

Parameters:

  • --wallet.name: Your wallet name (the real account that authorized the proxy)
  • --delegate: The SS58 address of the delegate account to remove
  • --proxy-type: Must match the proxy type used when adding
  • --delay: Must match the delay value used when adding

For example, let's remove the 0-delay SmallTransfer proxy relationship we established above between our PracticeSafeWallet and PracticeProxy wallets.

btcli proxy remove \
--wallet.name PracticeSafeWallet \
--delegate 5CZmB94iEG4Ld7JkejAWToAw7NKEfV3YZHX7FYaqPGh7isXe \
--proxy-type SmallTransfer \
--delay 0
This will remove a proxy of type SmallTransfer for delegate 5CZmB94iEG4Ld7JkejAWToAw7NKEfV3YZHX7FYaqPGh7isXe.Do you want
to proceed? [y/n]: y
✅Success!
Removal is immediate

Unlike delayed execution, removing a proxy takes effect immediately, regardless of any delay configured on the proxy.

info

The delegate_ss58, proxy_type, and delay parameters must exactly match those used when the proxy was added. The delay parameter is an identifier for the specific proxy relationship, not a delay before removal takes effect (removal is immediate). Use get_proxies_for_real_account() to retrieve the exact parameters for existing proxies.

Remove all proxies

Use this to remove all proxies associated with an account.

coldkey security!

Note that this operation requires the safe wallet's coldkey private key, which is a maximally sensitive and valuable cryptographic secret.

See: Coldkey and Hotkey Workstation Security.

BTCLI does not currently provide a single command to remove all proxies at once. You must remove each proxy individually using btcli proxy remove.

SDK alternative

To remove all proxies in one operation, use the SDK's remove_proxies() method.

Announce and Execute a Delayed Proxy Call

If a proxy wallet has been given proxy powers to make a transaction with a delay, they must announce the call beforehand, and then wait the delay interval (specified by the delay parameter when the proxy relationship is created).

For example, the following snippets give the PracticeProxy wallet the ability to make large transfers, but only after an announcement-delay period of 100 blocks:

btcli proxy add \
--wallet.name PracticeSafeWallet \
--delegate 5CZmB94iEG4Ld7JkejAWToAw7NKEfV3YZHX7FYaqPGh7isXe \
--proxy-type Transfer \
--delay 100 \
--network test
Added proxy delegatee '5CZmB94iEG4Ld7JkejAWToAw7NKEfV3YZHX7FYaqPGh7isXe' from delegator
'5CS9x5NsPHpb2THeS92zBYCSSk4MFoQjjx76DB8bEzeJTTSt' with proxy type 'Transfer' with delay 100.

Generate call hash

Announcing a delayed proxy call requires the hash of the call that you intend to execute. Therefore, you must first generate the call hash of the transaction you want to carry out. To generate the call hash:

When you run a BTCLI command with the --announce-only flag, BTCLI automatically generates and adds the call hash to your ProxyAnnouncements address book.


Announce a proxy call

Announcing a proxy call publishes the hash of a proxy-call that will be made in the future. To announce a delayed call:

To announce a delayed proxy call via BTCLI, include the --announce-only flag when submitting the transaction, as shown:

# The call hash is automatically generated and saved
btcli wallet transfer \
--wallet.name PracticeProxy \
--proxy 5CS9x5NsPHpb2THeS92zBYCSSk4MFoQjjx76DB8bEzeJTTSt \
--destination 5DA7UsaYbk1UnhhtTxqpwdqjuxhQ2rW7D6GTN1S1S5tC2NRV \
--amount 0.333 \
--network test \
--announce-only


Show sample output
Do you want to transfer:
amount: 0.3330 τ
from: PracticeProxy : 5CZmB94iEG4Ld7JkejAWToAw7NKEfV3YZHX7FYaqPGh7isXe
to: 5DA7UsaYbk1UnhhtTxqpwdqjuxhQ2rW7D6GTN1S1S5tC2NRV
for fee: 0.0000 τ
Transferring is not the same as staking. To instead stake, use btcli stake add instead.
Proceed with transfer? [y/n]: y
Enter your password:
Decrypting...
Added entry 1ca5322e23ea9e36e8c8f1b912c817b89ef1fdcaaf25cacb57c173147ca3abbd at block 5953000 to your
ProxyAnnouncements address book. You can execute this with
btcli proxy execute --call-hash 1ca5322e23ea9e36e8c8f1b912c817b89ef1fdcaaf25cacb57c173147ca3abbd

✅ Finalized
Block Hash: 0x1c6378ee38b8c27f161b646125ec301f1aa52bffd63b090ec0c0876c9cc56ba5
Balance:
98.4409 τ ➡ 98.4409 τ

What this does:

  • Creates and announces the call on-chain
  • Saves the announcement details to your local database
  • Does NOT execute the operation immediately

After announcing:

  1. Wait for the configured delay period (in blocks) to pass
  2. The real account has the option to reject the announcement during the delay period
  3. Execute the call after the delay expires (see next step)

Execute a delayed proxy call

After the announcement waiting period has passed, the delegate account can now execute the proxy if the real account did not reject it. Attempting to execute the proxy before the waiting period passes returns a proxy.Unannounced error. To execute a delayed proxy call:

After the delay period has passed, execute the announced call:

btcli proxy execute \
--wallet.name PracticeProxy \
--proxy 5CS9x5NsPHpb2THeS92zBYCSSk4MFoQjjx76DB8bEzeJTTSt \
--real 5CS9x5NsPHpb2THeS92zBYCSSk4MFoQjjx76DB8bEzeJTTSt \
--call-hash 1ca5322e23ea9e36e8c8f1b912c817b89ef1fdcaaf25cacb57c173147ca3abbd \
--network test

How it works:

  • Retrieves the previously announced call from your local database
  • Verifies the delay period has passed
  • Executes the call on-chain
  • Clears the announcement

BTCLI automatically tracks announcements you make with --announce-only in a local database, making execution easier. This allows you to execute a delayed proxy by using the --call-hash flag.

warning

Using the --call-hash flag attempts to resolve the call from the proxy announcements address book. Use this flag only if you made the proxy announcement via BTCLI.

If the proxy call was announced through a different method, you must provide the encoded hex for the call using the --call-hex flag or rebuild the call explicitly via the command prompts.

info
  • The call details on the executed proxy must exactly match the original announcement. Any change to the call or call hash will result in a proxy.Unannounced error.
  • Once a delayed proxy call is executed, its announcement is cleared. To execute another proxy with the same details, you must create a new announcement and wait for the waiting period to pass.

Monitor and Reject Proxy Announcements

Check pending announcements

The Proxy.Announcements chain state is keyed by delegate (proxy) account. To find all pending announcements against your coldkey, query each of your configured proxy delegates and filter for entries where your coldkey is the real account.

tip

No coldkey is required for this operation, like all chain reads.

No BTCLI command

BTCLI does not currently have a command to list pending on-chain announcements. Use the SDK or Polkadot.js Apps.

Reject an announcement

If you find an unexpected announcement, reject it immediately. Rejection cancels the announcement on-chain and prevents execution. You can reject through a NonTransfer proxy (the proxy pallet sets the origin to the real account, satisfying the on-chain check), so your primary coldkey can stay in cold storage.

No BTCLI command

BTCLI does not currently have a command to reject proxy announcements. Use the SDK.

Use a NonTransfer proxy for rejections

A NonTransfer proxy can reject announcements on behalf of the real account, so your primary coldkey stays in cold storage even during incident response. Set up a NonTransfer proxy with zero delay specifically for monitoring and rejection.

Reject all pending announcements

If you need to clear all pending announcements from a delegate at once — for example, after aborting a staking run or if you suspect your proxy key is compromised — use this script to fetch and reject every pending announcement as a single atomic batch transaction.

import asyncio
import bittensor as bt
from bittensor.core.chain_data.proxy import ProxyType
from bittensor.core.extrinsics.pallets import Proxy

async def main():
async with bt.AsyncSubtensor(network="test") as subtensor:
nontransfer_proxy_wallet = bt.Wallet(name="YOUR_NONTRANSFER_PROXY") # replace with your NonTransfer proxy wallet name
real_account_ss58 = "YOUR_REAL_ACCOUNT_SS58" # replace with your real account SS58
delegate_ss58 = "DELEGATE_SS58" # replace with your proxy account SS58 address

# Fetch all pending announcements for this delegate
announcements = await subtensor.get_proxy_announcement(delegate_ss58)

if not announcements:
print("No pending announcements found.")
return

print(f"Found {len(announcements)} pending announcements. Batch rejecting...")

# Build a reject call for each announcement
reject_calls = []
for ann in announcements:
print(f" Will reject: {ann.call_hash}")
call = await Proxy(subtensor).reject_announcement(
delegate=delegate_ss58,
call_hash=ann.call_hash,
)
reject_calls.append(call)

# Wrap in batch_all — reverts all rejections atomically on any failure
batch_call = await subtensor.compose_call(
call_module="Utility",
call_function="batch_all",
call_params={"calls": reject_calls},
)

# Submit via NonTransfer proxy — primary coldkey stays in cold storage
response = await subtensor.proxy(
wallet=nontransfer_proxy_wallet,
real_account_ss58=real_account_ss58,
force_proxy_type=ProxyType.NonTransfer,
call=batch_call,
)
print(response)

asyncio.run(main())

Troubleshooting

  • proxy.Duplicate: A proxy with the same configuration already exists on the real account. See source code: Duplicate error.
  • proxy.Unannounced: A non-zero delay proxy requires an announcement; announce and wait the delay. See source code: Unannounced error.
  • proxy.Unproxyable/system.CallFiltered: The call is not permitted under the current ProxyType. See source code: Unproxyable error.
  • proxy.TooMany: You exceeded MaxProxies or MaxPending. Remove unused proxies/announcements. See source code: TooMany error.
  • proxy.NotProxy: Ensure you're submitting from the delegate account and referencing the correct real account. See source code: NotProxy error.
  • Token.FundsUnavailable: Ensure that your real account has enough available funds to cover the transaction.