Skip to content
Guide · Observability (OTel)

Quick start · No new SDK

From your observability tool

If your assistant already sends OpenTelemetry traces, or uses Langfuse or LangSmith, add Ledger as one more destination. Your code stays as it is, and so does your current tool.

You need: a Ledger API key (Settings → Keys), and access to the environment variables or the start-up file of the app that calls your AI provider.

Which path? Plain OpenTelemetry: Node or Python. Langfuse: add a second exporter. LangSmith: turn on its OpenTelemetry mode. A Collector already in place: add one exporter.

Quick start

  1. Create an API key. It is shown once.IN LEDGER

    Open Settings → Keys and create a key. It starts with pls_live_. Keep it in your secrets, never in your code.

  2. Point your exporter at LedgerIN YOUR CODE

    Traces URL: https://ledger.100xnative.com/api/v1/otlp/v1/traces

    1. Create an API key in Settings → Keys. It starts with pls_live_ and is shown once.
    2. Set OTEL_EXPORTER_OTLP_TRACES_ENDPOINT to your Ledger traces URL (step 2).
    3. Set OTEL_EXPORTER_OTLP_TRACES_HEADERS to Authorization=Bearer%20 followed by your key. %20 stands for the space.
    4. To see conversations as well as costs, turn on message capture in your AI instrumentation.
    5. Restart your app and run one real conversation. Ledger receives it within seconds.
    Environment
    OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://ledger.100xnative.com/api/v1/otlp/v1/traces
    OTEL_EXPORTER_OTLP_TRACES_HEADERS=Authorization=Bearer%20pls_live_…
    OTEL_EXPORTER_OTLP_TRACES_PROTOCOL=http/protobuf
    OTEL_METRICS_EXPORTER=none
    OTEL_LOGS_EXPORTER=none

    Already sending traces to another tool? Keep it. Add Ledger as a second exporter, or as a second destination in your OpenTelemetry Collector. Your other tool keeps receiving everything.

  3. Run one real conversationIN YOUR CODE

    Send one message through your assistant, then open your dashboard or the status endpoint below. Exporters send every few seconds.

Node, with OpenTelemetry

Terminal
npm install @opentelemetry/sdk-node @opentelemetry/instrumentation-openai
instrumentation.mjs
// instrumentation.mjs, loaded before your app: node --import ./instrumentation.mjs app.mjs
import { NodeSDK } from "@opentelemetry/sdk-node";
import { OpenAIInstrumentation } from "@opentelemetry/instrumentation-openai";

new NodeSDK({ instrumentations: [new OpenAIInstrumentation()] }).start();
// the exporter reads the OTEL_EXPORTER_OTLP_TRACES_* lines above

Set the environment lines from step 2. The OpenAI instrumentation records the model, the tokens and the time of every call. In Node it writes message text to log events, which Ledger does not read: you see costs and calls. To see conversations too, send the lines with ledger.message() or connect your channel (WhatsApp, voice).

Python, with OpenTelemetry

Terminal
pip install opentelemetry-distro opentelemetry-exporter-otlp-proto-http \
  opentelemetry-instrumentation-openai-v2
Environment
OTEL_SERVICE_NAME=my-assistant
OTEL_TRACES_EXPORTER=otlp
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://ledger.100xnative.com/api/v1/otlp/v1/traces
OTEL_EXPORTER_OTLP_TRACES_HEADERS=Authorization=Bearer%20pls_live_…
OTEL_EXPORTER_OTLP_TRACES_PROTOCOL=http/protobuf
OTEL_METRICS_EXPORTER=none
OTEL_LOGS_EXPORTER=none
# conversations, not only costs: put the messages on the spans
OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=span_only
Start your app
opentelemetry-instrument python app.py

With the last two lines, each AI call carries the customer’s message and the reply, and Ledger shows them as one conversation. Leave them out and you still see every call, its cost and its time.

Group the calls into conversations

Ledger groups calls by gen_ai.conversation.id, else by a session id (session.id, langfuse.session.id, or LangSmith’s session_id, thread_id or conversation_id metadata). Put your chat thread id on the span of each turn:

Python
from opentelemetry import trace

tracer = trace.get_tracer("my-assistant")

# one span per turn; the AI call inside it is exported with it
with tracer.start_as_current_span("turn", attributes={"gen_ai.conversation.id": thread_id}):
    reply = client.chat.completions.create(**your_usual_params)

Your id lives under another name? Send its attribute name in the header x-ledger-conversation-attribute, for example OTEL_EXPORTER_OTLP_TRACES_HEADERS=Authorization=Bearer%20pls_live_…,x-ledger-conversation-attribute=app.chat_id. Without any id, each trace counts as its own conversation.

Langfuse users

Langfuse cannot pass your traces on to another tool: its webhooks cover prompt changes only, and its exports are scheduled files. The Langfuse SDK is built on OpenTelemetry, so you add Ledger as a second exporter in your app. Langfuse keeps receiving everything, as today.

Python · Langfuse SDK
import os
from langfuse import get_client
from opentelemetry import trace
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter

langfuse = get_client()  # as today: Langfuse keeps receiving everything

# add Ledger as a second exporter on the same tracer provider
trace.get_tracer_provider().add_span_processor(BatchSpanProcessor(OTLPSpanExporter(
    endpoint="https://ledger.100xnative.com/api/v1/otlp/v1/traces",
    headers={"Authorization": f"Bearer {os.environ['LEDGER_KEY']}"},
)))
Node · Langfuse SDK
import { NodeSDK } from "@opentelemetry/sdk-node";
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
import { LangfuseSpanProcessor } from "@langfuse/otel";

new NodeSDK({
  spanProcessors: [
    new LangfuseSpanProcessor(), // as today
    new BatchSpanProcessor(new OTLPTraceExporter({
      url: "https://ledger.100xnative.com/api/v1/otlp/v1/traces",
      headers: { Authorization: `Bearer ${process.env.LEDGER_KEY}` },
    })),
  ],
}).start();

Ledger reads the generations Langfuse records (model, usage, cost, input and output) and the session id you already set. Put your key in LEDGER_KEY.

LangSmith users

LangSmith cannot stream your traces to another tool either; its rule webhooks send batches of run summaries. With the Python SDK, turn on its OpenTelemetry mode. LangSmith keeps receiving your traces, and the same runs go to Ledger:

Python · LangSmith SDK
pip install "langsmith[otel]"

LANGSMITH_TRACING=true
LANGSMITH_OTEL_ENABLED=true   # hybrid: LangSmith keeps receiving your traces as today
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://ledger.100xnative.com/api/v1/otlp/v1/traces
OTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer%20pls_live_…

LangSmith reads OTEL_EXPORTER_OTLP_HEADERS (not the _TRACES_ one) and passes it on as written; Ledger accepts the key with %20 or with a space. If you group threads with session_id, thread_id or conversation_id metadata, Ledger uses the same id. With the JavaScript SDK, which has no supported OpenTelemetry mode, use the Node path next to LangSmith, or the Ledger SDK.

An OpenTelemetry Collector

Already sending traces through a Collector? Add Ledger as one more exporter in the same pipeline:

collector.yaml
exporters:
  otlphttp/current:            # the tool you use today, unchanged
    traces_endpoint: https://your-current-tool/v1/traces
  otlphttp/ledger:
    traces_endpoint: https://ledger.100xnative.com/api/v1/otlp/v1/traces
    headers:
      Authorization: "Bearer ${env:LEDGER_KEY}"

service:
  pipelines:
    traces:
      receivers: [otlp]
      exporters: [otlphttp/current, otlphttp/ledger]

What Ledger reads

  • AI calls. Spans that follow the OpenTelemetry GenAI conventions (chat, text_completion, generate_content), plus the older shapes still in use. Agent, tool and search spans are not counted, so nothing is counted twice.
  • Cost. The model, the tokens (cached tokens apart) and the time of each call. Ledger prices the call itself unless your tool already sent a cost.
  • Messages. When your instrumentation puts them on the span: the customer’s new message and the reply. The system prompt and tool lines are left out.
  • Everything else is dropped. HTTP, database and other spans are accepted and ignored.
  • Privacy. Text is masked before it is stored, a user id is kept only as a fingerprint, and a conversation id that holds a phone number or an email is replaced by a fingerprint. More.
  • Formats. OTLP over HTTP, protobuf or JSON, plain or gzip, up to 8 MB a request. gRPC is not supported: use http/protobuf.
  • Count your wins. Traces carry the conversation, not the result. Send each booking, lead or payment with ledger.outcome() or the API. How.

How to know it works

Run one real conversation, then call the status endpoint with the same key:

Terminal
curl -s https://ledger.100xnative.com/api/v1/status \
  -H "Authorization: Bearer $LEDGER_KEY"

A working connection answers with counts that go up as you test:

Answer
{
  "ok": true,
  "tenant": "your-workspace",
  "firstEventAt": "2026-10-25T15:44:03.420Z",
  "lastEventAt": "2026-10-25T15:44:03.419Z",
  "counts": { "conversations": 1, "messages": 4, "llmCalls": 0, "calls": 0, "outcomes": 0 }
}

llmCalls goes up with every AI call, and messages with every line when message capture is on.

Troubleshooting

“401 unauthorized” in your exporter’s log
  • The header must read Authorization=Bearer%20pls_live_…: the word Bearer, then %20, then the key in full.
  • Use an API key from Settings → Keys. A WhatsApp or voice link token does not work here.
  • Ledger shows each key once. Create a new one, then revoke the old one.
No data
  • Check the protocol is http/protobuf or http/json. gRPC (port 4317) is not supported.
  • Use OTEL_EXPORTER_OTLP_TRACES_ENDPOINT with the full URL. With the general OTEL_EXPORTER_OTLP_ENDPOINT, give https://ledger.100xnative.com/api/v1/otlp: the exporter adds /v1/traces itself.
  • A script or a serverless function can stop before the exporter sends: flush or shut down the tracer provider at the end.
  • Only AI-call spans count. If your AI client is not instrumented, Ledger receives spans but finds no calls.
Conversations are grouped wrongly

Without a conversation or session id, each trace counts as its own conversation. Set gen_ai.conversation.id on each turn, or name your own attribute with x-ledger-conversation-attribute (above). An id set on a parent span applies when the parent is exported with the call.

Costs but no messages

Your instrumentation does not put message text on the spans. In Python, set the two capture lines above. Otherwise send the lines with ledger.message() or connect your channel.

Stuck? Message us and a person from 100x answers, usually the same day.

Message us