Version 0.1.2 (2026-10-08). Current version. Other versions: 0.1.3 (2026-10-22), 0.1.1 (2026-09-15).
Open Agent Spend Attribution (OASA) Specification
Version 0.1.2 · Initial public draft 2026-09-14 · Revised 2026-10-08 Published by Onaro (BrianOnAI LLC) · Licensed CC BY 4.0
Canonical HTML version: https://www.onaro.io/spec Pinned version: https://www.onaro.io/spec/v0.1.2
Abstract
OASA defines a canonical record format that attributes AI-agent resource consumption and spend to agent identity, task, and cost object, and joins runtime telemetry, normalized billing, and payment settlement into a single auditable ledger row. It is designed to interoperate with OpenTelemetry GenAI semantic conventions (telemetry), the FOCUS specification (billing), and x402 payment-rail records (settlement). It is not a payment protocol and not a billing format; it is the join between them.
Partial records are valid. Every field outside the envelope is optional so adapters can emit what they know and reconcile later. Within a major version, the schema is additive-only.
Design principles
- Hub-and-spoke: canonical middle, adapters at the edges.
- Settlement-agnostic: invoice, card, wire, and on-chain rails reconcile into the same spine.
- Additive to FOCUS and OpenTelemetry — never competing with them.
- Envelope required; all other groups optional so partial records remain valid.
- Versioned; additive-only within a major version.
The three-layer model
┌───────────────────────────────┐
│ Telemetry — OTel GenAI │
└───────────────┬───────────────┘
│ trace_id
┌───────────────┴───────────────┐
│ Attribution — OASA │
│ (canonical) │
└───────────────┬───────────────┘
│ invoice_id + source_record_id
│ settlement_ref
┌───────────────┴───────────────┐
│ Billing — FOCUS │
│ + Settlement — x402 │
└───────────────────────────────┘
Join keys on the edges; attribution in the middle.
Record schema v0.1.2
Group 1 — Envelope (required)
| Field | Type | Required | Description |
|---|---|---|---|
record_id |
string (UUIDv7) | Yes | Globally unique record identifier. Adapters with a stable source key SHOULD derive it deterministically (see Deterministic record IDs) so that a retried export produces the same ID. |
record_type |
enum: usage | charge | settlement | allocation | outcome |
Yes | One row = one event class. |
schema_version |
string | Yes | Schema version string; "0.1" for this draft. |
occurred_at |
timestamp (RFC 3339, UTC) | Yes | When the event happened. |
recorded_at |
timestamp (RFC 3339, UTC) | Yes | When the record was ingested. |
source_system |
string | Yes | e.g. otel-collector, openai-billing, x402-facilitator. |
source_record_id |
string | Yes | Key in the source system. |
currency |
string | Yes | ISO 4217, or CAIP-19 asset ID for on-chain assets. |
source_system = otel-collector is the value emitted by oasa-otel-exporter, an OpenTelemetry Collector exporter that turns GenAI spans into OASA usage records.
Group 2 — Agent identity
| Field | Type | Required | Description |
|---|---|---|---|
agent_id |
string | No | Stable internal agent ID. |
agent_name |
string | No | Human-readable agent name. |
agent_version |
string | No | Agent build or prompt version. |
agent_owner |
string | No | Team or principal accountable. |
parent_agent_id |
string | No | Parent in a sub-agent chain. |
agent_framework |
string | No | e.g. langgraph, crewai, custom. |
identity_scheme |
enum: internal | erc8004 | other |
No | How external identity is named. |
external_identity_ref |
string | No | e.g. ERC-8004 registration reference. |
Group 3 — Task & session context
| Field | Type | Required | Description |
|---|---|---|---|
session_id |
string | No | Runtime session identifier. |
trace_id |
string | No | W3C Trace Context / OpenTelemetry trace id. |
span_id |
string | No | Span id within the trace. |
task_id |
string | No | Org-defined task identifier. |
task_type |
string | No | Free taxonomy; org-defined. |
workflow_id |
string | No | Workflow or pipeline id. |
trigger |
enum: human | scheduled | agent | event |
No | What started the run. |
initiating_principal |
string | No | Human or agent that authorized the run. |
Group 4 — Consumption
| Field | Type | Required | Description |
|---|---|---|---|
provider |
string | No | Model or API provider. |
service |
string | No | Service product name. |
model |
string | No | Model identifier. |
operation |
enum: chat | completion | embeddings | tool_call | inference | storage | api_call | other |
No | Operation class. |
input_tokens |
integer | No | Input token count. |
output_tokens |
integer | No | Output token count. |
cached_tokens |
integer | No | Cached / reused tokens when reported. |
reasoning_tokens |
integer | No | Reasoning tokens when reported. |
unit_type |
enum: tokens | requests | seconds | gb | custom |
No | Unit of consumption. |
units_consumed |
decimal | No | Quantity in unit_type. |
tool_name |
string | No | Tool invoked, if any. |
request_count |
integer | No | Request count for the event. |
Group 5 — Cost
| Field | Type | Required | Description |
|---|---|---|---|
list_cost |
decimal | No | List price; mirrors FOCUS ListCost semantics. |
billed_cost |
decimal | No | Billed amount; mirrors FOCUS BilledCost. |
effective_cost |
decimal | No | Effective cost after discounts; mirrors FOCUS EffectiveCost. |
cost_source |
enum: measured | rated | invoiced | allocated |
No | How cost was derived. allocated means the cost was assigned from a shared cost by an allocation rule rather than metered, rated, or invoiced for this record. |
pricing_ref |
string | No | Price-sheet row ID. |
invoice_id |
string | No | Vendor invoice identifier. |
focus_charge_ref |
string | No | Pointer into a FOCUS dataset row. |
Group 6 — Allocation
| Field | Type | Required | Description |
|---|---|---|---|
cost_center |
string | No | Cost center or department. |
project_id |
string | No | Project identifier. |
customer_id |
string | No | Customer or account identifier. |
product_line |
string | No | Product line. |
environment |
enum: prod | staging | dev | other |
No | Runtime environment. |
tags |
map<string,string> | No | Free-form tags. |
allocation_method |
enum: direct | rule | split |
No | How cost was allocated. |
allocation_rule_id |
string | No | Rule that produced the allocation. |
split_fraction |
decimal (0–1) | No | Share of a split allocation. |
Group 7 — Settlement join
| Field | Type | Required | Description |
|---|---|---|---|
settlement_rail |
enum: invoice | card | ach | wire | x402 | other_onchain |
No | How money moved. |
settlement_network |
string | No | CAIP-2 chain ID where applicable. |
settlement_asset |
string | No | CAIP-19 asset identifier. |
settlement_amount |
decimal | No | Settled amount. |
settlement_ref |
string | No | Tx hash, statement line, or remittance ID. |
settled_at |
timestamp | No | Settlement timestamp. |
reconciliation_status |
enum: unmatched | matched | partial | disputed |
No | Join status against billing/telemetry. |
Group 8 — Control & governance
| Field | Type | Required | Description |
|---|---|---|---|
budget_id |
string | No | Budget that constrained or funded the spend. |
policy_id |
string | No | Policy identifier. |
authorization_ref |
string | No | Mandate / verifiable-intent credential ID. |
approval_type |
enum: policy_auto | pre_authorized | human_approved |
No | How spend was authorized. |
Group 9 — Outcome (optional, forward-looking)
Outcome capture is intentionally minimal in v0.1; it exists so cost-per-outcome queries have a landing zone.
| Field | Type | Required | Description |
|---|---|---|---|
outcome_event_id |
string | No | Linked outcome event. |
outcome_type |
string | No | Outcome class (org-defined). |
outcome_value |
decimal | No | Numeric outcome value. |
outcome_unit |
string | No | Unit for outcome_value. |
business_metric_ref |
string | No | Pointer to a business metric definition. |
Record schema v0.2.0 (additive)
schema_version "0.2" / "0.2.0" unlocks Group 10 and the capacity record type. v0.1.x records remain valid unchanged. Receivers that advertise known-minor 0.2 MUST reject infrastructure.* keys and record_type: capacity on schema_version < 0.2.
Extended record_type
| Value | Meaning |
|---|---|
capacity |
Endpoint-level capacity observation over an interval (no caller). Agent and session/task fields MUST be absent. |
Group 10 — Infrastructure
Wire names are flat (same style as token/cost fields). OTLP attribute reservations use the oasa.infrastructure.* namespace and map onto these wire names.
| Field | Type | Cardinality | Description |
|---|---|---|---|
pool_id |
string, ≤128 | Optional on usage; required on capacity |
Customer-defined cost pool / cluster identity. Opaque to the spec; org-scoped at the receiver. |
endpoint_id |
string, ≤128 | Optional on usage; required on capacity |
Serving endpoint identity (e.g. deployment / NAI endpoint name). Opaque. |
gpu_seconds |
number ≥ 0 | Forbidden on usage; required on capacity |
GPU time consumed over the record's interval. Endpoint grain only — not per-request. |
gpu_busy_seconds |
number ≥ 0 | Forbidden on non-capacity records; optional on capacity |
Portion of gpu_seconds during which the GPUs were busy. MUST be ≤ gpu_seconds. |
energy_wh |
number ≥ 0 | Forbidden on non-capacity records; optional on capacity |
Energy consumed by the endpoint over the record's interval, in watt-hours. |
hosting |
enum: provider_api | self_hosted | dedicated_capacity |
Optional on any record type | Signals that a vendor public price list may not apply. |
interval_seconds |
number > 0 | Optional | Observation window length. occurred_at is the interval end. Added because usage has no interval/window fields. |
Token and cost groups remain permitted (not required) on capacity records.
Draft OTLP attribute reservations (v0.2 binding)
| OASA wire field | Reserved OTLP attribute |
|---|---|
pool_id |
oasa.infrastructure.pool_id |
endpoint_id |
oasa.infrastructure.endpoint_id |
gpu_seconds |
oasa.infrastructure.gpu_seconds |
gpu_busy_seconds |
oasa.infrastructure.gpu_busy_seconds |
energy_wh |
oasa.infrastructure.energy_wh |
hosting |
oasa.infrastructure.hosting |
interval_seconds |
oasa.infrastructure.interval_seconds |
These reservations are draft until OASA 0.2.0 is published. They MUST NOT collide with OpenTelemetry GenAI semantic conventions.
Deterministic record IDs
Receivers deduplicate on record_id. An adapter that generates a fresh random ID on every attempt turns each retry into a duplicate row. Adapters whose source has a stable key SHOULD derive record_id from it with the procedure below; two implementations given the same inputs then produce the same ID, and the result is still a valid UUIDv7 under RFC 9562.
Inputs: occurred_at and source_record_id of the record being emitted.
ms=occurred_atas Unix time in milliseconds, truncated (not rounded) to the millisecond.h= SHA-256 of the UTF-8 bytes ofsource_record_id.- Build 16 bytes
b:b[0..5]=msas a 48-bit big-endian integer.b[6]=0x70 | (h[0] & 0x0F)(version 7).b[7]=h[1].b[8]=0x80 | (h[2] & 0x3F)(RFC 9562 variant).b[9..15]=h[3..9].
- Format
bas a lowercase hyphenated UUID string (8-4-4-4-12).
For OpenTelemetry spans, source_record_id is <trace_id>/<span_id> in lowercase hex and occurred_at is the span end time.
Test vector: occurred_at = 2026-09-01T15:04:05Z, source_record_id = 4bf92f3577b34da6a3ce929d0e0e4736/00f067aa0ba902b7 → record_id = 01a05d7f-b288-7532-8207-8cb53896d9e1.
Derived IDs are predictable from their inputs. They are identifiers, not secrets, and MUST NOT be used as access tokens.
Machine-readable schema
The record tables above are published as JSON Schema (draft 2020-12) for adapters to validate their output:
schema/oasa-record.schema.json: one record,schema_version0.1.x.schema/oasa-batch.schema.json: a batch header plus an array of records.examples/: golden records, one per file, that validate against the record schema.
Schema identifiers ($id) live under https://oasaspec.org/schema/<major.minor>/ and never change once published; consumers may reference them directly. The schema is generated from this document by scripts/generate-schema.mjs and never edited by hand. A field change is a pull request against SPEC.md first; the schema is regenerated from it, and CI fails if the committed schema drifts from these tables. Run npm run validate to check every example.
Decimal fields, including every money field, are JSON numbers. Consumers SHOULD parse them with decimal precision rather than binary floating point.
The 0.2.0 draft (Group 10 and capacity) is not covered yet; it gets its own schema when 0.2.0 is published. The schema files, examples, and scripts are licensed Apache-2.0 (see LICENSE-APACHE); the specification text remains CC BY 4.0.
Batch envelope
Adapters that send records in bulk wrap them in this header. Every record in records is validated independently; the header carries no attribution.
| Field | Type | Required | Description |
|---|---|---|---|
schema_version |
string | Yes | Schema version of the records in the batch; "0.1" for this draft. |
source_system |
string | Yes | System that emitted the batch, e.g. otel-collector. |
emitted_at |
timestamp (RFC 3339, UTC) | Yes | When the batch was sent. |
count |
integer | Yes | Number of records in records. |
records |
array of records | Yes | OASA records, each valid against the record schema. |
Mapping tables
Verified against upstream docs on 2026-09-15. OpenTelemetry GenAI conventions are still experimental and may drift; attribute names here are current as of that date, and unmapped OASA fields are noted where no ratified equivalent exists. FOCUS token-economics columns track 1.4 (ratified) and 1.5 (scheduled). x402 V2 communicates requirements via PAYMENT-REQUIRED payloads (network, asset, amount, payTo).
OASA ↔ OpenTelemetry GenAI
Implemented by oasa-otel-exporter (OpenTelemetry Collector exporter, v0.1.0).
| OASA | OTel | Notes |
|---|---|---|
provider |
gen_ai.provider.name |
|
input_tokens |
gen_ai.usage.input_tokens |
|
output_tokens |
gen_ai.usage.output_tokens |
|
cached_tokens |
gen_ai.usage.cache_read.input_tokens |
Cached/reused input tokens. Where a provider reports cache reads separately, map the cache-read count here; do not double-count into input_tokens if the provider already reports them inclusively. OTel also defines gen_ai.usage.cache_creation.input_tokens for cache writes. |
reasoning_tokens |
gen_ai.usage.reasoning.output_tokens |
Output tokens used for reasoning (e.g. chain-of-thought). OTel says this value SHOULD be included in gen_ai.usage.output_tokens. Where a provider reports reasoning only in billing payloads, populate from the provider usage object. |
model |
gen_ai.request.model |
The requested model. Where a provider returns a resolved or versioned model, prefer that value and record the requested model in tags if both matter. |
operation |
gen_ai.operation.name |
|
tool_name |
gen_ai.tool.name |
Emitted on tool-execution spans. Pair with operation = tool_call. |
session_id |
gen_ai.conversation.id |
Where a runtime emits a conversation/thread identifier. OASA session_id is broader — a non-conversational agent run is still a session. |
agent_id |
gen_ai.agent.id |
|
agent_name |
gen_ai.agent.name |
|
trace_id / span_id |
W3C Trace Context / OTel trace_id, span_id |
Join key to the Attribution layer; carried from W3C Trace Context. |
OASA ↔ FOCUS
FOCUS 1.4 added token-economics columns; FOCUS 1.5 (Dec 2026 target) adds native token tracking and a Price Sheet dataset. OASA tracks those columns as they ratify.
| OASA | FOCUS |
|---|---|
list_cost |
ListCost |
billed_cost |
BilledCost |
effective_cost |
EffectiveCost |
provider |
ServiceProviderName (FOCUS 1.4; formerly ProviderName) |
service |
ServiceName |
tags |
Tags |
charge rows (record_type=charge) |
ChargeCategory / charge rows |
Full column-by-column mapping, including the Invoice Detail dataset and worked examples: OASA ↔ FOCUS 1.4 mapping.
OASA ↔ x402
| OASA | x402 |
|---|---|
settlement_network |
PaymentRequired.network (PAYMENT-REQUIRED) |
settlement_asset |
PaymentRequired asset / token |
settlement_amount |
PaymentRequired amount / price |
settlement_ref |
on-chain tx / SettlementResponse reference |
| (payee context) | PaymentRequired.payTo |
Worked example
One agent tool-call as a usage row, later joined by a charge row and an x402 settlement row sharing trace_id / settlement_ref.
1. Usage (telemetry)
{
"record_id": "0193f0a0-0000-7000-8000-000000000001",
"record_type": "usage",
"schema_version": "0.1",
"occurred_at": "2026-09-01T15:04:05Z",
"recorded_at": "2026-09-01T15:04:06Z",
"source_system": "otel-collector",
"source_record_id": "span-9f2c",
"currency": "USD",
"agent_id": "agent-support-47",
"agent_name": "Support Triage",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"span_id": "00f067aa0ba902b7",
"task_type": "ticket_resolve",
"provider": "openai",
"model": "gpt-4.1",
"operation": "tool_call",
"tool_name": "crm.lookup",
"input_tokens": 1200,
"output_tokens": 340,
"cached_tokens": 800,
"session_id": "sess-2f91",
"unit_type": "tokens",
"units_consumed": 1540,
"cost_source": "measured"
}
2. Charge (billing)
{
"record_id": "0193f0a0-0000-7000-8000-000000000002",
"record_type": "charge",
"schema_version": "0.1",
"occurred_at": "2026-09-02T00:00:00Z",
"recorded_at": "2026-09-02T06:12:00Z",
"source_system": "openai-billing",
"source_record_id": "inv_line_88421",
"currency": "USD",
"agent_id": "agent-support-47",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"invoice_id": "INV-2026-09-01",
"focus_charge_ref": "focus:row:88421",
"list_cost": 0.042,
"billed_cost": 0.038,
"effective_cost": 0.038,
"cost_source": "invoiced"
}
3. Settlement (x402)
{
"record_id": "0193f0a0-0000-7000-8000-000000000003",
"record_type": "settlement",
"schema_version": "0.1",
"occurred_at": "2026-09-01T15:04:07Z",
"recorded_at": "2026-09-01T15:04:08Z",
"source_system": "x402-facilitator",
"source_record_id": "pay_0xabc",
"currency": "eip155:8453/erc20:0x",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"settlement_rail": "x402",
"settlement_network": "eip155:8453",
"settlement_asset": "eip155:8453/erc20:0x",
"settlement_amount": 0.038,
"settlement_ref": "0xdeadbeef",
"settled_at": "2026-09-01T15:04:07Z",
"reconciliation_status": "matched"
}
References
Upstream specifications and standards this document maps to or depends on.
| Reference | Steward | Link |
|---|---|---|
| OpenTelemetry GenAI semantic conventions | OpenTelemetry (CNCF) | https://opentelemetry.io/docs/specs/semconv/gen-ai/ |
| FOCUS™ (FinOps Open Cost and Usage Specification) | FinOps Foundation (Linux Foundation) | https://focus.finops.org/ |
| x402 | x402 Foundation (Linux Foundation) | https://x402.org/ |
| W3C Trace Context | W3C | https://www.w3.org/TR/trace-context/ |
| CAIP-2 (chain ID) / CAIP-19 (asset ID) | Chain Agnostic Standards Alliance | https://chainagnostic.org/ |
| ERC-8004 (agent identity) | Ethereum | https://eips.ethereum.org/ |
| RFC 3339 (timestamps) | IETF | https://www.rfc-editor.org/rfc/rfc3339 |
| ISO 4217 (currency codes) | ISO | https://www.iso.org/iso-4217-currency-codes.html |
| UUIDv7 (RFC 9562) | IETF | https://www.rfc-editor.org/rfc/rfc9562 |
Referencing these specifications does not imply endorsement by their stewards. OASA is an independent publication of Onaro (BrianOnAI LLC).
Changelog
See CHANGELOG.md.
Cite this specification
Open Agent Spend Attribution (OASA) Specification, version 0.1.2. Onaro, 2026-09-14 (revised 2026-10-08). https://www.onaro.io/spec
License: CC BY 4.0. Attribution required; adaptations allowed.