Quickstart
In about ten minutes, put the cost of an AI call on the customer it was for, and see what that customer costs you to serve.
Everything else in Spondo Billing stands on this. Your AI bill arrives as one number from each vendor. Once each call is on a customer, and on the piece of work it was for, you can see your margin on every customer and feature, price your plans on it, and bill.
You'll need a Spondo Billing account, Node 20 or later, and an app that calls an LLM. This page shows OpenAI's SDK, Anthropic's SDK and the Vercel AI SDK; any client with an OpenTelemetry instrumentation works the same way. Not on Node? See the end of this page.
1. Get an API key
In the dashboard, open Developers and choose Create key. Copy the key when it's shown: it starts sb_live_, it's shown only once, and it can read and change everything in your account, so keep it on your server.
export SPONDO_API_KEY="sb_live_…"
Trying things out? Make a company just for it: open the account menu, choose New company, and create the key there. Your test customers and costs then never mix with your real ones.
2. Install
npm install @spondo/billing @opentelemetry/api @opentelemetry/sdk-trace \
@opentelemetry/context-async-hooks @opentelemetry/exporter-trace-otlp-http
@spondo/billing is Spondo Billing's client, with @spondo/billing/tracing for costs. The four @opentelemetry packages carry each call's cost to Spondo Billing. Step 3 adds one more, for your LLM client.
3. Turn on tracing for your LLM client
Put the set-up in a file of its own, tracing.ts. Pick your LLM client: every example in these docs follows your choice.
npm install @arizeai/openinference-instrumentation-openai
// tracing.ts
import OpenAI from "openai";
import { OpenAIInstrumentation } from "@arizeai/openinference-instrumentation-openai";
import { initTracing } from "@spondo/billing/tracing";
initTracing(); // reads SPONDO_API_KEY
// Records each call's model and token counts.
// hideInputs and hideOutputs keep prompts and answers off the spans.
new OpenAIInstrumentation({
traceConfig: { hideInputs: true, hideOutputs: true },
}).manuallyInstrument(OpenAI);
npm install @arizeai/openinference-instrumentation-anthropic
// tracing.ts
import Anthropic from "@anthropic-ai/sdk";
import { AnthropicInstrumentation } from "@arizeai/openinference-instrumentation-anthropic";
import { initTracing } from "@spondo/billing/tracing";
initTracing(); // reads SPONDO_API_KEY
// Records each call's model and token counts.
// hideInputs and hideOutputs keep prompts and answers off the spans.
new AnthropicInstrumentation({
traceConfig: { hideInputs: true, hideOutputs: true },
}).manuallyInstrument(Anthropic);
npm install @ai-sdk/otel
// tracing.ts
import { OpenTelemetry } from "@ai-sdk/otel";
import { registerTelemetry } from "ai";
import { initTracing } from "@spondo/billing/tracing";
initTracing(); // reads SPONDO_API_KEY
// Every AI SDK call now records its model and token counts.
registerTelemetry(new OpenTelemetry());
To keep a call's prompt and answer off its span, give it telemetry: { recordInputs: false, recordOutputs: false }.
Your LLM calls themselves don't change. Spondo Billing prices each one from its model and token counts alone, and never stores prompts or answers.
4. Create the customer
Do this when a customer signs up. externalId is your own ID for them, such as your account ID. You use it everywhere from now on, so you never need to store Spondo Billing's ID:
import { Spondo } from "@spondo/billing";
const spondo = new Spondo(); // reads SPONDO_API_KEY
await spondo.customers.create({ name: "Acme Legal LLP", externalId: "acme-legal" });
Each externalId can be used once, so creating the same customer again answers 409.
5. Trace the work, and say what it was for
Wrap each piece of work you do for a customer in trace(), with your LLM calls inside it as they are. When the work is done, send a signal for what the customer got: here, a document summarised.
import "./tracing.ts"; // first, so every call after it is traced
import { flush, signal, trace } from "@spondo/billing/tracing";
await trace({ externalCustomerId: "acme-legal" }, async () => {
const summary = await summarise(contract); // your code, calling your LLM as usual
await signal("document_summarised", { pages: 12 }, { idempotencyKey: "doc_8f2a" });
return summary;
});
await flush(); // scripts and serverless functions: send before exiting
| What it does | |
|---|---|
trace(tags, fn) | Marks everything inside fn as work for one customer. Every LLM call in it carries acme-legal. |
signal(name, data, options) | Records what the work was for. It carries the trace's ID, so the trace's cost becomes the signal's cost. Plans price by signals, and the value you deliver is measured by them. |
idempotencyKey | Your ID for the event, such as the document's. A signal sent twice with the same key is stored once, so retries are safe. |
flush() | Sends spans still waiting in the batch. A server that keeps running doesn't need it. |
signal() works only inside trace(). For work that has no AI call in it, send the signal with spondo.signals.send() instead: Send usage signals shows how.
6. See what it cost
In the dashboard, open Usage → Cost traces. The trace is there for Acme Legal LLP, with its model, tokens and cost, in your account's currency. Open Customers → Acme Legal LLP to see what they cost you to serve, and the cost of each document_summarised under Unit economics by signal. One call costs a fraction of a cent, so the totals round it down; Cost traces shows it to the fraction.
Or ask the API, with SQL over your analytics:
const result = await spondo.request("POST", "/analytics/query", {
body: {
query: `
SELECT signal_name, model, input_tokens, output_tokens,
cost_amount / 100 AS cost_usd
FROM fact_cost
WHERE external_customer_id = 'acme-legal'`,
},
});
{
"columns": [
{ "name": "signal_name", "type": "String" },
{ "name": "model", "type": "String" },
{ "name": "input_tokens", "type": "Int64" },
{ "name": "output_tokens", "type": "Int64" },
{ "name": "cost_usd", "type": "Float64" }
],
"rows": [
["document_summarised", "gpt-6.1-sol", "1200", "350", 0.0059]
],
"meta": { "rowCount": 1, "elapsedMs": 0.44, "truncated": false }
}
$0.0059 is 1,200 input tokens at $2 a million and 350 output tokens at $10 a million: the list price of gpt-6.1-sol, the model in this example. If you pay a different rate, set it under Developers → Model prices.
Nothing there?
| What you see | What to check |
|---|---|
| No trace on Cost traces | tracing.ts is imported before anything else, and registers your client's instrumentation (step 3). A script awaits flush() before it exits. The key is your org key (sb_live_…): customer keys can't send traces. |
| The trace's cost is 0, marked unpriced | Spondo Billing has no price for the model yet. Add one under Developers → Model prices, and its calls are priced again. |
| A signal with no cost | It was sent with spondo.signals.send(), or outside the trace() that made the call, so nothing links it to the call's cost. |
What's next
Measure: done
You know what each call costs, for each customer and each signal. Send usage signals covers work with no AI call in it, and Trace AI costs every way to send costs.
Price it
Put Acme Legal on a plan, and the dashboard shows revenue and margin next to the cost: Products, plans and pricing.
Bill
Checkouts and invoices through your own Stripe: Take payment with Stripe.
Prove value
Agree what each signal is worth, and send each customer a receipt: Prove value.
Not on Node?
- Another language. Point any OpenTelemetry SDK's exporter at
https://billing.getspondo.com/v1/tracesand setmeter.customer_idon your spans: From any language. - Another LLM client. Register its OpenTelemetry instrumentation in
tracing.tsinstead. Trace AI costs lists the token attributes Spondo Billing reads. - No SDK at all. The API is JSON over HTTPS, and the API reference shows curl for every call.