- API mode (managed) —
POST /core/v1/new_accountgenerates a fresh random secret server-side and returns it once as a base64 API key, along with the wallet address derived from that secret. You hold the key; that key is the account credential. Admin writes are sent as HTTP calls and the server submits the on-chain transaction on your behalf. - ChainSecured mode (unmanaged) — A wallet you control (an EOA, a Safe, or any contract account on Base) is the account owner directly on-chain. Admin writes are wallet-signed transactions you submit yourself. There is no account-level API key.
/core/v1 API for executing Lit Actions. The
difference is only in administrative operations — creating groups, adding
actions, registering PKPs, minting usage keys, etc.
Side-by-side
In the SDK and contracts, ChainSecured mode is the
sovereign mode
(mode: 'sovereign'); earlier material called it self-sovereign. Same
thing — wallet ownership of the account on Base, with no required server
round-trip for admin writes.
How billing splits in ChainSecured mode
This is the one place the two modes diverge on billing, and it surprises people: admin writes and Lit Action execution are billed on different rails.- Admin writes (create group, add action, mint usage key, …) in
ChainSecured mode are wallet-signed transactions you submit directly to
the
AccountConfigcontract. They never pass through the server’s metered HTTP endpoint, so they never hit the Stripe credit guard. You pay the Base gas for each write from your connected wallet, and nothing is charged to Stripe — there is no$0.01management charge. - Lit Action execution is unchanged. Runs are still metered at
$0.01/secondagainst your Stripe credit balance, exactly as in API mode, because execution goes through the server regardless of who owns the account.
$0.01
each (the server relays the tx and covers gas) and execution at
$0.01/second. Converting to ChainSecured moves only the admin-write half
off Stripe and onto your wallet’s gas; the execution half is untouched.
For ops and support: after a customer converts to ChainSecured, their
Stripe activity drops to Lit Action execution only — admin writes stop
appearing in Stripe entirely. This is expected, not “billing broken.” A
freshly-converted account that only does admin writes (and no executions)
can legitimately show zero Stripe charges. The Stripe credit balance
itself is preserved across conversion (see
Converting an API account to ChainSecured).
To confirm an account is sovereign, take the Stripe customer’s
metadata.wallet_address and read the account’s on-chain managed flag
(managed = false ⇒ ChainSecured). A recommended ops convenience is to
tag such customers in the Stripe dashboard (e.g. a mode: sovereign
metadata field) so they’re filterable without an on-chain lookup.When to pick which
Pick API mode if:
- You want to ship today and don’t want to manage gas, an RPC, or a wallet popup in your admin tooling.
- Your client is a server, a cron job, or a CI pipeline that needs a single shared credential.
- You’re prototyping or iterating quickly — onboarding is fast, usage API keys are rotatable (mint and revoke at will), and the dashboard reflects every change immediately.
- You don’t have a strong requirement that every configuration change be visible on-chain.
Recommended in the
dashboard’s login screen.
Pick ChainSecured mode if:
- You want self-custody of the account: no third party (including Lit) can unilaterally create groups, add actions, or mint usage keys on your behalf.
- A multisig (Safe) or DAO governs configuration changes — every action upgrade, every PKP added to a group, becomes a Safe proposal that signers can review.
- You want a fully on-chain audit trail of every admin operation, signed by your governance wallet.
- You’re integrating with a wallet-native dApp where the user’s connected wallet is already the natural source of authority.
managed = false on-chain and reject any admin
write that does not originate from the registered admin wallet.
How the wiring differs
API mode (mode: 'api', default)
Calls go over HTTP with your account API key in the header. The Core SDK
default constructor is API mode:
addGroup, addAction,
addUsageApiKey, …) takes that API key in X-Api-Key. See
API direct usage for the full workflow.
ChainSecured mode (mode: 'sovereign')
The Core SDK is constructed with mode: 'sovereign', an RPC URL, and the
on-chain AccountConfig contract address. Reads call the contract
directly; writes are wallet-signed and submitted via the connected signer.
Once a signer with a provider is attached (any ethers v6 signer that
carries a provider — for example, a JsonRpcSigner returned by
BrowserProvider.getSigner()), the SDK routes reads through that
provider instead of rpcUrl. The rpcUrl constructor option is the
fallback used for reads that happen before a signer is attached.
primaryType: "CreateWallet") that the server verifies before deriving
key material via the TEE; the client then registers the derivation path
on-chain in a second wallet-signed tx. Once the signer is connected and
the address-derived adminHashOverride is set on the client (the
dashboard does this automatically at login),
createWallet({ name, description }) does both steps for you — no
account-level apiKey is required in ChainSecured mode.
Call GET /get_node_chain_config (no auth required) for the live
contract_address and chain_id. The RPC URL is not returned by the API
— supply your own Base RPC endpoint as the rpcUrl fallback (any public
Base RPC works, e.g. https://mainnet.base.org or
https://base-rpc.publicnode.com). Once your signer is attached the SDK
prefers the wallet’s RPC, so this fallback only matters for the brief
window before connectSigner(signer) runs.
Using the dashboard
The Chipotle Dashboard offers both modes side-by-side on the login screen:- Sign in tab → “API mode” card (paste your API key) or “ChainSecured mode” card (Connect wallet).
- Create account tab → “API mode” card (email + name) or “ChainSecured mode” card (Connect wallet & create).
Converting an API account to ChainSecured
Conversion flips a managed account to unmanaged in a single on-chain transaction. The account’s on-chainapiKeyHash is preserved, so groups,
PKPs, action metadata, usage API keys, and the Stripe credit balance all
stay attached to the same account record. Only the admin wallet address
and the managed flag change.
The contract function is
WritesFacet.convertToChainSecuredAccount(uint256 apiKeyHash, address newAdminWalletAddress),
which is apiPayerOrOwner-gated. End users don’t call it directly — they
call POST /core/v1/convert_to_chain_secured_account with their existing
API key plus a wallet-signed proof of ownership of the new admin address;
the server’s api_payer signs the on-chain conversion on their behalf.
What’s preserved
- The on-chain
apiKeyHash(so all child resources stay attached). - Groups (permitted PKP IDs and CID hashes).
- PKPs (derivation paths, names, descriptions).
- Action metadata (registered IPFS CID names and descriptions).
- Usage API keys — they continue to authorize Lit Action execution exactly as before.
- Stripe credit balance — the wallet_cache entry is invalidated on conversion so billing routes to the new admin wallet’s customer record immediately.
What changes
account.adminWalletAddressbecomes the new wallet you signed with.account.managedflips fromtruetofalse.- Admin write authority moves entirely to the connected wallet. The original master API key can no longer authorize writes on this account (the contract rejects api_payer relays for unmanaged accounts).
- Read endpoints that key off
apiKeyHashcontinue to resolve to the same account.
Step-by-step (dashboard)
- Sign in to the dashboard in API mode with your existing master API key.
- Open the account dropdown and click Convert to ChainSecured. Confirm the irreversible-action prompt.
- Connect the wallet that will become the new admin. Both EOAs and
smart-contract wallets (a Gnosis Safe, a ZeroDev/Kernel account, etc.)
are supported. You pass the same request shape either way — the
server first attempts standard ECDSA recovery, and if the signature
doesn’t recover to the claimed address it treats that address as a
contract and verifies the signature via an on-chain ERC-1271
isValidSignature(digest, signature)call on the chain reported byGET /get_node_chain_config(Base). The contract wallet must already be deployed on that chain — counterfactual (ERC-6492) wallets are not supported. The dashboard prompts a chain switch if your wallet isn’t on that chain. - Sign the EIP-712 ownership-transfer typed data. The dashboard
composes a typed-data envelope with
primaryType: "ConvertAccount"— wallet UIs surface it as a labelled struct (address,issuedAt) under theLit ChainSecureddomain rather than a free-form message. The full canonical envelope is:typesis part of the EIP-712 type hash and the server schema validator rejects payloads where it differs by even one field — field declaration order matters. Your wallet produces an EIP-712 signature (eth_signTypedData_v4). - The dashboard
POSTs/core/v1/convert_to_chain_secured_accountwith{ new_admin_wallet_address, typed_data, signature }and your existing API key in the header. The server verifies the typed-data digest recovers to the new admin address, thechainIdmatches the node, theprimaryTypematchesConvertAccount(preventing cross-flow replay against the secret-emitting endpoints), and theissuedAttimestamp is within ±300 seconds, then has the api_payer submit the on-chain conversion. - On success, the dashboard switches mode to
sovereign, clears the stored API key, persists the wallet + preservedapiKeyHashto the ChainSecured session, and reloads.
Step-by-step (SDK)
convertToChainSecuredAccount is API-mode only and throws if called from
a sovereign-mode client. The returned api_key_hash is the same hash the
account had before conversion — keep it around if you need to seed a
sovereign-mode session for this account (the dashboard does this for you
automatically).
Things to verify after conversion
accountExistsByHash(api_key_hash)returnstruefrom the wallet context, whereapi_key_hashis the value returned byconvertToChainSecuredAccount(the preserved on-chain hash).- An admin write attempted with the original API key (e.g.
addUsageApiKey) fails — the contract rejects it now that the account is unmanaged. - An admin write signed by the new wallet succeeds.
- All groups, PKPs, action CIDs, and existing usage API keys are visible in the dashboard under the new wallet session, and a Lit Action run with one of those usage keys still succeeds.
- The Stripe credit balance is unchanged.
Reverse direction
There is no path back from ChainSecured to API mode. The contract revertsconvertToChainSecuredAccount if the account is already unmanaged. If
you need a managed account again, create a new one with newAccount
and re-add resources manually.
Transferring ownership of a ChainSecured account
Once an account is ChainSecured, you can hand it to a different admin wallet — for example, to rotate a compromised key, move control to a new team wallet, or migrate from an EOA to a fresh address. The transfer reassigns the on-chain admin in a single transaction while preserving the masterapiKeyHash and the billing wallet, so groups, PKPs, action
metadata, usage API keys, and the Stripe credit balance all stay attached
to the same account.
This is the ChainSecured-to-ChainSecured counterpart of conversion. It
does not change the managed flag — the account is unmanaged before
and after — it only swaps which wallet holds admin authority.
How it differs from conversion
Conversion is api_payer-relayed because a managed account has no on-chain admin to sign with. A ChainSecured account already has one, so the transfer is signed directly by the current admin wallet — there is no server endpoint, no EIP-712 typed-data envelope, and no api_payer involvement.What’s preserved
- The master on-chain
apiKeyHash(so all child resources stay attached). - The billing wallet and Stripe credit balance.
- Groups, PKPs, action metadata, and usage API keys — they continue to authorize Lit Action execution exactly as before.
What changes
account.adminWalletAddressbecomes the new wallet.- Admin write authority moves entirely to the new wallet. The previous admin wallet can no longer authorize writes on this account, effective immediately on confirmation.
- A new lookup entry —
uint256(keccak256(abi.encodePacked(newAdminWalletAddress)))→ masterapiKeyHash— is registered so the new wallet resolves to the account on login. (In ethers this isethers.solidityPackedKeccak256(['address'], [newAdminWalletAddress]).)
The contract function
apiKeyHash may be the master hash or any hash that resolves to it
(e.g. uint256(keccak256(abi.encodePacked(currentAdminWalletAddress)))
for an account that has already been transferred once); the contract maps
it to the master via allApiKeyHashesToMaster. On success it emits
ChainSecuredAccountOwnershipTransferred(masterApiKeyHash, previousAdminWalletAddress, newAdminWalletAddress)
— note the first topic is the resolved master hash, not the
apiKeyHash argument you passed in (they differ whenever you pass an
alias).
The call reverts if:
newAdminWalletAddressis the zero address.- No account resolves from
apiKeyHash(AccountDoesNotExist). - The account is managed (
InvalidRequest— useconvertToChainSecuredAccountinstead). - The caller is not the current admin wallet (
NoAccountAccess). The api_payer has no authority here. newAdminWalletAddressequals the current admin.newAdminWalletAddressalready owns an account (AccountAlreadyExists).
Step-by-step (dashboard)
- Sign in to the dashboard in ChainSecured mode by connecting the current admin wallet.
- Open the account dropdown and click Change Ownership (this item is hidden in API mode).
- Enter the new admin wallet address in the prompt. It must be a valid Ethereum address and must not already own an account.
- Confirm the irreversible-transfer prompt. Your connected wallet loses admin access the moment the transaction confirms and the new wallet becomes the sole admin.
- Sign the transaction with your current wallet when prompted. The dashboard shows a transaction preview before the wallet signs and a status banner while it confirms.
- On success the dashboard signs you out and reloads. Log back in by connecting the new admin wallet to continue managing the account.
Step-by-step (SDK)
transferChainSecuredAccountOwnership is sovereign-mode only and
throws if called from an API-mode client. It validates and checksums
newAdminWalletAddress, then resolves the current admin’s apiKeyHash
from the client’s adminHashOverride — so a ChainSecured session must
have that set to keccak256(abi.encodePacked(address)) (the dashboard
does this at login; set it via the constructor as above when driving the
SDK directly, otherwise the call reverts with AccountDoesNotExist).
After it resolves, the connected signer is no longer authorized for the
account — reconnect with the new wallet to keep managing it.
Things to verify after a transfer
- An admin write signed by the new wallet succeeds.
- An admin write signed by the previous wallet fails — the contract rejects it now that it is no longer the admin.
accountExistsByHash(ethers.solidityPackedKeccak256(['address'], [newAdminWalletAddress]))returnstruefrom the new wallet’s context (the on-chain key isuint256(keccak256(abi.encodePacked(newAdminWalletAddress)))).- All groups, PKPs, action CIDs, and existing usage API keys are visible under the new wallet session, and a Lit Action run with one of those usage keys still succeeds.
- The Stripe credit balance is unchanged.
Further reading
- Auth Model — owner / API key / scope model in detail.
- Architecture diagram — how the TEE, contracts, and dashboard fit together.
- Using the API directly — endpoint-by-endpoint reference (API mode).
- Pricing — credit model, identical in both modes.