Repository navigation
Keys and Scopes
Every SDK call authenticates with an API key created in your dashboard
(Integrations → API keys → + New key). Keys look like
2dai_sk_<id>_<secret>; the secret is shown once at creation. The full
step-by-step (account → dashboard → dialog → storing the secret) is in
Getting Started. An account can hold up
to 20 active keys.
| Scope | Unlocks |
|---|---|
read |
me(), creations.list/random/feed/get, queue.get/waitFor, folders.list, folders.groups.list, uploads.checkDuplicate, stats.*, finance.tokenPrice/tiers, all cdn.* downloads |
generate |
generate.* (image / refs / wallpaper / video / similar), uploads.image, queue.cancel, creations.explainNsfw/scoreFaceRef/scoreCharacterRef (their first, uncached call burns an LLM pass) |
manage |
creations.move/trash/restore/delete/clone/like/flagNsfw/reorder/batch, folders.create/update/delete, folders.groups.create/rename/remove
|
publish |
creations.publish/unpublish |
finance |
finance.wallet/tier/lock/transactions/balanceHistory/creditHistory/creditSources — read-only wallet data, off by default (see below) |
Calling a method your key doesn't cover throws a ScopeError
(HTTP 403, MISSING_SCOPE) carrying requiredScope and the key's actual
scopes — e.g. a read-only key calling generate.image():
403 { error: 'MISSING_SCOPE', requiredScope: 'generate', scopes: ['read'] }
finance unlocks the read side of your wallet — balance, tier, staking lock,
transaction history, balance/credit charts, credit sources. It exists as its
own scope because that data is more sensitive than your creation library, so
it follows two deliberate rules:
-
Opt-in, never default. The key dialog never pre-selects it — a key only
carries
financeif you ticked it at mint time. Your existing keys don't gain it retroactively either; mint a new key when you want it. -
Read-only, structurally. There is no deposit, withdraw, swap or lock
operation anywhere on the API-key surface — money mutations live in the
dashboard, behind its own confirmations. A leaked
financekey can therefore see your numbers, but there is no call it could make to move funds.
Two reads are deliberately outside the scope: finance.tiers() (the public
tier catalogue) and finance.tokenPrice() (the cached $2DAI quote) carry no
account data and only need read.
403 { error: 'MISSING_SCOPE', requiredScope: 'finance', scopes: ['read','generate'] }
The namespaces are documented in depth in Stats & Wallet.
A key may carry a spend cap — a USD ceiling on what that one key can burn. Your account credit is always the real limit; the cap just contains a single integration.
const me = await client.me();
const k = me.key; // KeyContext
const remaining = k.spendLimitUsd === null
? me.creditUsd // uncapped → account credit
: Math.min(me.creditUsd, k.spendLimitUsd - k.spentUsd);-
Cap reached → generations throw
SpendLimitError(HTTP 402,KEY_SPEND_LIMIT_REACHED) withcostUsd/spentUsd/spendLimitUsd. The refusal happens before any charge — a cap of0blocks the very first generation:402 { error: 'KEY_SPEND_LIMIT_REACHED', costUsd: 0.07, spentUsd: 0, spendLimitUsd: 0 } -
Account credit too low →
InsufficientCreditErrorwithdeficitUsd. -
Refunded generations (failures) are credited back to both the account and the key's
spentUsd.
- One key per integration (laptop agent, CI, server) — so revoking one doesn't break the others, and the dashboard Usage tab attributes spend per key.
- Keys are create/revoke only: to rotate, create a new key, switch your integration, then revoke the old one.
- Revocation is instant — in-flight clients get HTTP 401
INVALID_API_KEY(AuthError) on their very next call, including read-only ones likeme(). - Store secrets in env vars (
TWODAI_API_KEY), never in code or client-side bundles you ship publicly.