Trace ingest · OpenTelemetry
Send us your OTel traces
If your agent already emits OpenTelemetry, three environment variables are the whole integration. Traces land on your account in the same rollout shape the Simulations SDK produces, so the analyzer can score them: prompt grouping, gradient support, pass rate, reward-hack artifacts, and a training-regime verdict. The data stays yours, to pull back out and train your own models on.
Set up
Point your OTLP exporter at the gate and put a key from the platform page in the header. No Zero Proof code runs in your app.
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://wch04mgo2k.execute-api.us-east-1.amazonaws.com/v1/traces
OTEL_EXPORTER_OTLP_HEADERS=x-api-key=zp_...
OTEL_EXPORTER_OTLP_PROTOCOL=http/jsonTwo things to get right. Use the signal-specific ..._TRACES_ENDPOINT, which is sent as written; the generic OTEL_EXPORTER_OTLP_ENDPOINT appends /v1/traces itself, so drop the path if you use that one. And set http/json: the default is protobuf, which the endpoint answers with a 415.
What comes back
One call, one response. No presigned URL, no finalize step, nothing to poll. The exporter fires and moves on.
{ "datasetId": "ds_0123…", "dataset": "prod-refunds",
"day": "2026-08-24", "rows": 412, "bytes": 918004 }Batches append. Everything sent under one dataset name on one UTC day is one dataset, so the default five-second flush interval does not turn into thousands of them. Each batch is stored as its own part, named after a hash of its contents, so an exporter retry overwrites instead of duplicating. That matters more than it sounds: a duplicated rollout quietly skews the support and pass-rate numbers you are collecting the data for.
Tag your spans
A batch becomes one dataset row per trace, built from its root span. Standard GenAI semantic-convention attributes are read automatically, so most instrumentation libraries need nothing. The zeroproof.* ones you set yourself, and they are what make the metrics mean something.
Dataset name in your workspace. Defaults to traces.
Groups repeats of the same task. Without it every run is a group of one.
A score your own harness or judge produced, in any units.
The bar, in the same units as the score. Without it the score is recorded but not judged.
Optional. Only affects how the score is displayed.
Names the agent. Informational.
Model used, recorded per row.
Token counts, summed per row.
Tool call trace, recorded per row.
Makes the run an eval, and names the set it belongs to.
One id per run, so two runs can be compared case by case.
JSON array of checks to score this case against.
Facts only your machine can see, such as whether tests passed.
Without zeroproof.reward the dataset is prompts only: you get shape and prompt metrics, but no RL-health numbers. A judge score works well as the reward.
Replaying traces you have
The Python SDK sets the same variables from code, and pushes an OTLP batch already sitting on disk.
import os
from zeroproof.ingest import otel_env, ingest_traces
os.environ.update(otel_env("zp_...", dataset="prod-refunds"))
ingest_traces("zp_...", "traces.json", dataset="prod-refunds")
from zeroproof.ingest import list_traces
for t in list_traces("zp_...")["traces"]:
print(t["name"], t["rows"], t["sizeBytes"])Getting it back
Every route below takes the same key and only ever sees your own account’s data.
GET /traces runs, newest first, filterable
GET /traces?since=<ms> only what changed since, for following a stream
GET /traces/{traceId} one run and every span in it
DELETE /traces/{traceId} forget one run
GET /evals eval sets on the account
GET /evals/{set} runs in a set, with the case-by-case grid
GET /datasets/{id} one dataset's metadata
GET /datasets/{id}/download presigned URLs: concatenate `parts` for the JSONL
GET /datasets/{id}/report the analyzer's signal report
DELETE /datasets/{id} removes the row and every stored partA judge that answers after the turn has closed sends what it decided against the trace id. Measurements are named, so several judges sit side by side, and the bar is read in the same units as the value, so nothing is rescaled behind your back.
curl https://wch04mgo2k.execute-api.us-east-1.amazonaws.com/v1/scores \
-H "X-Api-Key: $ZEROPROOF_API_KEY" \
-H "Content-Type: application/json" \
-d '{"traceId": "...", "scores": [
{"name": "score", "value": 0.4, "pass_at": 0.7, "source": "my-judge"},
{"name": "tone", "label": "curt"}
]}'To score a whole dataset for training signal, send it to the analyzer instead.
curl https://zeroproofai--zeroproof-analyzer-api.modal.run/analyze \
-H "X-Api-Key: $ZEROPROOF_API_KEY" \
-H "Content-Type: application/json" \
-d '{"datasetId": "ds_0123"}'Limits
413. An exporter’s own batch size stays well under this.
429 until 00:00 UTC, or send to another dataset name.
507. Delete a dataset.
The caps exist because an exporter is a continuous writer, and a leaked key would otherwise be an unbounded one. The daily token allowance applies to model calls, not to ingest.
What we do with it
We store it and analyze it for you. Traces stay until you delete them, we do not train on them, and one account’s data is never readable with another account’s key. Message content is stored in full, because the gaps you are looking for live in the text. If that is not acceptable for a given service, leave the gen_ai.*.messages attributes off its spans: you still get structure, rewards, tool calls, and token counts.
Questions, or a higher limit: jacob@zeroproofai.com