Open Agent Spend Attribution (OASA) Specification
Version 0.1.2 · Published 2026-09-14 · Revised 2026-10-08 · CC BY 4.0
Abstract
OASA defines a canonical record format for agent spend attribution: attributing AI-agent resource consumption and spend to agent identity, task, and cost object, and joining runtime telemetry, normalized billing, and payment settlement into a single auditable ledger — a system of record for agent spend. 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 settlement, billing, and attribution.
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.
OASA is the record format behind FinOps for agentic AI.
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
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. |
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. |
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_at as Unix time in milliseconds, truncated (not rounded) to the millisecond.
- h = SHA-256 of the UTF-8 bytes of source_record_id.
- Build 16 bytes b: b[0..5] = ms as 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 b as a lowercase hyphenated UUID string (8-4-4-4-12).
For OpenTelemetry spans, source_record_id is the trace ID and span ID in lowercase hex joined by a slash (trace_id/span_id), 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: the record schema (one record, schema_version 0.1.x), the batch schema (a batch header plus an array of records), and golden examples that validate against the record schema.
Schema identifiers ($id) live under https://oasaspec.org/schema/0.1/ and never change once published; consumers may reference them directly. The schema is generated from the specification text and never edited by hand, and CI fails if the committed schema drifts from these tables.
Decimal fields, including every money field, are JSON numbers. Consumers SHOULD parse them with decimal precision rather than binary floating point.
The schema files, examples, and scripts are licensed Apache-2.0; the specification text remains CC BY 4.0. The 0.2.0 draft is not covered yet; it gets its own schema when 0.2.0 is published.
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
OpenTelemetry GenAI and x402 mappings verified against upstream docs on 2026-09-15; FOCUS columns verified against focus.finops.org on 2026-10-05. OpenTelemetry GenAI conventions are still experimental and may drift; attribute names here are current as of their 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
| 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, the FinOps Foundation's open billing specification, added token-economics columns in 1.4; 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) | opentelemetry.io/docs/specs/semconv/gen-ai/ |
| FOCUS™ (FinOps Open Cost and Usage Specification) | FinOps Foundation (Linux Foundation) | focus.finops.org/ |
| x402 | x402 Foundation (Linux Foundation) | x402.org/ |
| W3C Trace Context | W3C | www.w3.org/TR/trace-context/ |
| CAIP-2 (chain ID) / CAIP-19 (asset ID) | Chain Agnostic Standards Alliance | chainagnostic.org/ |
| ERC-8004 (agent identity) | Ethereum | eips.ethereum.org/ |
| RFC 3339 (timestamps) | IETF | www.rfc-editor.org/rfc/rfc3339 |
| ISO 4217 (currency codes) | ISO | www.iso.org/iso-4217-currency-codes.html |
| UUIDv7 (RFC 9562) | IETF | 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
- v0.1.2 (2026-10-08) — Adds
allocatedtocost_source. Adds machine-readable JSON Schema, golden examples and validation tooling. Documents deterministic record IDs. FOCUS mapping:providermaps toServiceProviderName(FOCUS 1.4; formerly ProviderName). No fields added, removed, or renamed;schema_versionremains"0.1". - v0.1.1 (2026-09-15) — Extended the OpenTelemetry GenAI mapping with tool, cached-token, conversation, and reasoning attributes; added a Notes column. Mapped
cached_tokenstogen_ai.usage.cache_read.input_tokens(registry name as of 2026-09-15). No schema fields added, removed, or renamed;schema_versionremains"0.1". - v0.1 (2026-09-14) — initial public draft.
Contributing
The specification is developed in the open at github.com/onaro-io/agent-spend-attribution. Open an issue for anything wrong, missing, or ambiguous — especially an upstream attribute name that has drifted — or a pull request against SPEC.md for concrete changes. Implementation reports are the most useful contribution: if you have tried to map real agent spend with this schema, what broke is what the next version needs to fix.
General comments: hello@onaro.io
Cite this specification
Open Agent Spend Attribution (OASA) Specification, version 0.1.2 (rev. 2026-10-08). Onaro. https://www.onaro.io/spec
Repository: https://github.com/onaro-io/agent-spend-attribution
License: CC BY 4.0. Attribution required; adaptations allowed.
Related: The Missing Ledger · Agent spend attribution · Agent payment data