> For the complete documentation index, see [llms.txt](https://docs.lighter.xyz/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.lighter.xyz/developers/integration-guides/create-accounts-programmatically.md).

# Create accounts programmatically

Create a Lighter account for a user and register an API key for your app, with a single wallet signature.

Onboarding a user takes three steps:

1. **Create the account** – the user deposits; Lighter assigns an account index.
2. **Register an API key** – your app generates a keypair; the user signs `ChangePubKey` once with their wallet.
3. **Trade** – your app signs orders with the API key. No more wallet pop-ups.

## 1. Create the account

There is no "sign-up" call. **The first deposit creates the account**, allowing you to interact with the exchange without ever needing to touch the front-end. See [Deposits, Transfers and Withdrawals](/developers/integration-guides/deposits-transfers-and-withdrawals.md) for all deposit methods.

* Direct Ethereum deposit: credited in a few minutes.
* [Universal Deposit Address](https://docs.lighter.xyz/developers/integration-guides/pages/Y821AkphGPXElJVgnI66#deposit-via-fun.xyz) from any supported chain: new accounts are created automatically.
* Legacy CCTP route: around 15–20 minutes.

Then look up the master account index with [accountsByL1Address](https://docs.lighter.xyz/api/account) (or [account](https://docs.lighter.xyz/api/account)):

```typescript
const r = await fetch(
  `${BASE_URL}/api/v1/accountsByL1Address?l1_address=${userAddress}`,
).then(r => r.json());

if (r.code === 21100) {
  // "account not found" – no deposit credited yet; poll again later
}
const masterAccountIndex = r.sub_accounts[0].index;
```

You can also read `addressToAccountIndex` (0xabf6a038) on [Lighter's Ethereum contract](https://etherscan.io/address/0x3B4D794a66304F130a4Db8F2551B0070dfCf5ca7#readProxyContract).

## 2. Register an API key

Generating a key doesn't need the user's wallet, but registering it for the account does (`ChangePubKey` with an L1 signature). The TypeScript example below uses the WASM signer from the [TypeScript quickstart](/developers/getting-started/quickstart-typescript.md#reusing-the-signer-in-your-app). With the SDKs, see [system\_setup.py](https://github.com/elliottech/lighter-python/blob/main/examples/system_setup.py) (Python) or the [lighter-go README](https://github.com/elliottech/lighter-go) (Go).

```typescript
import { lighter, check } from "./signer";

const API_KEY_INDEX = 4; // pick any free index in 4–254 for your app

// a. Generate a keypair locally
const { privateKey, publicKey } = check(lighter.GenerateAPIKey());

// b. Create a signer client bound to the new key (empty URL: all HTTP goes through fetch)
check(lighter.CreateClient("", privateKey, CHAIN_ID, API_KEY_INDEX, accountIndex));

// c. Sign ChangePubKey with the new key
const nonce = await fetch(
  `${BASE_URL}/api/v1/nextNonce?account_index=${accountIndex}&api_key_index=${API_KEY_INDEX}`,
).then(r => r.json()).then(r => r.nonce);

const tx = check(lighter.SignChangePubKey(publicKey, 0, nonce, API_KEY_INDEX, accountIndex));

// d. The user signs the returned message with their Ethereum wallet (personal_sign)
const l1Sig = await walletClient.signMessage({ account: userAddress, message: tx.messageToSign }); // viem
// ethers: const l1Sig = await signer.signMessage(tx.messageToSign);

// e. Attach the L1 signature and send; check that the API accepted it
const txInfo = JSON.parse(tx.txInfo);
txInfo.L1Sig = l1Sig;

const res = await fetch(`${BASE_URL}/api/v1/sendTx`, {
  method: "POST",
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
  body: new URLSearchParams({ tx_type: String(tx.txType), tx_info: JSON.stringify(txInfo) }),
}).then(r => r.json());
if (res.code !== 200) throw new Error(`ChangePubKey rejected: ${res.code} ${res.message}`);

// f. Wait until the key is registered before using it
await waitForApiKey(accountIndex, API_KEY_INDEX, publicKey);

// g. Persist privateKey securely on the user's device (see Key storage below)
```

`code: 200` only means the API accepted the transaction. It still has to execute before the key can sign orders, so poll [apikeys](https://docs.lighter.xyz/api/api-keys) until it returns your public key:

```typescript
async function waitForApiKey(accountIndex: number, apiKeyIndex: number, publicKey: string, timeoutMs = 30_000) {
  const want = publicKey.replace(/^0x/, "").toLowerCase(); // the API returns keys without 0x
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    const r = await fetch(
      `${BASE_URL}/api/v1/apikeys?account_index=${accountIndex}&api_key_index=${apiKeyIndex}`,
    ).then(r => r.json());
    if (r.api_keys?.[0]?.public_key?.toLowerCase() === want) return;
    await new Promise(res => setTimeout(res, 1_000));
  }
  throw new Error("API key not registered in time: check the tx status with GET /api/v1/tx?by=hash&value=<tx_hash>");
}
```

{% hint style="info" %}
**One app, one key index**

Registering a key **replaces** whatever key was previously at that index. Pick a dedicated index for your app so you don't log the user out of another app.
{% endhint %}

### Key storage

| Where the key lives         | Recommendation                                                                         |
| --------------------------- | -------------------------------------------------------------------------------------- |
| Browser                     | Encrypt at rest (e.g. WebCrypto with a non-extractable key); never send to your server |
| Mobile                      | OS keychain / keystore                                                                 |
| Server (on behalf of users) | KMS or secrets manager; one key per user; audit access                                 |

If a key is lost, just generate and register a new one at the same index.

### Multisig / smart-contract wallets

Smart wallets cannot produce an ECDSA `personal_sign`. Register the key with an on-chain `changePubKey` call instead. See [Multi-signature and smart wallets](/developers/integration-guides/multi-signature-wallets.md).

## 3. Sub-accounts (optional)

Use sub-accounts to isolate strategies or users' positions. Sign with `SignCreateSubAccount(skipNonce, nonce, apiKeyIndex, accountIndex)` from the master account. Each sub-account needs **its own API key** registered. The number of sub-accounts depends on the tier: see [Account Types](/developers/reference/account-types.md).

## 4. Upgrade the tier (optional)

Partner fees require the user to be on **Plus** or **Premium/Advanced**. See [Partner Attribution](/developers/integration-guides/partner-attribution.md) and [Account Types](/developers/reference/account-types.md).


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.lighter.xyz/developers/integration-guides/create-accounts-programmatically.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
