Agent docs

Contract: the usage event

One unit of spend, as a module records it.

One unit of spend, as a module records it. canon/08 says why (the module meters, the app enforces). This is the field list an app's billing and a module's runner both build against.

Audience: anyone writing a runner's metering, an app's credits, or a reconciliation.

Fields#

FieldTypeRequiredMeaning
event_idstringyesmodule-assigned, unique
idem_keystringyes{job_id}/{item_id}/{attempt}/{meter} or the equivalent; the ledger dedups on it; a replay carries the same key
occurred_atRFC 3339yeswhen the spend happened, not when it was recorded
recorded_atRFC 3339yeswhen the ledger accepted it; differs from occurred_at on replay
tenantstringyes
caller_idstringyes
envstringyes
module, module_versionstringyes
capabilitystringyes
variantmap of dimension to optionnothe same object the outcome carries, e.g. {"research": "extractor"}
judged_bystringnowhen the spend was a judge's
job_idstringyes
client_job_idstringnoechoed if the caller set one
item_idstringnoabsent for job-level spend (expansion, storage)
attemptintegernoabsent for job-level spend
attributionmapnothe job's attribution, copied
meterstringyesone of the capability's cost_model.meters; see the meter list
quantitynumberyes
unitstringyesitems, tokens, calls, bytes, ms, renders
detailmapnometer-specific: model name, prompt and completion tokens, vendor, endpoint
cost_usdnumber or nullyesthe module's estimate at record time from its price table; null when unknown
price_table_versionstringnowhich price table produced cost_usd, so it can be re-priced
cachedbooleannotrue when the meter recorded a cache hit (quantity is still recorded, cost_usd is 0)

Standard meters#

Every capability declares its meters in the manifest. These names are shared so an app can bill across modules:

MeterUnitWhen
itemitemsonce per item attempt, always; the base unit for a per-SKU price
llm.tokenstokensper model call; detail.model, detail.prompt, detail.completion
llm.callscallsper model call
research.callscallsweb research or SERP lookups
vendor.<name>.callscallsany outbound paid vendor: vendor.oxylabs.calls, vendor.openai.images
image.rendersrenders
storage.bytesbytesartifacts written
durationmswall clock per item attempt
cache.hititemsserved from cache; cost 0
review.helditemsa needs_review outcome; cost 0, so QC volume is countable

A module may add meters under its own domain prefix, but it may not rename these.

Guarantees#

  • Written idempotently. The ledger acknowledges a second event carrying the same idem_key and does not count it twice.
  • Never blocks the payload. A metering failure dead-letters and never fails the item.
  • Never deleted. Retention is forever (canon/05).
  • Readable back. GET /usage/events?job_id=&from=&to=&meter= pages them (the tenant comes from the caller context, never from a query param), and /jobs/{job_id}/stats and /stats?usage=true roll them up.
  • Deliverable forward. An app that asks for usage in callback_events gets batches, at least once, signed, with an event id on each for dedup.

usage_ref on outcomes#

Every outcome carries usage_ref (canon/03, module-surface.md): an array of the event_ids this item's attempts emitted, one per meter per attempt, so an app joins outcomes to spend without a query. Empty only for an item that never started.

Banned#

  • An event without idem_key, tenant, job_id, or meter.
  • A meter emitted at runtime that is neither a standard meter (above) nor in the capability's cost_model.meters.
  • Deleting or rewriting an event. A correction is a new event carrying corrects: <event_id> in detail.
  • Recording cost_usd without quantity. The quantity is what survives a price change.
@versable-git/ui · reference, canon, and method, read in place