Define an Accounted Service, and It Pays Its Way
KDCube counts LLM calls, embeddings, and web searches as usage — recorded, priced, and settled against a user's budget. Your application's own paid work can join them. A decorator, four small extractors, and one usage object turn a metered API call into first-class usage the economics model can charge.
Three layers stack, and keeping them apart is the whole trick. Tracking asks what did this call use? — a tracker decorator emits an accounting event. Pricing asks what did that usage cost? — a reported dollar figure or a descriptor price table turns usage into USD. Enforcement asks may it run, and who pays? — the guard reserves and settles that cost. Define the first two, and the paid work you do is countable and chargeable exactly like a model call.
The anatomy of a tracked call
A tracked call is an ordinary function wearing a tracker decorator. When it runs inside
a bound accounting context, the decorator reads four extractors off the
result and arguments, builds a ServiceUsage, and writes one
AccountingEvent carrying a snapshot of who/where/which-turn. The core
primitive is AccountingTracker; track_llm,
track_embedding, and track_web_search are thin factories over
it.
from kdcube_ai_app.infra.accounting import AccountingTracker, ServiceType, ServiceUsage
def track_document_ocr(**extractors):
return AccountingTracker(ServiceType.VISION, **extractors)
def _ocr_usage(result, *args, **kwargs) -> ServiceUsage:
pages = int(result.get("pages_processed", 0))
return ServiceUsage(
document_pages=pages,
requests=1,
cost_usd=round(pages * 0.01, 6), # the pricing lever — $0.01 / page
)
@track_document_ocr(usage_extractor=_ocr_usage)
async def ocr_document(path: str) -> dict:
return await call_acme_ocr(path)
Two facts shape the rest. The event is emitted only when accounting storage is bound — outside a bound context the decorator is a transparent no-op; a chat turn binds it in the processor, a standalone flow binds it through the economics guard. And the event carries a context snapshot, so attribution rides along without threading it through arguments.
Making it charge
Tracking records the usage; pricing turns it into the dollars economics reserves and
settles. Two bundle-friendly levers, no platform edits: report the cost
directly — set ServiceUsage.cost_usd to what the provider billed, and
the per-turn calculator honors it as the charge; or price from the
descriptor — the economics descriptor economics.yaml carries a
price_tables section, read live, a whole-table replacement whose llm
section must keep the reference model, so you replace the table and add your provider/model
to the right service section.
llm, embedding, and web_search today, each honoring a
reported cost_usd. A tracked call under a new service type shows up in usage
analytics but is priced at zero by the turn calculator — so to actually charge a user, emit
under the closest priced type and carry a real cost_usd.This is not hypothetical: the built-in Claude Code runtime runs on
exactly this path. A Claude Code turn is accounted as ordinary LLM usage — same
@track_llm tracker, provider="anthropic", tokens and a
cost_usd parsed from the CLI's stream-json output — and the
calculator prices it two ways: an alias-aware price-table lookup when the model is priced,
and the reported cost_usd as the fallback otherwise. Report the cost under a
priced type and your own service is accounted the same way the platform accounts its
runtimes.
Then it settles under the guard
Tracking and pricing make the cost visible; the economics guard makes it authorized and
paid. Run the tracked call inside EconomicsGuard, and the guard verifies the
user can afford it, reserves the estimate, and settles the reported cost when the block
exits — one ledger entry, no double counting.
async with EconomicsGuard(self, subject=subject, scope_id=f"ocr_{doc_id}",
flow="reports.ocr",
estimate=EconomicsEstimate(reservation_usd=0.20)):
result = await ocr_document(path) # emits into the guard's scope; settles on exit
That is the whole arc: define the service so it counts, price it so it costs, run it under the guard so it pays its way — the same treatment the platform gives its own model calls.