The Baseline package (the passive tap)
The wire the Baseline package runs on: one endpoint, a bearer tap key, the fifteen-field event, and what the server answers.
On this page
The Baseline package is the passive tap. Install Recovea beside the provider client you already use. Seven days of reporting later, your own numbers: by agent or workload if you run your own, by client or customer if they pay for what your AI does. Your calls keep going straight to your provider. Your application reports the envelope of each call to Recovea beside that path, after the call has returned: never a prompt, never a completion, never a provider key. Nothing routes through Recovea. The Baseline reports over this wire, and the platform shows what it found.
This page is the exact shapes, for the engineer who checks what leaves the process. Section 8 prints every batch on your own machine, with no key and nothing sent, if you would rather read it than read about it.
1 · What the tap does, and what it can never do
- Install Recovea beside the provider client you already use.
tap(client)returns the same client you passed in, wrapped, with the same behaviour. Nothing else in your code changes. - Your call is your call. The request goes to your provider on your own path. The wrapper does not change the request, the response, or the stream you see.
- Reported on the side. After the call returns, the wrapper builds one event from the response envelope and queues it. Delivery is asynchronous and batched: it never delays your call, never fails it, and never throws into your code.
- Never content. Not a prompt, not a message, not a completion, not tool arguments, not any content field. The wrapper does not read them, does not log them, and does not serialize them.
- Never a provider key. The tap never sees a provider key and we never ask for one. There is no usage-endpoint key, no admin key and no read-only key on this path.
- Nothing routes through Recovea. There is no proxy in the call path. If our endpoint is down, your call does not know.
Two enforcements, one on each side of the wire. The package applies the field list below when it serializes, so only those fields can leave your process. The server applies it again on arrival, and a field it does not know refuses the whole batch rather than trimming it. Neither side is trusted to be the only one holding the rule.
2 · The endpoint and the key
POST /tap/v1/events HTTP/1.1
host: platform-api.recovea.ai
authorization: Bearer rcv_tap_…
content-type: application/json
user-agent: your-app/1.2.3Send a user-agent. Both published packages do. The host sits behind a web
firewall whose managed rules can refuse a request that omits one, and that
refusal is a bare 403 from the edge: it never reaches the endpoint and carries
none of the bodies below.
| Rule | Value |
|---|---|
| Endpoint | POST https://platform-api.recovea.ai/tap/v1/events |
| Host | platform-api.recovea.ai, not the platform's host |
| Auth | Authorization: Bearer <tap key> |
| Key spelling | rcv_tap_ followed by 32 random bytes as 64 hex characters |
| Minted | one key per organization, when the Baseline is created |
| Shown | once, in the platform, at mint and at rotation, through the read below |
| Stored | as HMAC-SHA-256 under the server pepper, through the same function the platform's gateway keys use; the first 12 characters are kept in clear so the platform can show you which key a line is using |
| Held in clear | from the mint until your first read of it, and never again. Rotation opens the same one-read window on the new key and closes it the same way |
| Rotation | in the platform, on the Baseline's own page: the new key is shown once and the old key stops working at that moment |
| Session, CSRF, role | none. The route carries no platform session and no role check; the key is the whole authorization |
| Key, from the environment | RECOVEA_TAP_KEY |
| Endpoint, from the environment | RECOVEA_TAP_ENDPOINT, defaulting to the endpoint above |
| Inspect, from the environment | RECOVEA_TAP_INSPECT: print each batch, send nothing, no key needed. Section 8 |
The key names your Baseline. The server hashes the key you present, finds your Baseline's row from it, and takes your organization from that row. So nothing in the body has to name you, and nothing in the body can name anybody else: there is no account field to get wrong and none to forge.
Reading it, once
Recovea holds your tap key in clear from the moment it is minted until the first time you look at it, and never again. The read itself clears it, leaving the peppered hash and the 12-character prefix.
The read is a platform route, authenticated by your platform session, and not reachable with the tap key itself:
GET /platform/v1/assessments/{id}/tap-key{ "key": "rcv_tap_…", "prefix": "rcv_tap_9f2a" }Ask a second time and it is a 404 with error.code tap_key_already_shown.
POST /platform/v1/assessments/{id}/tap-key/rotate answers the same
{key, prefix} shape, once, on the same terms: the new key is readable until
you read it, the old hash is replaced, and the old key stops working at that
moment.
So there is one way back to a working key whether you never copied it or copied it and lost it: rotate, and copy the new one. There is no call that shows you the old one, and support cannot read it out to you, because after your first read nobody at Recovea has it either.
A wrong or missing key is 401, with no detail:
{
"error": {
"code": "unauthorized",
"message": "The tap key was missing or not recognized."
}
}It does not say whether the key is unknown, revoked or mistyped, and it never echoes what was presented.
3 · The request body
{
"events": [
// one to five hundred of them, each exactly as in section 4
]
}| Rule | Value |
|---|---|
| Top-level shape | one key, events, and no other |
| Events per batch | 1 to 500 |
| Body size | 256 KiB; above it the batch is refused 413 |
| Content type | application/json |
An object with a second top-level key is refused whole, the same as an event with a field the table does not name. The published packages post at most 100 events per batch.
4 · The event: recovea-tap-v1
One whole event, as the package sends it:
{
"schema": "recovea-tap-v1",
"provider": "openai",
"model": "gpt-5",
"request_id": "chatcmpl-9x7QvK2tRbN4",
"input_tokens": 3184,
"output_tokens": 512,
"cached_tokens": 2048,
"stop_reason": "stop",
"tool_calls": 2,
"status": 200,
"retries": 0,
"latency_ms": 1274,
"rate_limit": {
"requests_limit": 500,
"requests_remaining": 118,
"tokens_limit": 800000,
"tokens_remaining": 412300
},
"timestamp": "2026-09-18T04:12:07.412Z",
"tag": "matter-4821"
}If a field is not in this table, it is not in the object. The validator is built to match the table, and an object carrying a field the table does not name is refused whole.
| Field | Type | Rule |
|---|---|---|
schema |
string | exactly recovea-tap-v1 |
provider |
string enum | openai · anthropic · google · xai · mistral · other |
model |
string | 1 to 128 characters, [A-Za-z0-9._:/-]; the model the provider says it served, not the one you asked for |
request_id |
string | 1 to 128 characters, [A-Za-z0-9._:-]; the provider's response id or request-id header, and a UUID the package generates when the provider gives neither |
input_tokens |
integer | 0 to 1,000,000,000 |
output_tokens |
integer | 0 to 1,000,000,000 |
cached_tokens |
integer | 0 to 1,000,000,000; default 0 |
stop_reason |
string enum | stop · length · tool_calls · content_filter · error · other |
tool_calls |
integer | 0 to 10,000. The count, never the arguments |
status |
integer | 100 to 599, the provider's HTTP status. 599 is the sentinel for a call that failed with no HTTP response at all, and 200 is assumed where the provider's SDK hands back a successful response and no status |
retries |
integer | 0 to 100, the provider call's own retry count. Always 0 from version 1 of the packages: no provider SDK exposes one, and the field is on the wire so a later version can fill it without a schema change |
latency_ms |
integer | 0 to 3,600,000 |
rate_limit |
object or null | exactly requests_limit, requests_remaining, tokens_limit, tokens_remaining; each an integer 0 to 1,000,000,000 or null. No other key. Null when the SDK does not expose the headers |
timestamp |
string | RFC 3339, with a timezone. Refused more than 5 minutes in the future or more than 30 days in the past |
tag |
string | 1 to 64 characters, [A-Za-z0-9._:-]. Your own unit id: a matter, a claim, a client, an agent, a workload |
Two of those fields say less today than the schema allows. Read a 0 in
retries as not reported, never as no retry happened. status carries the
provider's HTTP status where there is one, 599 where the call failed without
ever getting a response, and 200 where the SDK hands back a success and no
status.
tag is the only free-text field on this wire, and the only field a human
types. Everything else is filled by the package from the provider's own
response, typed and bounded. That is why the tag is short, restricted to a plain
identifier, and validated when you wrap the client rather than on the call: an
invalid tag throws at tap() time, before a single request has run, so you find
out at start-up and not in production. One wrapped client carries one tag.
This tag is not the x-recovea-tags header. That header belongs to the
gateway, has four namespaces and a 128-character value
(Tags). The tap's tag is one plain identifier per
wrapped client.
5 · What the server does with a batch
| The batch | The answer |
|---|---|
| Valid | 202 with {"accepted": n}, where n is the number of events stored |
An unknown key at any level, a wrong type, a number out of range, a bad character, a wrong schema, or an events array outside 1 to 500 |
400, the whole batch, never the offending event alone |
| Over 256 KiB | 413, the whole batch, the same way |
| A key we do not know | 401, with no detail |
| A key belonging to an organization that asked to be erased | 401, the same bytes as the row above: the erasure revokes the stored key, so it is no longer a key we know |
| A key whose Baseline has closed | 410, nothing stored. Section 5a |
A request_id already stored for this Baseline |
the event is ignored, not an error; the batch still answers 202, and the ignored event is not counted in accepted |
The 400 message names the first offending path and a short reason from a
fixed list, and never echoes a value you sent; for a field the schema does not
know it names the enclosing object, $ or events[i], rather than the stray
key, because naming the key would echo something you sent.
{
"error": {
"code": "tap_invalid",
"message": "events[3].latency_ms: is outside its published range"
}
}An oversized body is refused the same way, with its own code:
{
"error": {
"code": "tap_too_large",
"message": "body over 256 KiB"
}
}A rejected payload is not logged anywhere. Not in an error log, not in a metric label, not in an exception report. It is refused and dropped. That is why the refusal is whole-batch rather than per-event: trimming an unknown field would mean reading it first.
Whole-batch is not data loss. The package retries a failed post twice, after 200 ms and after 800 ms, and then drops it, so a batch refused for a shape error costs the events in that batch and nothing else. It is also, deliberately, loud: an integration that is sending something it should not stops working rather than quietly half-working.
5a · The 410
A Baseline lasts seven calendar days. Your seven days are counted from the first
whole day your application reports priced calls. The page shows the days as
they pass; if no such day arrives within sixty days, the Baseline closes then
with what it has. After it closes, a batch on its key is answered 410, nothing
is stored, and the body's code says why:
code |
What the packages do |
|---|---|
baseline_closed |
print one line, drop the queue, stop posting, then re-check about once a minute |
| any other code | print one line, drop the queue, and stop posting for the life of the process |
The baseline_closed body carries its sentence:
{
"error": {
"code": "baseline_closed",
"message": "This Baseline has closed. Start a plan to keep reporting."
}
}A baseline_closed stops the reporting; it does not end it. The re-check
rides your application's own calls, so it costs nothing while you are idle.
When the key is admitted again, reporting starts on its own within about a
minute of your application's next call, with no restart and nothing to
re-integrate, and no second line is printed. What resumes is new calls only;
events dropped while the Baseline was closed are gone, because they were never
accepted.
An erasure is not a 410. It answers 401, exactly as an unknown key does:
the erasure removes the stored hash of the key. What the holder sees is the
401 ladder: after three consecutive 401s the package prints
recovea tap: the tap key was not accepted three times; reporting stopped,
drops its queue and goes quiet for the life of the process.
In every one of these endings the wrapped client goes on working exactly as it did.
6 · What a figure is called
Every money and token figure the platform shows from these events carries one of three labels, and the label is part of the figure.
| Label | What it means |
|---|---|
| reported | tokens as the provider's usage field, reported by your own client. The three token fields above are reported figures |
| modeled | cost from Recovea's frozen reference price list applied to reported tokens, with the price basis printed beside it. No cost is ever sent over this wire; it is computed when the figure is read |
| declared | a figure you typed |
A tap figure is never called measured. An event is emitted by your own process, from your provider's own response, and we did not stand in the path to watch it happen. That makes it your report of the call, which is a different and weaker kind of evidence than a record we kept ourselves, and the platform says so rather than rounding it up to a measurement.
A model with no price on the reference list produces no modeled cost: the figure is null and the platform says there is no reference price for that model. It is never guessed and never filled in from a neighbouring model.
There are no receipts over these events, and no hash chain over them. What the platform claims is the line at the foot of the Baseline page:
Computed from the events your own application reported. Export them and check the arithmetic.
7 · The install
import OpenAI from "openai";
import { tap } from "recovea";
const openai = tap(new OpenAI(), { tag: "matter-4821" });
// use openai exactly as beforefrom openai import OpenAI
from recovea import tap
client = tap(OpenAI(), tag="matter-4821")The packages are recovea on npm and recovea on PyPI. The key comes from
RECOVEA_TAP_KEY and the endpoint from RECOVEA_TAP_ENDPOINT unless you pass
them in options. Both post to the endpoint in section 2, with the body in
section 3, carrying the events in section 4. There is nothing else on the wire.
With no key configured the wrapped client still works and reports nothing, with one line to stderr when you construct it, unless inspect mode is on, which needs no key and prints instead.
8 · Inspect mode: read the batch before it leaves
Turn it on and both packages print every batch to stderr as the exact JSON body this endpoint would have received, and send nothing. There is no request at all. It is here so the engineer doing the checking can read the fifteen fields on their own machine, at their own traffic, before any of them leave it.
$ RECOVEA_TAP_INSPECT=1 node ./your-script.js
[recovea] tap inspect mode is on: each batch is printed below as the exact JSON body the endpoint would receive, and nothing is sent.
{"events":[{"schema":"recovea-tap-v1","provider":"openai","model":"gpt-5",…}]}| Rule | Value |
|---|---|
| Environment | RECOVEA_TAP_INSPECT set to 1, true, yes or on. Surrounding spaces and case are ignored; anything else, and unset, is off |
| Option | inspect: true in JavaScript, inspect=True in Python. A boolean wins over the environment in both directions, so inspect: false turns the variable off for one wrapped client |
| A key | not needed. Inspect mode enables the reporter on its own |
| What is printed | one line per batch: the body of section 3, up to 100 events, with no spaces and no newlines inside it, the bytes this endpoint receives |
| Where | stderr, never stdout, so it cannot get into a program's own output |
| The notice | one line, once per process however many clients you wrap, before any batch |
| On exit | whatever is still queued is printed on the way out, so a script that finishes before the 2,000 ms interval still shows you its batch |
| Validation | unchanged. An event that section 4 would refuse is dropped before it is queued, so it is not printed either |
The printed line cannot drift from the posted body. Each package builds the batch body in exactly one function, and the printing path and the posting path both call it. What you read in inspect mode is the string that would otherwise have gone over the wire, character for character.
With a tap key configured as well, inspect mode still wins, and nothing is sent. Everything is configured, batches print, and the Baseline stays on waiting for your first event for as long as the mode is on. The notice says so when both are set:
[recovea] tap inspect mode is on AND a tap key is configured: each batch is printed below as the exact JSON body the endpoint would receive, and nothing is sent. No event reaches your Baseline until inspect mode is turned off.See also
- Tags: the
x-recovea-tagsrequest header, a different tag on a different path. - Content-free by schema: the same rule on the gateway's side.