All articlesAI Engineering

Four dashboards, and none of them knows your customer

Your provider's console answers what they invoice you. It cannot answer what a customer costs, what an outcome costs, or what one workflow spent across three providers - because none of that crosses the wire.

Oct 15, 20269 min read

Four dashboards, and none of them knows your customer

Say you've accepted the argument that the number worth watching is what one finished piece of work costs. Reasonable next step: go and find it.

So you open your provider's console. The spend is there, to the cent, by model and by day. And the number you came for isn't, and no amount of clicking is going to produce it.

That isn't an oversight. It's structural, and it's worth understanding why, because the same limit applies to the second console, and the third.

What a provider console is for

It exists to answer the provider's question, which is: what do we invoice this account. It answers that question well. It's a billing artifact, and it's organised around the only entity in the relationship the provider can see, which is your API key.

Everything your business actually runs on sits on your side of that wire. Which customer the work was for. Which user. Which agent. Which workflow. Whether the thing succeeded. None of it crosses the boundary, because none of it is any of the provider's business. By the time a request arrives there, it's tokens and a model name.

So the console isn't failing to tell you. It was never in a position to know.

Four gaps, and none of them close

It stops at the key. The provider sees one account. You have customers. A console cannot split a bill along a line it has never been shown, so "which customer is unprofitable" has no answer on that screen, no matter how the data is grouped.

It can't see the other providers. Most teams past the prototype stage are using more than one — a frontier model for the hard path, something cheaper for classification, a third for embeddings, and Vertex underneath some of it. Each console is complete and parochial. It knows its own spend perfectly and nothing whatsoever about the rest. The cost of one workflow is a number that exists in no console, because the workflow crossed three of them.

It has no idea what finished. Billing counts calls. A call is not a result. The retry that eventually worked and the three failures before it are four identical line items, and the console has no way to tell you that you paid four times for one outcome. This is the denominator problem again, arriving from a different direction.

It is retrospective. Usage data lands hours late, and the invoice lands at month end. A ceiling you discover you've gone through is not a ceiling. If the control you want is "stop before this customer spends another hundred dollars," nothing downstream of the call can give it to you.

WHAT EACH CONSOLE SEESProvider Aits own spendProvider Bits own spendProvider Cits own spendProvider Dits own spendno customer, no agent, no outcome, and no view of each otherYOUR SIDE OF THE WIREone record per callcustomer · user · agent · unit of work · provider · model · costthe context only exists before the request leaves
Every dimension your business cares about is gone by the time the provider can see the call.

The fix that looks like it will work

Most providers let you attach metadata to a request, and the obvious move is to tag everything with a customer id and group by it later. It's the right instinct and it gets you perhaps a third of the way.

Where it stops: the tagging is different at every provider and the shapes don't reconcile, so you're normalising exports rather than reading dashboards. It only ever covers traffic through that provider, so the workflow spanning three is still three partial answers. And it still can't give you cost per outcome, because the provider has no concept of your unit of work — it can group calls, and a group of calls is not a finished piece of work.

The honest end state of the tagging approach is that you export everything and join it yourself. Which is the actual task. It's just better to notice that at the start than after you've built four fragile importers.

What the record has to carry

The work is to write one record per call, on your side, while the context still exists.

gometering/record.go
// Written on your side of the wire, where the business context still exists.
// Once a request crosses to the provider, every field below except Model and
// Provider is gone, and no console can reconstruct them.
type CallRecord struct {
    Customer   string // the account the work was done for
    User       string // an opaque identifier, never a name or an email
    Agent      string // which of your agents made the call
    WorkUnit   string // the outcome this call was part of
    Provider   string // "anthropic", "openai", "vertex"
    Model      string
    InTokens   int64
    OutTokens  int64
    RateCard   string // the version of the price list used to value this call
    CostMicros int64  // millionths of a dollar, estimated now
    BilledAt   int64  // the invoice figure, written later. Zero until reconciled.
}

Nothing here is clever. The difficulty is entirely that it has to happen at the moment of the call, in the one place every call already passes through, and it has to keep happening as people add agents.

Two fields are less obvious than the rest, and both exist because spend records are financial records rather than logs.

Price against a rate card with a version on it

If you value a call by looking up today's prices, then a provider cutting its prices next quarter silently rewrites what last quarter cost. Every margin you reported moves. Nobody notices, because nothing errors.

gometering/pricing.go
// Price the call against the rate card that was in force when it happened,
// and keep the version on the record. A provider cutting its prices next quarter
// should change next quarter's margin, not last quarter's reported one.
func (p *Pricer) Value(at time.Time, r *CallRecord) error {
    card, err := p.cards.InForceAt(at) // not "the current prices"
    if err != nil {
        return fmt.Errorf("no rate card covering %s: %w", at.Format(time.DateOnly), err)
    }

    rate, ok := card.Lookup(r.Provider, r.Model)
    if !ok {
        // A model nobody has priced must be loud. Valued at zero, it quietly
        // drags every per-customer and per-outcome figure downward instead.
        p.unpriced.Add(r.Provider + "/" + r.Model)
        return ErrUnpricedModel
    }

    r.RateCard = card.Version
    r.CostMicros = rate.InMicros(r.InTokens) + rate.OutMicros(r.OutTokens)
    return nil
}

The unpriced-model branch matters more than it looks. A model nobody has added to the rate card values at zero, and a zero doesn't announce itself — it just quietly drags every per-customer and per-outcome figure down until somebody finds it. Same principle as unattributed spend: the gap in your instrumentation has to be a number you can look at, not an absence.

Estimate now, reconcile later, keep both

The cost you compute at call time is an estimate. You priced tokens against a rate card. The invoice is the truth, and it will differ — committed-use discounts, free tiers, a provider's own rounding, traffic routed through a platform that bills differently.

So keep both figures on the record, and write the billed one server-side when the export lands. You want the estimate because it's available immediately and limits have to act on something, and you want the billed figure because that's what leaves your bank account.

The gap between the two is a signal in its own right. A persistent one means your rate card has drifted from reality, which means every forward-looking number you've been quoting is off by a consistent amount nobody has measured.

One property that is easy to get wrong

Everything above sits on the hot path of your product, which means the metering can become the reason the product is down. A recording call that blocks, or retries, or queues without a bound, will eventually take an agent with it.

The short version: recording must be non-blocking, its queue must be bounded, and under pressure it must drop its own data rather than slow down the thing it's measuring. Losing a minute of telemetry is an inconvenience. Losing a minute of production is not. That's a whole problem of its own and worth its own post.

What you get that no console can give you

Once the record exists, the questions that had no answer become ordinary queries.

  • What does this customer cost us, across every provider we use.
  • What does one resolved ticket cost, counting the attempts that failed.
  • Which agent's spend grew last week, and whether its output grew with it.
  • Whether this account is still inside its budget, answerable before the next call rather than after the invoice.

None of those are exotic. They're the questions anyone funding the system asks in the first five minutes, and they're all unanswerable from a billing screen.

The thing to take away

Your provider's console answers "what do we invoice you." It's a real question and it's well answered, and it is not your question.

Yours is "what did this cost us, for whom, and did it work." That one gets answered on your side of the wire or it doesn't get answered at all — and the longer you run without it, the more confidently wrong the numbers in your board deck become.

Thinking about an agent like this for your team?

Describe the job you want automated and our Automation Architect blueprints it in seconds — or talk it through with the engineers who ship them.