Skip to main content
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.
Want to try the protocol without minting a key first? Use Sandbox-firstPOST /sandbox/try lets you buy a real Base mainnet policy with no API key and no wallet.
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 — one POST funds the wallet you pass.

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

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

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

LimitValue
Active keys per wallet3 (revoke before issuing the 4th)
Onboard requests per hour per IP10
Timestamp window±300 seconds of server time
Rate limit, free tier10 requests / minute per API key
Rate limit, paid tier100 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

const keys = await lumina.agent.listKeys()        // metadata only — never plaintext
await lumina.agent.revokeKey(keys[0].keyId)       // owner-only

Errors

HTTPCodeWhy
400invalid_bodyMalformed payload
400stale_timestampOutside the ±300s window
401invalid_signatureRecovered signer ≠ walletAddress
409cap_reachedWallet already has 3 active keys
429rate_limit10/h/IP exceeded