# Open Agent Spend Attribution (OASA) Specification

Version 0.1.2 · Published 2026-09-14 · Revised 2026-10-08 · [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/)

## Abstract

OASA defines a canonical record format for [agent spend attribution](https://www.onaro.io/glossary/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](https://www.onaro.io/glossary/system-of-record-for-ai-labor) for agent spend. It is designed to interoperate with [OpenTelemetry](https://opentelemetry.io/docs/specs/semconv/gen-ai/) GenAI semantic conventions (telemetry), the [FOCUS](https://focus.finops.org/) specification (billing), and [x402](https://x402.org/) payment-rail records (settlement). It is not a payment protocol and not a billing format; it is the [join between settlement, billing, and attribution](https://www.onaro.io/glossary/settlement-vs-billing-vs-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](https://www.onaro.io/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.

## 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](https://www.rfc-editor.org/rfc/rfc9562).

Inputs: occurred_at and source_record_id of the record being emitted.

1. ms = occurred_at as Unix time in milliseconds, truncated (not rounded) to the millisecond.
2. h = SHA-256 of the UTF-8 bytes of source_record_id.
3. 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].
4. 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](https://github.com/onaro-io/agent-spend-attribution/blob/main/schema/oasa-record.schema.json) (one record, schema_version 0.1.x), the [batch schema](https://github.com/onaro-io/agent-spend-attribution/blob/main/schema/oasa-batch.schema.json) (a batch header plus an array of records), and [golden examples](https://github.com/onaro-io/agent-spend-attribution/tree/main/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

### 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

| 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](https://github.com/onaro-io/agent-spend-attribution/blob/main/docs/focus-mapping.md).

### 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` |

## References

| 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 |

Source: https://github.com/onaro-io/agent-spend-attribution

Canonical: https://www.onaro.io/spec
