# Configuration

## Networks

Lighter runs deployments with an identical API shape on its own zk L2 and on Robinhood Chain. Switching is env-only — no code changes. The chain ID is the **L2 signing domain**, distinct from any L1/EVM chain ID; getting it wrong makes every signature silently invalid.

| Network | API base URL | Chain ID | Docs |
|---------|--------------|----------|------|
| zkLighter testnet (default) | `https://testnet.zklighter.elliot.ai` | `300` | — undocumented |
| zkLighter mainnet | `https://mainnet.zklighter.elliot.ai` | `304` | [apidocs.lighter.xyz](https://apidocs.lighter.xyz) |
| Robinhood Chain testnet | `https://api.rh-testnet.lighter.xyz` | `300` | — |
| Robinhood Chain mainnet | `https://api.rh.lighter.xyz` | `466324` | [apidocs.rh.lighter.xyz](https://apidocs.rh.lighter.xyz) |

Chain IDs were verified live against each base URL (Lighter's docs don't state them); the source of truth is `endpoint_profiles.py` in the official Python SDK.

```env
# server/.env.local — switch to Robinhood Chain mainnet
LIGHTER_API_BASE_URL=https://api.rh.lighter.xyz
LIGHTER_CHAIN_ID=466324
```

## Funding routes

| Network | Route | Minimum | Credit time |
|---------|-------|---------|-------------|
| Testnet | `GET /api/v1/faucet?l1_address=<addr>` — creates **and** funds the account, no signature, no gas | — | Instant |
| Mainnet | Direct deposit from Ethereum (`approve` + `deposit` on the L1 contract) | 1 USDC | ~1 block |
| Mainnet | CCTP USDC transfer from Arbitrum, Base, or Avalanche | 5 USDC | ~15–20 minutes |

Every route creates the Lighter account on first credit. The testnet faucet is undocumented and intermittently flaky — the recipe server retries it three times with backoff. Testnet has no usable L1 (its reported L1 chain ID has no public RPC), so the faucet is the only testnet funding mechanism.

## Environment variables

### App (`.env.local`)

| Variable | Required | Description |
|----------|----------|--------------|
| `OPENFORT_PUBLISHABLE_KEY` | Yes | Openfort project publishable key |
| `OPENFORT_SHIELD_PUBLISHABLE_KEY` | Yes | Shield publishable key for embedded wallet recovery |
| `OPENFORT_SHIELD_RECOVERY_BASE_URL` | Yes | Base URL of a Shield recovery endpoint — this recipe's own `server/` exposes one |
| `OPENFORT_ETHEREUM_PROVIDER_POLICY_ID` | No | Gas sponsorship policy ID — only used by the mainnet deposit flow |
| `LIGHTER_SERVER_BASE_URL` | Yes | Base URL of this recipe's `server/` |
| `LIGHTER_SERVER_AUTH_TOKEN` | No | Shared secret matching the server's value — see [Securing the server](#securing-the-server) |
| `LIGHTER_DEPOSIT_CONTRACT_ADDRESS` | Mainnet only | Ethereum contract that accepts L1 deposits into Lighter |
| `USDC_CONTRACT_ADDRESS` | Mainnet only | Mainnet USDC, for the `approve()` before depositing |
| `LIGHTER_L1_CHAIN_ID` / `LIGHTER_L1_CHAIN_NAME` / `LIGHTER_L1_NATIVE_SYMBOL` / `LIGHTER_L1_RPC_URLS` | No | The embedded wallet's L1 chain — defaults to Ethereum mainnet; only the mainnet deposit path uses it |

### Server (`server/.env.local`)

| Variable | Required | Description |
|----------|----------|--------------|
| `PORT` | No | Defaults to `3008` |
| `CORS_ORIGINS` | No | Comma-separated allowed origins; empty allows all (dev only) |
| `LIGHTER_SERVER_AUTH_TOKEN` | No | Shared secret — when set, every route except `/api/health` requires `Authorization: Bearer <token>` |
| `OPENFORT_SECRET_KEY` | Yes | Openfort project secret key |
| `OPENFORT_SHIELD_PUBLISHABLE_KEY` / `OPENFORT_SHIELD_SECRET_KEY` / `OPENFORT_SHIELD_ENCRYPTION_KEY` | Yes | Shield keys, needed for the app's automatic embedded wallet recovery |
| `LIGHTER_API_BASE_URL` | No | Defaults to `https://testnet.zklighter.elliot.ai` — see the networks table |
| `LIGHTER_CHAIN_ID` | No | L2 signing domain ID. Defaults to `300` (testnet) |
| `LIGHTER_ACCOUNT_INDEX` | Auto-managed | Written by the server itself when it adopts a registered key |
| `LIGHTER_API_KEY_PRIVATE_KEY` | Auto-managed | Written by the server itself — key material never leaves the server |
| `LIGHTER_API_KEY_INDEX` | No | Defaults to `2` — indices 0/1 are commonly used by Lighter's own clients |

:::info
There is no manual credential step. When a `ChangePubKey` registration succeeds, the server **adopts the new key automatically**: it hot-swaps its signing client in memory and persists the three `LIGHTER_*` values into its own `.env.local`, so restarts survive. Markets are discovered live from Lighter's API rather than configured.
:::

## Securing the server

The server holds a live trading key. Unset, `LIGHTER_SERVER_AUTH_TOKEN` leaves it open — fine on localhost. Before exposing it to a network (for example, testing on a physical device over LAN), set the **same token** in both `.env.local` files; the app then sends `Authorization: Bearer <token>` on every request, and the server rejects anything without it. A Lighter API key can trade but can only withdraw to the account's own L1 address, so the exposure is unauthorized trading, not fund theft — bound it anyway.

## Verifying configuration

The server exposes its resolved config (without secrets) at `GET /api/lighter/config`:

```json
{
  "apiBaseUrl": "https://testnet.zklighter.elliot.ai",
  "chainId": 300,
  "network": "testnet",
  "serverWalletConfigured": true,
  "serverKeyInvalid": false,
  "accountIndex": 179
}
```

* `serverWalletConfigured: false` — no key registered yet; onboarding in the app fixes this automatically.
* `serverKeyInvalid: true` — the configured key failed the server's startup self-test (a real, harmless transaction). The usual cause is a stale key after a rotation; re-authorize from the app.
* `accountIndex` — which account the server signs for. The app compares this against its own account and refuses to trade across a mismatch (the server also rejects mismatched order requests with a 409).
