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.
Quick start
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.Point your exporter at LedgerIN YOUR CODE
Traces URL:
https://ledger.100xnative.com/api/v1/otlp/v1/traces- Create an API key in Settings → Keys. It starts with pls_live_ and is shown once.
- Set OTEL_EXPORTER_OTLP_TRACES_ENDPOINT to your Ledger traces URL (step 2).
- Set OTEL_EXPORTER_OTLP_TRACES_HEADERS to Authorization=Bearer%20 followed by your key. %20 stands for the space.
- To see conversations as well as costs, turn on message capture in your AI instrumentation.
- Restart your app and run one real conversation. Ledger receives it within seconds.
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=noneAlready 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.
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
npm install @opentelemetry/sdk-node @opentelemetry/instrumentation-openai// 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 aboveSet 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
pip install opentelemetry-distro opentelemetry-exporter-otlp-proto-http \
opentelemetry-instrumentation-openai-v2OTEL_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_onlyopentelemetry-instrument python app.pyWith 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:
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.
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']}"},
)))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:
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:
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:
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:
{
"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.
- The header must read
- No data
- Check the protocol is
http/protobuforhttp/json. gRPC (port 4317) is not supported. - Use
OTEL_EXPORTER_OTLP_TRACES_ENDPOINTwith the full URL. With the generalOTEL_EXPORTER_OTLP_ENDPOINT, givehttps://ledger.100xnative.com/api/v1/otlp: the exporter adds/v1/tracesitself. - 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.
- Check the protocol is
- Conversations are grouped wrongly
Without a conversation or session id, each trace counts as its own conversation. Set
gen_ai.conversation.idon each turn, or name your own attribute withx-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