Skip to content
KeylayerDocs
Docs menu
PricingGet started

Use Anthropic with a scoped key and a spend cap

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

Set up

Add your Anthropic provider key as a credential, then mint a Keylayer key from it; the quickstart walks through both. Create the provider key in a dedicated workspace with its own spend limit, and never add an Admin API key.

Put the Keylayer key in an environment variable:

Shell
export KEYLAYER_KEY=kl_k_YOUR_KEY

The SDK adds /v1/messages to the base URL itself, so the base URL is the bare relay host:

Python (anthropic SDK)
import os
from anthropic import Anthropic

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

message = client.messages.create(
    model="claude-sonnet-5-5",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Write a haiku about rate limits."}],
)
print("".join(block.text for block in message.content if block.type == "text"))
Node.js (@anthropic-ai/sdk)
import Anthropic from "@anthropic-ai/sdk";

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

const message = await client.messages.create({
  model: "claude-sonnet-5-5",
  max_tokens: 1024,
  messages: [{ role: "user", content: "Write a haiku about rate limits." }],
});
for (const block of message.content) {
  if (block.type === "text") console.log(block.text);
}
curl
curl https://relay.keylayer.dev/v1/messages \
  -H "x-api-key: $KEYLAYER_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "Content-Type: application/json" \
  -d '{"model": "claude-sonnet-5-5", "max_tokens": 1024, "messages": [{"role": "user", "content": "Write a haiku about rate limits."}]}'

Both SDKs also read ANTHROPIC_BASE_URL and ANTHROPIC_API_KEY, so a client created with no arguments switches over with two variables:

Environment variables
export ANTHROPIC_BASE_URL="https://relay.keylayer.dev"
export ANTHROPIC_API_KEY="$KEYLAYER_KEY"

The SDKs send the key in the x-api-key header, which the relay reads. ANTHROPIC_AUTH_TOKEN works too, since the relay also reads a bearer token. Keylayer passes the anthropic-version and anthropic-beta headers through, and streams arrive as Anthropic sends them.

Spend caps on Anthropic

Keylayer prices Claude requests at Anthropic's standard prices, fast mode at twice them on Opus 5.5, Opus 5 and Opus 4.8, and Priority Tier at the highest ones in its table, $10 per million input tokens and $50 per million output tokens. What a request reserves depends on its route:

Request Hard cap Estimated cap
POST /v1/messages Worst-case cost Worst-case cost
POST /v1/messages/count_tokens, and any GET, HEAD, DELETE or OPTIONS $0.000001 $0.000001
POST /v1/messages/batches, and Managed Agents sessions, session events, deployments, deployment runs and dreams Refused (403) Refused (403)
POST /v1/files, or a message batch's /cancel Refused (403) $0.000001
Any other POST, such as skills or memory stores Refused (403) Refused (403)

The worst case counts about one token per byte of the request body, plus max_tokens, which the Messages API requires. Each image adds up to 4,784 tokens, a tool list adds 1,024, and each client tool such as bash or text_editor adds 8,192 more. Each tool_reference in the conversation adds the definition it names once more, since Anthropic expands it there. For claude-sonnet-5-5, the example above reserves about $0.012, and max_tokens of 64,000 reserves about $0.70 per request in flight. A cache_control block reserves the whole input at the cache-write price, 1.25 times for 5 minutes and 2 times for 1 hour, and on Claude 4.6 and later the worst case adds 10% for US-only inference unless the body sets inference_geo to global, since a workspace can default to US-only. Fast mode reserves twice the standard worst case. When the response ends, Keylayer charges the cost from the usage Anthropic reports, counting every pass in usage.iterations, such as a compaction, and frees the rest. A pass on a model missing from the price table, such as an advisor, is charged at the table's highest prices, $10 per million input and $50 per million output tokens. A cancelled request, or a 504 or 524 timeout with no usage, is charged the full reservation; any other 5xx error with no usage, such as 529 overloaded_error, is free.

Under a hard cap, Keylayer refuses with 403 unpriced_operation any request it can't bound up front: models missing from its price table, such as anything before Claude 4.5, a service_tier other than standard_only, fast mode on any model but Opus 5.5, Opus 5 and Opus 4.8, the MCP connector, containers, server tools such as web search, web fetch and code execution, image URLs, more than 600 images, PDFs, including ones a tool returned, file references, tool lists Anthropic rejects (two tools with the same name, or a renamed built-in tool such as bash), cache TTLs other than 5m and 1h, and compaction, whether asked for with compaction, as the SDK tool runner does, or by a context_management edit other than clearing tool uses or thinking. It also sets service_tier to standard_only on Messages requests that name no tier, so priority capacity can't raise the price. If Anthropic still reports another tier, Keylayer suspends the key.

Under an estimated cap, those requests go through, except a model missing from the price table, a body Keylayer can't read, such as one with a duplicate key, and a request with fallbacks, which get 403 unpriced_operation under either cap, since a fallback reruns the request on another model with its own max_tokens. Keylayer reserves a quarter token per byte of the body at the model's input price, plus $0.001, then charges the reported usage when it can price it, or else that reservation, so spend can pass the cap. Each web search counts $0.01, and a response that ran code execution, or a request that uploads files with container_upload, is charged its tokens plus its reservation, since Anthropic doesn't report container hours. A request whose only server tools are web search, web fetch and code execution, and that doesn't finish, such as a stream cut short after your client hangs up, or that ends without usage Keylayer can price, is charged its reservation plus the worst case above for one pass, counting each of those tools like a client tool, unless something else in it, such as an image URL or a PDF, leaves that unknown. A cut stream adds $0.01 for each search it showed, failed ones aside, and if the usage it reported comes to more, it's charged that instead. Searches a code execution cell ran out of view, as with response_inclusion set to excluded, never reach the stream, so a cut doesn't charge them; a hard cap refuses these requests. With escape hatches on, a model missing from the table goes through at the highest input price in it, $10 per million tokens.

Batches and Managed Agents work bill after the response: a session bills its tokens plus $0.08 per session-hour while it runs. So either cap refuses, with 403 unpriced_operation, a POST that creates a session or sends it events, creates, runs or unpauses a deployment, or starts a dream.

Anthropic streams carry their own usage, so Keylayer leaves them as they are.

Blocked routes

Keylayer blocks /v1/organizations/** with 403 path_not_allowed. That's Anthropic's Admin API, which manages members, workspaces, invites and API keys. Path blocks are a backstop: the workspace key with its own spend limit is what limits your provider key. Escape hatches lift the block for one key; the key limits page explains how.

Errors

When Keylayer refuses a request, it answers in Anthropic's error format, with the code in error.type 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

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

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 SDKs raise their usual errors: PermissionDeniedError on 403 codes such as budget_exceeded, unpriced_operation and path_not_allowed, AuthenticationError on 401 codes such as invalid_token, and RateLimitError on 429. Errors from Anthropic itself, such as 529 overloaded_error, pass through with no keylayer-error header, so check for it:

Python (anthropic SDK)
import anthropic

try:
    message = client.messages.create(
        model="claude-sonnet-5-5",
        max_tokens=1024,
        messages=[{"role": "user", "content": "Write a haiku about rate limits."}],
    )
except anthropic.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 SDKs retry 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

  • Server tools, the MCP connector, containers, fast mode outside Opus 5.5, Opus 5 and Opus 4.8, priority tier, compaction and PDFs, also ones a tool returned, on a key with a hard cap. Use an estimated cap for those.
  • Server-side fallbacks on a key with any spend cap. Send the retry on another model as a new request instead.
  • 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