Skip to content

Candle CLI

The candle CLI is Candle from the terminal: it authorizes a device, provisions and revokes API keys, holds your keys in an encrypted vault on your own machine, gives agents dedicated TEE wallets to trade from, and trades, launches and provides liquidity through them. An agent machine never needs a browser session of its own; the one browser interaction is a device approval on whatever machine you are already signed in on.

The CLI is open source under the MIT license. Its full source is in the public candledottv/agentic repo, including the macOS Touch ID helper and the security-key helper, and every release binary is built and signed from that repository (see Verify a release).

Install the Candle CLI (macOS 13 or later, or Linux):
curl -fsSL https://candle.tv/install.sh | bash
or with Homebrew:
brew install candledottv/tap/candle
Then: candle setup

candle setup authorizes this device from your browser, shows the agent wallets as funding destinations, prints the skill and MCP install lines, and runs a full health check; candle auth login on its own does just the authorization step. The CLI targets production (https://api.alpha.candle.tv) by default; --api-url https://staging.api.candle.tv points a profile at staging for testing.

The npm package @candledottv/cli stays published for CI, programmatic use, and Windows until install.ps1 ships; npx -y @candledottv/cli@latest <command> runs it once without installing. It is also the way in on macOS 12 or earlier: the release binaries need macOS 13, and install.sh and Homebrew refuse an older Mac rather than install a binary not built for it. With no npm and no install.sh, bunx fetches the repo and runs the bin at its root:

Terminal window
bunx github:candledottv/agentic candle auth login

For repeated use, clone once and run the same bin locally:

Terminal window
git clone https://github.com/candledottv/agentic.git
cd agentic && bun install
bun packages/cli/dist/index.js auth status

Every release is signed; see Verify a Candle release to check a download by hand.

auth login runs an OAuth-style device flow:

  1. The CLI requests a device code and opens your browser to an approval screen (with --no-browser it prints the URL and code for you to open anywhere, including on a different machine).
  2. The screen shows the client name and the exact scopes being requested before anything is granted. A login with no --scopes flag requests Write (five scopes, swap:write included, never transfer:write or transfer:bound), and the screen says so. Write trades and launches and cannot move funds; pass --scopes to narrow the grant, or widen the key to Withdraw with candle keys access when an agent needs to transfer.
  3. On approval the CLI exchanges the code for a device token and immediately provisions an agent API key with the approved scopes.

The device token is the credential that manages keys from this machine. It can create, list, and revoke API keys and change their access, and nothing else: it cannot list or revoke other devices, and it cannot approve another device’s login, so a leaked token cannot quietly widen its own access. Device tokens do not expire; you revoke them from the Authorized devices panel in your Agent access dashboard, and every key a device minted is labeled with that device, so containing a compromise is one action, not an investigation. See device authorization on the access page for the server-side view.

If key provisioning fails (agent access is not approved on an embedded wallet yet, or the account is at its 12-active-key ceiling), login still succeeds and stores the device token, and the output says exactly why the key was not issued. Fix the cause, then run candle keys create --access write for the same Write key a login without --scopes mints.

CommandWhat it does
auth login [--scopes <a,b,c>] [--label <name>] [--no-browser]Authorize this device and mint its API key; --label names both the device and the key
auth statusShow which credentials are stored and where
auth logout [--keep-key]Clear local credentials; --keep-key skips revoking the stored API key on the server, and the key is still removed from this machine
agent profile show|set [--bio <text>] [--strategy <tag>] [--strategy-note <text>] [--models <list>] [--links <JSON>] [--avatar <URL>] [--banner <URL>]Read or edit this agent’s public page with its own key (unreleased source). Editing requires an acting scope, permission on and no admin lock. Name, slug and visibility remain owner-only. --models is comma-separated; empty clears. --links replaces website/X as a JSON object, or null to clear. The server validates fields and shares a 20-edit UTC-day quota across keys
agent post create --text <text> [--chain <solana|hood> --mint <address>] · agent post delete <post-id> · agent post list [profile-id] [--limit <n>] [--cursor <cursor>]Publish, delete and read the posts on an agent’s own public page (unreleased source). A post is 1 to 500 characters of free text with line breaks, shown as plain text, with an optional token chip; it is tied to no trade or launch. create and delete act as this key’s profile and need an acting scope, permission on and no admin lock; the profile’s keys and its owner share 30 posts per UTC day. list is public: with no profile id it reads this key’s own profile
keys list [--scopes]List the account’s API keys, revoked ones included: each key’s name, its Read, Write or Withdraw access, and whether it may use the embedded wallet (Embedded); --scopes adds the raw scopes
keys create [--access read|write|withdraw | --scopes <a,b,c>] [--label <name>] [--expires-in <days>] [--tx-limit <usd> [--reset daily|weekly|monthly|never]] [--embedded-wallet deny|allow]Create an API key over the device token; --access mints one of the three levels (withdraw can move funds out of the wallet it runs, to wallets you linked while signed in or marked trusted and to that wallet’s vault), --scopes names raw scopes, never both. With neither, the key gets the API’s default set (launch:write, launch:read, account:read, activity:write), which cannot trade; pass --access write for a trading key. --embedded-wallet defaults to deny, so the key cannot spend the account’s embedded wallet; allow asks for a confirmation at a terminal. See the embedded-wallet permission. --expires-in sets the key’s lifetime in days; --tx-limit sets an optional USD volume cap, with --reset choosing its window
keys access <prefix|label|self> --access read|write|withdraw [--yes]Change an existing key’s level in place: same key, secret, wallets and caps. A preview on stderr, then a confirmation. Widening takes the device token and the key’s prefix typed back at a terminal; narrowing takes y, or --yes. self uses the profile’s own API key and can only narrow it
keys access <prefix|label> --historyOne key’s access changes, newest first: when, from, to, and the device or key that made each
keys update <prefix|label> --embedded-wallet allow|deny [--yes]Allow or deny a key the account’s embedded wallet, over the device token. A preview on stderr, then a confirmation. Allowing takes the key’s prefix typed back at a terminal; denying takes y, or --yes
keys self embedded-wallet denyThe profile’s own API key gives up the embedded wallet, with no prompt, so a bot box can run it. A key can never allow itself; the owner does that with keys update or in the web key manager
keys revoke <prefix>Revoke one API key by prefix
keys wallets <prefix>The wallets an agent profile can use. TEE wallets move between keys with tee rebind
keys wallets set <prefix> --wallets <id,id>Replace the profile’s wallet set. From a key it can only narrow the set; to give a key TEE wallets, use tee rebind
keys wallets scope <prefix> --scope all|selectedselected limits the profile to its assigned wallets; all lifts that
keys signer approve <code> --key <prefix|label> [--reject]Approve (or reject) a key’s signer request as the account owner, over the device token. Shows the key, the full fingerprint and what happens to the key’s wallets, then asks for the full fingerprint the trading machine printed (hidden, all three groups; one group is refused)
keys signer move <prefix|label> [--to-key <prefix|label>]On the machine holding a wallet’s current signer: move every wallet on the key onto its active key signer, per-wallet (legacy) wallets included, signing each owner change here. The key’s API key or the device token. Resumable; deletes an old signer from this machine only after the server shows no wallet left on it, never the active one. --to-key moves a revoked key’s wallets to a live key (device token)
walletShow the account’s launch and linked wallets (wallets is an alias)
wallet import --chain <solana|evm> [--address] [--label] [--key-file] [--signer-out]Import a wallet you own; see below
wallet revoke <wallet-id>Stop a linked wallet’s agent; exit 0 only when the remote denial is verified, 3 while it is pending
wallet trust <label|address|id|prefix*>...Mark linked wallets as yours, as the account owner only: it takes the device token, never an API key. A trusted wallet counts as your own, like one you linked while signed in: a move into it from the embedded wallet is uncapped, and candle transfer from a TEE wallet may send to it (capped by the bound key). The server resolves the selectors in a preview; a screen, then type confirm
wallet untrust <label|address|id|prefix*>... [--yes]Clear the mark: moving funds into the wallet needs the withdrawal allowlist again. Same preview and confirm; --yes skips the prompt
wallet allow-launch <label|address|id|prefix*>...Let TEE wallets pay for a launch, so candle launch can create a token from them, as the account owner only: it takes the device token, never an API key. The server resolves the selectors in a preview; a screen shows each wallet’s label, address and bound key, then type confirm. A wallet that is not a TEE wallet is named and left alone
wallet disallow-launch <label|address|id|prefix*>... [--yes]Stop TEE wallets paying for a launch; trading is unchanged. Same preview and confirm; --yes skips the prompt
wallet close-empty [--wallet embedded] [--keep <mint>]... [--client-trade-id <id>] [--yes]Close the embedded wallet’s empty token accounts and return their rent to it, after a preview (transfer:write). --keep spares a mint’s account; --yes skips the prompt
vault init [--own-passphrase]Create vault.enc: one passphrase factor and a 24-word recovery root. Creates no key. The generated passphrase is shown once and is not typed back: save it where you keep passwords, because this CLI keeps no copy. --own-passphrase chooses your own (16+ characters), and the prompt offers that choice too. On a Mac with iCloud Drive it offers to back the new vault up
vault status [--unlock]What the vault holds and what opens it; --unlock adds the keys, counters and exposure flags, and reports any label more than one key shares; one line per key with optional balances is vault list
vault list [<filter>] [--balances] [--rpc-url <url>] [--evm-rpc-url <url>]One line per key: address, label, role and derivation. The index is encrypted, so it prompts for the passphrase. An optional <filter> keeps the keys whose label or address contains it, case-insensitively. Offline unless you ask for balances: --balances adds a SOL column and a total over the matched Solana keys, read from --rpc-url, else CANDLE_SOLANA_RPC_URL, else your profile’s RPC, else the public endpoint (which rate-limits, and sees every matched address), in one request per 100 matched keys. EVM keys are read over their own endpoint (--evm-rpc-url, else CANDLE_EVM_RPC_URL, else the built-in Hood RPC): ETH per key, and USDG when the chain id is 4663; that host is printed on stderr before the read, and the Solana endpoint is never sent an EVM address. Every matched address goes to its one endpoint together, which links them. SPL token balances are never read
vault new-key --chain solana|evm [--label <name>] [--count <n>] [--labels-from <file>]Derive the next Solana key (m/44'/501'/n'/0') or EVM key (m/44'/60'/n'/0/0, BIP-32 secp256k1, one address on every EVM chain) inside the vault. --count or --labels-from (one label per line) derives a batch of up to 256 under one unlock. The name must be free, and a batch refuses before it derives anything. Refuses in a vault built by restore
vault rename <label|address|id> <new-label> [--id <entry-id>]Rename one vault or external key. New labels are at most 64 characters, without edge whitespace, control characters or a leading -; names are never trimmed. Existing longer labels still work as targets. The address, the derivation path, the key blob and every envelope are unchanged; only the index is rewritten. Names a key by label, or by address, entry id or --id when more than one key shares a label. A TEE wallet is refused: its label also lives on the linked wallet Candle holds, and tee rename changes both
vault rename --pairs-from <file> [--dry-run]Rename 1–256 vault or external keys with one unlock and one commit. One <label|address|id> <new-label> per line, or CSV columns from,to picked by name; blank lines and # comments are skipped. Use CSV for names containing spaces. All findings are reported before any write; duplicate targets/new names and collisions outside the batch refuse the whole file. Swaps work. --dry-run prints the checked plan and writes nothing; --json returns { ok, renamed: [{ id, address, role, from, to }] } (plus dryRun: true for a dry run). A terminal is required
vault phrase showShow the 24-word recovery phrase, after a fresh passphrase or security key (PIN and touch). Terminal only, both ends; never in --json, a log or a pipe
vault restore --phrase [--own-passphrase] [--count <n>] [--tee-count <k>] [--external-count <e>] [--evm-count <m>] [--evm-tee-count <k>] [--rpc-url <url>]Rebuild a vault from the recovery phrase. Derived keys only, on all three Solana branches and, with --evm-count <m>, EVM indices 0 to m-1, and with --evm-tee-count <k>, Hood TEE wallets 0 to k-1 on m/44'/60'/n'/1'/0' (both default 0; the EVM branches are never gap-scanned, and --rpc-url stays the Solana endpoint). The sealed EVM record does not come back, so a later Hood sweep needs --from-block or --token; a gap scan needs --rpc-url on the command; no default or stored endpoint is used for it; see below. The restored vault gets a NEW passphrase: the one that opened the vault the words came from does not carry over. It is generated and typed back by default; --own-passphrase chooses your own
vault reconcile-exposureRe-read this account’s linked wallets and flag any that match. Adds exposure, never clears it
vault factor listThe factors that can open this vault, with their domains and whether this machine can drive them
vault factor add passphrase [--own-passphrase] [--label <name>]Add a second passphrase factor; the vault is re-opened with it before this reports
vault factor add security-key [--label <name>] [--device <id>] [--install-helper]Enroll a FIDO2 security key (CTAP2 hmac-secret, user-verified): the key registers a credential, one assertion derives the vault’s key, and the vault is re-opened with the key before this reports. Three touches. Opens the vault with the passphrase or another security key; with two keys attached, --device names the one to add, or the CLI asks for it after the vault opens; a key already enrolled on this vault is refused (VAULT_KEY_ALREADY_ENROLLED). --install-helper first downloads and verifies this release’s candle-fido2 (the same checks candle update makes) when a release binary has none beside it, then enrols
vault factor add touch-id [--label <name>]Not yet released: waits on Apple’s approval of the signed macOS helper. Enroll this Mac’s Secure Enclave behind Touch ID: the signed helper creates a P-256 key the Enclave holds, the vault’s key is wrapped to it, and the vault is re-opened with Touch ID before this reports. Opens the vault with the passphrase or a security key. This Mac only; never a recovery factor
vault factor add passkey [--label <name>]Not yet released: waits on the signed macOS helper and its passkey entitlement. Enroll a synced passkey (iCloud Keychain) through the same signed helper on macOS 15 or later: the platform registers a credential under cli.candle.tv with the PRF extension, one assertion derives the vault’s key, and the vault is re-opened with the passkey before this reports. Three passkey sheets. Opens the vault with the passphrase or a security key. Recoverable through the Apple account, and never counted as independent of anything else in it
vault factor remove <id>Remove a factor from this file. Not revocation; see below
vault enroll <kind> [--label <name>] [--install-helper]The same command as vault factor add <kind>, spelled shorter: passphrase, security-key, touch-id, passkey. Same flags, same refusals, same --json
vault backup --to <path>|icloud [--accept-shared-domain]Copy the vault and verify the copy in full, all eight checks (nine for a vault with Hood TEE wallets, which also writes its sealed EVM record beside the copy as <copy minus .enc>.evm-record.sealed). --to icloud is a shorthand for a timestamped file in ~/Library/Mobile Documents/com~apple~CloudDocs/Candle/. The copy is sealed unless it goes to a recognised local disk or removable drive: it carries the passphrase and any security key, never a synced passkey or Touch ID, so iCloud Drive, another cloud folder, and any path the CLI cannot place are all sealed; --accept-shared-domain writes the full copy
vault verify-backup <path>Verify an existing copy in full: the root, every key, and the address set against this vault, plus, for a vault with Hood TEE wallets, every line of the sealed EVM record beside the copy. Opens the copy with the passphrase or a security key it carries
vault promote --in-place <label> --sweep-to <label> [--rpc-url <url>] [--to-key <prefix|label>]Promote one vault key in place. Reads its holdings and, over your RPC, whether it is a token mint, freeze, program upgrade or stake authority (multisig membership is not checked); prints one sentence, the account, API key and API that will control it, read live; then the last six of the address and the word confirm. --to-key names the key the wallet ends up on, whose secret never touches the machine. With no key signer on it, the import runs under the calling key, then a rebind (device token) moves the wallet to the named key; a wallet whose rebind fails is named as still on the calling key with the tee rebind command that finishes. With a key signer, the CLI checks that signer against its pin before the unlock (the first time, you type its full fingerprint), then imports onto it and binds the wallet to the named key in one call; no signer is stored here and the wallet trades from the machine holding the key signer
vault promote --from <label> [--label <name>] [--to-key <prefix|label>]Promote a fresh key: derive the next key on the vault’s TEE branch (m/44'/501'/n'/1', a path wallet apps do not scan; for an EVM key, a Hood TEE wallet on m/44'/60'/n'/1'/0') and promote that, pinned to the cold vault key --from names as its sweep destination. --label names the new TEE wallet (default tee-<n>). The new wallet starts empty; fund it with vault fund, which signs from that pinned key and takes no --from. Refuses in a vault built by restore. --to-key works as it does for --in-place. An EVM key, --from or --in-place, becomes a Hood TEE wallet: --sweep-to is then a cold EVM vault key, --rpc-url is the Hood RPC, and the vault moves to version 4
vault promote-batch --pairs-from <file> [--rpc-url <url>] [--to-key <prefix|label>] [--token-holdings]Promote many vault keys in place under one unlock. Every row is checked against the whole set first, one acknowledgement covers the set (the word confirm), and each key is committed on its own. --to-key names the key the wallets end up on: the import runs under the calling key, then a rebind (device token) moves every promoted wallet to the named key in requests of at most 200; the target is checked before the unlock, a wallet whose rebind fails is named as still on the calling key, and re-running the same file rebinds the rows that landed. When the named key has a key signer, each import goes onto that signer and is bound to the named key in the same call, after the same pin check as vault promote. --token-holdings reads each key’s Solana token accounts into the table (refused on a Hood batch). A file of EVM keys promotes them to Hood TEE wallets; one file is one chain
vault transfer <to> --amount <n|max> --asset SOL|<mint>|ETH|USDG|<0x token> --from <label> [--rpc-url <url>]Sign a transfer locally from a vault key or a promoted TEE wallet. From a Solana key: SOL, or any SPL or Token-2022 mint, over --rpc-url, else CANDLE_SOLANA_RPC_URL, else your profile’s RPC, else the public endpoint. From an EVM vault key: ETH (the chain’s native asset) or an ERC-20 (USDG on Hood, else its contract address), on Hood by default through the built-in RPC, or on any EVM chain by --rpc-url (else CANDLE_EVM_RPC_URL); the chain id is always read from the RPC, never from a flag, and max sends the balance minus the fee cap. One verification: the unlock, whose prompt names the amount, asset and destination. Then the transfer is decoded and displayed with the fee quote, the destination is printed in full in groups of four (for an ERC-20, the recipient decoded from the calldata, not the contract), and you type confirm; anything else signs nothing. There is no --yes. An EVM exit 0 means depth-confirmed (1 block on Hood, 2 elsewhere), not finalized; exit 3 with the hash means the transaction may still land, so do not resend blindly. From a promoted wallet it refuses while a sweep is pending, warns before the confirmation when the wallet is enabled (an agent may be trading it), and reports the finalized transfer to Candle’s history when the API key has activity:write
vault fund <tee-address|external> --amount <n> [--asset SOL|USDC|ETH|USDG] [--rpc-url <url>] [--from <label>]Sign a funding transfer from a vault key locally: to a TEE wallet from its pinned vault key, or to an external wallet (--from names the source when the vault has several keys). A Hood TEE wallet takes ETH or USDG from its pinned EVM vault key. One unlock that names the send, then decoded, displayed with the destination grouped in fours, and confirmed by typing confirm; there is no --yes
vault demote <tee-address> [--rpc-url <url>] [--emergency] [--sweep-to <label>] [--token <0x...>] [--from-block <n>]Stop a promoted wallet’s agent, then sweep everything it holds back to its pinned vault key, signed locally. It tries the stop request first. --emergency sweeps even when that request fails, for when the API or the key is unavailable or compromised; see tee sweep. A Hood wallet takes tee sweep’s --token and --from-block
vault export-key <label> --to <new-file>Export one key as plaintext to a new file, through an interactive ceremony. The vault records the export, and the key is treated as exposed from then on
vault import-legacy --tee [--from <path>]Copy TEE wallets created with the older tee new flow from tee-wallets.enc into the vault as imported keys (the recovery phrase does not restore them); retire-legacy then renames the old store
vault retire-legacy [--from <path>]Rename the old tee-wallets.enc store once a verified backup of the vault exists
external new [--label <name>]Derive an external wallet on the vault’s third branch (m/44'/501'/n'/2') for outside tools: never delegated, never registered with Candle. Refuses in a vault built by restore; see below
external listThe external wallets in the vault
external sweep <external> --to <vault> [--rpc-url <url>]Send everything an external wallet holds (SOL, SPL, Token-2022) back to a named vault key, signed locally with the external key; one unlock that names the send, then the vault key’s address grouped in fours and a typed confirm
sign [--file <path>] --wallet <external>... [--rpc-url <url>] [--broadcast] [--yes]Decode, simulate, display and sign a base64 transaction any tool built, with an external wallet only; --rpc-url is the RPC for the lookup tables, the simulation and --broadcast; see below
sign message --wallet <external> [--file <path>] [--yes]Sign an off-chain message: the exact bytes of the file or of stdin
secrets set <name>Store one of your own third-party API keys in the OS keychain, under a namespace separate from Candle’s credentials; the value is typed on a hidden prompt and never shown again
secrets listThe names of the secrets stored for this profile
secrets remove <name>Delete a stored secret
pluginsList the candle-<name> executables on your PATH, each of which runs as candle <name>; see below
tee new [--label <name>]Seal a fresh dedicated Solana TEE wallet key into tee-wallets.enc; no network
tee enable <address> --vault <address>|--vault-key <label>Delegate the TEE wallet key to this profile’s agent with the sweep vault pinned; --vault-key names the vault key by label instead of address
tee fund <address> --amount <n> [--asset SOL|USDC]Print the funding instruction for your vault to sign; signs nothing
tee status <address> [--rpc-url <url>]The server’s lifecycle state and on-chain balances; for a Hood TEE wallet, ETH, USDG, the gas reserve and gas: low
tee disable <address>Stop the agent; typed state, exit 3 while remote enforcement is unconfirmed
tee sweep <address> [--rpc-url <url>] [--emergency] [--token <0x...>]... [--from-block <n>]Sign locally and move everything to the pinned vault, then re-check the balances; an open DAMM v2 LP position is closed first through a server-built close the CLI verifies before signing, and --emergency moves the position NFT instead. --emergency lets the sweep run when the API cannot be read (down, or the key revoked); if the API answers, a wallet still enabled is refused, so run tee disable first. On a Hood wallet, --token (repeatable) adds ERC-20s to sweep and --from-block sets where the Transfer-log scan starts; see below
tee rebind <wallet...> --to-key <prefix|label> [--label-prefix <p>]Move promoted TEE wallets to another API key on this account; this is how a key gets TEE wallets (keys wallets set from a key can only narrow its set). Name the wallets, or pass --label-prefix <p> for every TEE wallet whose label starts with <p>. As the account owner only: it takes the device token, never an API key, and never opens the vault. Preview, a screen, then type confirm; the funds do not move. Onto a key with no key signer each wallet keeps its signer, and the screen names the machine that holds it. Onto a key with a key signer, the owner moves to that signer with the binding, signed on this machine by the wallet’s current signer; the rebind refuses before confirm when this machine does not hold it. Not incident response: after a key leak the path is still stop, sweep, new wallet
tee signer new --key <prefix|label> [--out <pem>] [--force]On the trading machine, with that key’s API key: generate the key’s signer here, request it, and wait for the owner’s approval. The key’s wallets on that signer then trade from this machine. Run it again to resume a pending signer. Refused while an earlier signer here still owns wallets, unless --force. --out also writes a plaintext PEM (mode 0600) for an SDK process; the secret store keeps its copy. Warns when this machine also holds a device token
tee rebinds [wallet]List this account’s TEE wallet rebinds: time, wallet, from, to, actor and the device prefix that did it. A device token cannot revoke devices; revoke one you do not recognise from the portal
tee rename <label|address|id> <new-label>Rename one TEE wallet: the label Candle holds and, when the wallet’s key is in this vault, the vault entry. New labels are at most 64 characters, without edge whitespace, control characters or a leading -; names are never trimmed. As the account owner only: it takes the device token, never an API key. The server resolves the wallet by id, address or exact label. The funds, the address, the key and the bound API key do not change
tee rename --pairs-from <file> [--dry-run]Rename 1–256 TEE wallets with one server call, all or nothing, then one vault commit. One <label|address|id> <new-label> per line, or CSV columns from,to picked by name; blank lines and # comments are skipped. All findings are reported before any write. The vault is unlocked once, only when a named TEE wallet may be in it, and a new name that a vault key outside the rename already has refuses before the server is called. --dry-run prints the checked plan and changes nothing; --json returns { ok, renamed: [{ id, chain, address, from, to, server, vault }] } (plus dryRun: true for a dry run). If the vault write fails after the server accepted, the command prints address-keyed rows to re-run (rerun under --json): a re-run leaves the server label alone and finishes the vault
swap <from> <to> --amount <n>|--percent <n> --wallet <tee> [--to <tee>] [--slippage-bps <n>] [--client-trade-id <id>] [--rpc-url <url>] [--yes] [--wait]Quote, confirm and swap on one chain through the TEE wallet’s bound key, or bridge SOL or USDC and ETH or USDG into the same key’s TEE wallet on the other chain (below). Every plan can sell a token it holds that was not launched on Candle; buying one needs Pro or Max (TIER_REQUIRED otherwise). --client-trade-id makes a retry safe, --yes skips the confirmation
swap status <id> [--kind trade|swap|launch] [--wait]Read an operation without resending it; for a bridge, whether the deposit landed, the fill or refund, and whether it still holds a sweep. --wait follows it for up to ten minutes
transfer --to <address|wallet name|vault> --asset <SOL|USDC|CNDL|ETH|USDG>|--mint <mint>|--token <0x...> --amount <decimal|max> [--wallet <tee>] [--rpc-url <url>] [--yes]Move funds out of a TEE wallet through its bound Withdraw key: to the wallet’s own pinned vault (--to vault, any token, max allowed) or to another of the account’s wallets you linked while signed in or marked trusted (base assets, counted against the key’s caps, which must include a per-asset cap for the asset; a USD transaction limit binds only if the owner set one). The destination and its kind are shown before you confirm; Candle builds, the relay signs, Candle broadcasts. ETH, USDG or --token moves out of a Hood TEE wallet (below). --rpc-url is your own Solana RPC, or on Hood the EVM RPC a --token’s decimals are read over
launch --name <name> --symbol <symbol> --image <path>|--image-url <url> --wallet <tee> [--buy <amount>] [--description <text>] [--quote-asset <asset>] [--dex-version v3|v4] [--mode <mode>] [--client-trade-id <id>] [--rpc-url <url>] [--yes]Create a token on the wallet’s chain. The image is --image-url <url> for one already hosted at an https:// URL, or --image <path> for a PNG, JPEG, GIF or WebP file on this machine, at most 10MB; never both. With --image the file is uploaded first and the launch is made with the URL that comes back (imageUrl in the --json result). The upload happens before the confirm prompt and makes the file public and permanent (more). Without --buy it makes no first buy, and the buy is a separate candle swap. --buy <amount> adds a first buy from the same wallet, in whole units of the quote asset (as --amount is for a swap), within the dev-buy ceilings and reserved against the key’s USD spend window before anything is signed: in the launch transaction on Solana, as a last leg after the curve and the fee on Hood. --buy needs the server’s TEE_LAUNCH_BUYS_ENABLED, which is off by default; while it is off Candle refuses the buy and nothing is built or signed. Launching needs launch:write and allowLaunch on the wallet, which only the account owner can turn on (candle wallets allow-launch; more), and launching from a TEE wallet needs Pro or Max (TIER_REQUIRED otherwise). Every plan can launch from its embedded wallet, with an optional dev buy (same transaction on Solana; best-effort follow-up on Hood), through a headless launch (MCP candle_launch_and_seed, SDK launch()); this command does not use that path. A Hood TEE wallet launches on Hood and needs --dex-version. --mode is the launch mode, as in a headless launch; --client-trade-id makes a retry safe; --yes skips the confirmation
image upload <path>Upload a PNG, JPEG, GIF or WebP file, at most 10MB, and print the https:// URL Candle hosts it at, ready to be a launch’s imageUrl. The file becomes public and permanent. Needs launch:write; --json adds the content type and the size in bytes (more)
pnl [--profile <name>] [--json]P&L: realized net, fees, unrealized and total, then open positions with average entry, mark and unrealized. The figures are the console’s, to the cent for the same moment: Total is the server’s totalUsd. Without --profile it is the account’s books, one ledger for every profile, the web app and the CLI, and needs a key with the Read scope (account:read); it adds a By agent table (each key, then Manual for web-app trades, then the account total the rows sum to). --profile <name> reads that profile’s key’s share of the same P&L: realized on the sales it made and the open positions that belong to it (the wallet’s bound key, else the key whose buy opened them), with a row per wallet that sums to the total. Positions live in wallets: each wallet is its own average-cost pool, one token in two wallets is two rows (a WALLET column where the API names the wallet), and a recorded move between the account’s wallets carries its cost and realizes nothing. Both show a Solana block, a Hood block, then the combined figure, the closed positions as money made, minus money lost, plus partial sells, and a CHAIN column on positions and wallet rows; --json passes the API’s body through, with chain on each, solana and hood (account) or pnl.byChain (profile), and the additive fields (totalUsd, closed, openPositionsExDust, dust, byAgent). A bridge between Solana and Hood is not a trade: it opens and closes nothing, and bridge and Relay costs are not netted from P&L. Unpriced positions are shown as unpriced, never valued at zero; a position worth under one cent is marked dust and still counted; a truncated history says so. DAMM v2 LP positions are included as their own lines and table: realized (withdrawals against the cost basis at the add, plus claimed fees), unrealized (open positions at their share of the pool plus unclaimed fees, at current marks, against that basis) and vs holding (what the deposited tokens would be worth held); Total stays the console’s token figure and Total with LP covers tokens and LP. A position that is unpriced, unreadable, or closed outside the ledger (a sweep close writes no confirmation) is shown as such and never valued. With --profile, a separate Hyperliquid section shows the key’s realized perps P&L (gross, funding, fees, net), never added to the spot or LP total; unrealized perps are left out, and coverage is limited to the wallet’s current binding
portfolio [<filter>] [--chain solana|hood] [--rpc-url <url>] [--evm-rpc-url <url>] [--json]Every wallet on Solana and Hood in one table: vault, TEE and embedded, each token with its chain, amount, price and value, then subtotals per group and per chain and a total. TEE and embedded balances on both chains come from Candle. Solana vault and external wallets are read over --rpc-url, else CANDLE_SOLANA_RPC_URL, else your profile’s RPC, else the public endpoint, and Candle is sent only the mints they hold, for prices, never a vault address. EVM vault keys are read over --evm-rpc-url, else CANDLE_EVM_RPC_URL, else the built-in Hood RPC, for ETH and USDG only (the CLI cannot list an EVM key’s other tokens), priced by asset and never by address; an RPC that is not Hood (chain id 4663) is refused with EVM_CHAIN_MISMATCH. --chain shows one chain and reads nothing on the other. Unpriced tokens are shown as unpriced and left out of the total, and a Hood token says why (no-market-row, stale-mark, unusable-price, source-unavailable); empty wallets are counted, not listed; a wallet that could not be read in full exits 3, and the footer names its chain. When Candle listed only part of the account’s wallets, the result stays partial even if every wallet shown was read, and no match says so. --json carries chain on every wallet and holding, byChain subtotals, and unread halves (sol, tokens, eth, usdg, hood-tokens). Each Solana TEE wallet’s DAMM v2 LP positions follow the tokens in a second table (share of the pool plus unclaimed fees, valued by Candle at the same marks, no Meteora code in the CLI) and count in the wallet, group and total figures; a position whose pool could not be read is shown as not read, left out, and exits 3 With <filter>, only the wallets whose label or address contains it are read and shown, in every group (case-insensitive, no glob): vault keys it leaves out are never sent to your RPC, the subtotals and total are for the matched wallets, and the first line says how many matched.
plans [--json]The plans this API serves: prices, fees, limits and what each can do. No key
feescandle fees reads the account’s claimable creator fees. A Phantom-signup account’s Solana fees read as wallet_not_signable: the agent cannot sign for that external wallet.
fees claim <mint> [--dry-run]candle fees claim <mint> [--dry-run] claims creator fees only to the recipient fixed at launch; there is no destination option. --dry-run sends no transaction.
stake [--wallet <wallet>]Read Solana CNDL/sCNDL, withdrawals, rewards, pool wait and eligibility notes
stake add <amount> [--wallet <wallet>] [--client-stake-id <id>] [--yes]Stake decimal CNDL; embedded by default or a TEE wallet with a local relay signer
stake remove <amount> [--wallet <wallet>] [--client-stake-id <id>] [--yes]Start a withdrawal of decimal sCNDL, without instant unstake
stake finish [<withdrawal>] [--wallet <wallet>] [--client-stake-id <id>] [--yes]Finish after the pool wait; omit only when exactly one withdrawal is ready
stake claim <rewardId> [--wallet <wallet>] [--yes]Pay a Solana staking reward to its recorded owner
stake claim --all [--wallet <wallet>] [--yes]Claim all listed rewards for the selected wallets
lp pools <token> [--page <n>]Meteora DAMM v2 pools listing a token: liquidity, fee rate, 24h volume and an estimated yield (indexed figures, not a quote)
lp add <pool> --amount <n> <token> --wallet <tee> [--position <nft>] [--slippage-bps <n>]Open a DAMM v2 position, or add to one, from a TEE wallet through its bound key (lp:write, opt-in); the other side follows the pool’s ratio, both pool tokens’ Token-2022 warnings are shown before you confirm, and there is no Candle fee
lp positions [--wallet <tee>]Every DAMM v2 position across your TEE wallets, with unclaimed fees and USD marks where a price exists
lp remove <position> --percent <n> [--wallet <tee>] [--slippage-bps <n>]Withdraw part of a position; at 100% it also claims the fees and closes the position, returning its rent
lp claim <position> [--wallet <tee>]Claim a position’s fees and any rewards

Every lp write (add, remove, claim) also takes --rpc-url <url> (your own Solana RPC), --client-trade-id <id> (the same id never deposits or withdraws twice) and --yes (skips an ordinary confirmation). --slippage-bps is 0 to 1000, default 100.

CommandWhat it does
perps setup [--wallet <tee>]Approve Candle’s Hyperliquid builder fee (0.1%) once for the key’s EVM TEE wallet, and show the Hyperliquid account’s mode and balance. Idempotent: nothing is signed when the approval is already there
perps deposit <amount> <SOL|USDC|ETH|USDG> [--wallet <tee>] [--to <evm tee>] [--id <clientDepositId>] [--slippage-bps <n>] [--rpc-url <url>] [--yes] [--no-submit]Move funds onto the key’s Hyperliquid account through Relay, from the key’s Solana or Hood TEE wallet (swap:write). From Solana, --to names the EVM TEE wallet whose account receives it and --rpc-url is the RPC the deposit check reads; --id is the idempotency id (default: a new one); --slippage-bps is Relay’s slippage (default 100)
perps deposit status <id>Read one deposit by its id
perps open <coin> <long|short> <size> [--price <px>] [--tif Gtc|Alo|Ioc] [--slippage-bps <n>] [--tp <px>] [--sl <px>]Open or add to a perpetual position on Hyperliquid’s main perp exchange from the key’s EVM TEE wallet (perps:write, opt-in): at market (IOC within the slippage bound) without --price, a limit order with it, and optional reduce-only take-profit and stop-loss triggers. Candle builds the action within the key’s limits; the CLI recomputes its hash and checks the action type and Candle’s builder before anything is signed, then the relay signs and the CLI submits to Hyperliquid. Every plan except Max pays Candle a builder fee on each order (0.1% today; candle plans shows the rate in force); Max pays none. --no-submit stops after the check
perps close <coin> [--size <n>] [--slippage-bps <n>] [--wallet <tee>]Close all or part of a position with a reduce-only IOC order. It reserves nothing, so a key at its USD cap can still close
perps cancel <cloid>Cancel an order Candle built (an open, or one of its take-profit or stop-loss legs) by its cloid
perps orders [--wallet <tee>] [--limit <n>]Open orders on Hyperliquid, and every action Candle built for the wallet with what the venue shows for it
perps positions [--wallet <tee>]Positions, account value and withdrawable balance, read live from Hyperliquid by the wallet’s address
perps leverage <coin> <x> [--isolated]Set a market’s leverage and margin mode on the account, within the key’s maximum leverage

setup, open, close, cancel and leverage also take --sign-only (build, check and sign through the relay; submit nothing), --no-submit (build and check only) and --yes (skip the confirmation).

CommandWhat it does
profile listProfiles on this machine, with their cached accounts
profile add <name> --api-url <url>Create a profile before authenticating it
profile use <name>Make a profile the active one
profile rename <old> <new>Rename a profile
profile remove <name> --yesDelete a profile and its stored credentials
profile set <name> --rpc-url <url> | --clear-rpc-urlStore this profile’s Solana RPC, used when --rpc-url and CANDLE_SOLANA_RPC_URL are not given, or clear it. Stored in config.json (not a secret store: a provider key in the URL sits in plain JSON on this machine); only its host is ever shown, in profile list and on every command’s host line
setup [--no-browser]One wizard: authorize, fund, connect an agent, verify
mcp [--tools <a,b,c>] [--read-only] [--print-config]Run the Candle MCP server, built into the binary; see below
doctor [--role owner|bot|auto]Diagnose CLI setup: credentials, storage backend, API reachability, wallet delegation, the account’s plan and trade fee, security key helper. --role picks which rows apply; auto (the default) is bot when an API key is stored and no device token is
verify <file> --bundle <path> [--identity <uri>] [--issuer <url>]Verify a release asset against its Sigstore bundle. --identity is required when no latest.json sits beside the bundle; --issuer defaults to GitHub Actions’ token issuer
update [--check] [--to <tag>]Update the CLI and its security key helper to the latest signed release
completion <zsh|bash|fish>Print a shell completion script to stdout; see below
help [<command>]The top-level screen, or one command’s subcommands, flags, examples and environment

Global flags: --api-url <url>, --profile <name>, --json for machine-readable output, --factor <id|kind> and --device <id> (vault commands: which envelope or security key unlocks), --no-verify-account (skip the check that the stored key belongs to the profile’s account), --help (-h), --version (-v).

Every command that reads or writes vault.enc (vault *, tee new, enable, fund, status, disable and sweep, external *, sign, portfolio) takes -k <path>, the short form of --keystore <path>, for one invocation. For a permanent location set CANDLE_CONFIG_DIR instead: it moves vault.enc, tee-wallets.enc, the credentials and the config together, and it is state you can see in your shell. A path that still begins with a literal ~ is refused rather than expanded, on every flag and positional whose value is a file the CLI reads or writes, because a shell leaves ~name alone when no such user exists and guessing at a path that holds keys is worse than stopping.

Two escape hatches stay off the help screen on purpose. --accept-older-copy (most vault commands) opens a vault file older than one this machine has already seen, for when you restored an older copy deliberately. --accept-unknown-exposure (vault promote, vault promote-batch, tee enable, and vault demote on Hood) admits a key whose history is unknown, as every key in a restored vault is.

The CLI carries the Candle MCP server inside its own binary, so candle mcp starts it with no download and no Node runtime on the host. The key comes from the CLI’s credential store at launch, which is what keeps it out of your MCP client’s config file:

Terminal window
candle mcp --print-config

That prints a ready-to-paste block naming this binary by absolute path. --read-only pins the server to the eight tools that authenticate with nothing; --tools <a,b,c> takes an explicit allowlist. The MCP server page documents the tools themselves and the other two ways to run it.

candle vault is self-custody on your own machine: vault.enc in your config directory, holding one data key wrapped once per factor, one encrypted blob per private key, and a 24-word BIP-39 root every derived key comes from. No API, no relay and no server ever sees a vault key, and there is no environment variable and no --yes that opens one. Every vault command reads its passphrase from a hidden prompt on a terminal, and refuses without one.

Terminal window
candle vault init # one passphrase factor, a recovery root, no key yet
candle vault new-key --chain solana --label treasury # derives m/44'/501'/0'/0' and verifies it re-reads
candle vault new-key --chain evm --label hood-cold # derives m/44'/60'/0'/0/0, one address on every EVM chain
candle vault list # addresses, labels, roles
candle vault status --unlock # keys, derivation counters, exposure flags
candle vault backup --to /Volumes/BACKUP/vault.enc # copies, then verifies the copy in full
  • The passphrase is generated by default. vault init shows eight words from a 7776-word list, about 103 bits, once, and does not ask you to type them back: save them in your password manager or on paper, because this CLI keeps no copy and cannot recover them. If you lose them, the 24-word recovery phrase is the way back in. --own-passphrase takes 16 characters or more that are not on an embedded list of common ones, and records that this CLI cannot know its entropy. Under --json the generated form is refused (exit 2), because the words are shown only on the terminal and never inside a JSON value; pass --own-passphrase there, for vault factor add passphrase too. Keep the passphrase and the recovery phrase outside the Apple account that holds a synced passkey.
  • A backup is verified, not just copied. Opening a vault authenticates its header and its index and nothing else, so a copy whose root blob is corrupt, or whose key blob you have not needed yet is unrecoverable, would compare equal and be worthless on the day you needed it. vault backup and vault verify-backup decrypt the root, decrypt every key one at a time, check each against the address recorded for it, re-derive every derived key from the root along its recorded path, and compare the copy’s addresses against the live vault. Anything short of all of that fails, records nothing, and leaves the copy in place. A copy is sealed unless it goes to a recognised local disk or removable drive: it carries the passphrase and any security key, and no synced passkey or Touch ID envelope, so a synced passkey that lives in the same Apple account cannot open it, and the output says so; the live vault keeps every factor. A sealed copy opens with the passphrase it was sealed under, which may predate a passphrase rotation, or with a security key it carries (its PIN and a touch). Removing a key from this vault later does not remove it from this copy. iCloud Drive and other cloud folders are sealed for that reason, and so is any path the CLI cannot place, because it cannot tell whether that path syncs to an account: a network share, an rclone or WebDAV mount, or a sync client it does not recognise. The output says that in those words, and names the flag. --accept-shared-domain writes the full copy instead and vault status records that you accepted it. vault verify-backup verifies a sealed copy with the passphrase or a security key it carries. Keep the passphrase outside the Apple account, and treat the live vault plus the recovery phrase as the everyday path.
  • The recovery phrase is an export ceremony. vault phrase show runs only on a terminal at both ends, asks for a fresh factor: the passphrase, or a security key’s PIN and a touch, makes you acknowledge what you are about to do, records in the vault that the phrase was exported before the words reach the screen, and then asks you to type three of them back. It is refused under --json and it is refused through a pipe, so an agent driving this CLI cannot obtain the phrase. A process running as you that allocates a terminal still can; the record and the read-back are what make that visible afterwards. Remove a security key from the machine when you step away: with the key left plugged in, its PIN alone is what stands between an unattended terminal and this ceremony.
  • A restored vault does not mint new addresses. vault restore --phrase rebuilds a vault from the 24 words, derives the indices you bound it to with --count, --tee-count, --external-count and --evm-count (or gap-scans the Solana branches with --rpc-url; a gap scan needs --rpc-url on the command, no default or stored endpoint is used for it, and the EVM branch is never scanned), and reads your account’s linked wallets to flag any of them that this account has already imported. It cannot know how many addresses the phrase produced elsewhere, so every key it recovers is marked as having an unknown history, and vault new-key, vault promote --from and candle external new refuse in that vault permanently. The way forward is a second vault init with a fresh root and vault transfer to move the funds across. Keep the derivation counters vault status prints beside your written-down phrase: they are the two numbers a restore wants.
  • Which Solana RPC a command uses. Every command that reads or broadcasts on Solana resolves one endpoint, in this order: --rpc-url, else CANDLE_SOLANA_RPC_URL, else the acting profile’s RPC (candle profile set <name> --rpc-url <url>), else the public endpoint https://api.mainnet-beta.solana.com. The URL itself is never printed; before its first request the command prints the host on stderr, as Solana RPC: <host> (<source>). The first time the public endpoint is used on a machine, a notice says that it rate-limits heavily and sees every address the CLI asks it about, and names the two ways to set your own; the notice is recorded in config.json as publicRpcNotice and shown once per machine. A rate-limited read is retried once (after the server’s Retry-After, capped at 10 seconds, else 2 seconds); a second rate limit before anything is signed stops the command with RPC_RATE_LIMITED, exit 1, and the fix command. A rate limit after a signature is the uncertain outcome: exit 3 with the local signature, and nothing is ever re-sent, not even the same bytes. vault restore’s gap scan is the one exception to the default: it runs only with --rpc-url on the command. Candle never proxies these reads: a Candle-operated RPC would teach Candle’s server every vault address, which the vault exists to prevent.
  • EVM vault keys are a bank, not a signer. vault new-key --chain evm derives one secp256k1 key on m/44'/60'/n'/0/0, which is one address on every EVM chain. It signs exactly two shapes, both through vault transfer: the native asset to an address, and an ERC-20 transfer(address,uint256). No contract call, no approval, and no message or EIP-712 signing. An agent reaches an EVM key only once it is promoted to a Hood TEE wallet. Hood (chain id 4663) is built in; any other EVM chain is --rpc-url, and the chain id is read from the RPC every time, never typed. vault promote, vault promote-batch (one chain per file), vault fund, vault demote, tee sweep and tee status also work on Hood TEE wallets; tee new, external sweep and candle sign are Solana-only and refuse an EVM key named to them with SOLANA_COMMAND_EVM_KEY, and auto-selection of a Solana key never picks an EVM one. The transfer’s confirmed is depth-confirmed (one block deep on Hood, two elsewhere) and is not finality; --json carries depth and finalized: false so a script cannot mistake it for Solana’s exit 0.
  • A batch of promotions is one decision about a set. vault promote-batch --pairs-from <file> promotes many vault keys in place under one unlock. The file is one <label> <destination> per line, or a CSV with label and sweep_to columns picked by name (order,label,family,sweep_to,value_usd is accepted as is; value_usd is echoed, never computed). Every row is checked against the whole set before anything is written, against the vault as it will be once the rows above it have run, so a batch never promotes key 90 and then refuses key 91. Before the table it reads how many linked wallets the account has room for, and refuses the whole batch when the keys it would promote exceed that room, so a Pro account never promotes ten keys and then exposes an eleventh it cannot link. One sentence above the table says what you are accepting: a permanent copy of these keys in Privy’s TEE that demoting does not remove. On every run the batch reads, over your RPC, whether each key is a token mint, freeze, program upgrade or stake authority (nine getProgramAccounts per key, group by group, up to eight in flight), prints any finding in the row’s authority column and counts them in the footer; a scan your endpoint refuses is named as not read and never blocks the batch, and multisig membership is never checked. Directly above the prompt it prints which Candle account, API key (prefix and name) and API the keys will be registered to, read live from the API, and refuses the whole batch when that cannot be read rather than print a cached value. The acknowledgement is the word confirm, one attempt, and anything else writes nothing. Each key is committed on its own, so an interrupted batch keeps what landed, and re-running the same file skips the rows that landed, resumes an interrupted one, and refuses a row whose destination disagrees with the vault. There is no non-interactive form: no flag, no environment variable and no --yes supplies the acknowledgement. --to-key <prefix|label> names the API key the wallets end up on instead of the one that imports them: the import runs under the calling key as before, then the promoted wallets are moved to the named key through tee rebind’s route (device token, at most 200 per request), so the target’s secret never touches the machine. When the named key has a key signer, each import instead goes onto that signer and is bound to the named key in the same call, with no rebind after. The target is checked before the unlock (it is on this account, active, a production key with swap:write, with room if it is scoped to selected wallets), the block names it as the controller, and a wallet whose rebind fails or is never reached is reported as still on the calling key, with the exact candle tee rebind ... --to-key command that finishes; re-running the same file picks up where it stopped.
  • A security key is a factor, not a recovery factor. It is also not a second step: any one factor opens the vault on its own, so enrolling a key adds another way in, and the passphrase still opens the vault without it. There is no two-factor unlock. vault factor add security-key enrolls a FIDO2 key that supports the hmac-secret extension and has a PIN set (or a built-in biometric); a key without either is refused with a typed code and nothing is written. The CLI collects the PIN on its own hidden prompt and hands it to the bundled candle-fido2 helper inside a piped request; the helper never reads the terminal. Every command that opens the vault then takes --factor <id> (or passphrase or security-key) to say which envelope to use. Without --factor, on a terminal, the CLI lists the factors that can open the vault here by the labels you gave them, marks a security key as attached when it is plugged in, lists attached keys first, and takes the row’s number. It always asks. --factor is how you skip the question. Without a terminal it never asks. --device <id> names the key when more than one is attached; with two keys attached and none named, the key you pick at the prompt is used; with --factor and no --device, the command refuses and lists both rather than touching either. One key is not a recoverable factor (a lost key is a lost factor); two envelopes on two different keys are a pair. A second key can be enrolled while the first opens the vault: vault factor add security-key opens with the passphrase or another security key (never Touch ID or a synced passkey), and with a key as the opener --device names the key to add, or, with only the opening key attached, the CLI asks you to swap keys after the vault opens; a key that already holds this vault’s credential cannot be enrolled twice (VAULT_KEY_ALREADY_ENROLLED, checked by the CLI and by the key itself). The helper loads libfido2 at run time (brew install libfido2 on macOS; your distribution’s libfido2 package on Linux, plus its udev rule for /dev/hidraw*), and the npm package ships no helper, so there the factor is VAULT_HELPER_MISSING until a release build’s candle-fido2 is installed and CANDLE_FIDO2_HELPER points at it. The installer script places candle-fido2 beside candle, and candle update replaces both together; a release binary that has no helper beside it (one that was only ever updated by an older candle update) is refused at enrolment with the remedies named, one of which is --install-helper: it downloads this release’s helper, checks it exactly as candle update does, and installs nothing on any failure. candle doctor reports the helper’s state in its own row.
  • Touch ID is a daily-use factor for one Mac. (Not yet released; it needs Apple’s approval of Candle’s Developer ID enrolment and a hardware gate on real Macs first, and until that release vault factor add touch-id refuses with VAULT_FACTOR_UNSUPPORTED_ON_PLATFORM and says the signed helper is not in the build.) vault factor add touch-id asks the signed candle-enclave.app helper to create a P-256 key inside this Mac’s Secure Enclave, bound to the fingerprints enrolled at that moment, wraps a fresh random key to it with Apple’s ECIES, and re-opens the vault with one Touch ID prompt before it reports. Every unlock afterwards is one Touch ID prompt whose text names the operation, and a transfer names the amount, asset and destination in a second prompt. Before trusting the helper the CLI verifies its code signature with /usr/bin/codesign against the team id and bundle id the envelope recorded; an unsigned, re-signed or wrong-team helper is VAULT_HELPER_UNTRUSTED, a missing one VAULT_HELPER_MISSING, an Intel Mac without a T2 chip a typed refusal, and Linux always a typed refusal. The key never leaves the Mac: the same vault file on another Mac opens with the passphrase, and vault status lists the envelope as unavailable there. The Enclave resists extraction of its key; it does not stop a process running as you from asking the helper to unwrap, which is why the prompt shows the operation. --factor touch-id selects it.
  • A synced passkey is a recoverable factor that lives in your Apple account. (Not yet released; it needs the same Apple-signed helper as Touch ID, macOS 15 or later, an associated-domains entitlement and provisioning profile on that helper, an apple-app-site-association served on cli.candle.tv, and a hardware gate on real Macs first. Until that release vault factor add passkey refuses with VAULT_FACTOR_UNSUPPORTED_ON_PLATFORM and says the signed helper is not in the build.) vault factor add passkey asks the platform authenticator for a discoverable, user-verified passkey under cli.candle.tv with the PRF extension, derives the vault’s key from one assertion, and re-opens the vault with the passkey before it reports; unlocking afterwards is one passkey sheet (Touch ID or the account password). Every missing gate is a typed refusal before any sheet appears: macOS 14 or earlier, a helper without the entitlement or the profile, or a domain that does not serve the association file (the refusal states exactly what must be served), and nothing else is tried instead. The credential syncs through iCloud Keychain, so it follows your Apple account to a new Mac and counts as recoverable; two synced passkeys are one factor, not two, and a passkey the platform reports as not synced is refused rather than recorded. The PRF output and the vault’s key never leave the Mac. --factor passkey selects it. Keep the passphrase and the recovery phrase outside that Apple account.
  • Removing a factor is not revocation. vault factor remove deletes an envelope from the file on this machine and nothing else. The data key never rotates, so any copy of the vault made while that factor existed still opens with it, and the key that yields opens every blob in the current file, including keys you create afterwards. vault status lists the removed ids from then on so you know which copies to worry about. The only revocation of a compromised factor is a new vault and moving the funds to its keys.
  • wallets.enc is not read. wallets generate and wallets export were removed in 0.10.0 and the vault never opens the file they wrote. If one is sitting in your config directory vault status says so; a key in it that you imported is already a linked wallet, and one you never imported opens with CLI 0.9.2’s wallets export, from which you move the funds on chain to a vault key.

candle tee is a dedicated, capped agent wallet. The usual way to get one is to promote a vault key with vault promote (see Custody tiers); the vault then holds its key and its pinned sweep destination, and the relay signer that approves its trades sits in this machine’s CLI secret store (the OS keychain, or credentials.enc), so trade it from the machine that promoted it. To trade from another machine instead, give the key a key signer there and promote with --to-key. The older candle tee new / tee enable flow below still works: a key the CLI generates and seals in its own tee-wallets.enc (separate file, separate passphrase, never read from the environment), delegated to exactly one API key on the trade rail only, funded explicitly from your vault, and recovered by a sweep the CLI signs locally after the agent is stopped. The remote signer is never needed to get funds back. The account owner can move a wallet to another key on the same account with tee rebind (the device token, a preview and a typed confirm; nothing changes on Privy and the funds stay put); that is a routine move, not a response to a compromise.

Terminal window
candle tee new --label scalper
candle tee enable <tee-address> --vault <your-vault-address> # confirms the vault, imports it as a TEE wallet
candle tee fund <tee-address> --amount 0.5 --asset SOL # prints what your vault should sign
candle tee disable <tee-address> # exit 0 = verified stop, 3 = pending
candle tee sweep <tee-address> # tokens first, SOL last, finalized receipts

The server admits a TEE wallet on the trade rail, and on swap and self-signed launch where Candle has enabled them. Every other route is refused. A same-chain TEE swap uses SOL, USDC or CNDL on Solana, or ETH and USDG on Hood, and returns to its payer. The API has a separate bridge path for SOL/USDC and ETH/USDG pairs; it needs a raw cap on the origin asset and any USD budget the key has. See TEE bridge API. candle swap runs that bridge between two TEE wallets on the same key; see Bridging between Solana and Hood. Only the bound key may pay. Other trades and swaps are held to the caps the owner set, while transfers, LP deposits and launches require an origin-asset cap. A TEE wallet key has no plaintext export path. Once enabled, a copy of the key stays in the TEE for good, so after a sweep the address is retired, never reused.

  • A stop is recorded before it is requested. tee disable writes the stop intent into the sealed store first, then asks the server. If the server cannot be reached, or the API key no longer works, the command exits 1 and says that remote enforcement is unconfirmed; tee fund refuses the address from then on, and you re-run the disable (or revoke the linked wallet from your Candle session) until the stop is verified.
  • Recovery never needs the API key. tee sweep reads the server’s lifecycle state when it can and waits for quarantined. When it cannot (no key, a revoked key, the API down), --emergency sweeps with only the local key and the pinned vault, prints the race warning, and reports the state as pending: a sweep never claims the remote signer is gone.
  • Open LP positions sweep too. The sweep lists the wallet’s Token-2022 accounts over your RPC and treats an account holding raw amount 1 of a decimals-0 mint as a Meteora DAMM v2 position NFT; it never asks the server which positions exist. For each one, an ordinary sweep fetches one unsigned close from POST /api/v1/agent/lp/positions/<mint>/close-build (the bound key, no lp:write, refused while the wallet is still enabled) and verifies every close before signing any: the server’s ordered account keys are rebuilt element for element from the v0 message and its lookup tables, every top-level program is on a fixed allowlist, a simulation with signature verification off must succeed with every gain landing on the TEE wallet or its vault and the NFT account emptied, and every writable account is classified (the wallet, its vault, the pool’s own vaults, DAMM v2 state, or a not-yet-created associated token account of the wallet or vault; anything else refuses). A close that cannot be fetched is the leftover DAMM_POSITION_CLOSE_UNAVAILABLE; one that fails a check is DAMM_CLOSE_TRANSACTION_REFUSED, and neither stops the token and SOL transfers. The tokens a close withdrew are swept in the same run. --emergency never fetches a close: it moves the position NFT itself to the pinned vault under Token-2022, to be unwound later from a fresh TEE wallet.
  • swept is decided after finality. Once the transfers finalize the CLI re-reads the SOL balance and every token account. Anything still there (a deposit that landed mid-sweep, a token account it could not close) is listed as a residual and the address stays quarantined for another pass; exit 0 and the server’s swept record need an observed-empty wallet. The server refuses the record while the remote authority is still pending, and the CLI then reports the address as pending, not quarantined.
  • Receipts are kept, so a retry finishes the job. Every sweep transaction is recorded inside the sealed store as pending before it is broadcast, and as a receipt once it finalizes. A transfer that is still unconfirmed when the 90-second wait ends is reported as uncertain, never as failed; the next tee sweep checks it first, turns a finalized one into a receipt, drops one that failed at finality or that was never seen and can no longer be admitted (blockhash expired, confirmed by a second status read) and sweeps that balance again, and signs nothing new while one is still in flight or while the RPC’s answers are inconclusive. The transaction’s own signature, known before it is sent, is the identity used throughout; the RPC’s echo never replaces it. If recording the sweep fails (an outage, a lost key, an emergency sweep whose stop was verified later), re-running tee sweep reconciles the retained receipts against a fresh inventory and records them without another transfer. A repeat after success is a no-op that exits 0.
  • Two commands cannot lose a key. Every write to tee-wallets.enc takes a lock (tee-wallets.enc.lock, a directory beside the file) and re-reads the file before writing, so a key another command sealed meanwhile is carried forward. A lock left behind by a crash is never broken automatically; the command names it and you remove it once nothing else is running.

candle swap works on Hood as well as Solana. There is no --chain flag: with --wallet, the wallet’s chain is the operation’s chain, and an asset from the other chain is CHAIN_MISMATCH before anything is built. Without --wallet, the assets decide (ETH, USDG or a 0x... token is Hood; SOL, USDC, CNDL or a base58 mint is Solana), and the key’s single wallet on that chain pays, as on Solana. The embedded Hood wallet can trade tokens; ETH to USDG needs a Hood TEE wallet.

Terminal window
candle swap ETH USDG --amount 0.05 --wallet hood-agent
candle swap ETH 0xToken... --amount 0.01 --wallet hood-agent
candle swap 0xToken... ETH --percent 100 --wallet hood-agent
candle tee status 0xYourHoodTeeWallet
  • One leg at a time. A Hood trade can be up to four transactions: an approval, a Permit2 approval, the trade, and the fee. For a Hood TEE wallet Candle returns only the next one, with the nonce, gas and fees it set itself. The CLI relay-signs that leg exactly as built (eth_signTransaction), checks the signature covers that leg and nothing else, posts it, and waits for the server to read its receipt before it is given the next. The fee is only ever offered after the trade landed. The quote lists every leg, the first leg’s gas and the gas reserve before you confirm.
  • One operation per wallet. A build holds the wallet for its 30-minute window. Declining the quote signs nothing, but the wallet stays held until that window closes, and another Hood trade from it is WALLET_BUSY until then.
  • Outcomes. A leg that fails or reverts exits 1 and lists the legs that landed, with their hashes. A leg whose receipt was not seen exits 3 with its hash; candle swap status <id> reads the operation without resending anything.
  • The gas reserve. Candle keeps enough ETH in the wallet to sweep it home at twice the current fee, and refuses a trade that would spend it (GAS_INSUFFICIENT, with the candle vault fund command). candle tee status <0x address> shows ETH, USDG, the reserve’s floor over your EVM RPC, and gas: low when ETH is under twice it.
  • Reads only over your RPC. A token’s decimals and a --percent balance are read over --rpc-url, CANDLE_EVM_RPC_URL or the built-in Hood RPC, and the host is printed. Nothing is sent there: the server broadcasts every leg.
  • The sealed EVM record. After each leg lands, the CLI appends the traded token to the sealed EVM record beside your vault, without unlocking it, so a later sweep knows to look for it. An append that cannot run never fails the trade; it prints a notice naming --token and --from-block.

A launch takes its image as an https:// URL. When the image is a file on your machine, the CLI uploads it for you, from @candledottv/cli 0.11.24:

Terminal window
candle launch --name Demo --symbol DEMO --image ./logo.png --wallet agent
candle image upload ./logo.png
  • candle launch --image <path> uploads the file, then launches with the URL Candle returns. Pass --image or --image-url, never both. The upload runs once, after every check this machine can make (the wallet, the chain, --buy, the Solana RPC) and before the launch id is claimed. If the upload fails, the launch was not started and the same --client-trade-id can be used again. Re-running a launch that already started resumes it and uploads nothing. The file is still read and checked on every run, so leave it in place until the launch has finished.
  • candle image upload <path> does only the upload and prints the URL on stdout, so candle image upload logo.png can feed another command. Use it when you want the link without launching, or when you launch through the hosted connector, which runs on Candle’s servers and cannot read your disk: upload here, then pass the URL as imageUrl.
  • The file is checked before any request. It must be a PNG, JPEG, GIF or WebP of at most 10MB. The type is read from the file’s first bytes, not its name, so a text file renamed logo.png is refused (IMAGE_TYPE_UNSUPPORTED), as is a larger file (IMAGE_TOO_LARGE) or a path that cannot be read (IMAGE_UNREADABLE). A path that begins with a literal ~ is refused as it is for every other file the CLI reads.
  • candle mcp with CANDLE_IMAGE_DIR lets an agent on the local connector launch with a file itself. Set the variable to an absolute directory path in the environment candle mcp starts with, and the two launch tools accept imagePath for a file inside that directory (more). From @candledottv/cli 0.11.25. The upload is the same publication, on a dry run too.
  • An upload is a publication, and it cannot be undone. Candle pins the file to public storage and the URL cannot be withdrawn. With candle launch --image this happens before the confirm prompt, so the file is public even if you decline or the launch fails. A square image keeps its original bytes, including any metadata such as EXIF or GPS data; other shapes are cropped to a square. Upload only an image you mean to be public.
  • What it costs. No funds, and nothing against the daily launch cap. It uses the key’s launch:write scope and the plan’s uploads-per-minute limit (10 on Free, 30 on Pro, 60 on Max); past it the route answers RATE_LIMITED, which the CLI passes through with the route’s own message, as it does UPLOAD_INVALID.
  • --image-url takes only an https:// URL. Anything else, a local path included, is refused before any request with a message naming --image.
  • The banner is not a file. The route crops to a square, so it cannot host a banner; a banner stays an https:// URL wherever one is accepted.
Terminal window
candle launch --name Demo --symbol DEMO --image-url https://example.com/d.png --wallet hood-agent --dex-version v4
candle swap ETH 0xNewToken... --amount 0.01 --wallet hood-agent
candle transfer --to vault --asset ETH --amount max --wallet hood-agent
candle transfer --to vault --token 0xToken... --amount max --wallet hood-agent
candle transfer --to cold --asset USDG --amount 250 --wallet hood-agent
  • Launch. The wallet decides the chain, so a Hood TEE wallet launches on Hood. --dex-version v3|v4 is required there, and --quote-asset is eth (the default) or usdg. A flag from the other chain (--quote-asset sol, or --dex-version with a Solana wallet) is CHAIN_MISMATCH before anything is built. Without --buy the launch creates the token only, and the first buy is a separate candle swap, gated like any trade. With --buy <amount> (ETH; a USDG launch takes no dev buy) Candle adds its own curve buy for exactly that amount as the last leg and reserves it against the key’s spend window when the operation opens; this needs the server’s TEE_LAUNCH_BUYS_ENABLED, off by default. If the buy leg never lands, the token exists with no first buy. Its legs, the curve, then the fee when one applies, then the buy, run one at a time as a trade’s do, and the launched token is appended to the sealed EVM record. A leg whose receipt was not seen exits 3 with its hash; re-running with the same --client-trade-id posts nothing while the operation still holds the wallet, and once it has let go it records the launch from the legs Candle sent, without re-signing anything.
  • Transfer. Through a Withdraw key, with Solana’s rules: the pinned vault takes any token (--token <0x...>) and max, uncapped; a trusted linked wallet takes ETH or USDG only, counted against the key’s caps. Candle refuses ETH above the agent-wallet wei ceiling to either. ETH max is the balance minus this send’s gas and the gas reserve. The one leg runs through the same loop, and before it is signed the CLI checks it moves exactly the confirmed amount of that asset to that destination.

A swap between a base asset on each chain, SOL or USDC on Solana and ETH or USDG on Hood, in either direction, is a bridge. It runs through Relay from a TEE wallet into the same API key’s TEE wallet on the other chain, and nowhere else: not a vault, not a linked wallet, not an address you type. Candle resolves that destination on its server and charges no fee; you pay Relay’s fees and gas. The key needs swap:write and a raw cap on the origin asset. Every other cross-chain pair, CNDL or a token, is still CHAIN_MISMATCH: swap it into a base asset first.

The vault never bridges. Vault to vault is four commands:

Terminal window
candle vault fund <sol-tee> --amount 1 --asset SOL
candle swap SOL ETH --amount 1 --wallet <sol-tee>
candle swap status <id> --wait
candle tee disable <hood-tee> && candle tee sweep <hood-tee>
  • The destination. With one TEE wallet on the other chain bound to the key, it is used. With several, name one with --to <label|id|address>; with none, the command refuses BRIDGE_DESTINATION_MISSING before anything is built, and vault promote puts one on the key. The CLI refuses a quote whose recipient is not a TEE wallet it lists for the key, and a quote that carries a Candle fee.
  • The quote. Venue relay, the destination wallet, the minimum received (Relay’s estimate less the slippage bound), Relay’s fees and estimated time as Relay reports them, Candle fee: none, and on Hood the legs, then the usual confirmation (--yes skips it).
  • This machine checks Relay’s deposit before it is signed, from its own copy of the pinned Relay addresses. On Hood: at most one approve, only for USDG, to Relay’s depository, for exactly the amount (a USDG bridge whose allowance already covers the amount is the deposit alone; an ETH bridge never has one), then the deposit to the depository with this wallet as depositor, the origin token and exactly the amount. On Solana: the transaction decodes, every lookup table resolves (one that does not is a refusal, never skipped), this wallet is the fee payer and only signer, and Relay’s depository is the only program, with no Compute Budget instruction. Anything else is RELAY_STEP_REFUSED and nothing is signed. Candle’s server runs the same checks, and on Solana also simulates the deposit.
  • It returns after the deposit. The command prints the deposit hash and filling, and exits. --wait, or candle swap status <id> --wait, follows it for up to ten minutes: deposit landed, filling, filled with the amount received, failed or refunded to the source wallet (Relay reports both the same way), or uncertain. The status also prints Relay’s status URL, where the destination transaction is named.
  • A sweep waits for it. From its build until it fills, fails or is refunded, tee sweep of the source or the destination wallet refuses BRIDGE_IN_FLIGHT for two hours, naming the operation. After two hours an unresolved bridge is a warning and the sweep goes ahead; a fill or refund that lands later needs a second sweep. tee disable is never blocked and says a bridge is still open. tee sweep --emergency asks Candle nothing, so it cannot see a bridge; it says so.

Candle is never a party to a transaction you make on another service. There is no Candle API key for an exchange, no Candle server in the request path, no fee or markup, and no Candle-offered private transfer. What the CLI does is make it easy to supplement it with your own keys and tools for any crypto service: your secrets stay in your keychain, a plug-in runs on your machine with only what you hand it, and an external wallet signs for outside tools without ever touching the vault’s cold keys or a TEE wallet.

Terminal window
candle secrets set EXCHANGE_KEY # typed on a hidden prompt; never sent to Candle, never shown again
candle external new --label desk # a key on the vault's third branch, never delegated to Privy
candle vault fund desk --amount 0.5 # vault-signed; check the desk address, type confirm
candle mytool --secret EXCHANGE_KEY --wallet desk buy 0.1 # runs candle-mytool with only those two values
candle sign --file tx.txt --wallet desk # decode, simulate, confirm, sign
candle external sweep desk --to treasury # everything back to a vault key
  • Secrets live in their own keychain namespace. candle secrets stores your third-party API keys under a keychain service of its own (tv.candle.cli.secrets), distinct from the one holding Candle’s device token and API key, so a keychain grant or an approved prompt for one never reaches the other; on the encrypted-file fallback they are two files (secrets.enc beside credentials.enc, both opened by CANDLE_KEYRING_PASSPHRASE). set reads the value on a hidden prompt; list shows names only; nothing here is ever sent to Candle or logged. A name is uppercased, and a plug-in receives it as CANDLE_SECRET_<NAME> only when you pass --secret <name>.
  • A plug-in’s environment is an allowlist, built from empty. An executable candle-<name> on your PATH runs as candle <name>. It inherits nothing from the shell that launched the CLI except what a process needs to run and reach the network as you: PATH, HOME, TMPDIR, TERM, TZ, LANG, LC_* and the proxy variables. On top of those it receives CANDLE_PLUGIN_NETWORK (solana-mainnet), CANDLE_PLUGIN_RPC_URL when CANDLE_SOLANA_RPC_URL or your profile names an RPC (in that order; a plug-in is never given the public default), the external wallet addresses you name with --wallet <label> as CANDLE_PLUGIN_WALLET_<LABEL>, and the secrets you name with --secret <name> as CANDLE_SECRET_<NAME>. No parent CANDLE_* variable reaches it: not CANDLE_API_KEY, not CANDLE_DEVICE_TOKEN, not CANDLE_KEYSTORE_PASSPHRASE, and never a private key. --secret and --wallet are consumed by the CLI and stripped from the arguments the plug-in sees; everything else after the plug-in name is passed through verbatim. A plug-in cannot make a Candle-held key sign: the only way it reaches one is to hand a transaction to candle sign, which refuses any signer that is not an external wallet. Candle ships no plug-in for any service and endorses none.
  • An external wallet is the key that signs for outside tools. candle external new derives it on the vault’s third branch, m/44'/501'/n'/2', a change index neither the vault branch (0') nor the TEE branch (1') can reach and one no wallet app scans. It is never delegated to Privy and never registered with Candle, so the vault stays cold and TEE wallets stay on Candle’s rails. It is funded from the vault by vault fund <external>, a vault-signed transfer that is decoded, displayed with the external address grouped in fours, and confirmed by typing confirm, with no --yes: what an external wallet holds is exactly what candle sign --yes can spend, so fund a session, not a float. external sweep <external> --to <vault> sends everything back, signed locally with the external key; both ends are named on the command, neither is inferred, and the address shown before you type confirm is the vault key’s. External addresses are read over your own RPC; Candle is not in their transactions.
  • The vault format moves to version 3 with the first external key. The external branch adds a counter, an exposure list and a role the version 2 format has no field for, so the write that allocates the first external key rewrites the vault as version: 3. Nothing else changes: the same envelopes, key blobs and root, byte for byte. A vault you never run candle external new in stays version 2 and opens in CLIs that predate the external branch; a version 3 vault is refused by those CLIs with VAULT_VERSION_UNSUPPORTED, and nothing is written. vault restore --phrase takes --external-count <e> on the same rules as the other two counts (1 when another count is given, a gap scan with --rpc-url), and recovers external keys as external keys; like every recovered key their history is unknown, and because candle external new is an allocation it refuses in a restored vault with VAULT_ALLOCATION_BOUNDARY_UNKNOWN. Funding, sweeping, signing and listing keep working on a recovered external key, since moving funds off a key of unknown history is the right move.
  • candle sign is a wallet’s approval screen for one key. It takes a base64 transaction built by any tool (from --file or stdin), legacy or v0, and refuses anything that is not exactly one such transaction (SIGN_TRANSACTION_UNDECODABLE) before displaying anything. A v0 message only commits to lookup tables and indexes, so the CLI fetches every table over your RPC and rebuilds the account list in compiled order first; a table it cannot fetch is SIGN_LOOKUP_TABLE_UNRESOLVED. It then simulates the transaction with signature verification off, and a simulation that fails, or an RPC it cannot reach, is SIGN_SIMULATION_FAILED with the program error and logs: there is no “sign anyway”. Every required signer, the fee payer included, must be one of this vault’s external wallets: a vault key, a TEE wallet, or an address the vault does not hold is SIGN_SIGNER_NOT_EXTERNAL, and an external signer you did not name with --wallet is SIGN_SIGNER_NOT_PROVIDED rather than a half-signed transaction. What you see before the prompt: the fee payer, every program called, the signing wallet’s own balances before and after (SOL and each token account, with the mint’s decimals), every other account that gains a balance, and the Token-2022 warnings for every mint involved, with a note that a simulation is evidence and not a guarantee. A change of control over any account a signing wallet owned (a new owner, a new or larger delegate approval, a close authority, or the wallet itself assigned to another program) is shown as a CONTROL line and refused with SIGN_CONTROL_CHANGE, --yes included: a balance that does not move can still be taken. The prompt is your vault factor a second time; --yes skips that prompt and nothing else, so a scripted plug-in can sign on a terminal you have already unlocked, bounded by what vault fund put in the wallet. The signed transaction is printed to stdout for the tool to send, or --broadcast sends it over your RPC (SIGN_BROADCAST_FAILED still prints it). candle sign message signs the exact bytes of a file or of stdin, never a command-line argument: it always shows the byte length and the SHA-256, shows the text only when it is UTF-8 with no control character other than newline, and shows hex otherwise, so nothing that dropped or altered a byte is ever signed.
  • Paying a deposit to a service you have an account with is vault transfer <address> from the vault, or candle sign and external sweep from an external wallet. Nothing in the CLI knows or checks what an address belongs to.

Linked wallets (Pro and Max; see Agent wallets & spend limits and candle plans) let the rail trade from a wallet you already control. The import is the most security-sensitive command the CLI has, and its shape reflects that:

Terminal window
candle wallets import --chain solana --key-file ~/.config/solana/id.json --label "trading wallet"
  • The private key never appears in the command line. It comes from --key-file (accepts an id.json byte array, base58, or hex) or, with no flag, a hidden terminal prompt. There is no argv or environment form, so the key cannot land in shell history or process listings.
  • A wrong key cannot import silently. For Solana the CLI derives the wallet’s address from the decoded secret; if you pass --address and it doesn’t match, the import refuses before any network call. EVM keys can’t be address-derived without keccak, so --address is required there and the server validates.
  • Only ciphertext leaves your machine. The key is HPKE-sealed locally to Privy’s receiver key (the same RFC 9180 exchange the SDK import documents); Candle never sees plaintext.
  • The signer stays in your keychain. The import generates a P-256 signer keypair; the public half registers with the wallet, and the private half, which every later trade from this wallet signs with, is stored in the same OS keychain as your credentials, keyed by wallet id. --signer-out <path> additionally exports a PEM (written owner-only) for use on another machine; treat that file like the private key it is. On the same machine, no export is needed at all: the SDK’s KeychainSecretStore reads the CLI’s storage directly, so an agent process trades from the imported wallet with zero manual key handling.

candle wallets revoke <wallet-id> unlinks the wallet server-side and removes its stored signer locally. The wallet id is the id printed at import time, also visible via candle wallets.

A wallet an API key imported is not trusted: from an ordinary wallet an agent can move funds into it only through the withdrawal allowlist, capped by the key’s limits, and from a TEE wallet it cannot move funds into it at all. candle wallets shows a Trusted column. To say “these are mine” in one step, run candle wallets trust 'tr-*' 'dest-*' (a selector is a label, address, id, or a prefix ending in *; quote it so the shell leaves it alone). It needs the device token from candle auth login; a profile with only an API key is refused with DEVICE_TOKEN_REQUIRED. The same marks can be set from the Wallets tab of the agent console. Revoking a wallet ends its trust, and re-importing the address starts untrusted.

Secrets never land in a plaintext dotfile. The CLI picks the strongest backend the machine has:

PlatformBackend
macOSKeychain, via security (the secret is piped, so it never appears in process arguments)
Linuxlibsecret, via secret-tool
Anywhere elseAn AES-256-GCM encrypted file, unlocked by CANDLE_KEYRING_PASSPHRASE

auth status and doctor both name the backend in use. Non-secret configuration (the stored API URL, the device label) lives in a plain config file; CANDLE_CONFIG_DIR relocates it, which is also how tests and multi-account setups isolate themselves.

Environments without a keychain or a browser skip storage entirely and inject credentials through the environment:

Every variable the CLI reads is below, in the order candle --help prints them, so the two lists cannot disagree. A test pins them together: a variable added to the code and documented in neither place fails CI.

VariableMeaning
CANDLE_CONFIG_DIRWhere vault.enc, tee-wallets.enc, credentials and config live (default ~/.config/candle). Set it once instead of passing --keystore
CANDLE_PROFILEThe profile to act as, when --profile is not given
CANDLE_API_URLAPI base URL, when --api-url is not given
CANDLE_API_KEYAn agent API key; takes precedence over any stored key
CANDLE_DEVICE_TOKENA device token, for key-management commands
CANDLE_KEYRING_PASSPHRASEUnlocks the encrypted-file backend where no OS keychain exists
CANDLE_SOLANA_RPC_URLSolana RPC endpoint, when --rpc-url is not given. Beats the profile’s (candle profile set <name> --rpc-url); the public endpoint when none is set
CANDLE_EVM_RPC_URLEVM RPC endpoint for an EVM vault key, when --rpc-url (transfer) or --evm-rpc-url (list, portfolio) is not given; without it the built-in Hood RPC is used
CANDLE_FIDO2_HELPERPath to the candle-fido2 helper, when it is not beside the binary
CANDLE_ENCLAVE_HELPERPath to the signed candle-enclave.app helper (macOS)
CANDLE_HYPERLIQUID_BUILDERcandle perps: Candle’s Hyperliquid builder address, pinned for the check every build passes before it is signed; without it the release’s, else the one the server reports
CANDLE_HYPERLIQUID_NETWORKcandle perps: set to testnet to trade Hyperliquid testnet
CANDLE_NO_UPDATE_NOTIFIERSet to 1 to silence the update notice
CANDLE_KEYSTORE_PASSPHRASERefused: no command reads its value, and vault and tee commands stop while it is set

Authorize once on a machine with a browser (or use --no-browser and approve from your phone), copy the key into your runner’s secret store, and the runner itself never performs a login. A bot that trades TEE wallets from its own box follows the same rule and holds a key signer as well: see Run a bot on a headless machine.

candle completion <shell> prints a completion script to stdout; install it by redirecting it where your shell looks:

Terminal window
candle completion zsh > "${fpath[1]}/_candle" # then: autoload -Uz compinit && compinit
candle completion bash > ~/.local/share/bash-completion/completions/candle
candle completion fish > ~/.config/fish/completions/candle.fish

The scripts are generated from the same data candle help prints, so they offer exactly the commands, subcommands and flags the help documents, and never one that does not run. They are static: profile names, key labels and factor ids are not completed, because doing so would mean reading config or opening the vault at tab time. Regenerate after every candle update.

  • candle doctor first: it checks stored credentials, the storage backend, and API reachability in one pass, and its output is designed to be pasted into a bug report.
  • A 404 on auth login usually means the profile points at an old API URL. candle profile list shows each profile’s host; production is https://api.alpha.candle.tv.
  • bunx caches the fetched repo per temp directory and can serve a stale build after the repo updates. Clear it with rm -rf "$TMPDIR"/bunx-*agentic* and rerun.

candle stake reads eligible Solana wallets’ CNDL/sCNDL, withdrawals, rewards, pool wait and eligibility notes. Writes require swap:write and an enabled staking rail (off by default), and MOVE REAL FUNDS. Approve the displayed wallet, amount and SOL costs before passing --yes.

CommandPurpose
candle stake [--wallet <wallet>] [--json]Read all eligible wallets, or one by main (embedded), id, address or label
candle stake add <amount>Stake decimal CNDL in the selected wallet
candle stake remove <amount>Start a withdrawal of decimal sCNDL
candle stake finish [<withdrawal>]Finish after the pool wait; omit only when exactly one withdrawal is ready
candle stake claim <rewardId>Pay the reward to its recorded owner
candle stake claim --allClaim all listed unclaimed rewards for the selected wallets

Builds default to the embedded wallet; --wallet <tee> selects a TEE wallet whose relay signer is on this machine. Ordinary linked wallets use SDK builds and external signing. Reuse --client-stake-id <id> on a retry: the signed TEE transaction is stored before submit and a rerun posts it to submit again without re-signing. The server reads the chain for that signature first, so it lands once. The signed transaction is kept in the CLI operations directory for that retry. Embedded calls replay the saved build before another balance preview. No agent surface offers instant unstake. Pending withdrawal CNDL counts toward nothing for Pro. sCNDL in any eligible wallet of the account earns Solana and Hood rewards and counts toward Pro. See the staking reference.