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#
| Field | Type | Required | Meaning |
|---|---|---|---|
event_id | string | yes | module-assigned, unique |
idem_key | string | yes | {job_id}/{item_id}/{attempt}/{meter} or the equivalent; the ledger dedups on it; a replay carries the same key |
occurred_at | RFC 3339 | yes | when the spend happened, not when it was recorded |
recorded_at | RFC 3339 | yes | when the ledger accepted it; differs from occurred_at on replay |
tenant | string | yes | |
caller_id | string | yes | |
env | string | yes | |
module, module_version | string | yes | |
capability | string | yes | |
variant | map of dimension to option | no | the same object the outcome carries, e.g. {"research": "extractor"} |
judged_by | string | no | when the spend was a judge's |
job_id | string | yes | |
client_job_id | string | no | echoed if the caller set one |
item_id | string | no | absent for job-level spend (expansion, storage) |
attempt | integer | no | absent for job-level spend |
attribution | map | no | the job's attribution, copied |
meter | string | yes | one of the capability's cost_model.meters; see the meter list |
quantity | number | yes | |
unit | string | yes | items, tokens, calls, bytes, ms, renders |
detail | map | no | meter-specific: model name, prompt and completion tokens, vendor, endpoint |
cost_usd | number or null | yes | the module's estimate at record time from its price table; null when unknown |
price_table_version | string | no | which price table produced cost_usd, so it can be re-priced |
cached | boolean | no | true 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:
| Meter | Unit | When |
|---|---|---|
item | items | once per item attempt, always; the base unit for a per-SKU price |
llm.tokens | tokens | per model call; detail.model, detail.prompt, detail.completion |
llm.calls | calls | per model call |
research.calls | calls | web research or SERP lookups |
vendor.<name>.calls | calls | any outbound paid vendor: vendor.oxylabs.calls, vendor.openai.images |
image.renders | renders | |
storage.bytes | bytes | artifacts written |
duration | ms | wall clock per item attempt |
cache.hit | items | served from cache; cost 0 |
review.held | items | a 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_keyand 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}/statsand/stats?usage=trueroll them up. - Deliverable forward. An app that asks for
usageincallback_eventsgets 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, ormeter. - 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>indetail. - Recording
cost_usdwithoutquantity. The quantity is what survives a price change.