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 panel on your Candle profile (staging.candle.tv/dev/agent), 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. Enable agent features and 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 the three key routes below; an agent API key is never accepted here, so a leaked key cannot mint more keys.

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

Requires a linked Solana wallet on the account. This step alone does not require delegation; delegation is checked when a key is actually issued.

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, activity:write, swap:write. Defaults to the first three; swap:write (trading) is only ever granted when asked for explicitly, here or on a device approval screen. 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.
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 requires agent features to be enabled and at least one chain delegated; otherwise it fails AGENT_WALLET_NOT_DELEGATED (403).

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

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 5 active keys. Revoke an unused key before issuing a sixth; past the limit, POST /api/v1/agent/keys returns VALIDATION_FAILED.

GET /api/v1/agent/keys

Lists every key the account owns, revoked keys included, with keyPrefix, 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" }, { "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.

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.

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 POST /api/v1/trade/agent/build; 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).

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 asks for all four, swap:write included, and the screen says so.
  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 exactly three routes, the key-management ones above (POST /keys, GET /keys, DELETE /keys/:prefix), and nothing else: 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 Authorized devices panel on your Agent access dashboard 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

Every account sits in one of four trust tiers. The tier sets rate limits, daily launch caps, image-upload limits, how many wallets can be linked (see Agent wallets & spend limits), and the platform fee on API-built transactions (see Headless launch API and Agent trading API):

TierRequests/minLaunches/dayUploads/minLinked walletsFee
Free3051001%
Believer30201000.5%
Pro3005030100.25%
Max6001,000601,000Free

A key’s own row caps are set once at issuance, from whether the account’s wallet held a Believer NFT at that moment. On top of that, every account has a live tier, re-evaluated continuously; the cap actually enforced on each request is the higher of the two, so an individually raised key is never lowered by a later tier re-evaluation. 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.

An account qualifies for Pro by staking 500,000 CNDL (as sCNDL) or holding 1,000,000 CNDL, on the account’s primary wallet, whichever comes first. This is checked live, continuously, not just at key issuance. If the balance drops below both thresholds, Pro is not lost immediately: a 48-hour grace window keeps it active, falling back to Free only if the balance is still short once the window closes.

Max is sales-led, not self-serve. Contact Candle via Telegram from the /dev/agent page to request it. An admin grants Max manually, with an expiry date; it does not renew itself.

Where a display value is needed (the /dev/agent dashboard, the tier endpoint below), tiers resolve in order: max > pro > believer > free. Believer resolves from the calling key’s own issuance-time cap, not a separate live check.

The staging.candle.tv/dev/agent page opens with a tier strip: the four-tier table above, the current tier highlighted, a Pro-qualification progress line for Free and Believer accounts, a grace-window banner while Pro is held open by the 48-hour window, a Max expiry line, the account’s current fee rate, and lifetime fee totals by chain and asset.

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.

{
"success": true,
"tier": "pro",
"liveTier": "pro",
"stakedCndl": 600000,
"heldCndl": 0,
"thresholds": { "minStakedCndl": 500000, "minHeldCndl": 1000000, "graceMs": 172800000 },
"grace": { "active": false, "startedAt": null },
"maxTierExpiresAt": null,
"feeBps": 25,
"feeTotals": [{ "chain": "solana", "quoteAsset": "sol", "feeRawSum": "9007199254740993", "count": 3 }]
}
FieldNotes
tier / liveTiertier is the display tier (max > pro > believer > free); liveTier never reports believer, since that label exists only on the display tier.
grace.startedAtnull unless grace.active is true.
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.