Skip to content

Agent access & API keys

Any Candle account can issue its own agent API keys and launch tokens headlessly, on Solana or Hood, with no partner agreement required. The keys authenticate the Headless launch API and activity reporting; market and feed reads need no key at all.

A headless launch needs a wallet that can sign server-side with no human in the loop. That is only possible for a wallet whose keys Candle’s wallet infrastructure actually holds: an embedded wallet, the kind an account gets by signing up with Google, X, or email. Getting a working key takes three steps, in order:

  1. Get an embedded wallet. Sign up, or add a login method, with Google, X, or email. An account that already has an embedded wallet for a chain skips this step.
  2. Delegate agent access. From the Agent access section on the Agents tab of the Agent Console (alpha.candle.tv/agents?tab=keys), grant Candle’s backend permission to sign with that embedded wallet on the account’s behalf. Delegation is per chain: an account can delegate its Solana wallet, its Hood (EVM) wallet, or both independently. It is revocable at any time from the same panel.
  3. Issue a key, as below.

Delegation is re-resolved live from the wallet provider on every launch; it is never cached or trusted from an earlier check. Revoke it and the very next launch attempt fails closed (DELEGATION_REVOKED), even though the key itself stays valid and works again the moment delegation is re-approved.

Keys are managed from the Agent access panel or directly against the API. The management endpoints accept the normal authenticated Candle session, or a device token, on every /keys management route below. POST /agent/enable takes the session only. An agent API key is never accepted here, so a leaked key cannot mint more keys.

Agent features are on by default. If an admin turned them off, this turns them back on:

POST /api/v1/agent/enable
content-type: application/json
{ "enabled": true }

Session only, and it requires a linked Solana wallet on the account. Only an admin can turn agent features off: { "enabled": false } is refused (403).

POST /api/v1/agent/keys
content-type: application/json
{ "environment": "production" }
FieldNotes
environmentproduction (default) or test. See test keys below.
scopesOptional subset of launch:write, launch:read, account:read, activity:write, swap:write, transfer:write, transfer:bound, lp:write. transfer:bound requires transfer:write in the same list. Omitting scopes on this call grants launch:write, launch:read, account:read, and activity:write. swap:write, transfer:write, transfer:bound, and lp:write are granted on POST /keys only when named. A device approval that names no scopes grants launch:write, launch:read, account:read, activity:write, and swap:write (the Write level). transfer:write, lp:write, transfer:bound, and the partner pair (entitlement:read and hacc:sync) are excluded from that default. No default path grants transfer:write: a key moves funds only when it is Withdraw or names the scope. account:read reads the whole account across every profile (trades, positions, P&L, transactions, workers) and writes nothing. The three access levels below are fixed lists of these scopes. Keys issued before account:read existed keep the scopes they were issued with until their owner changes them; to give an existing integration account-wide read, change its access level to Read or Write in place. launch:read is enforced by the launch jobs endpoint; a key issued without it cannot poll job status.
labelOptional display name, 1 to 64 characters. Renameable later. Without one, the dashboard derives a name from the key’s capabilities.
expiresInDaysOptional integer, 1 to 365, converted to an absolute expiresAt at creation. Mutually exclusive with expiresAt.
expiresAtOptional absolute epoch ms, strictly in the future. Omit both for a key that never expires (the default).
txLimitOptional { "usdMicros": <positive integer>, "reset": "never" | "daily" | "weekly" | "monthly" }. See Transaction limits.
embeddedWalletOptional allowed or denied (default denied). Whether this key may use the account’s embedded wallet; see The embedded wallet: a per-key permission.
refOptional referral code. Attributes the account once, best-effort, and never blocks issuance. A referred account then pays 10% less on the platform fee for every trade.

Issuance fails VALIDATION_FAILED (403) while agent features are off, and AGENT_WALLET_NOT_DELEGATED (403) when no chain is delegated.

{
"success": true,
"key": "cndl_live_...",
"keyPrefix": "a1b2c3d4",
"scopes": ["launch:write", "launch:read", "account:read", "activity:write"],
"environment": "production",
"rateLimitPerMin": 30,
"dailyLaunchCap": 5,
"launchChains": ["solana"],
"embeddedWallet": "denied"
}

The response also carries issuedCaps, effectiveCaps, raisedBy and checkedAt, echoes label, expiresAt and txLimit when they were set, and, when known, apiBaseUrl: the API this key was minted against. Send the key there.

The plaintext key is returned only in this response. Store it immediately; it cannot be recovered later. keyPrefix identifies the key in listings and in the revoke call.

launchChains records which chains were delegated at issuance. It is a display snapshot only, not the authority checked at launch time; delegation can change at any moment, so every launch re-resolves it live.

An account may hold at most 12 active keys. Revoke an unused key before issuing a thirteenth; past the limit, POST /api/v1/agent/keys returns VALIDATION_FAILED with the message Active key limit reached (max 12 active keys).

Every key is one of three access levels, or a custom scope set. The dashboard’s key manager (Agents tab of the Agent Console, alpha.candle.tv/agents?tab=keys) offers the three levels as presets, and the CLI mints them with candle keys create --access:

AccessScopesCanCannot
Readaccount:readSee the whole account: every profile, the books, P&LChange anything: no trades, launches, or transfers
Writelaunch:write, launch:read, activity:write, swap:write, account:readEverything Read can, plus trade, swap, bridge, launch, and report activityMove funds: no POST /transfer, candle_transfer, candle_sweep, or candle wallet close-empty
WithdrawWrite plus transfer:write and transfer:boundEverything Write can, plus move funds: between the account’s own wallets, to an address on the withdrawal allowlist, and out of the TEE wallet the key is bound to, to that wallet’s pinned vault or to a wallet you linked while signed in or marked trustedSend to any other address

Withdraw is the only level that moves funds. Until 2026-10-08 Write also held transfer:write. A key minted as Write before then keeps its scopes, matches no level, and shows as custom: Custom: Launch, Trade, Transfer, Report in the web key manager and Launch, Trade, Transfer, Report in candle keys list. The Transfer word is the flag. Nothing is migrated. Change its access to Write to drop transfer:write (a narrowing), or to Withdraw to keep it and add transfer:bound (a widening).

Terminal window
candle keys create --access read --label reporter
candle keys create --access write --label trader
candle keys create --access withdraw --label rebalancer

--scopes <a,b,c> mints a custom set instead (never both flags). An existing key’s level can be changed in place with PUT /api/v1/agent/keys/:prefix/access (see Changing a key’s access). candle keys create also takes --label, --expires-in <days>, --tx-limit <usd> with --reset daily|weekly|monthly|never (see Transaction limits), --environment production|test, and --embedded-wallet allow|deny (default deny; see Agent wallets).

Where each path starts. The web key-create form starts on Write for every plan; pick Read or Withdraw before you create the key. candle keys create with neither --access nor --scopes sends no scopes, so the key gets the omitted-scopes set from Issue a key: launch:write, launch:read, account:read and activity:write. That is a custom set, listed as Launch, Report, and it cannot trade; pass --access write for a trading key. A device approval (candle auth login without --scopes) grants Write, as Device authorization describes. No path starts on a level that moves funds: pick Withdraw yourself when an agent needs to transfer. On every path the embedded wallet starts denied unless you allow it.

lp:write is opt-in and in no preset: name it in a custom set to let a key provide liquidity. A key whose scopes match none of the three levels is shown by its capability words instead of a level name in candle keys list (for example Trade for a key holding only swap:write). The web key manager always shows the capability chips, never a level name. candle keys list --scopes adds the raw scopes.

The documented --access values are read, write and withdraw. The older read-write, transfer and read-write-transfer values remain accepted as silent, permanent aliases.

A device approval cannot grant transfer:bound yet, so candle auth login never mints a Withdraw key. Mint one with candle keys create --access withdraw or in the web key manager, or widen an existing key to that level over the device token with PUT /api/v1/agent/keys/:prefix/access. TEE wallets and their pinned vaults are explained in Custody tiers: the vault and TEE wallets.

GET /api/v1/agent/keys

Lists every key the account owns, revoked keys included, with keyPrefix, label (absent when the key was never named), scopes, environment, caps, createdAt, lastUsedAt, and revokedAt. Keys created over a device token also carry mintedByDevicePrefix, naming the device that minted them; the panel renders it as “Minted by device”. Attribution only, never authority: the field grants the device nothing.

Each row also carries a usage object (todayUsdMicros, weekUsdMicros, monthUsdMicros, lifetimeUsdMicros, days30UsdMicros, tradeCount30, launchCount30, lifetimeLaunches) beside a top-level usageSince.

DELETE /api/v1/agent/keys/:prefix

Revokes one key. An unknown prefix returns 404.

PATCH /api/v1/agent/keys/:prefix

Carries exactly one of { "label" }, { "avatar" }, { "txLimit" }, or { "clearTxLimit": true } per call; sending more than one is VALIDATION_FAILED. There is deliberately no expiration field here, because expiration is fixed at creation, and no scopes field: a key’s access level is its own route, below.

GET /api/v1/agent/keys/:prefix/usage?days=N

A zero-filled daily series for charting: days clamped 1 to 90 (default 30), ascending, exactly one entry per calendar day whether or not anything happened that day.

PUT /api/v1/agent/keys/:prefix/access

Moves one key between the three access levels in place. The prefix, the secret, the profile, the wallets, the caps, and the txLimit stay; only the stored scopes change, to exactly the target level’s list. Any key on the account can be the starting point, a legacy or custom scope set included (scopes the target lacks are removed and named in the preview), except a key issued for a partner integration, which is refused. Widening is an owner action: this route runs on a signed-in session or the CLI device token, and an agent key is refused before the handler runs. A key can never widen itself.

Every change is a preview, then a commit that must name the scopes the preview showed:

{ "access": "readwritetransfer", "dryRun": true }
{ "access": "readwritetransfer", "expectScopes": ["account:read", "activity:write", "launch:read", "launch:write", "swap:write"] }
FieldNotes
accessRequired. One of read, readwrite, readwritetransfer.
dryRuntrue runs every check and writes nothing.
expectScopesRequired on a commit: the key’s stored scopes as the preview returned them, compared as a set. A mismatch is KEY_ACCESS_STALE (409) carrying the current scopes, so a change made in another tab or on another device between preview and commit is refused rather than overwritten.

Both modes answer the same shape:

{
"success": true,
"dryRun": false,
"keyPrefix": "Ab3dEf9h",
"label": "cndl",
"direction": "widen",
"from": { "access": "readwrite", "scopes": ["account:read", "activity:write", "launch:read", "launch:write", "swap:write"] },
"to": { "access": "readwritetransfer", "scopes": ["account:read", "activity:write", "launch:read", "launch:write", "swap:write", "transfer:bound", "transfer:write"] },
"added": ["transfer:bound", "transfer:write"],
"removed": [],
"effects": { "boundTeeWallets": { "count": 3, "sample": [{ "id": "k57…", "label": "tr-01", "address": "7xKX…gAsU" }] }, "walletScope": "all", "linkedTransferReady": { "txLimit": true, "assets": { "sol": true, "usdc": false, "cndl": false } } },
"warnings": [{ "code": "LINKED_TRANSFER_NEEDS_LIMITS", "message": "…" }],
"changeId": "j97…",
"at": 1790000000000
}

direction is decided by the server from the stored scopes: unchanged (nothing written), narrow (the target is a strict subset), or widen (anything is added, a lateral move included). from.access is null for a custom or legacy source. effects is advisory and never gates the commit; each field appears only when it matters to this direction: the TEE wallets bound to the key, the wallet scope, whether a linked-wallet transfer would be admitted today (linkedTransferReady), and, on a narrowing, the key’s open sign claims, open limit orders, and workers. warnings names each effect in words (MOVES_FUNDS, VAULT_TRANSFER_READY, LINKED_TRANSFER_NEEDS_LIMITS, BOUND_WALLETS_STOP_TRADING, OPEN_ORDERS_STRANDED, OPEN_SIGNS_REFUSED, WORKERS_STOP, DROPS_UNLISTED_SCOPES). changeId and at are present on a committed change only.

Widening to Withdraw grants its two transfer scopes (only transfer:bound from an old Write key that already holds transfer:write) and nothing else. A transfer to a linked wallet still needs a spend cap for the asset and a USD txLimit on the key, which this route does not set; a transfer to each bound wallet’s own vault needs neither and is possible as soon as the change commits. Narrowing takes effect on the key’s next request: there is no cache between the key row and the check. A Withdraw transfer built but not yet signed is refused at the sign relay once the key no longer holds transfer:bound (SCOPE_MISSING, nothing signed), and POST /transfer/submit refuses it too. Open limit orders stay open (any key on the account holding swap:write can cancel them), spend reservations stay counted, and wallets bound to the key stay bound.

CodeHTTPWhen
VALIDATION_FAILED400malformed body, access not one of the three ids, expectScopes missing on a commit
VALIDATION_FAILED404the prefix is not one of the caller’s keys (Key not found, the same answer for a missing key and another account’s key)
VALIDATION_FAILED409the key is revoked (Key has been revoked) or expired (Key has expired)
KEY_ACCESS_STALE409expectScopes differs from the stored scopes; the body carries the current scopes
KEY_ACCESS_PARTNER_KEY409the key holds entitlement:read or hacc:sync
LOOSEN_REQUIRES_SESSION403the self route below, and the direction is widen
SWAP_FAILED503the change could not be verified, or a deploy is half rolled out and the two halves disagree on a level’s scopes (retryable; nothing changed)

Every refusal writes nothing.

PUT /api/v1/agent/keys/self/access

The same body and response, authenticated by the agent key itself (x-api-key, any scope). A key may narrow its own access and never widen it: a widening is LOOSEN_REQUIRES_SESSION (403). There is no prefix, so a key cannot change another key. It is not stopped by the kill switches or by a pause, because a rein-in must survive an incident.

GET /api/v1/agent/keys/:prefix/access-changes?limit=20

One key’s access changes, newest first (limit 1 to 100), on a session or the device token. Each row carries changeId, at, direction, from and to (level and scopes), actor (session, device, or agent for the key itself), actorDevicePrefix (device only), and actorKeyPrefix (agent only). The device prefix is attribution, not a credential: a device token cannot revoke devices, so the owner revokes a named device from the Connected devices (Agents tab) panel, with a session.

Each committed change fires key.access_widened or key.access_narrowed (see the webhook events table in Agent trading). Both are new events, so an endpoint receives them only once it adds them to its event list.

A key’s expiresAt is set at creation and immutable. There is no renewal call and no PATCH field for it. Once it passes, every authenticated call with that key, on any endpoint and any scope, fails KEY_EXPIRED (401) rather than the generic UNAUTHORIZED a bad key gets. The check runs after the key’s secret is verified, so a bare prefix (which can leak through a display UI) still cannot probe a key’s expiry. Recovery is always the same: issue a new key.

A key’s txLimit caps its own cumulative USD-valued trading volume over the window reset names: daily (UTC day), weekly (ISO week), monthly (UTC calendar month), or never (its whole lifetime). It is checked at build time on every rail that spends (POST /api/v1/trade/agent/build, /agent/swap/build, and transfers); exceeding it is KEY_LIMIT_REACHED (403), carrying resetsAt (epoch ms, or null for a lifetime cap, which is why only a windowed cap reports retryable: true). A token sell on /trade/agent/build is the exception: it counts toward the cap when it books and is never refused by it, so a position can always be exited. A base-pair trade (a base asset against a base quote) keeps the cap on both sides. See KEY_LIMIT_REACHED in Agent trading.

An unsigned TEE transfer to a linked wallet counts until a bounded sweep page reaches and releases its hold, strictly after the hold is 30 minutes old and each unconsumed sign claim has passed its own 30-minute lifetime (and, on Hood, the operation’s expiry).

This is a different mechanism from the per-asset spend limits: a spend limit caps one asset’s raw amount on a single transaction, while txLimit caps cumulative USD volume over time. A key can carry both.

The session-based flow above assumes a browser. Agent machines usually do not have one, so the rail also supports an OAuth-style device flow: the machine asks for access, a human approves it once from any signed-in browser, and the machine ends up holding its own key-management credential. The Candle CLI drives this end to end: install it with curl -fsSL https://candle.tv/install.sh | bash (or brew install candledottv/tap/candle), then run candle auth login.

How it works:

  1. The device requests a code and shows a verification URL plus a short user code.
  2. You open the URL in a signed-in browser. The approval screen names the client and lists the exact scopes being granted before you approve; a request that names no scopes gets launch:write, launch:read, account:read, activity:write, and swap:write (the Write level, which cannot move funds). transfer:write, lp:write and the partner pair (entitlement:read and hacc:sync) are excluded, and the screen says so; a request may still name transfer:write, and the screen flags it as moving funds. transfer:bound is refused on this route even when named.
  3. On approval the device exchanges its code for a device token (cndl_dvc_...) and an agent API key is provisioned immediately with the approved scopes. If provisioning fails (no delegation, or the account is at its key ceiling) the token is still issued and the response says why the key was not.

A device token is deliberately narrow. It authenticates only the owner’s management routes: the key-management ones above, changing a key’s access level included, plus owner-only wallet administration such as rebinding TEE wallets to another key and marking linked wallets trusted. It cannot launch or trade (that is what the keys it mints are for), it cannot list or revoke devices, and it cannot approve another device’s login. The last two mean a stolen token can neither widen its own access nor cut off the session that could revoke it. Device tokens do not expire; revocation is the lifecycle.

The Connected devices (Agents tab) panel on your Agents tab lists every device authorization with its label and last use, and groups the API keys each device minted. Revoking a device offers to also revoke those keys; accepting revokes the device and every active key it minted in a single atomic operation, so a containment action cannot be interrupted halfway. Keys you leave in place keep working: minting is attribution, not a dependency.

Send the key on every headless request as the x-api-key header:

x-api-key: cndl_live_... # production key
x-api-key: cndl_test_... # test key

The same header works on the hosted MCP server at https://mcp.candle.tv/mcp, using a client or platform that can send x-api-key through its documented secret mechanism. Hosted access is the intersection of the key’s permissions and the hosted read-tool subset; Use a dedicated key with account:read for account reads. A chat app without custom-header support can use only the public tools there.

Every account is on a plan. The plan sets rate limits, daily launch caps, image-upload limits, how many wallets can be linked (see Agent wallets & spend limits), the platform fee on API-built transactions (see Headless launch API and Agent trading API), and what the account’s agents can do. Plans, pricing & limits has the full tables, generated from the API’s own plan table.

Read the plan table from the API, not from a page. GET /api/v1/agent/plans needs no key and returns the table this deployment enforces and charges, overrides included: each plan’s price, feeBps, perpFeeBps (the Hyperliquid builder fee), limits and capabilities, one boolean per plan gate, plus promoMaxDays. candle plans, the SDK’s getPlans() and the MCP tool candle_get_plans read it.

The defaults:

PlanPriceRequests/minLaunches/dayUploads/minLinked walletsFee
Freefree3051001%
Pro$50 a month, or through CNDL3005030100.5%
Max$200 a month6001,000601,0000.25%

A web-app swap pays the same plan rate as an agent trade.

A key’s limits are at least its plan’s limits; a key issued above them keeps its higher issued limits. Every account has a live plan, re-evaluated continuously, and the cap enforced on each request is the higher of the key’s issued cap and the plan’s, so a key issued under older, lower limits reads today’s limits without being reissued, and an individually raised key is never lowered by a later plan change. The daily launch cap is per account, not per key: every launch attempt, confirmed or still in flight, consumes one slot of it, whichever key made the request.

Agent staking uses the embedded Solana wallet or an eligible linked/TEE wallet, with swap:write. The staking write rail is off by default. No agent surface offers instant unstake. Read the pool’s withdrawalTimelockSeconds before starting a withdrawal; CNDL stays unavailable until finish.

RuleWhat it readsWallet outside the account’s primary address
Pro by staking or holdingCNDL and sCNDL summed over the account’s eligible Solana walletsCounts, when only this account claims the wallet
Hood staking rewardsEach Solana holder mapped to the account it is an eligible wallet of, paid to that account’s EVM walletEarns the account’s Hood reward. A holder of no account, of more than one account, or of an account with no EVM wallet earns none, and its share goes to the others, permanently for that token
Solana staking rewardsThe holding walletEarned normally; claimable by a key allowed to use that wallet
Pending withdrawal CNDLNeither the CNDL nor sCNDL balanceCounts toward nothing for Pro throughout the wait

client.staking in the SDK, candle stake in the CLI and the five staking connector tools drive these routes. The hosted connector is embedded-wallet only and registers the tools only while the flag is on. See the staking reference for raw versus decimal amounts, costs, wallet selection and replay-safe retries.

An account qualifies for Pro by staking 500,000 CNDL (as sCNDL) or holding 1,000,000 CNDL, whichever comes first, summed over all of the account’s Solana wallets: the primary wallet, the embedded wallet and every active linked wallet (TEE wallets included) that only this account claims. A wallet another account also claims counts for neither. At most 200 wallets count, the primary and embedded wallets first and then linked wallets oldest first. The same wallets earn Hood staking rewards for the account, in snapshots taken from 2026-10-14; earlier snapshots are final. This is checked live, continuously, not just at key issuance. There is no grace window: if the balance drops below both thresholds, the account is Free on the next check, within 5 minutes, unless it pays for Pro. Pro can also be bought, $50 a month by card or in USDC, and a paying Pro member who qualifies through CNDL stops being billed automatically.

Max is self-serve: $200 a month, by card or in USDC, for a paid period that renews unless you stop it. A first Pro purchase can include a period of Max; promoMaxDays in GET /api/v1/agent/plans gives its length (30 on 2026-10-07, and 0 when no promotion runs). An admin can also grant Max manually, with an expiry date.

Where a display value is needed (the /agents console’s plan line, the tier endpoint below), plans resolve in order: max > pro > free.

The Agent Console (alpha.candle.tv/agents) opens with a plan line above its tabs: the current plan, the Max grant source (subscription or trial) and, for a trial, the days left, with a link to the pricing page (/agents/pricing). The console’s Overview shows the account’s current fee rate and caps once the agent has made its first request; the Pro-qualification progress line appears there while it applies.

GET /api/v1/agent/tier returns the calling account’s full tier picture in one call. It accepts either an agent key or a Privy session (dual auth), is never tier-gated, and stays available even when headless launches are disabled. It also carries planTable (the same table GET /api/v1/agent/plans serves), maxPricing, proPricing (null while Pro is not sold), tierCaps and checkedAt.

{
"success": true,
"tier": "pro",
"liveTier": "pro",
"stakedCndl": 600000,
"heldCndl": 0,
"thresholds": { "minStakedCndl": 500000, "minHeldCndl": 1000000, "graceMs": 0 },
"grace": { "active": false, "startedAt": null, "endsAt": null },
"maxTierExpiresAt": null,
"maxExpired": false,
"feeBps": 50,
"feeTotals": [{ "chain": "solana", "quoteAsset": "sol", "feeRawSum": "9007199254740993", "count": 3 }]
}
FieldNotes
tier / liveTiertier is the display tier (max > pro > free); liveTier is the live plan. Neither reports believer.
grace.startedAt / grace.endsAtnull unless grace.active is true; endsAt is startedAt + graceMs. There is no grace window, so grace.active is always false and thresholds.graceMs is 0.
maxExpiredAlways present: true when the account’s Max has ended. It then carries maxExpiredAt, maxExpiredSource, maxExpiredNotice (a pre-written sentence to show as is) and renewalUrl.
maxExpiredSourcePresent when maxExpired: subscription, trial, team, or promo (the Max included with a first Pro purchase has ended; not reported while the account still pays for Pro). Treat an unknown value as a generic Max expiry.
maxTierExpiresAtnull unless the account is currently Max. A lapsed or revoked grant reports null rather than a past date, so this field never has to be date-compared.
feeBpsThe account’s resolved platform fee, in basis points, on API-built transactions.
feeTotals[].feeRawSumA raw-unit BigInt string (lamports, wei, and so on), never a JSON number. Format it with the quote asset’s own decimals; do not coerce it with Number().

A test-environment key authenticates as the real account, but the server scopes what it can launch: the headless endpoint rejects a test-key request whose visibility is not test or hidden (VALIDATION_FAILED, field visibility). Use a test key for integration testing so nothing lands in the production feed by accident.

Agent launches were previously funded by Candle service wallets. That path no longer exists: a key issued before the change cannot launch again until its owner delegates at least one chain, with no grandfathering and no platform-funded fallback.