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

# Onboarding (mint your API key)

> Self-service key issuance — sign one message, get a key.

API keys are minted via wallet signature, not via a request to a human
admin. The flow is one signature → one POST → one `lk_…` plaintext key.

<Note>
  Want to try the protocol without minting a key first? Use [Sandbox-first](/agents/sandbox-first) — `POST /sandbox/try` lets you buy a real Base mainnet policy with no API key and no wallet.
</Note>

<Note>
  Wallet prereqs: a Base mainnet EOA with a bit of ETH to sign onboarding
  (signing is gasless, but you'll want gas before any later wallet-side
  ops). Need test USDC + 0.05 ETH for gas? Use the
  [faucet](/agents/get-test-usdc) — one POST funds the wallet you pass.
</Note>

## The flow

1. Build the canonical message: `Lumina onboarding for {address} at {timestamp}`.
2. Sign it with your wallet (EIP-191 `personal_sign`).
3. `POST {walletAddress, signature, timestamp, label}` to `/api/v1/agent/onboard`.
4. The API verifies the signature recovers to `walletAddress`, then mints a key.
5. The plaintext key (`lk_…`) is in the response body — **store it now**, it is never returned again.

The wallet's private key never leaves your process — only the signature
goes over the wire. The server cannot impersonate the wallet because it
doesn't hold the private key.

## SDK

```ts theme={null}
import { Wallet } from 'ethers'
import { LuminaClient } from '@lumina-org/sdk'

const wallet = new Wallet(process.env.PRIVATE_KEY!)
const lumina = new LuminaClient({ apiKey: '' })

const result = await lumina.agent.onboard(wallet, { label: 'my-trading-bot' })
console.log(result.apiKey)         // "lk_…" — store this now
```

## curl

```bash theme={null}
ADDRESS=0xYourWalletAddress
TIMESTAMP=$(date +%s)
MESSAGE="Lumina onboarding for ${ADDRESS} at ${TIMESTAMP}"
SIGNATURE=$(cast wallet sign "$MESSAGE" --private-key "$PRIVATE_KEY")

curl -X POST https://lumina-api-production-ac85.up.railway.app/api/v1/agent/onboard \
  -H "Content-Type: application/json" \
  -d "{\"walletAddress\":\"${ADDRESS}\",\"signature\":\"${SIGNATURE}\",\"timestamp\":${TIMESTAMP},\"label\":\"my-bot\"}"
```

## Caps and limits

| Limit                            | Value                                 |
| -------------------------------- | ------------------------------------- |
| Active keys per wallet           | 3 (revoke before issuing the 4th)     |
| Onboard requests per hour per IP | 10                                    |
| Timestamp window                 | ±300 seconds of server time           |
| Rate limit, free tier            | **10 requests / minute** per API key  |
| Rate limit, paid tier            | **100 requests / minute** per API key |

Free vs paid is determined server-side from the wallet's tier — there is
no separate API key class. Upgrade by contacting the team.

## Storing the plaintext key

The `lk_…` prefix tells you it's a Lumina key. The full string is shown
**once** in the `apiKey` field of the onboard response. After that, only
the metadata (`keyId`, `label`, `createdAt`, `lastUsedAt`) is retrievable
via `/api/v1/agent/keys`.

Best practices:

* **Secret manager.** AWS Secrets Manager, GCP Secret Manager, Doppler, 1Password CLI — any of them are fine. Never commit the key to git, never put it in a `.env` file that's shipped in a Docker image, never log it.
* **One key per logical bot.** If you run a fleet, mint a separate key per process and label them (`spot-arb-1`, `funding-rate-monitor`, …). The audit trail then maps 1-to-1 to your infrastructure.
* **Rotate on suspicion.** `lumina.agent.revokeKey(keyId)` is instant. Re-onboard for a fresh key — same wallet can hold the new and old key simultaneously up to the cap of 3.
* **Never put it in the URL.** Always send via the `x-api-key` header. URLs end up in proxy logs, browser histories, and CDN access logs.

## Listing and revoking your keys

```ts theme={null}
const keys = await lumina.agent.listKeys()        // metadata only — never plaintext
await lumina.agent.revokeKey(keys[0].keyId)       // owner-only
```

## Errors

| HTTP | Code                | Why                              |
| ---- | ------------------- | -------------------------------- |
| 400  | `invalid_body`      | Malformed payload                |
| 400  | `stale_timestamp`   | Outside the ±300s window         |
| 401  | `invalid_signature` | Recovered signer ≠ walletAddress |
| 409  | `cap_reached`       | Wallet already has 3 active keys |
| 429  | `rate_limit`        | 10/h/IP exceeded                 |
