Integrations

LLM cost per user from OpenTelemetry traces

In short: point an OTLP/HTTP trace exporter at https://fiscallm.com/api/v1/otel with your ingest key in the Authorization header, and put a user.id attribute on your spans. Every AI call span becomes a cost event for that user. There is no FiscaLLM SDK to install.

Traces already say which model ran and how many tokens it used. What they don't say is whether the user behind them pays you more than they cost. FiscaLLM adds that: it prices each call, adds it up per user, and sets it next to revenue from RevenueCat, Stripe or your own billing.

1. Add the exporter

Create an app and copy the ingest key. If your OpenTelemetry SDK is configured by environment variables and FiscaLLM is the only place traces go:

OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://fiscallm.com/api/v1/otel/v1/traces
OTEL_EXPORTER_OTLP_TRACES_HEADERS=Authorization=Bearer%20<FISCALLM_KEY>

Most teams already export somewhere else. Add a second exporter in code (Node shown):

import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";

const fiscallm = new OTLPTraceExporter({
  url: "https://fiscallm.com/api/v1/otel/v1/traces",
  headers: { Authorization: `Bearer ${process.env.FISCALLM_KEY}` },
});

new NodeTracerProvider({
  spanProcessors: [
    new BatchSpanProcessor(yourExistingExporter),
    new BatchSpanProcessor(fiscallm),
  ],
}).register();

Or in the OpenTelemetry Collector, with no code change at all:

exporters:
  otlphttp/fiscallm:
    endpoint: https://fiscallm.com/api/v1/otel
    headers:
      Authorization: Bearer ${env:FISCALLM_KEY}

service:
  pipelines:
    traces:
      exporters: [otlphttp/fiscallm, your-existing-exporter]

JSON and protobuf are both accepted, with or without gzip. Metrics and logs sent to the same endpoint are accepted and dropped.

2. Say who the user is

Set user.id to the same ID your billing knows (the RevenueCat app user ID, or the user_id in Stripe metadata). The safest place is on the AI call span itself, or as a resource attribute if one process serves one user.

import { trace } from "@opentelemetry/api";

trace.getActiveSpan()?.setAttribute("user.id", userId);

FiscaLLM also looks at the span's parents and the rest of the trace, so an ID set on your request span usually works. One caveat: a batch exporter can send a child span before its parent has ended. If the ID is only on the parent, that call can arrive without a user and be skipped. Setting the ID on every span avoids it.

These attribute names are read, in this order:

fiscallm.user_id, user.id, enduser.id, app.user.id,
ai.telemetry.metadata.userId, ai.settings.context.userId,
langfuse.user.id, traceloop.association.properties.user_id,
langsmith.metadata.user_id, session.user.id

What is read from a span

ValueAttributes
Which spans countgen_ai.operation.name of chat, text_completion, generate_content or embeddings; OpenInference LLM and EMBEDDING spans
Modelgen_ai.response.model, gen_ai.request.model, llm.model_name
Providergen_ai.provider.name, gen_ai.system
Tokensgen_ai.usage.input_tokens, gen_ai.usage.output_tokens, gen_ai.usage.cache_read.input_tokens, gen_ai.usage.cache_creation.input_tokens (and the OpenInference llm.token_count.* names)
Feature (optional)fiscallm.feature, gen_ai.agent.name, traceloop.workflow.name
Exact cost (optional)fiscallm.cost_usd, when you already know the cost and want it used as is
Job (optional)fiscallm.job_id, for pay-per-job apps

Cached input is priced at the cached rate and not counted twice. A model that is not in the price table is stored and shown as unpriced until you set a price.

3. Check that it works

The app's Setup page shows the first event when it arrives. From the exporter's side, a 200 with an empty body means every AI call span was stored. A 200 with partialSuccess names what was skipped and why, for example spans with no user ID. A 401 means the key is missing or wrong.

Then connect revenue (RevenueCat, Stripe or a CSV) and the dashboard shows margin per user. The same numbers are available to scripts and AI agents through the MCP server and the read API.

Questions

Do I have to stop sending traces to Langfuse, Datadog or Grafana?

No. Add FiscaLLM as a second exporter. Your current backend keeps getting every span; FiscaLLM reads the AI call spans and ignores the rest.

Does FiscaLLM store prompts or responses from the spans?

No. It reads the model, the token counts, the user ID and an optional feature name. Message content on a span is never read or stored.

Which instrumentation libraries work?

Anything that follows the OpenTelemetry GenAI semantic conventions: the Vercel AI SDK, OpenLLMetry (Traceloop), OpenInference (Arize) and the OpenTelemetry instrumentations for the OpenAI and Anthropic SDKs.

What if a span has no user ID?

It is skipped and counted in the response as a rejected span, with a message that says to set user.id. Cost without a user cannot be matched to revenue.

Will an agent run be counted twice?

No. Only the spans that are one model call are counted. Wrapper spans (the agent, a step, a tool call) are skipped, and each span is stored once even if the exporter retries.