Skip to content

Cost and margin per end customer

If your product is built on AI and sold to customers, the question after “what does AI cost us?” is “what does each customer’s AI use cost us, and are we still making money on them?”. Costs → Customers (/costs?tab=customers) answers it from the same rows as the total AI cost and Costs → Apps.

Tell the SDK which of your customers a call is for. Every span started while a customer is in effect carries two attributes, whichever instrumentation started it:

AttributeWhat it is
shield360.customer.tenant_idYour customer (tenant, workspace, account)
shield360.customer.user_idOptional: the end user inside that customer
import Shield360 from "shield360";
Shield360.init({ applicationName: "support-bot" });
// Per request: wins over a default, follows async work, and nests.
await Shield360.withCustomer({ tenantId: req.tenantId, userId: req.userId }, async () => {
await openai.chat.completions.create({ model: "gpt-4o", messages });
});
// Or a process-wide default, e.g. a worker that serves one customer. null clears it.
Shield360.setCustomer({ tenantId: "acme" });
import shield360
shield360.init(application_name="support-bot")
# Per request, as a context manager (works with asyncio) or a decorator.
with shield360.customer(tenant_id=request.tenant_id, user_id=request.user_id):
client.chat.completions.create(model="gpt-4o", messages=messages)
# Or a process-wide default. None clears it.
shield360.set_customer(tenant_id="acme")

Ids are strings (numbers are converted), 1–256 characters, with no control characters. An invalid id is dropped silently, and a user id without a tenant id is dropped too. If you pass your own tracer to init(), add CustomerSpanProcessor to your tracer provider yourself.

Customer ids are applied at ingest, before anything is written, on every OTLP entrance: SDK API keys, enrolled computers, the platform’s own writes, and the bundled collector’s own ports on a self-hosted install (the platform pushes the setting and key to it, and a collector that has not received them keeps neither id):

  • End-user ids are only ever stored as a keyed hash (uh_…): HMAC-SHA256 under your organisation’s own key, the same key AI account identity uses. The raw id is never stored or logged. Without the key, the user id is dropped.
  • Tenant ids are kept as your apps send them, because that is how you name customers and match them to billing. To hash them too (th_…), turn on Store tenant ids as keyed hashes on the Customers tab. Calls already stored keep the form they were stored in.
  • If the setting cannot be read, neither id is kept for that batch.

For the period in the time filter, each customer shows:

ColumnHow it is worked out
AI cost (est.)Sum of the cost each tagged call recorded: tokens × the model’s price. Always an estimate. Where a vendor bill is connected, the bill counts in the total AI cost instead; the hover shows the split.
ChangeAgainst the same length of time just before.
Calls, tokensPriced model calls and input + output tokens.
RevenueEach month’s revenue for the customer × the share of that month the period covers.
Gross margin(revenue − AI cost) ÷ revenue.
AI cost / revenueFlagged when it passes your threshold (30% unless you change it).

Untagged calls is the app usage with no customer tag. It has its own row so the customers always add up to the apps. If they ever do not (for example while telemetry is still arriving), the tab says which apps differ.

Select By app and model on a row to drill into that customer. Export CSV downloads the table with the basis of each column in its header. The Finance overview and its CSV export show the same split By customer, next to By app.

Margin needs revenue per customer per month. There are three ways to add it, and a month entered by hand or imported is never overwritten by a sync:

  1. By hand: pick the month, type the customer’s tenant id and the monthly revenue.
  2. CSV import: paste a CSV with the columns customer, month (YYYY-MM), amount and optionally currency (USD when absent). One bad row refuses the whole import and names the line.
  3. Base316: the Base316 connector writes each customer’s MRR as this month’s revenue.

Revenue in another currency is shown, but never set against USD costs, so those rows have no margin.

Add an alert rule of type Customer AI cost over a share of revenue (Alerts → Rules). It runs every hour on the month to date and fires when any customer’s AI cost passes the share you set, or your Customers-tab threshold when you leave it empty. The alert names the customer with the highest share; end-user ids are never in it.

Reading the tab needs costs read. Entering or importing revenue and changing the settings needs costs manage, and every change is in the audit log. The tab is part of the Finance module.