Skip to content
KeylayerDocs
Docs menu
PricingGet started

Use OpenRouter with a scoped key and a spend cap

Point the OpenAI SDK at https://relay.keylayer.dev/api/v1 with a Keylayer key to put an estimated spend cap on OpenRouter per UTC hour, day, week or month. Only the base URL and the API key change: Keylayer swaps in your OpenRouter key and forwards each request to openrouter.ai with the same method, path and query. A request can run for up to 900 seconds.

Set up

Add your OpenRouter provider key as a credential, then mint a Keylayer key from it; the quickstart walks through both. Use a regular API key with a credit limit, never a management key.

Put the Keylayer key in an environment variable:

Shell
export KEYLAYER_KEY=kl_k_YOUR_KEY

Then create the client with the relay and OpenRouter's /api/v1 path as its base URL:

Python (openai SDK)
import os
from openai import OpenAI

client = OpenAI(
    base_url="https://relay.keylayer.dev/api/v1",
    api_key=os.environ["KEYLAYER_KEY"],
)

completion = client.chat.completions.create(
    model="anthropic/claude-sonnet-5.5",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Write a haiku about rate limits."}],
)
print(completion.choices[0].message.content)
Node.js (openai SDK)
import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://relay.keylayer.dev/api/v1",
  apiKey: process.env.KEYLAYER_KEY,
});

const completion = await client.chat.completions.create({
  model: "anthropic/claude-sonnet-5.5",
  max_tokens: 1024,
  messages: [{ role: "user", content: "Write a haiku about rate limits." }],
});
console.log(completion.choices[0].message.content);
curl
curl https://relay.keylayer.dev/api/v1/chat/completions \
  -H "Authorization: Bearer $KEYLAYER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model": "anthropic/claude-sonnet-5.5", "max_tokens": 1024, "messages": [{"role": "user", "content": "Write a haiku about rate limits."}]}'

The OpenAI SDKs also read OPENAI_BASE_URL and OPENAI_API_KEY, so a client created with no arguments switches over with two variables:

Environment variables
export OPENAI_BASE_URL="https://relay.keylayer.dev/api/v1"
export OPENAI_API_KEY="$KEYLAYER_KEY"

OpenRouter's app attribution headers, such as HTTP-Referer and X-OpenRouter-Title, pass through unchanged. Streams work as usual: Keylayer passes server-sent events through as they arrive.

Spend caps on OpenRouter

Keylayer refuses a hard cap on an OpenRouter key with 400 budget_mode_unsupported. Under an estimated cap, Keylayer reserves an amount before forwarding and charges the cost OpenRouter reports in usage.cost afterward:

Request Reserved Charged
POST /api/v1/chat/completions or /api/v1/responses, model in Keylayer's table Worst-case cost Reported cost
Other models, image inputs, and POST /api/v1/embeddings, /api/v1/messages, /api/v1/rerank, /api/v1/images or /api/v1/audio/transcriptions Body-size estimate Reported cost, or the estimate when none is reported
A streamed messages call, or the completions route Refused (403) Nothing
Any request with built-in tools such as web search Body-size estimate Reported cost
POST /api/v1/videos, and any other POST, PUT or PATCH, such as presets or /api/v1/audio/speech Refused (403) Nothing
Any GET, HEAD, DELETE or OPTIONS $0.000001 Free

Keylayer's table holds 28 OpenRouter models, such as anthropic/claude-sonnet-5.5, google/gemini-3.8-flash and openai/gpt-5.6-luna. Their worst case counts about one token per byte of the request body, plus max_completion_tokens or max_tokens on chat completions, or max_output_tokens on responses. Without one, Keylayer reserves the model's maximum output: for anthropic/claude-sonnet-5.5 that's about $1.28 per request in flight, where the example above reserves about $0.011.

For other models, the routes in the second row, and requests with image inputs or built-in tools, Keylayer reserves a quarter token per byte of the body, plus $0.01, at the model's input price or at the highest one in its table, $30 per million tokens. Because that reservation can be far below the real cost, spend can pass the cap until the response's usage.cost is counted. Keylayer can't read the cost of any other POST, PUT or PATCH, so it refuses them with 403 unpriced_operation. It also refuses a messages call whose stream is anything but false or left out, since its stream events report no cost. Escape hatches let it through at the body-size estimate. A response without usage.cost is charged its reservation, unless it's a 4xx or 5xx error other than a 504 or 524 timeout, which is free.

Keylayer matches these routes exactly as OpenRouter spells them, so a trailing slash or different capitals get 403 unpriced_operation. It also refuses a streamed /api/v1/images request with n above 1, since nothing documents what each completed event in such a stream reports, a transcription whose input_audio holds a url instead of base64 data, and a multipart body on either route. OpenRouter bills a video job when it finishes, after the response, so /api/v1/videos is refused too. A preset route such as /api/v1/presets/{slug}/chat/completions saves a preset rather than calling a model, and /api/v1/completions and intern chats aren't priced, so they get 403 unpriced_operation as well.

Keylayer never rewrites OpenRouter requests or streams.

Blocked routes

These OpenRouter routes return 403 path_not_allowed:

  • /api/v1/keys/**, /api/v1/auth/** and /api/v1/oauth/**
  • /api/v1/credits/**, /api/v1/activity/** and /api/v1/analytics/**
  • /api/v1/byok/**, /api/v1/workspaces/**, /api/v1/guardrails/** and /api/v1/organization/**

They manage keys, OAuth, credits, usage history, your own provider keys, workspaces, guardrails and the organization. Path blocks are a backstop: the credit limit on your provider key is what limits it. Escape hatches lift the block for one key; the key limits page explains how.

Errors

When Keylayer refuses a request, it answers in OpenRouter's error format, with the code in error.metadata.keylayer_error and in a keylayer-error header:

Response
HTTP/1.1 403 Forbidden
content-type: application/json
cache-control: no-store
keylayer-error: budget_exceeded
retry-after: 3600

{
  "error": {
    "code": 403,
    "message": "Keylayer budget_exceeded: this key's spend cap is reached; retry after 60m.",
    "metadata": {
      "keylayer_error": "budget_exceeded"
    }
  }
}

Refusals before Keylayer knows the provider, such as 401 missing_token and invalid_token, have a plain body like {"error": "invalid_token"} instead, with the same keylayer-error header.

The OpenAI SDK raises its usual errors: PermissionDeniedError on 403 codes such as budget_exceeded and path_not_allowed, AuthenticationError on 401 codes such as invalid_token, and RateLimitError on 429. Errors from OpenRouter itself pass through with no keylayer-error header, so check for it:

Python (openai SDK)
import openai

try:
    completion = client.chat.completions.create(
        model="anthropic/claude-sonnet-5.5",
        max_tokens=1024,
        messages=[{"role": "user", "content": "Write a haiku about rate limits."}],
    )
except openai.APIStatusError as error:
    reason = error.response.headers.get("keylayer-error")
    if reason is None:
        raise
    print(f"Keylayer refused the request: {reason} (HTTP {error.status_code})")

The SDK retries a 429 twice by default. When the wait is over 60 seconds or unknown, Keylayer adds x-should-retry: false, so the SDK stops at once. The error reference lists every code.

What doesn't work

  • Hard caps. Keylayer can't hold OpenRouter to a worst case, so it takes estimated caps only.
  • Request bodies over 16 MiB on a key with a spend cap. Keylayer reads the whole body to price it, so it answers 413 body_too_large_for_budget, and 415 content_encoding_unsupported to a compressed body. Keys without a spend cap stream the body straight through.
  • Requests that run past 900 seconds. Keylayer ends them with 504 deadline_exceeded, or cuts off a started response.

Next steps

Updated

Create your first scoped key

Add a provider key once, then mint Keylayer keys with their own limits for each app, agent or teammate.

Get started