Inference Pricing with OpenTelemetry
Inference pricing burns credits for every model call at rates you set per request and per token type. Your app can report each call by posting an inference event after it, but that means a tracking call next to every model request, and one more place to get the token math wrong.
If your app already traces its model calls with OpenTelemetry, you can point an exporter at Schematic instead. Schematic turns each model-call span into an inference event, reading the token counts from the standard gen_ai.* attributes your instrumentation already sets. The Anthropic TypeScript SDK emits these spans on its own once you register a tracer provider, so a Claude app needs no instrumentation library at all.
The pricing side is the same for both paths. Set up your credit type, inference features and plan rates as described in Set up pricing in Schematic, then send spans as described here.
How it works
- Your app calls a model. Its instrumentation records a span with the provider, model and token counts.
- You add the company, and the event name of the feature to bill, as
schematic.*attributes on that span. - An OTLP exporter sends the span to
https://c.schematichq.com/v1/traceswith your secret API key. - Schematic derives one inference event from the span and burns credits from it, exactly as it would for an inference event you posted.
Point an exporter at Schematic
Schematic accepts OTLP over HTTP, as protobuf or JSON, gzip-compressed or not. Authenticate with a secret API key in the X-Schematic-Api-Key header. An export with a publishable key is rejected with a 401.
Most OpenTelemetry SDKs read these from the standard environment variables:
Schematic derives events only from model-call spans and spans you tag with an event name, so you can export whole traces and the rest of each trace bills nothing.
Tag spans with the company and event name
Schematic reads who to bill and which feature to bill from attributes on the span:
Schematic reads each schematic.company.* and schematic.user.* attribute from the span first, then from the span’s resource. A process that serves a single company, such as an agent running on the customer’s behalf, can set the company once as a resource attribute instead of on every span.
A model-call span with no company is rejected, and Schematic reports the rejection in the export response. A span with a company but no schematic.event is still stored and costed, but it burns no credits.
The hard part is getting these attributes onto a span your instrumentation creates. Your code never holds the span, so it can’t call setAttribute on it. The usual answer is to put the attributes on the active OpenTelemetry context around the model call, and copy them onto each span as it starts with a span processor. The next section shows that processor.
Set up the Anthropic TypeScript SDK
@anthropic-ai/sdk creates an anthropic.messages.create span for every Messages API call once a global tracer provider is registered. The span carries the provider, the requested and served model, and token counts that already include cache reads and writes, the way Schematic counts them.
Install the OpenTelemetry SDK and the OTLP exporter:
Then register a tracer provider before your app makes its first model call. In Next.js, call this from register() in instrumentation.ts.
The SDK sets gen_ai.operation.name when it starts a span, so tagModelCalls tags only the chat spans of real model calls. Wrapper spans such as a tool runner’s invoke_agent span stay untagged, which matters because a non-model span that carries schematic.event becomes a separate event (see Which spans become inference events). The context manager keeps the context that withSchematic sets active across every await inside its callback.
If you already run a tracer provider, add tagModelCalls and the Schematic exporter’s BatchSpanProcessor to its spanProcessors instead of registering a second provider.
Wrap each model call in withSchematic:
For a streamed call, wrap the call that starts the stream. The SDK ends the span when your code reads the stream to the end or closes it, and the span carries the usage from the final message. A stream your code stops reading without closing never ends its span, so nothing is exported for it.
A workflow that makes several calls for one feature can wrap the whole workflow in one withSchematic call, and every model call inside it bills to the same company and event name.
Which spans become inference events
Schematic picks a reader for each batch of spans by the batch’s instrumentation scope. Every scope without a reader of its own is read by the OpenTelemetry GenAI semantic conventions, which most LLM instrumentation follows.
A span counts as a model call when its gen_ai.operation.name is chat, text_completion, embeddings or generate_content. Agent and tool spans such as invoke_agent and execute_tool are skipped, even when they carry rolled-up token totals, so an agent loop never counts the same tokens twice.
A model-call span needs a provider, a model and both token counts. One missing any of them is rejected.
The semantic conventions count cache reads and cache writes inside gen_ai.usage.input_tokens, which is how Schematic counts them, so Schematic uses the span’s input count as it is. If your instrumentation passes through a provider’s raw count instead, such as Anthropic’s usage.input_tokens, which leaves out cached tokens, the uncached part of each prompt goes unbilled. See Count input tokens the way Schematic does.
Reasoning tokens bill at your output rate, because the GenAI conventions count them inside gen_ai.usage.output_tokens. Leave the reasoning rate on the plan at 0.
new Anthropic({ openTelemetry: false }) or ANTHROPIC_OPEN_TELEMETRY=false if you keep the other one.A span that carries schematic.event but is not a model call becomes a track event for that event name, with a quantity from schematic.quantity, or 1 when it is unset. That lets you meter non-model work from your traces too, and it’s why the processor above tags model-call spans only.
Reserve credits for a call
To hold credits before a long or concurrent call runs, take a reservation as described in Leases and reservations, then pass its id to withSchematic as schematic.reservation_id alongside the company and event name. The inference event the span becomes settles the reservation, and the unspent credits return to the balance. Set schematic.lease_id instead to commit the call against a lease.
Check that it works
Make a model call as a company on the plan, then:
- Open Events. The inference event appears with the company, event name and token counts from the span.
- Open the company and click View Ledger under Credit Balance. Each call appears as an inference burn on your credit type, with its burn breakdown.
If nothing arrives, read the export response. When Schematic rejects a span, it answers with an OTLP partial success that gives the number of rejected spans and the reason, such as a missing company or missing token counts. The JavaScript exporter logs it as a warning through OpenTelemetry’s diagnostic logger, which stays silent until you register one with diag.setLogger.
Each event’s idempotency key is the span’s trace id and span id, so an exporter that retries a batch never bills a call twice.
If the event arrives but nothing burns, run through the checklist in Inference Pricing.
Related
- Inference Pricing sets up the credit type, inference features and per-token rates, and covers posting inference events directly.
- Leases and reservations hold credits before a call runs, then settle the hold from the span.
- Seat and Agent Budgets reports burns per user, from
schematic.user.*on each span.