Skip to content

Custody tiers: the vault and TEE wallets

The Candle CLI holds your keys in two custody tiers. Tier 1, the vault, is self-custody: keys that live encrypted on your machine and that nothing but you can sign with. Tier 2, TEE wallets, is agent access: wallets an agent can trade from through one API key, while your vault keeps the key and a sweep home. You move a wallet from Tier 1 to Tier 2 by promoting it, and funds come back to Tier 1 whenever you choose.

These custody tiers are separate from your account plan (Free, Believer, Pro, Max), which is covered in Agent access & API keys. Every command on this page is documented flag by flag in the CLI reference.

Tier 1: the vaultTier 2: TEE wallets
What it is forHolding funds. Your bankAn agent trading on your behalf
Where the key livesvault.enc on your machine, encryptedYour vault, and a copy in Privy’s TEE
Who can signOnly you, after unlocking a factorThe one API key the wallet is bound to, within its scopes and caps; and you, from the vault
Does Candle see itNever: no API, relay or server sees a vault keyYes: it is a linked wallet on your account
How funds leavevault transfer, signed on your machine, to any addresscandle transfer (agent), vault transfer (you), or a sweep home
I want toRun
Give a key TEE wallets, or move them to another keycandle tee rebind <wallets...> --to-key <key> (or --label-prefix <p> for many)
Trade a key’s wallets from another machine (a server that runs the bot)candle tee signer new --key <key> on that machine, then promote with --to-key <key>; see Key signers
Let a key use a linked wallet you importedThe agent console’s Agents tab, signed in (keys wallets set from a key can only narrow)
Turn a vault key into a TEE walletcandle vault promote --in-place <label> --sweep-to <cold key>, or --from <cold key> for a fresh one
Send funds out of a TEE wallet as the agentcandle transfer --to vault (or to a trusted wallet) with a Read:Write:Transfer key
Send funds anywhere yourselfcandle vault transfer <address> --from <label>
Mark wallets as yours so agents can send to themcandle wallets trust <selectors...>
Stop an agentcandle tee disable <address>, then candle keys revoke <prefix>
Bring everything homecandle tee disable <address>, then candle tee sweep <address> or candle vault demote <address> (--emergency: see In an emergency)

The vault is one file, vault.enc, in your config directory (~/.config/candle by default; set CANDLE_CONFIG_DIR to move it, or pass -k <path> for one command). It holds a 24-word recovery root every derived key comes from, one encrypted blob per key, and one wrapped copy of the data key per factor. No environment variable and no --yes opens it: every unlock is a factor you present on a terminal.

Terminal window
candle vault init # one passphrase factor and a recovery root, no keys yet
candle vault new-key --chain solana --label treasury # derive a key
candle vault list --balances # every key, with SOL balances over your RPC
candle vault backup --to /Volumes/BACKUP/vault.enc # copy, then verify the copy in full

A factor is something that opens the vault. You can hold several, and any one of them opens it.

FactorStatusNotes
passphraseAvailablevault init generates eight words (about 103 bits) and shows them once; --own-passphrase takes your own, 16 characters or more
security-keyAvailableA FIDO2 key with hmac-secret and user verification (a PIN or the key’s built-in check), through the bundled candle-fido2 helper. Two keys make a recoverable pair. A security key can authorize adding another one; --device picks which key
touch-idNot yet releasedThis Mac’s Secure Enclave behind Touch ID. Needs the Apple-signed candle-enclave.app helper
passkeyNot yet releasedA synced iCloud Keychain passkey. Needs the same signed helper, on macOS 15 or later

A security key is another way in, not a second step. Any one factor opens the vault on its own, a security key included: adding one does not make the passphrase plus the key a requirement, and anyone holding the passphrase still opens the vault without the key. There is no unlock that needs two factors together; that would be a separate proposal. Guard each factor as if it were the only one.

Touch ID and passkey are built and wait on Apple’s approval of Candle’s developer enrolment; until they ship, vault factor add touch-id and vault factor add passkey refuse with VAULT_FACTOR_UNSUPPORTED_ON_PLATFORM.

Terminal window
candle vault factor list
candle vault factor add security-key --label yubikey-a
candle vault factor add security-key --label yubikey-b --device <id> # the second key; the first can open the vault

Setting a security key’s PIN. A key with no PIN and no built-in biometric is refused at enrolment (VAULT_UV_UNSUPPORTED), and nothing is written. Set the PIN with a tool that prompts for it, and never pass a PIN as a command-line flag: an argument lands in shell history and is visible to other users in ps.

  • YubiKey: ykman fido access change-pin. With no PIN set it creates one; leave out --pin and --new-pin so it prompts.
  • Other keys, any platform: Chrome’s chrome://settings/securityKeys, then Create a PIN.
  • Linux: libfido2’s fido2-token -S <device>, which prompts. fido2-token -L lists the devices.

The CLI itself never takes a PIN on its command line. It asks on a hidden prompt when a key needs one.

Keep the recovery phrase offline and separate from any factor. vault phrase show prints it once per ceremony; vault restore --phrase rebuilds a vault from it with a new passphrase. Removing a factor is not revocation: a copy of the vault made while the factor existed still opens with it, so after a compromised factor the answer is a new vault and moving the funds.

Terminal window
candle vault transfer <address> --amount 5 --asset SOL --from treasury --rpc-url https://<your-rpc>

vault transfer builds and signs on your machine, to any address, for SOL or any SPL or Token-2022 mint. It shows the decoded transfer and the fee, makes you type the destination’s last six characters, then asks for your factor again. There is no --yes.

A TEE wallet is a Solana or Hood wallet an agent can use (Hood: see Hood TEE wallets). Its key is held in a trusted execution environment (Privy’s TEE) and bound to exactly one of your API keys, so an agent holding that API key can trade from it without your vault being unlocked. The server always admits a TEE wallet on trades and the relay. Swaps and self-signed launch are admitted only where the deployment enables them (launch also needs the wallet to allow it), LP only where the deployment enables it and the key has lp:write, and candle transfer only for a key that carries transfer:bound. Everything else refuses.

Three things are true of every TEE wallet:

  • Your vault still holds its key. You can always move its funds yourself with vault transfer, or sweep it home, with no help from Candle’s servers.
  • It has a pinned vault: one cold vault key that is its sweep destination, fixed when it was promoted. Sweeps, demotes and candle transfer --to vault all go there.
  • Trade it from the machine that holds its signer. Every TEE transaction is approved by the signer that owns the wallet. Without a key signer, that is a per-wallet relay signer in the promoting machine’s keychain, and it does not move, even when the wallet is rebound to another API key. A wallet on its key’s signer trades from the machine that holds that signer (see Key signers).

A copy of a promoted key stays in Privy’s TEE permanently. Demoting stops the agent and brings the funds home, but it does not delete that copy, which is why an in-place or batch promote asks you to acknowledge it and why a swept fresh TEE wallet is retired rather than reused.

Promoting is how a wallet moves from Tier 1 to Tier 2. There are two ways, and they differ in which address the agent gets.

--in-place--from
Which addressAn existing vault key, at its own addressA fresh key, derived for this purpose
Its funds and historyKept: the agent trades the address as it isNone: you fund it explicitly
Where the key is derivedThe ordinary vault path wallet apps scanThe vault’s TEE branch (m/44'/501'/n'/1'), which wallet apps do not scan
Pinned vault--sweep-to <cold key>The --from <cold key> itself
In a restored vaultRefused (every restored key is marked exposureUnknown, for good)Refused (a restored vault never allocates new keys)

In place, for an address that already holds what the agent should use:

Terminal window
candle vault promote --in-place trading-1 --sweep-to treasury --rpc-url https://<your-rpc>

Before anything is written the CLI shows the wallet’s holdings, checks over your RPC whether it is a token mint, freeze, program-upgrade or stake authority (nine requests; public endpoints refuse the token scans; multisig membership is not checked), prints the Candle account, API key and API that will control it, read live, and asks for the address’s last six characters and the word confirm.

Fresh, for a clean agent wallet you fund on purpose:

Terminal window
candle vault promote --from treasury --label scalper
candle vault fund <tee-address> --amount 1 --asset SOL --rpc-url https://<your-rpc>

--label names the new TEE wallet; without it the wallet is tee-<n>. vault fund to a TEE wallet always signs from that wallet’s pinned vault key (here treasury), so it takes no --from: --from applies to an external wallet only, and passing it for a TEE wallet is refused.

Many at once. vault promote-batch promotes 2 to 256 vault keys in place under one unlock and one confirm. The file is one <label> <destination> per line, or a CSV with label and sweep_to columns. Every row is checked against the whole set first, the batch checks your account has linked-wallet room for all of them, each key is committed on its own, and re-running the same file resumes an interrupted batch.

Terminal window
candle vault promote-batch --pairs-from ./promote-plan.csv --rpc-url https://<your-rpc>

Choosing the API key. A promote binds the wallet to the key the CLI is using. --to-key <prefix|label>, on promote and promote-batch, binds it to another key on your account instead. The named key’s secret never touches the machine. What happens next depends on whether the named key has a key signer:

  • No key signer: the import runs under the calling key, then the wallet is rebound to the one you named. The per-wallet signer stays on this machine, so trade the wallet from here.
  • A key signer: before the unlock the CLI reads that signer and checks it against the fingerprint this machine pinned. The first time, it asks you to type the full fingerprint the trading machine printed. The import then puts the wallet on that signer and binds it to the named key in one call, and this machine stores no signer for it. The wallet trades from the machine holding the key signer.

Promoted wallets are linked wallets, so they count against your plan’s linked-wallet cap (see Tiers and caps).

There are three ways, for three situations.

CommandWho uses itDestinationsSigned by
candle transferAn agent, through the wallet’s bound keyThe wallet’s pinned vault, or a wallet on your account that you linked while signed in or marked trustedPrivy’s TEE, after this machine approves the relay
candle vault transfer --from <wallet>YouAny addressYour vault, on your machine
candle tee sweep / candle vault demoteYouThe pinned vault, everythingYour vault, on your machine

candle transfer needs the bound key to be Read:Write:Transfer (see below). Widen the bound key with candle keys access <prefix> --access read-write-transfer, or mint a Read:Write:Transfer key and candle tee rebind the wallet to it. To the pinned vault it can send any token and max is allowed; no spend cap or volume window applies, but a single SOL transfer is limited to the server’s backstop ceiling (50 SOL by default), so a larger balance takes more than one transfer. To a linked wallet it sends base assets (SOL, USDC, CNDL, USDG), and the bound key must carry a per-asset spend cap for that asset and a USD --tx-limit; the amount is valued and counted against that window, shared with the key’s trades. A key without both is refused for linked destinations and can still send to the vault. The destination and its kind are shown before you confirm.

Terminal window
candle transfer --to vault --asset SOL --amount max --wallet trading-1
candle transfer --to treasury-2 --asset USDC --amount 250 --wallet trading-1

candle vault transfer from a promoted wallet works exactly as from any vault key, with three extra checks: it refuses while a sweep is pending, it warns before you confirm when the wallet is still enabled (an agent may be trading it at the same moment), and it records the finalized transfer in Candle’s history when the key has activity:write.

Terminal window
candle vault transfer <address> --amount 5 --asset SOL --from trading-1 --rpc-url https://<your-rpc>

Sweeping home. tee sweep moves everything a TEE wallet holds to its pinned vault, tokens first and SOL last, and re-reads the balances after finality before it reports the wallet empty. vault demote stops the agent first and then sweeps. Open Meteora DAMM v2 LP positions are closed first, through a server-built close the CLI verifies before signing.

Terminal window
candle vault demote <tee-address> --rpc-url https://<your-rpc>

A wallet you linked while signed in is trusted, so an agent’s candle transfer can send to it; a wallet an API key imported is not until you mark it. wallet trust runs as the account owner over the device token, shows a preview, and asks you to type confirm. What trust allows is in Trusted wallets.

Terminal window
candle wallet trust 'treasury-*' 'dest-*' # a label, address, id, or prefix ending in *; quote it for the shell
candle wallet untrust dest-3

If an agent misbehaves, its API key leaks, or Candle’s API is unreachable, you can stop it and recover the funds with only your vault.

  1. Stop the agent. candle tee disable <tee-address> records the stop locally first, then asks the server. It exits 0 only on a verified stop and 3 while the stop is pending. With no API key or no answer from the server it exits 1: the stop is recorded locally but not enforced, so go straight to the sweep.

  2. Cut off the key. candle keys revoke <prefix>, or revoke it in the agent console.

  3. Sweep home without depending on the API. With --emergency, a sweep does not need Candle’s API: if the API is unreachable or the key is revoked, it sweeps with only the local key and the pinned vault. If the API does answer, it still reads the wallet’s state and refuses a wallet that is still enabled, so run step 1 first. vault demote --emergency tries the stop request before it sweeps.

    Terminal window
    candle tee sweep <tee-address> --rpc-url https://<your-rpc> --emergency
    candle vault demote <tee-address> --rpc-url https://<your-rpc> --emergency

    An emergency sweep moves an open LP position’s NFT to the vault instead of closing it; unwind it later from a fresh TEE wallet.

  4. Start again with a new wallet. A swept address is retired, because its key is still in the TEE.

tee rebind (below) is not incident response: after a leak, the path is stop, sweep, new wallet.

An EVM vault key (vault new-key --chain evm) can be promoted into a TEE wallet on Hood (chain id 4663), with the same two modes and the same safeguards as on Solana. Its pinned vault is a cold EVM vault key. Funding, sweeping and vault transfer read over --rpc-url, else CANDLE_EVM_RPC_URL, else the built-in Hood RPC, and the host is printed before the first read.

Terminal window
candle vault promote --in-place hood-1 --sweep-to hood-cold # an existing EVM vault key, at its own address
candle vault promote --from hood-cold # a fresh key on m/44'/60'/n'/1'/0'
candle vault fund <0x tee-address> --amount 0.02 --asset ETH # or --asset USDG, from the pinned EVM vault key

The fresh branch m/44'/60'/n'/1'/0' is hardened at every level, which no wallet app scans. The first EVM TEE wallet moves the vault file to version 4, which a CLI older than this one refuses to open; vault restore --phrase --evm-tee-count <k> with k of 1 or more does the same.

The sealed EVM record. An EVM RPC cannot list what tokens a wallet holds, so the vault keeps a hint file beside itself: vault.evm-record.sealed (for -k <path>, the path minus .enc, plus .evm-record.sealed). Every promote writes its wallet’s start height to it, and a trade leg that lands on this machine adds the token it touched, without unlocking the vault. Each line is encrypted to a key only the unlocked vault can read, so the file shows no addresses. A lost record loses the hint, never the funds.

  • vault backup writes the record beside the copy (<copy minus .enc>.evm-record.sealed), copying only the lines that decrypt and reporting how many it dropped. It refuses if that path already exists.
  • vault verify-backup adds a ninth check: every line of the record beside the copy decrypts under the copy’s key. A copy with no record beside it passes and says so.
  • To bring a backup’s record back, move the backup file and its record together to the vault path you will open, or open the backup itself with -k. A phrase restore never brings the record.
  • vault restore refuses while a vault exists at the path, and names its record path too: move both aside. A record left behind by an earlier vault is renamed <record path>.orphaned-<UTC timestamp> (never deleted) by the write that creates the new vault’s record key.

Sweeping home. tee sweep <0x address> and vault demote <0x address> send every token the sweep finds, then ETH last as the balance minus gas, to the pinned vault. The tokens come from USDG and WETH, the sealed record, the server’s traded-token list (not under --emergency), every --token <0x...> you pass, and the ERC-20 Transfer logs to the wallet from its recorded start height or --from-block <n>. The output names every source that ran. A token received before the start height needs --token.

Terminal window
candle tee sweep <0x tee-address>
candle tee sweep <0x tee-address> --emergency --from-block 1200000 --token 0x...
  • A wallet with tokens and no ETH for gas is refused with EVM_SWEEP_NEEDS_GAS, and the output prints the vault fund command.
  • An ordinary sweep, and vault transfer --from a Hood TEE wallet, first read whether a sequenced trade holds the wallet’s nonce. They refuse while it does (WALLET_BUSY, with the operation id) and when that cannot be read (WALLET_LOCK_UNKNOWN).
  • A wallet with no linked-wallet id (never enabled, or a restored entry left stranded) has no server row. An ordinary sweep refuses with WALLET_LOCK_UNKNOWN. tee sweep --emergency moves the funds and exits 3; it does not record sweptAt or retire the entry. vault demote of that wallet takes the same emergency path, so it exits 3 as well.
  • --emergency makes no Candle API call. It compares the chain’s latest and pending nonce: when a transaction is in flight it signs at that nonce and says it may replace it, which is not guaranteed if that transaction paid a higher fee.
  • After a phrase restore there is no record on the new machine: pass --from-block (the promote height, or any earlier Hood block) or --token. Without either, the sweep says the traded tokens were not scanned and does not exit 0.
  • Exit 0 means the wallet was observed empty (ETH at most the gas a final transfer could not move) and every source the sweep needed ran.

candle tee rebind moves TEE wallets to another API key on the same account. The funds do not move. It is owner only (device token), never opens the vault, and shows a preview before you type confirm. candle tee rebinds [wallet] lists every rebind with time, from, to and the device that did it.

What happens to the signer depends on the key you move the wallets to:

  • It has no key signer: only the binding moves. Each wallet keeps its signer and trades from the machine that holds it: the machine that promoted it, or, for a wallet already on a key signer, the machine holding that key signer. The preview names which.
  • It has a key signer: the wallet’s owner moves to that signer together with the binding, in one step. The machine you run the rebind on signs each owner change with the signer that owns the wallet now, so run it on that machine. If it does not hold that signer, the rebind refuses before confirm and names the machine that does.
Terminal window
candle tee rebind trading-1 trading-2 --to-key agent-b
candle tee rebind --label-prefix tr- --to-key agent-c
candle tee rebinds trading-1

Without a key signer, a TEE wallet trades only from the machine that promoted it, because that machine holds its signer. A key signer lets a bot on another machine (an always-on server) trade a key’s wallets while the vault stays on your own machine. The key signer is generated on the machine that will trade and never leaves it.

  1. On the trading machine, with that key’s API key configured and no device token:

    Terminal window
    candle tee signer new --key tr-2

    It prints the signer’s fingerprint (CNDL-XXXX-XXXX-XXXX), an approval link and a code, and waits.

  2. The account owner approves, from the link or from a machine with the device token, by typing the full fingerprint the trading machine printed:

    Terminal window
    candle keys signer approve ABCD-EFGH --key tr-2
  3. On the vault machine, promote onto the key. The first time, type the same full fingerprint; the CLI pins it and refuses a different signer later until you confirm the new one.

    Terminal window
    candle vault promote-batch --pairs-from ./plan.csv --to-key tr-2

    The wallets trade from the trading machine at once.

Existing wallets keep trading from the machine that holds their current owner until candle keys signer move runs there. Approving a key signer changes no wallet’s owner. Wallets promoted before it keep their per-wallet signer, and wallets on an earlier signer of the key keep that one:

Terminal window
candle keys signer move tr-2

Run it on the machine holding the old signer. It signs each owner change there and moves every wallet it can onto the key’s signer. It can be run again: a wallet whose owner already moved completes without a new signature. It deletes an old signer from this machine only after the server shows no wallet left on it, and it never deletes the key’s active signer. For a revoked key, move its wallets to a live key with --to-key <key> and the device token. When the live key has a signer, the first time you type its full fingerprint, as for vault promote --to-key. Neither --to-key nor tee rebind signs an owner change to a signer other than the one you confirmed: if the key’s signer changes before the commit, nothing is signed and the command asks again next run.

A key with no signer is not a break. tee rebind and vault promote --to-key onto it work exactly as before.

Keep these in mind:

  • Do not log the trading machine in. A machine with a device token and a key signer can approve its own signer request, which removes the owner’s approval. tee signer new and candle doctor warn when both are present.
  • One trading machine per key. Running tee signer new again for the same key on a machine whose earlier signer still owns wallets is refused and names the count; --force adds the new signer and keeps the old one until keys signer move has moved its wallets. Trading from two machines means two keys.
  • --out <path> is opt-in and weaker. It also writes the signer as a plaintext PEM (mode 0600) for an SDK process that cannot read the secret store. The secret store still holds it.
  • If the trading machine is lost, its wallets cannot move to a new signer. Recover with tee disable, then tee sweep, then a fresh promote. A vault promote wallet is derived from the vault, so the sweep recovers its funds; a tee new wallet is swept only from the tee-wallets.enc on the machine that made it.
  • On a server, the signer lives in the CLI’s secret store. Without a keychain that is credentials.enc, opened with CANDLE_KEYRING_PASSPHRASE; under systemd give it through LoadCredential= or a file only the service can read, not an exported shell variable. candle doctor reports a signer this machine cannot open. Run a bot on a headless machine covers systemd, launchd and SSH, and SDK: trade with a key signer PEM covers an SDK bot using --out.

Every agent uses an API key, and a key’s access level (Read, Read:Write or Read:Write:Transfer) decides what it can do. Only Read:Write:Transfer can move funds out of the TEE wallet it is bound to, and a device login cannot mint one yet. Change an existing key’s level in place with candle keys access (below). The levels, their scopes, custom scope sets, --tx-limit and the active key limit are in Access levels.

Terminal window
candle keys create --access read-write-transfer --label rebalancer

Each key is an agent profile with its own wallet set: candle keys wallets <prefix> shows it, keys wallets set replaces it, and keys wallets scope <prefix> --scope selected|all limits it to assigned wallets. From the CLI these run under the API key, which needs launch:write; under a key, set can only narrow the set and --scope all is refused with LOOSEN_REQUIRES_SESSION. Widen it in the agent console.

Changing a key’s level in place. candle keys access <prefix|label> --access <level> moves a key between the three levels and keeps its prefix, secret, wallets, caps and --tx-limit. It shows a preview first. Widening needs the device token and the key’s prefix typed back at a terminal; nothing else supplies it. Narrowing asks for y, and --yes skips the prompt. candle keys access self --access <level> lets a key narrow its own access, never widen it. Widening to Read:Write:Transfer lets the key move its TEE wallets’ funds to their vaults at once; sending to a linked wallet still needs a per-asset spend cap and a --tx-limit on the key. candle keys access <prefix> --history lists every change with the device or key that made it.

Vault and TEE commands read Solana over an RPC endpoint you choose, never over Candle’s servers, so those reads do not tell Candle your vault addresses. (Candle does know each promoted wallet’s pinned vault, because the promote records it.)

vault list, vault promote-batch, tee status and tee sweep take --rpc-url or fall back to CANDLE_SOLANA_RPC_URL, which you can set once in your shell profile:

Terminal window
export CANDLE_SOLANA_RPC_URL="https://mainnet.helius-rpc.com/?api-key=<your-key>"

vault transfer, vault fund, vault demote and vault promote --in-place ignore that variable and need --rpc-url on every run.

vault list --balances sends every matched address to that one endpoint together, which links them for whoever runs it.