# Trade on Lighter

[Lighter](https://lighter.xyz) is a zk L2 perpetuals and spot DEX with zero trading fees. Openfort's role is narrow but load-bearing: an Openfort wallet owns the account on L1 (it deposits and authorizes API keys), while all order signing happens with a separate, protocol-native API key — not with EVM signatures.

:::info
This recipe defaults to **Lighter testnet**, where a free faucet call both creates and funds your account — no real money and no gas anywhere in the flow. Switching to mainnet (or Robinhood Chain) is an env-only change; see [Configuration](/docs/recipes/lighter/configuration). On mainnet, every deposit, order, and withdrawal moves real funds.
:::

## The two-key model

Lighter splits authority into two keys, which maps directly onto Openfort's backend-wallet-plus-server pattern:

* **Your Openfort wallet (L1 owner)** — deposits USDC (which creates the Lighter account) and authorizes new API keys with a `personal_sign`. It never signs an order directly.
* **A Lighter API key (Poseidon-Schnorr, held server-side)** — signs every order, cancel, and read-auth token. It can trade freely but can only withdraw back to the L1 owner's own address, so a compromised key has bounded blast radius.

The API key is a low-risk, delegatable key that a server can hold without custody risk to user funds. See [API keys](/docs/recipes/lighter/api-keys) for the full authorization flow.

## Architecture

```text
┌──────────────────┐     ┌──────────────────┐
│ Openfort wallet  │     │   Your server    │
│    (L1 owner)    │     │ (API key + WASM  │
│                  │     │     signer)      │
└────────┬─────────┘     └────────┬─────────┘
         │ deposit +              │ sign + submit
         │ authorize API key      │ orders
         ▼                        ▼
┌───────────────────────────────────────────┐
│               Lighter zk L2               │
└───────────────────────────────────────────┘
```

## Prerequisites

* An Openfort account with API keys ([Sign up](https://dashboard.openfort.io))
* Node.js 22+
* `@openfort/openfort-node` and `viem` installed

```bash
npm install @openfort/openfort-node viem
```

* Lighter's order signer has no official TypeScript SDK — only Go (`lighter-go`) and Python. Your server needs a vendored WASM build of `lighter-go` to sign orders and API key registrations; see the [recipe's `server/signer/README.md`](https://github.com/openfort-xyz/recipes-hub/blob/main/lighter/server/signer/README.md) for the exact commit and build command.

## Quickstart

Create a backend wallet, fund a Lighter account, register a server-held API key, and place your first order.

::::steps
### Create a backend wallet

```ts
import Openfort from '@openfort/openfort-node'

const openfort = new Openfort(process.env.OPENFORT_SECRET_KEY!, {
  walletSecret: process.env.OPENFORT_WALLET_SECRET!,
})

const account = await openfort.accounts.evm.backend.create()

console.log('Wallet address:', account.address)
```

### Fund the wallet — this creates your Lighter account

There's no separate "create account" transaction. Funding *is* the registration: Lighter assigns an `account_index` the first time your address is credited.

:::code-group
```ts [Testnet (default)]
// One call both creates AND funds the account with test USDC — no wallet
// signature, no on-chain transaction, no gas. The endpoint is undocumented
// but verified live; it is intermittently flaky, so retry on a 29500 error.
await fetch(
  `https://testnet.zklighter.elliot.ai/api/v1/faucet?l1_address=${account.address}`,
)
```

```ts [Mainnet]
import { createWalletClient, http, parseUnits, erc20Abi } from 'viem'
import { toAccount } from 'viem/accounts'
import { mainnet } from 'viem/chains'

// Wrap the backend wallet as a viem LocalAccount
const viemAccount = toAccount({
  address: account.address,
  sign: ({ hash }) => account.sign({ hash }),
  signMessage: ({ message }) => account.signMessage({ message }),
  signTransaction: (tx) => account.signTransaction(tx),
  signTypedData: (typedData) => account.signTypedData(typedData),
})

const walletClient = createWalletClient({ account: viemAccount, chain: mainnet, transport: http() })

const LIGHTER_DEPOSIT_CONTRACT = '0x3B4D794a66304F130a4Db8F2551B0070dfCf5ca7'
const USDC = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'
const depositAbi = [
  {
    type: 'function',
    name: 'deposit',
    stateMutability: 'payable',
    inputs: [
      { name: '_to', type: 'address' },
      { name: '_assetIndex', type: 'uint16' },
      { name: '_routeType', type: 'uint8' },
      { name: '_amount', type: 'uint256' },
    ],
    outputs: [],
  },
] as const

const amount = parseUnits('10', 6) // 10 USDC

await walletClient.writeContract({
  address: USDC,
  abi: erc20Abi,
  functionName: 'approve',
  args: [LIGHTER_DEPOSIT_CONTRACT, amount],
})

await walletClient.writeContract({
  address: LIGHTER_DEPOSIT_CONTRACT,
  abi: depositAbi,
  functionName: 'deposit',
  args: [account.address, 3, 0, amount], // asset index 3 = USDC, route type 0 = perps margin
})
```
:::

Once the account is credited (instant on testnet, a block or two for direct mainnet deposits), look up the assigned `account_index`:

```ts
const res = await fetch(
  `https://testnet.zklighter.elliot.ai/api/v1/account?by=l1_address&value=${account.address}`,
)
const { accounts } = await res.json()
const accountIndex = accounts[0].index
```

### Register an API key

Trading needs a Lighter API key, authorized by a plain `personal_sign` from your Openfort wallet — not EIP-712 typed data. Your server builds the exact message using the vendored WASM signer (see [API keys](/docs/recipes/lighter/api-keys)) and returns it for your wallet to sign:

```ts
const { messageToSign } = await fetch('https://your-server.example.com/api/lighter/changepubkey/message', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ accountIndex }),
}).then((res) => res.json())

// Plain personal_sign (EIP-191) — not EIP-712 typed data.
const l1Sig = await account.signMessage({ message: messageToSign })

await fetch('https://your-server.example.com/api/lighter/changepubkey/submit', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ accountIndex, l1Sig }),
})
```

The recipe server **adopts the freshly registered key automatically** — it hot-swaps its signing client and persists the credentials itself, so there is no copy-values-and-restart step and key material never leaves the server.

:::warning
`ChangePubKey` is a **rotation**, not a re-print: every submit replaces the on-chain key at that index, and whatever key was registered before stops working immediately. Sign it once per intentional rotation — see [API keys](/docs/recipes/lighter/api-keys).
:::

### Place your first order

Orders are signed by the server-held API key, not by your Openfort wallet. Once your server has the key active, call its order endpoint directly:

```ts
const result = await fetch('https://your-server.example.com/api/lighter/order', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    accountIndex, // the server refuses (409) if this isn't the account it signs for
    marketIndex: 0, // ETH perp — discover markets and their precision via /api/lighter/markets
    clientOrderIndex: 0,
    baseAmount: 1000, // integer, scaled by the market's own size precision
    price: 300000, // integer, scaled by the market's own price precision
    isAsk: false, // buy
    orderType: 0, // 0 = limit order
    timeInForce: 0, // 0 = immediate-or-cancel
    orderExpiry: 0, // required to be 0 for market/IOC orders — see Executing trades
  }),
}).then((res) => res.json())

// { txHash, filled: true, trade: { size, price } } — the server confirms the fill
// against Lighter's own trade record before responding, because sendTx alone
// never reports whether an order matched.
console.log(result)
```
::::

## Demo app

A complete Expo mobile trading app with Openfort embedded wallets: guest or email-OTP login, fully automatic onboarding (faucet funding, API key registration with server self-adoption), five live markets, a Cash App-style buy/sell flow, a portfolio view, and order confirmations backed by Lighter's own trade record.

<HoverCardLink
  title="View Sample Code"
  subtitle="GitHub Repository"
  description="Complete source code for the Lighter trading application, including the vendored WASM signer server."
  href="https://github.com/openfort-xyz/recipes-hub/tree/main/lighter"
  img={{
  src: '/img/icons/github-icon.svg',
  alt: 'GitHub Icon',
  className: 'rounded-none',
}}
  color="#333"
  external
/>

## Next steps

<HoverCardLayout>
  <HoverCardLink title="API keys" description="The L1 wallet vs API key authorization model, registration, and rotation." href="/recipes/lighter/api-keys" icon={Key} color="#22D3EE" />

  <HoverCardLink title="Executing trades" description="Order types, the orderExpiry regime, precision, and cancel semantics." href="/recipes/lighter/trading" icon={ListOrdered} color="#22D3EE" />

  <HoverCardLink title="Configuration" description="Lighter chain vs Robinhood Chain, deposit routes, and env vars." href="/recipes/lighter/configuration" icon={Settings2} color="#22D3EE" />

  <HoverCardLink title="Policies" description="Restrict the Openfort wallet to Lighter's deposit contract with the policy engine." href="/recipes/lighter/policies" icon={ShieldCheck} color="#22D3EE" />

  <HoverCardLink title="Client-side SDKs" description="Deposit and register API keys from a React Native embedded wallet." href="/recipes/lighter/client-side" icon={Smartphone} color="#22D3EE" />

  <HoverCardLink title="Sub-accounts" description="Lighter's native sub-accounts for isolating strategies under one L1 wallet." href="/recipes/lighter/subaccounts" icon={Users} color="#22D3EE" />

  <HoverCardLink title="Partner attribution" description="Earn integrator fees on the orders your platform routes to Lighter." href="/recipes/lighter/partner-attribution" icon={Percent} color="#22D3EE" />
</HoverCardLayout>
