Agent wallets & spend limits
Pro- and Max-tier accounts can attach additional Solana or EVM wallets to their Candle identity, beyond the primary embedded wallet used for headless launches. A linked wallet can pay for its own self-signed launch or trade, or simply count toward attribution.
Two ways to add a wallet
Section titled “Two ways to add a wallet”| Path | Grants | Use it for |
|---|---|---|
| Import | A signer key the agent holds and a spend policy, so the agent can sign with it | Self-signed launches, linked-payer trades |
| Link existing | Attribution only, no signer, no spend authority | Counting an already-owned wallet’s activity toward the account |
Both kinds count toward the tier’s linked-wallet cap (see Plans and caps) and are revocable at any time. A third way in is promoting a vault key to a TEE wallet from the CLI, which also lands as a linked wallet.
Importing a wallet
Section titled “Importing a wallet”Importing is a ciphertext-only exchange: Candle’s server never receives, stores, or logs the wallet’s plaintext private key, at any point.
The interactive path is the CLI: candle wallet import --chain solana|evm reads the key from a file or a hidden prompt and stores the generated signer in your OS keychain, so nothing sensitive ever sits in a shell history or a loose file. The SDK’s KeychainSecretStore reads that same storage, so a CLI-imported wallet is immediately usable from an agent process: pass secretStore: KeychainSecretStore.detect() ?? undefined in the client options and linked-wallet signing finds the key itself. Programmatically, the SDK’s CandleClient.importWallet({ chain, address, privateKey, signerPublicKey, label? }) does the same work locally:
POST /api/v1/agent/wallets/import/initreturns Privy’s HPKE receiver public key for the wallet’s chain and address.- Client-side, never sent to Candle. The private key is decoded to raw bytes and HPKE-sealed to that public key. Only the resulting ciphertext, an encapsulated key, and a caller-generated signer public key ever leave the calling process.
POST /api/v1/agent/wallets/import/submitposts just those three values. Candle registers a signer from the public key and attaches Candle’s fixed backstop policy to it (a per-transaction SOL or ETH ceiling and a deny on instructions that hand away spend authority) atomically, in the same call, so an agent-controlled signer is never created without a policy already on it, then links the wallet.
{ "success": true, "id": "...", "address": "...", "chain": "solana", "privyWalletId": "..." }TEE wallets are linked wallets
Section titled “TEE wallets are linked wallets”A TEE wallet is a Solana or Hood wallet promoted out of the CLI’s vault (candle vault promote or candle vault promote-batch) so that one API key can trade it without the vault being unlocked. On the server, every promoted TEE wallet is an ordinary linked wallet:
- It counts against the plan’s linked-wallet cap (Plans and caps), which is why promote reads
GET /api/v1/agent/wallets/roombefore it commits anything. - It is bound to one API key, the only key that can act with it.
candle tee rebindmoves it to another key on the same account without moving funds. - It carries a pinned vault: exactly one cold vault key, fixed at promotion, that
candle tee sweep,candle vault demote, andcandle transfer --to vaultall send to. It is where the wallet’s funds go home. - Candle accepts it only on the trade and signing routes (swaps and trades, self-signed launches once
allowLaunchis on, LP withlp:write, andtransfer:boundtransfers) and refuses it everywhere else. An atomic launch refuses a TEE wallet, as the creator and as a first-buy payer, unless the server hasTEE_LAUNCH_BUYS_ENABLEDon (off by default): then it takes one bound to the calling key, as creator withallowLaunchand as a buyer without it, and reserves its legs against the key’s USD spend window before anything is signed. - Launching from a TEE wallet needs
allowLaunch, which is off by default. Only the account owner can turn it on, signed in with a device token (candle auth login) or a web session; an agent key is refused, so a leaked key cannot give itself launch rights. From the CLI, runcandle wallets allow-launch <wallet>(a label, address, id, orprefix*): it shows each wallet’s label, address and bound key, then asks you to typeconfirm.candle wallets disallow-launch <wallet>turns it off. There is no console control for it yet. Without the CLI, sendPUT /api/v1/agent/wallets/<wallet id>/capabilitieswith{"capability":"allowLaunch","enabled":true}(falseturns it off); the wallet id is theidthatcandle walletsprints. A TEE wallet launches with no first buy (buyAmountmust be 0) and buys afterwards with a separate swap, unless the server hasTEE_LAUNCH_BUYS_ENABLEDon. With it on, a self-signed launch from a TEE wallet may carry a dev buy (candle launch --buy <amount>), within the usual dev-buy ceilings and reserved against the key’s USD spend window before anything is signed. The flag is off by default; while it is off, a non-zerobuyAmountis refused withVALIDATION_FAILEDonbuyAmount.
Promoting, funding, sweeping, and the emergency path are covered in Custody tiers: the vault and TEE wallets.
Trusted wallets
Section titled “Trusted wallets”A linked wallet is either trusted (yours, as a destination) or not. A wallet linked while signed in is trusted; a wallet an API key imported is not until you mark it. An agent can move funds into a trusted wallet, for example with candle transfer from a TEE wallet under a Withdraw key; from a TEE wallet, anything other than its pinned vault or a trusted wallet is refused outright, withdrawal allowlist included. From a non-TEE wallet, an untrusted destination goes through the withdrawal allowlist, capped by the key’s limits.
candle wallet # Signer and Trusted columnscandle wallet trust 'tr-*' 'dest-*' # ids, addresses, labels, or prefix*; typed confirmcandle wallet untrust dest-3Trust is owner-only: the commands use the device token, and an API key cannot change trust. The same marks are on the Wallets tab of the Agent Console.
Over HTTP, the route takes a signed-in session or a device token, never an agent key:
POST /api/v1/agent/linked-wallets/trustcontent-type: application/json
{ "walletIds": ["wal_1", "wal_2"], "trusted": true }It takes up to 200 ids and is idempotent. The answer lists changed, unchanged, sessionLinked (wallets linked while signed in, which are always trusted) and skipped, plus a batchId when anything changed. To preview which rows a set of selectors names, send { "dryRun": true, "wallets": ["tr-*", "dest-3"], "trusted": true }: selectors are ids, addresses, labels or a label prefix, and nothing is written.
Renaming a wallet
Section titled “Renaming a wallet”A linked wallet’s label is display only: no authority, policy or signer reads it. The owner can change it; an API key cannot, because a label is how a person picks a destination.
candle tee rename tr-01 desk-01 # the label here and the vault entry, when there is onecandle tee rename --pairs-from ./tee-names.csv # 1 to 256 walletsOver HTTP, the route takes a signed-in session or a device token, never an agent key:
POST /api/v1/agent/linked-wallets/renamecontent-type: application/json
{ "renames": [{ "id": "wal_1", "label": "desk-01" }] }It takes 1 to 256 rows and applies them together or not at all. A new label must not be empty, hold a control character, begin with -, begin or end with whitespace, or exceed 64 characters; it is refused, never trimmed. A wallet that is not yours, a revoked wallet, an id named twice or a label that fails the rule refuses the whole request with a 400 whose error.findings lists every bad row. The answer lists renamed and unchanged (already carrying that label). To resolve handles first, send { "dryRun": true, "renames": [{ "wallet": "tr-01", "label": "desk-01" }] }: each wallet is an id, an address or an exact label, and nothing is written.
Linking an existing wallet
Section titled “Linking an existing wallet”POST /api/v1/agent/walletsAttaches a wallet Candle’s own Privy project already manages on the caller’s account, for attribution only (agent key or Privy session). The address is verified against the caller’s Privy account first, so an account can never claim a wallet it does not own. No signer or policy is created; there is nothing spend-capable to revoke later.
Listing and revoking
Section titled “Listing and revoking”GET /api/v1/agent/walletsLists the account’s linked wallets, active ones first. Each row carries walletKind, which says what the wallet can do:
walletKind | The wallet | What it can do |
|---|---|---|
tee | A TEE wallet, bound to one API key | Trade, sign and transfer through the bound key only; launch once allowLaunch is on |
imported | A wallet imported with a signer and policy | Sign: self-signed launches, linked-payer trades |
linked | A wallet linked for attribution only | Count toward attribution; it cannot sign |
Branch on walletKind. allowLaunch is stable too. profile is still served, but its values are internal names that may change: use walletKind instead. GET /api/v1/agent/wallets/trading rows carry walletKind as well, always tee.
GET /api/v1/agent/wallets/roomHow many more linked wallets the account can take, with the numbers an import enforces: { "success": true, "tier": "pro", "active": 3, "cap": 10, "room": 7 }. cap is the enforced cap for Pro and Max and 0 for any other tier; room is max(0, cap - active). candle vault promote and candle vault promote-batch read it before they commit anything locally. Agent key with account:read or launch:write, or a Privy session.
GET /api/v1/agent/wallets/self/balancesBalances for exactly the wallets the calling key may spend from: the embedded wallets when its permission allows them, the TEE wallets bound to it, and the linked wallets its scope admits. Base assets always, plus up to 10 extra mints; 50 wallets a page, continued with cursor. Any agent key; a key with no spend scope gets an empty page. Its rows name their kind in kind, which is not walletKind: it is embedded, tee or linked, and its linked covers both an imported and an attribution-only wallet.
GET /api/v1/agent/wallets/import-failuresThe account’s recorded import failures, each with chain, address, stage (privy_submit or convex_link), privyWalletId and createdAt, for telling a half-finished import from one that never started. Any agent key.
DELETE /api/v1/agent/wallets/:idTombstones the wallet immediately; an unknown or foreign id 404s either way. For an imported (spend-capable) wallet, revocation also neutralizes its Privy spend policy back to default-deny, reported as policyNeutralized: true/false. The field is omitted for an attribution-only link, which never had a policy to neutralize.
Attribution
Section titled “Attribution”Every linked wallet, spend-capable or attribution-only, counts toward trade attribution once linked: a transaction it signs is recorded against the Candle account that linked it, exactly like a transaction from the account’s own primary wallet. This applies both to activity an agent reports itself and to trades the Hood curve-trade ingester picks up directly from the chain.
Agent profiles: which wallets a key can use
Section titled “Agent profiles: which wallets a key can use”An API key is an agent profile: it has a name, an avatar, a stable profileId that survives key rotation, and its own set of wallets.
By default a profile spends only from the wallets assigned to it. Assignment is many-to-many in both directions: one profile can hold several wallets, and several profiles can hold the same wallet (a read-only reporting profile and a trading profile over one treasury wallet is the usual reason).
| Scope | Meaning |
|---|---|
selected | The profile can spend only from its assigned wallets. This is the default for every profile created since profiles shipped. |
all | The profile can spend from every wallet on the account. Keys issued before profiles existed are all, so nothing they could do before stopped working. |
Scoping applies to linked wallets. It does not restrict the account’s own embedded launch wallet, which is reached through delegation rather than assignment. Whether a key may use the embedded wallet is a separate per-key setting: see The embedded wallet: a per-key permission.
The profile that imports a wallet is assigned it automatically, so the usual keys create → wallets import → trade flow needs no extra step.
Reading and setting the set
Section titled “Reading and setting the set”candle keys wallets a1b2c3d4candle keys wallets set a1b2c3d4 --wallets wal_1,wal_2candle keys wallets scope a1b2c3d4 --scope selectedThe same three operations exist over HTTP (GET/PUT /agent/keys/{prefix}/wallets, PUT /agent/keys/{prefix}/wallet-scope), in the SDK (getProfileWallets, setProfileWallets, setProfileWalletScope) and as MCP tools (candle_get_profile_wallets, candle_set_profile_wallets).
To give a key TEE wallets, do not set them here: move them to the key with candle tee rebind <wallets...> --to-key <prefix> (or --label-prefix <p>), as the account owner. See Rebinding a wallet to another key.
Setting the wallets replaces the whole set: a wallet left out of the list loses access, and an empty list leaves the profile with none. Read walletScope before drawing any conclusion from the list: an empty list means every wallet under all and no wallet under selected.
A trade naming a wallet the profile does not hold is refused at build time, before anything is signed, with a message naming the fix.
The embedded wallet: a per-key permission
Section titled “The embedded wallet: a per-key permission”The account’s embedded wallet is the delegated wallet Candle signs for on the server. Each API key carries its own embeddedWallet setting, allowed or denied, which decides whether that key may use it.
A denied key is refused with EMBEDDED_WALLET_NOT_PERMITTED (403, not retryable) before anything is signed, on every route that spends from, signs for, or launches with the embedded wallet:
- trades with
payer: {"type": "main"}at/trade/agent/build, and again at/trade/agent/execute, because the setting can change between the two; - the one-shot
POST /agent/swap; POST /agent/transfer;POST /agent/wallets/embedded/close-empty(the rent returns to the same wallet, but the wallet still signs);- atomic launches with a main leg, headless launches and their dry runs, and Hood metadata edits.
Paper trades, quotes, and reads are not refused. GET /agent/wallets/embedded reports the calling key’s setting as embeddedWalletPermission. A quote from a denied key carries payerPermitted: false. The embedded wallet may still receive: a swap or transfer that lands in it is unaffected.
Existing keys are allowed. A key issued before this setting existed has no stored value, and no value means allowed, so no running agent stopped working.
New keys are denied unless you allow them. Every way to create a key asks: the key-create form in the web key manager and the device approval page (a login from the CLI) both show May use the account’s embedded wallet, unchecked, and candle keys create takes --embedded-wallet deny|allow, which defaults to deny and asks you to confirm allow. POST /agent/keys stores whatever it is sent as embeddedWallet. If the request leaves it out, the key is created denied. That includes a CLI released before this setting, so a key such a CLI creates cannot use the embedded wallet until the owner allows it. The response echoes the value it stored.
Changing it. A key can deny itself the embedded wallet, but it can never allow itself:
PUT /api/v1/agent/keys/self/embedded-walletx-api-key: cndl_live_...
{ "permission": "denied" }From the CLI on the bot box, that is candle keys self embedded-wallet deny.
{"permission": "allowed"} on that route is LOOSEN_REQUIRES_SESSION (403). The route acts only on the calling key, so one key cannot change another’s setting. Only the account owner can allow a key: in the web key manager (the key’s Embedded wallet section), with candle keys update <prefix> --embedded-wallet allow|deny over the CLI device token, or directly on PUT /api/v1/agent/keys/{prefix}/embedded-wallet. Preview with {"permission": "allowed", "dryRun": true}, then commit with {"permission": "allowed", "expect": "denied"}. expect is the value the preview showed. If the key changed since the preview, the commit is refused with KEY_ACCESS_STALE (409) instead of overwriting it. Every change is recorded in the key’s access history, GET /api/v1/agent/keys/{prefix}/access-changes, as a row with change: "embedded_wallet", along with the credential that made it.
Recommended setup for a bot. Give each bot its own key. Give that key TEE wallets only, and deny it the embedded wallet. Such a key cannot launch through the headless route or with an embedded-wallet leg of the atomic route; it launches from one of its TEE wallets with allowLaunch on (above), and through the atomic route only where TEE_LAUNCH_BUYS_ENABLED is on. A key you already share with a bot is allowed. Deny it from the bot with the self route above, or as the owner with the owner route.
What is per profile and what is per account
Section titled “What is per profile and what is per account”| Scope | Notes | |
|---|---|---|
| Wallet access | Per profile | The assignment set above. |
| Per-asset max-per-transaction cap | Per profile | An asset the profile does not cap is unlimited. |
Windowed USD volume cap (txLimit) | Per profile | Daily, weekly, monthly or lifetime. Independent of the per-asset caps; whichever binds tighter wins. |
| Request rate limit | Per profile | Each key carries its own rateLimitPerMin. |
| Embedded-wallet permission | Per profile | Each key is allowed or denied the account’s embedded wallet. |
| Daily launch cap | Both | Each profile carries its own cap value, but all of them draw down a single per-account counter, so two profiles cannot each spend the full allowance. |
| Linked-wallet cap | Per account | Set by tier. Assigning a wallet to several profiles does not consume it more than once. |
| Tier and its floors | Per account | A profile cannot exceed what the account’s tier allows. |
| Trading and launch fees | Per account | Charged on the trade, identical whichever profile submitted it. |
| Usage metering | Per profile | Trade volume, trade count and launches are recorded against the acting key, so each profile’s activity is separable. |
| Fee totals | Per account | Accumulated per account and asset. GET /agent/keys/{prefix}/pnl also reports the fees charged against one profile’s own fills. |
| Realized and unrealized P&L | Per profile | Computed from the profile’s own fills. Unrealized marks the open positions against current prices and is reported separately from realized, alongside how many positions could not be priced and how old the marks are. Deposits and withdrawals are excluded, since funding a wallet is not profit. |
The rule of thumb: anything that protects the account from a key is per profile; anything the account pays for or is entitled to is per account.
Spend limits
Section titled “Spend limits”Every agent-controlled signer, the account’s own delegated launch wallet and every spend-capable linked wallet, starts unlimited: no cap exists until one is set. Caps are set per API key, per asset, and an asset the key does not cap is unlimited. There is no account-wide cap.
PUT /api/v1/agent/keys/:prefix/limitscontent-type: application/json
{ "limits": [{ "asset": "sol", "maxPerTxRaw": "2000000000" }] }Each limit is { "asset": "sol" | "usdc" | "cndl" | "eth" | "usdg", "maxPerTxRaw": "<raw units>" }: sol, usdc and cndl cap Solana spends, eth and usdg cap Hood spends, and one list may mix both. Send limits: null to clear the key back to unlimited. A key reads its own caps with GET /api/v1/agent/keys/self/limits, which is the check to make before a large trade.
Tightening (a new cap, or a lower one) works from a session or from an agent key with launch:write. Loosening (raising or clearing a cap) requires a signed-in session: an agent key that tries is rejected LOOSEN_REQUIRES_SESSION (403) before anything is written, so a compromised key can never widen its own leash.
A SPEND_LIMIT_EXCEEDED response names the key’s cap in spendLimit (limitSource is always key).
Two things this gate deliberately does not cover: sells (a sell spends the token being sold), and base-pair swaps between SOL, USDC, and CNDL, which are never spend-capped.
A separate, independent dimension is the per-key transaction limit (txLimit): a cumulative USD volume ceiling over a rolling window rather than a per-transaction cap. See transaction limits.
See the Agent trading API for how a trade’s total spend, amount plus fee, is checked against the payer’s scope at build time.
The wallet manager
Section titled “The wallet manager”The Agent Console’s Wallets tab (alpha.candle.tv/agents?tab=wallets) lists the account’s linked wallets with revoke and the Trusted mark. Each key’s spend caps are edited on that key in the Agents tab. The plan line above the tabs shows the current plan.
Errors
Section titled “Errors”| Code | Status | Meaning |
|---|---|---|
TIER_REQUIRED | 403 | A Free account attempted an import, link, self-signed launch, or linked-payer trade. |
WALLET_LIMIT_REACHED | 400 | The account already has the maximum active linked wallets for its tier; revoke one first. An import is refused before the key is sent to the wallet provider, and the error carries linkedWallets.active and linkedWallets.cap. |
WALLET_ALREADY_LINKED | 409 | The address already has an active linked-wallet row, on this account or another one. |
LOOSEN_REQUIRES_SESSION | 403 | PUT /api/v1/agent/keys/:prefix/limits loosened a cap from an agent key instead of a signed-in session, a key tried to widen its own wallet set, scope or access level, or a key asked to allow itself the embedded wallet. |
EMBEDDED_WALLET_NOT_PERMITTED | 403 | This key is denied the account’s embedded wallet. Trade a TEE wallet bound to the key, or have the owner allow it. |
Next steps
Section titled “Next steps”- Launch from a linked wallet: Headless launch API
- Trade from a linked wallet: Agent trading API
- The tier a wallet needs, and how to earn it: Agent access & API keys
- A typed client for the import flow: TypeScript SDK