# Agent Spend Format (ASF) v0.1

**An open, minimal JSON shape for one unit of AI-agent spend** — a token usage
event or a payment — carrying enough identity (agent/principal/policy) and trace
context (run/span) to be attributed, budgeted, and audited. It's the wire format
Veritally ingests, published so **any** tool can emit it and any receiver can
consume it, the way OpenTelemetry's GenAI semantic conventions did for traces.

**Why publish this:** every agent framework, gateway, and billing dashboard
invents its own event shape. That fragmentation is *why* attribution is hard —
teams re-derive "which agent spent this" from five incompatible logs. ASF is a
small, honest core (4 required fields) any framework can bolt onto its existing
telemetry, so the attribution problem gets solved once, upstream, instead of
per-integration.

**Status:** v0.1, Veritally's working format, offered as a starting point for
public review — not a standards-body spec. Open to change based on real
integrations. Source of truth: `product/asf.mjs`; JSON Schema: `asf-schema.json`,
published live at [`/.well-known/agent-spend-format.schema.json`](/.well-known/agent-spend-format.schema.json).

## The event

```json
{
  "asf_version": "0.1",
  "ts": "2026-07-06T14:03:00Z",
  "source": "llm",
  "model": "gpt-4o",
  "input_tokens": 1200,
  "output_tokens": 300,
  "agent": "checkout-agent",
  "principal": "jane@acme.com",
  "policy": "Growth",
  "run_id": "8f2a1c",
  "span_id": "b91e",
  "parent_span_id": null
}
```

A payment (money movement rather than token usage) looks the same shape, with
`source: "payment"` and an explicit `cost_usd` (there's no token price to fall
back on):

```json
{
  "asf_version": "0.1",
  "ts": "2026-07-06T14:05:00Z",
  "source": "payment",
  "model": "Cloudflare Workers",
  "cost_usd": 5.00,
  "agent": "ops-agent",
  "principal": "jane@acme.com",
  "policy": "Ops"
}
```

## Fields

| Field | Type | Required | Meaning |
|---|---|---|---|
| `asf_version` | string | **always** | Spec version. Currently `"0.1"`. |
| `ts` | string | **always** | ISO-8601 date (`YYYY-MM-DD`) or date-time. When the spend happened. |
| `source` | string | **always** | Where this came from: `"llm"`, `"payment"`, `"sdk"`, a vendor name (`"openai"`), or a tool name. The literal value `"payment"` marks a money-movement event — everything else is a usage event. |
| `model` | string | **always** | Usage event: the model id (`"gpt-4o"`). Payment event: the merchant/payee. |
| `input_tokens` | integer ≥ 0 | conditional | Prompt/input tokens. Usage events need this, `output_tokens`, or `cost_usd`. |
| `output_tokens` | integer ≥ 0 | conditional | Completion/output tokens. Same rule as above. |
| `cost_usd` | number ≥ 0 | conditional | Explicit USD cost. **Required** for payment events (no tokens to price from). For usage events, an explicit cost overrides token-based pricing when both are present — trust the emitter's own billing data over a guess. |
| `event_key` | string | optional | Idempotency key. See below if omitted. |
| `project` | string | optional | Project/workspace tag. |
| `api_key` | string | optional | An **opaque label** for the provider key/card/wallet used, for grouping/display. **Never put a live secret here.** |
| `agent` | string | optional | The identity of the AI agent/service making the call. |
| `principal` | string | optional | The human or account that authorized the spend. |
| `policy` | string | optional | The governing budget or policy scope. |
| `run_id` | string | optional | Groups every event from one agent invocation / call graph. |
| `span_id` | string | optional | This step's id within the call graph. |
| `parent_span_id` | string \| null | optional | The parent step's `span_id`, or `null` at the root. |

**The conditional rule** ("usage needs tokens-or-cost; payment needs cost") isn't
expressible in static JSON Schema without `if`/`then` gymnastics, so it's kept as
plain logic — see `hasSpendSignal()` in `asf.mjs`, or just call the validator.

**Open format:** unrecognized fields are allowed (`additionalProperties: true`).
ASF is a minimal common core, not a closed schema — a framework's existing
telemetry (currency, region, request id, whatever) can ride alongside it.

## Idempotency key derivation

If an emitter can't track its own idempotency keys, a receiver **should** derive
one deterministically so re-sending the same event never double-counts:

```
event_key = sha256([source, ts, model, api_key, project, input_tokens, output_tokens, cost_or_cost_usd].join("|")).slice(0, 32)
```

This is exactly `ingestutil.eventKey()` — the formula Veritally itself uses.
Two events with identical values on those fields collapse to one; change any of
them (including `ts`) to make two calls that happen to cost the same not collide.

## Mapping to OpenTelemetry GenAI semantic conventions

Veritally's own JS SDK exports its tracked events as OTel-style spans (see
`Veritally.toOTelSpan()` in `sdk/veritally.js`) using the same field names ASF
uses. That converter operates on the SDK's own internal row (which predates ASF
and isn't itself an ASF object — see the `/ingest` note below), but the field
mapping is identical, so if you're already emitting `gen_ai.*` spans, you're
most of the way to producing valid ASF:

| ASF field | OTel GenAI attribute |
|---|---|
| `model` | `gen_ai.request.model` |
| `input_tokens` | `gen_ai.usage.input_tokens` |
| `output_tokens` | `gen_ai.usage.output_tokens` |
| `run_id` | trace id |
| `span_id` | span id |
| `parent_span_id` | parent span id |
| `cost_usd`, `agent`, `principal`, `policy`, `project` | not in OTel GenAI — Veritally's `veritally.*` extension attributes |

ASF isn't trying to replace OTel — it's the flattened, transport-agnostic event
Veritally's ledger actually books, whether it arrived via a trace exporter, a
webhook, an SDK call, or a CSV import.

## Validate an event

No account needed — this is a pure, stateless check:

```bash
curl -s -X POST https://veritally.ai/public/asf/validate \
  -H "Content-Type: application/json" \
  -d '{"asf_version":"0.1","ts":"2026-07-06","source":"llm","model":"gpt-4o","input_tokens":1200,"output_tokens":300}'
# → {"valid":true,"errors":[],"asf_version":"0.1"}
```

Send `{"events":[...]}` to validate a batch. `asf.mjs` is the single source of
truth for the validator and the published JSON Schema — `asf-test.mjs`
programmatically drift-guards those two against each other and against real
internal event shapes. This doc's prose (the field table above, the examples)
is maintained by hand alongside `asf.mjs`, not parsed/checked by that test —
if the two ever disagree, `asf.mjs` and the published schema are authoritative.

## Ingest an ASF event into Veritally

Any valid ASF **usage** event (`source` other than `"payment"`) is already a
valid `POST /v1/events` body — `/v1/events` and MCP's `record_spend` are the
ASF-canonical path for new integrations. A **payment** event (`source:"payment"`)
books best via `POST /v1/payments` / MCP's `record_payment` instead, which has
its own field names (`amount_usd`, `merchant`) rather than ASF's (`cost_usd`,
`model`) — rename `model`→`merchant` and `cost_usd`→`amount_usd` to call it.
Posting the ASF shape as-is to `/v1/events` also works (it *is* accepted) — with
an `agent` set, it books to the usage default GL account (`6000 · AI Services`)
instead of `6100 · Agent Purchases`; **without** `agent` (optional in ASF but
common on a payment), it doesn't get a default GL substitution at all and lands
as an unattributed exception (`6900 · Unallocated AI`) instead, since attribution
there is conditioned on `agent` being present. Use `/v1/payments` for payments.
**Note:** the older `POST /ingest`
raw-row path (what the JS/Python SDKs' `.track()` currently posts to) predates ASF
and is intentionally more lenient — it accepts several historical field-name
aliases (`cost` as well as `cost_usd`, `prompt_tokens` as well as `input_tokens`,
no `asf_version` required) so years of vendor CSV exports keep working. That
leniency is a deliberate compatibility choice for `/ingest`, not a second version
of the spec — new emitters should target the ASF shape above and `/v1/events`.

```bash
curl -s -X POST https://api.veritally.ai/v1/events \
  -H "Authorization: Bearer <your-key>" -H "Content-Type: application/json" \
  -d '{"model":"gpt-4o","input_tokens":1200,"output_tokens":300,"agent":"checkout-agent"}'
```

See `quickstart.md` for authentication and `mcp-connection.md` for the MCP path.

## Framework integrations

- **LiteLLM** (Python) — `sdk/litellm_adapter.py`, a `CustomLogger` that books
  every `litellm.completion()` call as Veritally spend automatically, via the
  existing Python SDK's `.track()` (the `/ingest` path, not a literal ASF
  object — see `sdk/README.md`).
- **LangChain / Vercel AI SDK** (JS) — `sdk/adapters.js`, same `.track()` path,
  ASF-field-compatible (same names/semantics for model, tokens, agent,
  principal, policy) but not literally ASF-shaped either — see the `/ingest`
  note above.
- Building an integration that emits **literal** ASF objects? POST them to
  `/v1/events` directly (or validate first at `/public/asf/validate`) — the
  shape at the top of this doc is the whole contract. Open a PR to list it here.

## Versioning

Backward-compatible additions (new optional fields) bump the minor-ish suffix
without breaking `asf_version: "0.1"` consumers, since unknown fields are always
allowed. A breaking change (removing/renaming a required field, changing a
type) will ship as `"0.2"`; validators reject an unrecognized `asf_version` by
design, so a breaking change can't silently misparse.
