# OpenTelemetry

> Understand Badge's gated OTLP receiver contract and current integration readiness.

> **Release preparation only. Customer setup is not supported yet.** The
> agent-scoped credential, run-correlation backend, and BPP-gated
> create/rotate/revoke card have shipped, as has a real multi-node evidence run
> — published end to end in
> [Screen a local Ollama multi-agent app](/docs/guides/ollama-agent-end-to-end),
> which is the one worked path that exists today. Version-pinned framework
> validation has not, so the readiness table below governs every framework that
> guide does not cover. This page documents the receiver contract without
> presenting an unverified quickstart.
>
> Whether a given environment accepts OTLP at all is a server-side setting that
> an operator can change. Check it rather than assuming — the public, no-credential
> probe `GET /api/v1/provenance/status` returns `{"enabled":true}` or
> `{"enabled":false}`.

OpenTelemetry traces can describe the model and tool calls an HTTP agent
reports, their parent-child relationships, token counts, latency, and
self-reported or estimated cost.

**Evidence boundary:** a Blueprint is **self-reported telemetry — consistent
with claims, not proof**. Badge cross-checks bounded metadata against its
registered run and trace context, but the agent still controls the spans it
emits. Estimated cost is not provider billing.

Receiver contract [#receiver-contract]

* **Endpoint:** `POST https://api.badgeia.com/api/v1/provenance/otlp`
* **Authentication header:** `Authorization: Bearer <agent OTLP credential>`
* **Transport:** OTLP/HTTP with `application/x-protobuf` or `application/json`;
  OTLP/gRPC is not accepted. On the JSON path you do not need to do anything
  special about ID encoding — Badge accepts both the hex `traceId`/`spanId`
  the OTel spec mandates (what the JS SDK and other conforming `http/json`
  exporters send) and the base64 form protobuf's canonical JSON mapping
  produces. Both are stored identically.
* **Bounds:** 1 MiB per request and 200 stored spans per run.
* **Feature gate:** when Blueprint/provenance processing is disabled, the
  receiver returns `404` and this page is excluded from navigation,
  `llms.txt`, and the sitemap.

The owner/editor-only backend routes
`POST /api/v1/agents/{agent_id}/otlp-token` and
`DELETE /api/v1/agents/{agent_id}/otlp-token` create, rotate, and revoke a
single-reveal credential. The credential is reusable for that agent until
rotated or revoked. The agent-detail card masks and single-reveals the value
for eligible editors. Customer publication remains blocked until the exact
staging trace and compatibility matrix pass.

Every batch authenticated with an agent credential must assign
`badge.run_id` to each effective span partition. The run must belong to that
agent, and every trace ID must match a trace Badge registered before
dispatch. Badge sends the run ID in `X-Badge-Run-Id` and W3C trace context in
`traceparent` to an eligible HTTP agent.

Use the exact trace endpoint above with your SDK's **trace-specific** OTLP
HTTP setting. Send the credential only in the authorization header. Never
put the credential, a Badge API key, prompts, completions, tool arguments, or
tool results in span attributes, URLs, logs, screenshots, or support tickets.

Framework readiness [#framework-readiness]

### OpenAI Agents SDK — Unsupported

The SDK's built-in tracing does not become OTLP when standard exporter variables are set.

- **What Badge reads:** Nothing through the built-in tracing path. A separate adapter would need to emit Badge's supported GenAI attributes.
- **Evidence still required:** A selected, security-reviewed, version-pinned adapter exercised against staging.

### LangChain / LangSmith — Partial — bridge verified

A manual callback bridge (the worked path in the Ollama multi-agent guide) is verified end to end on a deployed environment with pinned versions: agent hierarchy, tool and retrieval structure, and token counts all reconstruct. Cost is whatever the bridge self-reports — Badge priced nothing in that run, because the locally-hosted model is not on the price allowlist. LangSmith's own OpenTelemetry emission remains unverified and moves independently of LangChain.

- **What Badge reads:** Supported gen_ai.* model, token, tool, and agent-hierarchy (gen_ai.agent.name) metadata emitted by the bridge. Tool names arrive as opaque hashes and a locally-hosted model renders opaque and unpriced — both unconditional, not bridge limitations.
- **Evidence still required:** Auto-instrumentation does not exist — the bridge is ~200 lines you add yourself. Content-hiding controls are unverified. For LangSmith-native OTel emission: pinned LangChain and LangSmith versions and a successful staging row.

### Vercel AI SDK — Partial — staging unverified

The AI SDK can emit OpenTelemetry metadata with input and output recording disabled.

- **What Badge reads:** Supported gen_ai.* model and token fields. ai.toolCall.name is ignored, so tool nodes are not reconstructed.
- **Evidence still required:** A pinned AI SDK version, exporter bootstrap, force-flush behavior, and a successful staging row.

### Pydantic AI — Partial — staging unverified

Pydantic AI exposes instrumentation controls, but the activation API is version-sensitive.

- **What Badge reads:** Only metadata that is also emitted under Badge's supported gen_ai.* keys.
- **Evidence still required:** A pinned package version, include-content-off configuration, and a successful staging row.

### Plain OTLP exporter — Reference draft — staging unverified

A standard OTLP/HTTP exporter can send metadata-only spans without a framework adapter.

- **What Badge reads:** Supported model, token, tool, retrieval, timing, response-hash, and run-correlation metadata.
- **Evidence still required:** A version-pinned plain-exporter example of its own. The multi-node staging trace exists, but it was captured through the LangChain bridge — a stock-exporter row still needs its own receipt.

Partial and reference-only rows are deliberately not copy-paste examples.
They become supported only after exact package versions, content-capture
controls, accepted-span results, expected Blueprint nodes, and privacy-safe
staging receipts are recorded.

Attribute mapping [#attribute-mapping]

Badge applies a strict allowlist before persistence. Unknown attributes are
discarded.

| Exported metadata                            | Badge interpretation                    | Notes                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| -------------------------------------------- | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Span `trace_id`, `span_id`, `parent_span_id` | Sequence and invocation edges           | Standard OTLP fields, not attributes                                                                                                                                                                                                                                                                                                                                                                                                                  |
| Span start/end timestamps                    | Per-node latency                        | An end before start is clamped to start and contributes `0ms`                                                                                                                                                                                                                                                                                                                                                                                         |
| `gen_ai.response.model`                      | Model node                              | Preferred over request model when both are present                                                                                                                                                                                                                                                                                                                                                                                                    |
| `gen_ai.request.model`                       | Model node                              | Used when response model is absent                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `gen_ai.usage.input_tokens`                  | Input tokens                            | Current GenAI semantic convention                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `gen_ai.usage.output_tokens`                 | Output tokens                           | Current GenAI semantic convention                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `gen_ai.usage.prompt_tokens`                 | Input tokens                            | Accepted legacy alias                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `gen_ai.usage.completion_tokens`             | Output tokens                           | Accepted legacy alias                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `gen_ai.usage.cost`                          | Self-reported USD cost                  | Badge extension; numeric, bounded, and labeled                                                                                                                                                                                                                                                                                                                                                                                                        |
| `gen_ai.tool.name`                           | Tool node                               | Use a stable non-secret identifier; arguments and results are ignored                                                                                                                                                                                                                                                                                                                                                                                 |
| `gen_ai.agent.name`                          | Agent/role node (multi-agent hierarchy) | Stored as a bounded `agent:<display>~<hash>` projection. The name must start with a letter and contain only letters, digits, `_` or `-` — a name that doesn't is **dropped, not cleaned up** (see "Agent nodes do not appear" under Troubleshooting). At most 16 stored characters (the public read view cuts this to 12 — see "What Badge stores"); at most 8 distinct roles per run; supervisor→subagent edges come from real parent span structure |
| Span name `retrieval`                        | Retrieval node                          | One of exactly seven accepted names: `retrieve`, `retrieval`, `retriever`, `vector_search`, `vector.search`, `semantic_search`, `semantic.search`. Anything else becomes `unknown_operation`                                                                                                                                                                                                                                                          |
| `badge.response_sha256`                      | Answer-binding evidence                 | SHA-256 hex digest only; never the answer body                                                                                                                                                                                                                                                                                                                                                                                                        |
| `badge.run_id`                               | Agent-credential correlation            | Required for every effective partition using an agent credential                                                                                                                                                                                                                                                                                                                                                                                      |

Badge reads OpenInference, OpenLLMetry, LangSmith, Vercel, or other
framework output only when it also emits the supported `gen_ai.*` keys.
Framework-native prompt, message, content, `llm.*`, and
`ai.toolCall.name` attributes are not remapped.

Unknown models are `unpriced`, never guessed as zero. A valid
`gen_ai.usage.cost` remains self-reported telemetry. Badge estimates cost
only when the exact model ID has a price effective on the run date and both
token counts are valid nonnegative integers.

What Badge stores [#what-badge-stores]

Badge stores a bounded metadata projection: safe span, parent, and trace IDs;
operation kind; model identifier; token counts; timestamps; safe tool
identifier; optional numeric cost; run correlation; and answer hash. It does
not persist prompt or completion content from OTLP.

**What is stored is not what is published.** A second, narrower projection is
applied at read time for anyone who cannot edit the agent:

* Model and tool digests are replaced by per-response ordinals (`model:1`,
  `tool:1`). Model IDs on Badge's vendored price allowlist stay verbatim.
* An agent identity keeps its **readable role name** — case-folded and cut to
  12 characters — with an ordinal in place of the digest: `agent:researcher~2`.
  This is the only caller-authored text on the public surface. It is bounded,
  not anonymised. Name nodes for the role, never for the customer, tenant, or
  internal system.
* Token counts are floored to two significant figures; latencies and costs are
  rounded to nearest at two significant figures, so a published latency or cost
  can exceed the measured one. Owners get exact token counts and latencies;
  every stored cost is quantised to four significant figures.

Blueprint architecture and cost remain labeled self-reported or estimated.
They do not change the run's composite score or fitness radar formula.

Architecture versions (where enabled) [#architecture-versions-where-enabled]

Whether versioning is enabled is a server-side setting, exposed by the same
public probe as `"blueprint_versions"`: `GET /api/v1/provenance/status`.

On environments with versioning enabled, Badge assigns every traced run a
**content-addressed architecture version**: two runs whose span graphs have
the same structure and identities — the same roles, model identities, tool
set, and call relations — share one version; a structural change mints the
next one (`v1`, `v2`, …). Volatile signals (token counts, latency, cost,
timestamps, retries of an existing path) never change the version. A version
identifies **structure and identities, not behaviour**: prompts, sampling
settings, and routing logic are invisible to it. Blueprint responses carry
the version as `blueprint_version` (`id`, `version_number`,
`canonicalization_version`); the field is `null` where versioning is
disabled or the run predates it.

**Reading the timeline.** The agent's Evidence page lists its versions most
recently seen first — so the version currently running sits at the top, even
after a rollback to an older architecture (a rollback re-uses the existing
version; it never mints a new one). Each entry shows when that version was
first and last seen, how many traced runs are bound to it, and a summary of
what changed against the version before it. Dates and counts cover the runs
you can see: a run the owner keeps private moves nothing on the public
timeline. The summary names a model only where the identity is one Badge
recognises (`gpt-4o-mini → gpt-5-mini`); everywhere else it says *model
identity changed*, because the comparison is over identifiers that were
privacy-projected before storage — Badge can see that a model changed, not
which model. A change to an agent node reads *role renamed or replaced*: the
comparison cannot tell a rename from a replacement. An entry marked *hashing
rules updated* is not an architecture change at all — it is the same
architecture re-recorded after Badge changed how it canonicalises graphs.
Signed out, or on someone else's agent, the same list appears without version
numbers: versions are identified by their id, because the counter is minted
across private runs too.

For agents with a signed manifest, the Evidence page's comparison panel also
records **declared vs observed** against the architecture version it was
computed on — reported, never enforced, and visible to the agent's owner and
editors only. What it
does and does not prove is covered in the
[Provenance FAQ](/docs/methodology/provenance#what-is-the-declared-vs-observed-comparison-on-the-evidence-page-and-what-does-it-prove).

Troubleshooting [#troubleshooting]

`404` from the OTLP endpoint [#404-from-the-otlp-endpoint]

Blueprint/provenance processing is disabled. Check
`GET /api/v1/provenance/status` or ask the environment operator.

Exporter sends to an added `/v1/traces` path [#exporter-sends-to-an-added-v1traces-path]

The generic OTLP endpoint setting was used. Configure the trace-specific
setting with the complete Badge endpoint exactly as shown above.

Connection error on port `4317` [#connection-error-on-port-4317]

The exporter is using OTLP/gRPC. Select OTLP/HTTP with protobuf or JSON.

`401 Invalid or missing OTLP ingest token` [#401-invalid-or-missing-otlp-ingest-token]

Use the single-reveal credential created for the same agent. It remains
usable until rotated, revoked, or made ineligible. Do not send a Badge API
key to the receiver.

`415` or `422` [#415-or-422]

For `415`, use `application/x-protobuf` or `application/json`. For
`422 Malformed OTLP payload`, make the body encoding and declared content
type agree. Agent-credential batches also return `422` when a run ID is
missing or a trace does not match the run's registered dispatch.

`200` with `accepted_spans: 0` [#200-with-accepted_spans-0]

The span IDs were already ingested, or the run reached its storage cap.
Retries are idempotent.

Run completes but no Blueprint appears [#run-completes-but-no-blueprint-appears]

Force-flush the exporter, confirm every partition carries the correct
`badge.run_id`, and confirm at least one span has a supported model, tool, or
retrieval signal.

Spans arrive but model or token data is absent [#spans-arrive-but-model-or-token-data-is-absent]

Inspect the exported attribute names. Framework-native `llm.*` fields are not
automatically treated as `gen_ai.*`.

Tool names become opaque [#tool-names-become-opaque]

**This is unconditional and not a misconfiguration.** Every accepted
`gen_ai.tool.name` is stored as `tool:<sha256[:16]>`; there is no
readable-tool allowlist and no identifier you can choose that stays readable.
An anonymous reader sees less still — a per-response ordinal (`tool:1`),
which is positional and not a stable key across responses.

Tool names disappear entirely [#tool-names-disappear-entirely]

The value failed the ingest guard rather than being hashed. Use a short
stable identifier, not arguments, URLs, credentials, or natural-language
content. Vercel's `ai.toolCall.name` is not mapped today.

Agent nodes do not appear [#agent-nodes-do-not-appear]

The value failed the ingest guard rather than being shortened.
`gen_ai.agent.name` must start with a letter and, in its first 16
characters, contain only letters, digits, `_` or `-`. Dots, `/`, `:`, `@`,
or a leading digit drop the name entirely and the span keeps its non-agent
classification — `research.agent` yields no node, `research_agent` does.
Internal spaces are fine (`Math Tutor` → `Math_Tutor`). At most 8 distinct
roles per run are kept.

Cost shows `unpriced` [#cost-shows-unpriced]

The exact model ID had no price effective on the run date, or one of the
token counts was missing, invalid, or negative. Badge does not infer aliases
or missing usage.

Trace is accepted but provenance stays unbound [#trace-is-accepted-but-provenance-stays-unbound]

Confirm that the trace ID matches the dispatched `traceparent`, the root span
has the exact canonical response hash, and the agent echoes
`X-Badge-Trace-Id`. Accepted late spans trigger recomputation; arrival after
the initial evaluation does not by itself leave a run permanently unbound.
