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 for all deposit methods.

  • Direct Ethereum deposit: credited in a few minutes.
  • Universal Deposit Address 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 (or account):

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.

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. With the SDKs, see system_setup.py (Python) or the lighter-go README (Go).

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 until it returns your public key:

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>");
}
📘

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.

Key storage

Where the key livesRecommendation
BrowserEncrypt at rest (e.g. WebCrypto with a non-extractable key); never send to your server
MobileOS 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.

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.

4. Upgrade the tier (optional)

Partner fees require the user to be on Plus or Premium. See Partner Attribution and Account Types.


Did this page help you?