Skip to content

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.

PathGrantsUse it for
ImportA Candle-managed signer and a spend policy, so the agent key can sign with itSelf-signed launches, linked-payer trades
Link existingAttribution only, no signer, no spend authorityCounting an already-owned wallet’s activity toward the account

Both kinds count toward the tier’s linked-wallet cap (10 on Pro, 1,000 on Max) and are revocable at any time.

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 wallets import 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:

  1. POST /api/v1/agent/wallets/import/init returns Privy’s HPKE receiver public key for the wallet’s chain and address.
  2. 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.
  3. POST /api/v1/agent/wallets/import/submit posts just those three values. Candle registers a signer from the public key and attaches a default-deny spend policy to it 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": "..." }
POST /api/v1/agent/wallets

Attaches 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.

GET /api/v1/agent/wallets

Lists the account’s linked wallets, active ones first.

DELETE /api/v1/agent/wallets/:id

Tombstones 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.

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.

Every agent-controlled signer, the account’s own delegated launch wallet and every spend-capable linked wallet, starts unlimited: no cap exists until the account sets one. Limits live in three layers, and the narrowest one that names an asset wins:

ScopeGovernsEnforced
mainThe account’s own delegated walletServer-side, when Candle builds a launch dev buy or a trade
linkedEvery spend-capable linked walletServer-side at build time, and compiled into each wallet’s own Privy policy
Per keyOne API key, per asset, via PUT /api/v1/agent/keys/:prefix/limitsServer-side at build time, replacing the account scope for any asset it names

A key’s cap replaces the account’s for the assets it names rather than combining with them; an asset the key does not name still falls through to the account scope. A SPEND_LIMIT_EXCEEDED response says which layer bit, in spendLimit.limitSource (key or account). Only a signed-in session may loosen an effective cap; an agent key may only tighten one (LOOSEN_REQUIRES_SESSION, 403). A key can read its own effective caps with GET /api/v1/agent/keys/self/limits, which is the check to make before a large trade.

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.

GET /api/v1/agent/limits
{ "success": true, "main": [{ "asset": "sol", "maxPerTxRaw": "2000000000" }], "linked": null }

null means unlimited for that scope.

PUT /api/v1/agent/limits
content-type: application/json
{ "scope": "main", "limits": [{ "asset": "sol", "maxPerTxRaw": "2000000000" }] }

Each limit is { "asset": "sol" | "usdc" | "cndl" | "eth" | "usdg", "maxPerTxRaw": "<raw units>" }. Send limits: null (or []) to clear a scope back to unlimited.

Tightening (a new cap, or a lower one) works from either an agent key or a Privy session. Loosening (raising or clearing a cap) requires a Privy 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.

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.

staging.candle.tv/dev/agent lists an account’s linked wallets with revoke, and exposes both spend-limit scopes as editors, next to the tier strip.

CodeStatusMeaning
TIER_REQUIRED403Free or Believer account attempted an import, link, self-signed launch, or linked-payer trade.
WALLET_LIMIT_REACHED400The account already has the maximum active linked wallets for its tier; revoke one first.
WALLET_ALREADY_LINKED409The address already has an active linked-wallet row, on this account or another one.
LOOSEN_REQUIRES_SESSION403PUT /api/v1/agent/limits loosened a cap from an agent key instead of a Privy session.