Getting started
Access and rate limits
Tiers
| Public | Self-serve key | Partner key | |
|---|---|---|---|
| Sign-up | None | "Get an API key": sign a message with your wallet | Issued (or raised) by the operator |
| Limit | RATE_LIMIT_PER_MIN per IP | Public tier, shared with your IP | Its own allowance, per key |
| Usage tracking | — | ✅ GET /v1/keys/me | ✅ |
Below, "partner key" covers both operator keys (API_KEYS) and self-serve keys the operator raised.
| Public | Partner key | |
|---|---|---|
| Sign-up | None | Key issued by the operator |
| How | Just call the API | Authorization: Bearer <key> header |
| Endpoints | All | All |
| Limit | RATE_LIMIT_PER_MIN requests/minute per IP (default 120) | Its own allowance per key, set by the operator (default 10× public) |
| Use from | Browsers, scripts | Your backend only |
⚠️ Never ship a partner key to a browser or a mobile app. Anyone can read it there and spend your allowance. Call the API from your server, or use the public tier from the client.
Sending a key
curl -H "Authorization: Bearer pk_live_abc123" "https://api.lane.markets/v1/health"- No
Authorizationheader → public tier. - A valid key → that key's allowance, counted per key (shared across all your servers).
- An unknown key →
401 {"error":"invalid API key"}. The request is not silently downgraded to the public tier, so a typo shows up immediately. Wrong keys count against the caller's IP public limit, so keys can't be brute-forced.
Getting a key yourself ("Get an API key")
No email or password: the key belongs to a wallet.
const { message } = await fetch(`${API}/keys/challenge?address=${address}`).then((r) => r.json());
const signature = await walletClient.signMessage({ account: address, message }); // not a transaction, costs nothing
const { key } = await fetch(`${API}/keys`, {
method: "POST", headers: { "content-type": "application/json" },
body: JSON.stringify({ address, signature, label: "my bot" }),
}).then((r) => r.json());
// store `key` now: it is shown once- Smart-contract wallets work too (ERC-1271/6492 signatures).
- A challenge is valid 10 minutes and works once. A wallet can hold 3 active keys; one IP can create 5 keys a day.
GET /v1/keys/me(with the key) shows its limit and daily usage over 30 days;DELETE /v1/keys/merevokes it.- Why the public tier? Wallets are free, so a self-serve key can't add capacity by itself (anyone could create hundreds). It identifies you and tracks your usage; ask the operator for more and they raise it with
pnpm keys:limit <prefix> <perMinute>, which gives the key its own bucket.
Rate-limit headers
Every rate-limited response carries:
| Header | Meaning |
|---|---|
x-ratelimit-limit | Requests allowed per minute for this caller |
x-ratelimit-remaining | Left in the current window |
x-ratelimit-reset | Seconds until the window resets |
retry-after | On 429 only: seconds to wait |
GET /v1/health is exempt from rate limits, so load balancers and monitors can poll it.
When REDIS_URL is set, counters are shared by every API instance, so the limit holds across a load balancer.
Issuing keys (operators)
Keys are configured with the API_KEYS environment variable: comma-separated key:perMinute entries. perMinute is optional.
# partner A: 2,000/min; partner B: default (10 × RATE_LIMIT_PER_MIN)
API_KEYS=pk_live_partnerA_9f3c...:2000,pk_live_partnerB_71aa...Keys must be at least 24 characters (the API refuses to start otherwise). Generate them with openssl rand -hex 24. Restart the API to apply changes. To revoke a key, remove it and restart: it gets 401 from then on.
Request ids
Every response carries an x-request-id header, and error bodies repeat it as requestId. Send your own x-request-id (letters, digits, _, ., -, up to 64 chars) and the API reuses it, so your logs and the server's logs share one id. Quote it when reporting a problem.
Next: Quickstart