> ## Documentation Index
> Fetch the complete documentation index at: https://developer.litprotocol.com/llms.txt
> Use this file to discover all available pages before exploring further.

# API Mode vs ChainSecured Mode

> How the two account ownership models differ, when to choose each, and how to move from API mode to ChainSecured.

Chipotle accounts come in two flavors. Both speak to the same on-chain
contracts and run the same Lit Actions; they only differ in **who owns the
account and how administrative writes are signed**.

* **API mode** (managed) — `POST /core/v1/new_account` generates 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.

Both modes share the same `/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

| Dimension                | API mode                                                  | ChainSecured mode                                   |
| ------------------------ | --------------------------------------------------------- | --------------------------------------------------- |
| Account owner            | Wallet derived from a server-generated random secret      | Your wallet (EOA / Safe / contract) on Base         |
| Account-level credential | Base64 API key (`X-Api-Key` header)                       | None — wallet signature is the credential           |
| Admin write path         | HTTP `POST /core/v1/...` → server submits the tx          | Direct contract call from your wallet               |
| Gas for admin writes     | Server pays (covered by the per-call credit charge)       | You pay gas from the connected wallet               |
| Recovery                 | Retain/back up the API key; if lost, create a new account | Whatever your wallet supports (seed, Safe signers)  |
| On-chain `managed` flag  | `true`                                                    | `false`                                             |
| Onboarding speed         | Fastest — paste an email, get a key                       | Requires a funded wallet on Base                    |
| Trust model              | You trust Lit's server to relay your intent               | Trust-minimized — every admin write is on-chain     |
| Auditability             | Server logs + on-chain events                             | On-chain events only; every change is wallet-signed |
| Dashboard surface        | Same management UI                                        | Same management UI; writes prompt the wallet        |
| Lit Action execution     | Usage API key in `X-Api-Key`                              | Usage API key in `X-Api-Key` (minted from contract) |
| Billing                  | Stripe credits on the account                             | Stripe credits on the account (same flow)           |

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.

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

API mode is the **default** and is shown as `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.

ChainSecured accounts have `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:

```javascript theme={null}
import { createClient } from './core_sdk.js';

const client = createClient('https://api.chipotle.litprotocol.com');

const res = await client.newAccount({
  accountName: 'My App',
  accountDescription: 'Optional',
  email: 'optional@example.com',
});
console.log('API key:', res.api_key);          // store this
console.log('Wallet:', res.wallet_address);
```

Every subsequent management call (`addGroup`, `addAction`,
`addUsageApiKey`, ...) takes that API key in `X-Api-Key`. See
[API direct usage](/management/api_direct) 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.

```javascript theme={null}
import { LitNodeSimpleApiClient } from './core_sdk.js';
import { ethers } from 'ethers';

const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();

const client = new LitNodeSimpleApiClient({
  baseUrl: 'https://api.chipotle.litprotocol.com',
  mode: 'sovereign',
  rpcUrl: 'https://mainnet.base.org',
  contractAddress: '0xYourAccountConfigDiamond',
  signer,
});

// Creates an unmanaged account whose admin is the connected wallet.
const res = await client.newChainSecuredAccount({
  accountName: 'My App',
  accountDescription: 'Optional',
});
console.log('Admin wallet:', res.wallet_address);
console.log('Tx hash:', res.transaction_hash);
```

Logging back in is a wallet connect — no key to paste:

```javascript theme={null}
client.connectSigner(signer);
const apiKeyHash = ethers.solidityPackedKeccak256(
  ['address'],
  [await signer.getAddress()],
);
const exists = await client.accountExistsByHash(apiKeyHash);
```

PKP minting in ChainSecured mode uses an EIP-712 typed-data signature
(`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).

Once authenticated the dashboard renders the same surface in both modes.
ChainSecured admin operations open a transaction preview before prompting
the wallet to sign; API-mode operations submit immediately. Billing,
balance display, and Add Funds are identical in both modes — Stripe
credits fund Lit Action execution either way.

API-mode users see one extra item in the account dropdown: **Convert to
ChainSecured**, which kicks off the conversion flow described below.
ChainSecured-mode users instead see **Change Ownership**, which transfers
the account to a different admin wallet (see
[Transferring ownership of a ChainSecured account](#transferring-ownership-of-a-chainsecured-account)).

## Converting an API account to ChainSecured

Conversion flips a managed account to unmanaged in a single on-chain
transaction. The account's on-chain `apiKeyHash` 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.adminWalletAddress` becomes the new wallet you signed with.
* `account.managed` flips from `true` to `false`.
* 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 `apiKeyHash` continue to resolve to the
  same account.

### Step-by-step (dashboard)

1. **Sign in to the dashboard in API mode** with your existing master API
   key.
2. Open the account dropdown and click **Convert to ChainSecured**.
   Confirm the irreversible-action prompt.
3. **Connect the wallet** that will become the new admin. **EOA only —
   the server verifies the EIP-712 signature via plain ECDSA `recover()`
   and does not yet validate ERC-1271 contract-account signatures**, so
   Safes and other contract wallets are not supported in this flow today.
   The dashboard prompts a chain switch if your wallet isn't on the chain
   reported by `GET /get_node_chain_config`.
4. **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 the `Lit ChainSecured` domain rather than a free-form message.
   The full canonical envelope is:
   ```json theme={null}
   {
     "types": {
       "EIP712Domain": [
         { "name": "name",    "type": "string"  },
         { "name": "version", "type": "string"  },
         { "name": "chainId", "type": "uint256" }
       ],
       "ConvertAccount": [
         { "name": "address",  "type": "address" },
         { "name": "issuedAt", "type": "uint256" }
       ]
     },
     "primaryType": "ConvertAccount",
     "domain":  { "name": "Lit ChainSecured", "version": "1", "chainId": "<chain id>" },
     "message": { "address":  "<new admin address>", "issuedAt": "<unix seconds>" }
   }
   ```
   `types` is 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`).
5. The dashboard `POST`s `/core/v1/convert_to_chain_secured_account` with
   `{ 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, the `chainId` matches the node,
   the `primaryType` matches `ConvertAccount` (preventing cross-flow
   replay against the secret-emitting endpoints), and the `issuedAt`
   timestamp is within ±300 seconds, then has the api\_payer submit the
   on-chain conversion.
6. On success, the dashboard switches mode to `sovereign`, clears the
   stored API key, persists the wallet + preserved `apiKeyHash` to the
   ChainSecured session, and reloads.

### Step-by-step (SDK)

```javascript theme={null}
import { createClient } from './core_sdk.js';
import { ethers } from 'ethers';

// Sign in with API mode (default) and your existing master API key.
const client = createClient('https://api.chipotle.litprotocol.com');

// Connect the wallet that will become the new on-chain admin.
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();

// Look up the chain id the server expects so the EIP-712 domain matches.
const cfg = await client.getNodeChainConfig();

const res = await client.convertToChainSecuredAccount({
  apiKey: existingApiKey,
  signer,
  chainId: Number(cfg.chain_id),
});
console.log('New admin wallet:', res.wallet_address);
console.log('Preserved apiKeyHash:', res.api_key_hash);
```

`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)` returns `true` from the wallet
  context, where `api_key_hash` is the value returned by
  `convertToChainSecuredAccount` (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 reverts
`convertToChainSecuredAccount` 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
master `apiKeyHash` 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.

| Dimension        | Convert to ChainSecured                          | Transfer ChainSecured ownership                    |
| ---------------- | ------------------------------------------------ | -------------------------------------------------- |
| Starting mode    | API (managed)                                    | ChainSecured (unmanaged)                           |
| Who signs        | api\_payer relays on your behalf                 | The current admin wallet, directly                 |
| Server endpoint  | `POST /core/v1/convert_to_chain_secured_account` | None — direct contract call                        |
| Signature scheme | EIP-712 ownership proof (`ConvertAccount`)       | Plain transaction signature from the current admin |
| `managed` flag   | `true` → `false`                                 | Stays `false`                                      |
| Requires API key | Yes (your existing master key)                   | No — wallet signature is the only credential       |

### 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.adminWalletAddress` becomes 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)))`
  → master `apiKeyHash` — is registered so the new wallet resolves to the
  account on login. (In ethers this is
  `ethers.solidityPackedKeccak256(['address'], [newAdminWalletAddress])`.)

### The contract function

```solidity theme={null}
WritesFacet.transferChainSecuredAccountOwnership(
    uint256 apiKeyHash,
    address newAdminWalletAddress
)
```

`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:

* `newAdminWalletAddress` is the zero address.
* No account resolves from `apiKeyHash` (`AccountDoesNotExist`).
* The account is managed (`InvalidRequest` — use
  `convertToChainSecuredAccount` instead).
* The caller is not the current admin wallet (`NoAccountAccess`). The
  api\_payer has **no** authority here.
* `newAdminWalletAddress` equals the current admin.
* `newAdminWalletAddress` already owns an account
  (`AccountAlreadyExists`).

<Warning>
  The previous admin's lookup entry is intentionally left in place, so a
  wallet that has *ever* been admin of any account can't become the target
  of a transfer — even the wallet you just transferred away from. Plan
  rotations forward to fresh addresses; you cannot transfer ownership back.
</Warning>

### Step-by-step (dashboard)

1. **Sign in to the dashboard in ChainSecured mode** by connecting the
   current admin wallet.
2. Open the account dropdown and click **Change Ownership** (this item is
   hidden in API mode).
3. **Enter the new admin wallet address** in the prompt. It must be a
   valid Ethereum address and must not already own an account.
4. **Confirm the irreversible-transfer prompt.** Your connected wallet
   loses admin access the moment the transaction confirms and the new
   wallet becomes the sole admin.
5. **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.
6. 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)

```javascript theme={null}
import { LitNodeSimpleApiClient } from './core_sdk.js';
import { ethers } from 'ethers';

// Connect the CURRENT admin wallet in sovereign mode.
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
const address = await signer.getAddress();

const client = new LitNodeSimpleApiClient({
  baseUrl: 'https://api.chipotle.litprotocol.com',
  mode: 'sovereign',
  rpcUrl: 'https://mainnet.base.org',
  contractAddress: '0xYourAccountConfigDiamond',
  signer,
  // Identity hash for the connected wallet. Required for ChainSecured
  // sessions — without it the SDK hashes an empty apiKey string and the
  // transfer reverts with AccountDoesNotExist. The dashboard sets this
  // automatically at login.
  adminHashOverride: ethers.solidityPackedKeccak256(['address'], [address]),
});

const res = await client.transferChainSecuredAccountOwnership({
  newAdminWalletAddress: '0xNewAdminWallet',
});
console.log('Previous admin:', res.previous_admin);
console.log('New admin:', res.new_admin);
console.log('Tx hash:', res.transaction_hash);
```

`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]))`
  returns `true` from the new wallet's context (the on-chain key is
  `uint256(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](/architecture/authModel) — owner / API key / scope model in detail.
* [Architecture diagram](/architecture/diagram) — how the TEE, contracts, and dashboard fit together.
* [Using the API directly](/management/api_direct) — endpoint-by-endpoint reference (API mode).
* [Pricing](/management/pricing) — credit model, identical in both modes.
