Documentation

Swap your base_url in ten minutes.

Recovea speaks the OpenAI wire protocol on /v1 and Anthropic's native protocol on /anthropic. Swap the base URL, keep your SDK and your provider account, and your first metered request lands in minutes. Coverage today is those two providers, no others.

QUICKSTART

Your first metered request

  1. Mint a Recovea key. Create a workspace (free, no card) and mint an rcv_live_ key. The full secret is shown exactly once. Store it as RECOVEA_API_KEY in your secret manager.
  2. Connect your provider key (BYO). Paste your OpenAI or Anthropic key into the console once. It is encrypted at rest and used only to sign your upstream calls; we live-probe it at connect, so a dead key fails at setup, not on your first production request.
  3. Swap the base URL. One line in your client, or two environment variables and no code change at all.
  4. Send a request. Spend is metered live in the console, and the response carries an x-recovea-request-id you can quote back to us for any question about it.
from openai import OpenAI
import os

client = OpenAI(
    base_url="https://api.recovea.ai/v1",   # the only change
    api_key=os.environ["RECOVEA_API_KEY"],  # rcv_live_..., your provider key stays in the vault
)

resp = client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "Say hello in one word."}],
)
print(resp.choices[0].message.content)
# zero code change if your app already reads the standard variables
export OPENAI_BASE_URL="https://api.recovea.ai/v1"
export OPENAI_API_KEY="rcv_live_..."

Everything else (request shapes, streaming, tool calls, error envelopes) is byte-for-byte the OpenAI API, so the official SDKs work unmodified. Reverting is the same line in reverse: point the base URL back at your provider and traffic flows direct.

What does not change

Your provider account, your provider rates, your model names, your streaming behaviour, and your bill from your provider. We sign your upstream call with your own stored key and take no margin on provider spend. Gateway overhead: ~1ms p50 / 2ms p95 local gateway overhead (measured July 2026, non-production) -- a benchmark of the gateway hop, dated so you can see how old it is.

TOOL GUIDES

Point your tools at the gateway

Anything that lets you set a base URL routes without a patch. Below are the four we are asked about most. Coverage is OpenAI and Anthropic: those are the two wire protocols we speak and the two providers we price.

Claude Code and Anthropic-shaped agents

Claude Code reads ANTHROPIC_BASE_URL. Point it at https://api.recovea.ai/anthropic, set your Recovea key, and start it as usual: no flags, no patches. The surface accepts the key on either x-api-key or Authorization: Bearer.

export ANTHROPIC_BASE_URL="https://api.recovea.ai/anthropic"
export ANTHROPIC_API_KEY="rcv_live_..."   # your Recovea key

claude   # run Claude Code as you normally would

An agent spends while nobody is watching it, so bound it before you start it: put a hard monthly cap on the workspace your agent's key belongs to, and the request that would cross the line is refused in path, mid-run, rather than discovered on next month's invoice. It works on Free as well, up to the Free ceiling, and the kill-switch is never gated by plan (caps, budgets & the kill-switch has the ceiling, the refusal codes, and the scopes a cap does not reach yet).

The Anthropic SDK

The official SDK takes the same override, so an application already using it needs one argument changed.

from anthropic import Anthropic
import os

client = Anthropic(
    base_url="https://api.recovea.ai/anthropic",   # the only change
    api_key=os.environ["RECOVEA_API_KEY"],
)

msg = client.messages.create(
    model="claude-sonnet-4-5",
    max_tokens=32,
    messages=[{"role": "user", "content": "Say hello in one word."}],
)
print(msg.content[0].text)

Cursor, Codex, and the CLI

Or let the CLI wire the OpenAI-shaped tools with one reversible command. It detects Claude Code, Cursor, and Codex on disk, but deliberately routes only the OpenAI-shaped ones. Claude Code keeps talking to Anthropic on your own account unless you point it manually with the two variables above:

npx recovea install    # writes a fenced, reversible block into ~/.zshrc + ~/.bashrc
recovea uninstall      # removes exactly what install wrote, nothing else

# OpenAI-only by design: the CLI routes OpenAI-shaped tools (Cursor, Codex).
# Claude Code is NOT routed by the CLI - it stays on your Anthropic account.
# To meter Claude Code, set the two ANTHROPIC_* variables above yourself.

Frameworks and gateways of your own

Anything that exposes an OpenAI-compatible base URL setting (application frameworks, orchestration layers, an internal proxy of your own) points at https://api.recovea.ai/v1 the same way. If the tool sends the OpenAI or Anthropic wire format, we relay it unchanged; if it does not, we are the wrong layer and would rather say so than pretend.

Tagging what you route

Two optional headers put a name on the spend. They are ours, they are opaque to us, and they are stripped before we forward, so they never reach your provider. A malformed value never fails a request:

# two headers, both ours, both stripped before we forward upstream
-H "x-recovea-project: checkout"
-H "x-recovea-tags: team:payments,feature:cart"

At most 32 tags per request, each truncated at 128 characters; a request with no project lands in a reserved unassigned bucket. The cuts they feed start on Developer, and the full arithmetic is on how spend is attributed.

Metering only on this surface: savings proof pending

The /anthropic surface meters and caps today; no optimization levers run on it. You can see every call, cap it, and keep a re-derivable receipt, but we claim no savings on agentic traffic, because we haven't measured any there yet. Baseline equals realized until a lever is measured live.

KEYS

API keys, yours and ours

Two kinds of key, never confused: the rcv_ key is what your app sends to Recovea; your provider key is pasted into the console once and used pass-through to sign the upstream call. They never swap roles, and the rcv_ key is stripped from every request before we forward it, so our secret cannot land in a provider's logs.

Minting and storing a Recovea key
An rcv_live_ secret is shown exactly once at mint. We store only a keyed hash plus the prefix and last four characters for display; the secret itself is never persisted and never reappears. Mint one key per application or environment: the rate-limit buckets are per key as well as per workspace, so a noisy job cannot consume the whole workspace allowance on its own.
Rotation and revocation
Recovea keys rotate with an optional grace window for zero-downtime cutover, and a revoked key cannot be un-revoked. Revoke a key and it stops being used within seconds: the change is pushed to the request path, not waited for on a timer.
KMS envelope encryption for provider keys
Provider keys are encrypted at rest with AES-256-GCM under envelope encryption with a managed KMS, bound to your workspace so the ciphertext is useless anywhere else. Display is masked and non-reconstructive; the plaintext is never echoed and never logged.
No-bodies logging
We never store message content. The meter records tokens, price, and attribution: not prompts, not completions, on any surface.
Pass-through: we never resell tokens
The gateway signs your upstream call with your stored provider key and takes no margin on provider spend. Your provider bills you directly; a connected key is live-probed against the provider (reachable, plus the model list) before you trust it.
Removing a provider key
Remove or rotate a provider key whenever you choose; removal is pushed to the request path and takes effect within seconds. Destroying a provider key writes an append-only audit record. With no key connected for a provider, that surface answers 401 provider_key_missing rather than substituting anyone else's credential.

The full security posture, including what we deliberately cannot see, is on the Trust page.

CONTROL

Caps, budgets & the kill-switch

Alerts warn; caps act. Budget alerts fire at 50%, 80%, and 95% of each budget, by email to the recipients you choose, and every fired alert is kept in a history you can audit. A hard cap is different: it sits in the request path and refuses the request that would cross the line. Caps and the kill-switch start on Free; the alerts on the way up start on Team. Alerts are also not an ordering guarantee: the sweep runs about once a minute, so a fast burst can cross a threshold and reach the cap between checks.

At the cap, your app sees an exact OpenAI-shaped error, so your SDK raises its normal exception and your existing handling keeps working:

HTTP/1.1 402 Payment Required

{
  "error": {
    "message": "Monthly budget reached. Traffic resumes at reset, or raise the cap.",
    "type": "insufficient_quota",
    "param": null,
    "code": "budget_exceeded"
  }
}

Treat any 402 as a hard spend stop: retrying will not help until the budget is raised. A pause-style cap refuses with a 429 instead of blocking, carrying the same budget_exceeded code and a Retry-After. That is a different 429 from the throughput limit, so branch on error.code, never on the status alone (the full table is below). The kill-switch is the same machinery armed at once: flip it in the console and over-cap traffic stops there, not at month end.

What Free keeps
Caps up to $1,000/mo on Free -- Developer raises the ceiling to your full band. The kill-switch itself sits outside that ceiling and works on every plan, with no ceiling at all: safety is never the thing we take away. A cap you armed above the Free ceiling is clamped down to it with an explicit notice at downgrade, never switched off silently, so the protection you set up keeps acting at the level Free supports.
Fail-open: the default posture
If Recovea's own layer degrades, your request flows straight through to your provider on your own key. An outage never takes your traffic down, and never masquerades as the gateway working.
Fail-closed: an armed cap refuses to guess
A hard cap that cannot verify live spend (metering stale, budget cache cold) blocks with 402 budget_unverifiable rather than let spend leak past your wall. A retry clears it once the store recovers.
Observe-only never blocks
Traffic on observe-only metering is never blocked, altered, or rerouted. Caps only act where you have armed them.
What a cap does not see
Spend that did not route through the gateway, your provider's own invoice adjustments (discounts, credits, taxes, committed-use rates), and your Recovea subscription. A cap governs routed model spend, and only workspace-scoped caps reach the request path today: key- and project-scoped budgets can be created and alerted on, but nothing refuses on them yet.
A cap is a stop, not a ceiling
The counter is read before this request's own estimate lands, so expect to finish roughly one request past the cap. The admission estimate is deliberately a lower bound: a cap may trip a hair late on real spend, and must never trip early on spend that never happened.

The decision order, the arithmetic, and the known drift between layers are on how a cap decides; the alert sweep and its ordering caveats are on how budget alerts fire.

PROVE

Receipts, and how to check them yourself

Every metered request writes a receipt: the model, the token counts, an exact-decimal cost priced against a version-pinned frozen reference price list, the attribution (key, project, route), and the timestamps. It is body-free: prompts and completions are never stored, so the receipt can be shared with finance without sharing your data.

Receipts roll into the ledger: an append-only, hash-chained record in the recovea-chain-v1 format. Export it from the console (Ledger → Export, JSONL, JSON, or CSV) or from the key-authed GET /v1/ledger/export; the export carries each row's hash, the genesis, and the chain head.

Re-derive the chain offline with no database access, no Recovea service, just a laptop and a SHA-256 implementation:

  1. Export the period's ledger from the console.
  2. Start from the genesis: a 64-zero string, printed in the export header. It is the "previous hash" of the first row.
  3. Walk the rows in append order: for each row, join the recipe id, the previous hash, and the row's fields in the published order with the U+001F separator, and SHA-256 the UTF-8 bytes.
  4. Compare: every recomputed hash must equal the stored rowHash, and the final hash must match the chain head. One edited, inserted, or deleted row breaks every link after it, so tampering is evident, not asserted.
input    = recipe_id ␟ prev_hash ␟ request_id ␟ timestamp_ms ␟ provider ␟ model
           ␟ route ␟ tokens_in ␟ tokens_out ␟ tokens_cached ␟ cost_micro_usd
           ␟ price_list_version ␟ basis ␟ mandate_id     # ␟ = U+001F
row_hash = lowercase-hex SHA-256( utf-8 bytes of input )
genesis  = "000...0" (64 zeros; printed in the export header)

In code, against a session export, that is about twenty lines and no dependency beyond the standard library:

import hashlib, json

SEP = "\x1f"                        # the field separator, U+001F
prev = "0" * 64                     # the genesis, printed in the export header

def field(value):                   # null is the empty string, integers base-10
    return "" if value is None else str(value)

for line in open("ledger-2026-07.jsonl"):
    row = json.loads(line)
    if "rowHash" not in row:        # the first line is the export header
        continue
    parts = ["recovea-chain-v1", prev, row["requestId"], field(row["timestampMs"]),
             row["provider"], row["model"], row["route"],
             field(row["tokensIn"]), field(row["tokensOut"]), field(row["tokensCached"]),
             field(row["costMicroUsd"]), row["priceListVersion"],
             row["basis"], field(row.get("mandateId"))]
    digest = hashlib.sha256(SEP.join(parts).encode("utf-8")).hexdigest()
    assert digest == row["rowHash"], row["requestId"]
    prev = digest

print("chain head:", prev)          # must equal chainHead in the export header

Two spellings exist and both re-derive to the same hashes: the session export is camelCase, the key-authed export is snake_case, and on the key-authed route the integer cost is cost rather than costMicroUsd. The field map for both is on how the ledger chain works. Hash the integer, never a formatted display string.

To check the money rather than the linking, re-price a row from its own token counts at the rates of the price list version it pins: a published version is frozen and never changes, and a correction ships as a new version rather than an edit. That arithmetic is on how a receipt is built, and the same rate table ships in our MIT-licensed CLI so you can do it without us.

measured · not applied

Any savings figure on the ledger is measured (computed against the provider's own reported numbers) and is labeled that way. Verified savings rows are off · proof pending until the evaluation gate is calibrated on real traffic. We never write the word "verified" next to a number the gate hasn't passed.

REFERENCE

Rate limits & error codes

Two throughput dimensions, checked in path before your provider is contacted: requests per minute and tokens per minute, on your key and on your workspace. They are about throughput and never about money.

PlanRequestsTokensNotes
Free100 rpm200K tpmCardless and in path. Caps and the kill-switch up to the Free ceiling.
Developer600 rpm1M tpmCaps and attribution across your whole band.
Team600 rpm1M tpmThe same throughput as Developer: throughput was never the axis we price on.
Growth1,200 rpm2M tpmHeadroom for shared, bursty workloads.
Scale3,000 rpm5M tpmThe top published tier.
EnterpriseSet in your agreementContract terms. The limits endpoint returns null rather than a guess.

rpm = requests per minute; tpm = tokens per minute, per key and per workspace.

Indicative, not a guarantee

Rate limits are indicative, never a guarantee: the limiter runs inside each gateway process, so the throughput you actually get can land above or below the published figure.

Every bucket holds 2x its per-minute figure, so a burst of 2x the sustained rate is admitted at once and then refills at the sustained rate. Agent fan-out is normal traffic, not abuse. We size these to be generous rather than to price throughput: limits exist to bound infrastructure, not to sell you a bigger plan. If one of them is in your way, tell us and we will look at the number rather than at your invoice.

The token dimension is admitted on an estimate and reconciled with the provider's reported usage after the response, so sustained throughput converges on the real figure. A throttled request is rolled back out of every bucket it touched, so a refusal costs you nothing. The bucket arithmetic, the reconcile, and the sharp edges are on how rate limits work.

Every error we originate

Nine pairs, and nothing else is ours. Branch on error.code, never on the status alone: 429 arrives for two unrelated reasons and 402 for two, and 402 is always money, never rate. Every Recovea-origin error is OpenAI-shaped and carries x-recovea-request-id.

Statuserror.codeerror.typeRetry?What it means
401invalid_api_keyinvalid_request_errornoThe rcv_ key did not resolve: unknown, revoked, or not a Recovea key at all. A key whose tenancy cannot be trusted returns this same body, byte for byte, so a bad key and an ambiguous one are indistinguishable from outside.
401provider_key_missinginvalid_request_errornoNo provider key of that provider is connected for this workspace. Connect the OpenAI or Anthropic key you want that surface to sign with. Retrying changes nothing.
401provider_key_unavailableinvalid_request_erroryes, shortlyA provider key exists but its plaintext cannot be obtained right now. Keys are never pooled or substituted, so there is nothing to sign with; a retry clears it once custody recovers.
402budget_exceededinsufficient_quotanoAn armed cap in block mode is at its ceiling. This is money, never throughput. Raise the cap or wait for the UTC month reset; retrying will not help.
402budget_unverifiableinsufficient_quotayesAn armed cap could not verify live spend, so it refused rather than let spend leak past your wall. A retry clears it once the spend store answers again.
429rate_limit_exceededrate_limit_erroryes, after Retry-AfterThroughput, not money. Too many requests or tokens this minute. Retry-After is whole seconds, at least 1 and at most 60. A throttled request is never forwarded, never metered, and never billed.
429budget_exceededrate_limit_errornot until the resetA cap in pause mode rather than block mode. Same money wall as the 402, delivered as a pause: Retry-After is the seconds to the next UTC month, floored at 1.
404unknown_urlinvalid_request_errornoThe path is outside the surfaces we proxy. Almost always a base URL with a duplicated or missing suffix.
502upstream_unreachableapi_erroryesWe could not reach your provider at all, on connect or before its first byte. This is the only 5xx we originate, and it is never metered. A stream that dies after the first byte ends honestly instead: bytes cannot be faked.
HTTP/1.1 429 Too Many Requests
Retry-After: 3
x-recovea-request-id: req_9f2c41d87ab34e60

{
  "error": {
    "message": "Rate limit reached for this workspace.",
    "type": "rate_limit_error",
    "param": null,
    "code": "rate_limit_exceeded"
  }
}

Anything not in that table came from your provider and passed through untouched, including its own Retry-After. We never rewrite a provider's error into one of ours.

Metering is priced against a frozen, version-pinned reference price list; every receipt cites the list version it was priced with. Plan pricing, what each tier includes, and how gain-share works (currently off · proof pending) are on the Pricing page.

TROUBLESHOOTING

When something is wrong

Written so you can get yourself unstuck without waiting for anyone. Each entry names what you see, what actually causes it, and what to do; every one of them is a real behaviour of this system rather than a generic checklist.

401 invalid_api_key on the very first request

The key you sent is not an rcv_live_ key, or it was revoked. The most common version of this is sending your provider key to us: the gateway takes YOUR Recovea key, and your provider key lives in the console.

What to doCheck the value actually reaching the client (a stale export in another shell beats the one you just set), that it starts with rcv_live_, and that no quote or trailing newline rode along. Mint a fresh key if you are not sure: secrets are shown once, and a revoked key never comes back.

401 provider_key_missing on a model that used to work

You routed to a provider whose key is not connected. Coverage is per provider, not per model: an Anthropic model needs a connected Anthropic key even if your OpenAI key is already there.

What to doConnect the missing provider key in the console. It is live-probed at connect, so a dead key fails at setup rather than on your next production call.

402 with code budget_exceeded

An armed cap in block mode is at its ceiling. 402 is always money on this API and never throughput, so no backoff strategy clears it.

What to doRaise the cap, disarm it, or wait for the UTC calendar-month reset. If you did not expect to be near it, read the request feed for the period: a cap counts routed spend only, so anything you sent outside the gateway is not in that number.

402 with code budget_unverifiable, and nothing is over budget

An armed cap could not confirm live spend within its verification window, so it failed closed on purpose rather than guessing on the permissive side.

What to doRetry. It clears once a fresh spend checkpoint lands. If it persists past a minute or two, that is a bug on our side and we want the request id.

429 and you cannot tell which kind

Both throughput and a pause-mode cap answer 429 with type: rate_limit_error, so the type cannot tell them apart. Only error.code can.

What to dorate_limit_exceeded means slow down and honour Retry-After (1 to 60 seconds). budget_exceeded means the money wall, and Retry-After runs to the month reset. Branch on the code in your client once and you never have to guess again.

A single large request 429s forever, however long you wait

Its token estimate is larger than the whole per-minute bucket, so it does not fit in an empty bucket either. Waiting cannot help, and the Retry-After you get back is still computed from the deficit.

What to doSplit the request, or move to a plan whose token ceiling is larger than the call you are making. This is a sharp edge we have not smoothed, and we would rather document it than let you discover it in a retry loop.

404 unknown_url

The base URL is wrong. Most SDKs append their own path, so a base URL that already ends in the same segment produces a doubled path.

What to doUse exactly https://api.recovea.ai/v1 for OpenAI-shaped clients and https://api.recovea.ai/anthropic for Anthropic ones, with no trailing slash and nothing appended by hand.

502 upstream_unreachable

We could not reach your provider on connect or before its first byte. It is the only 5xx we originate, and it carries api_error.

What to doRetry with your normal backoff, then check your provider's own status. The call was never metered and never billed, because it never happened.

An error you do not recognise at all

It is probably your provider's. Provider responses, including their errors, pass through byte for byte and are never rewritten.

What to doCompare the error.code against the table above: those nine pairs are the complete set we originate. Anything else came from upstream and should be handled exactly as you handled it before you routed through us.

The request worked, but no spend shows up

Three honest causes. A refused or throttled request is never metered. A streaming response whose usage frame fell outside the bounded window we read yields null tokens and a null cost rather than a guess. And a model absent from the pinned price list prices to null.

What to doFind the request in the feed by its id: a row with null tokens tells you which of the three happened. The arithmetic behind all of it is on how a request is metered. If a normal, non-streaming call is missing entirely, send us the request id.

Project and tags are not showing on your spend

The headers are ours and exact: x-recovea-project (one value) and x-recovea-tags (comma-separated, repeatable). They are never validated and never fail a request, so a typo is silent by design. Attribution cuts also start at Developer.

What to doCheck the spelling and that you are on a plan whose cuts are unlocked. In the request feed the project column is still a placeholder that always reads default; the attribution surface is the one that answers.

Claude Code is not being metered after running the CLI

That is deliberate. recovea install routes OpenAI-shaped tools only, and leaves Claude Code talking to Anthropic on your own account.

What to doSet ANTHROPIC_BASE_URL and ANTHROPIC_API_KEY yourself, as in the tool guide above. Nothing routes your agent without you saying so.

You changed plan and the old limits are still in force

The plan on the hot path rides an entitlement snapshot the control plane republishes every few seconds. Until it arrives, a workspace is admitted at the most conservative published tier.

What to doWait a few seconds and retry. If a changed plan has not landed after a minute, that is a bug, not a support question.

You want out, right now

Reverting is the same one line as joining. Nothing about your provider account, your keys, or your SDK changed when you pointed at us.

What to doPoint base_url back at your provider and traffic flows direct. Your receipts and your ledger stay yours and stay re-derivable offline, because the proof is a hash chain, not a login.

Before you write to us

Quote the x-recovea-request-id from the response, the UTC timestamp, and the exact status and error.code. That is enough to find one request out of a month of them, and it is the difference between an answer and a conversation about what you might have sent.

Support is docs + email -- bug reports and billing issues always answered, on every plan. The line is about hand-holding, not about being ignored: a bug, a billing problem, or a security report is answered on every plan, including Free. Traffic refused inside your own limits, arithmetic that disagrees with ours, or a receipt that does not re-derive are all bugs, and we want the request id.

Live incidents are posted on the status page, which deploys on its own pipeline so an outage here cannot silence it.

Swap one line. See your spend live.