Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions .changeset/committed-metrics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
---
"adcontextprotocol": minor
---

Add `package.committed_metrics` and `package.committed_vendor_metrics` —
frozen snapshots of the product's `reporting_capabilities.available_metrics`
and `vendor_metrics` stamped at `create_media_buy` response time. Closes
#3481.

**The audit gap.** PR #3472 established that the product's
`available_metrics` becomes the binding reporting contract carried into
the resulting media buy. That holds **only if** the product is immutable
AND the seller stores a snapshot at buy creation. Neither is guaranteed:

- Products mutate (sellers add/remove metrics from `available_metrics`
as their reporting infrastructure evolves)
- Without a per-package snapshot, `missing_metrics` on
`get_media_buy_delivery` is computed against "what the product
*currently* advertises" — a 90-day-old buy is incorrectly judged as
"clean" because the seller quietly dropped a metric they originally
committed to
- An ops team auditing a 90-day-old buy will not trust an implicit
contract reference

This was flagged on PR #3472 by the product expert as the primary
sell-side audit gap.

**Changes.**

- `core/package.json`: new `committed_metrics: AvailableMetric[]` field
and new `committed_vendor_metrics: { vendor, metric_id }[]` field. Both
optional in v1; sellers without per-package snapshot infrastructure
fall back to the product's live state (absence is conformant). Both
MUST NOT change post-creation — `update_media_buy` cannot modify them.
Renegotiating the metric contract requires a new buy.
- `media-buy/get-media-buy-delivery-response.json`: `missing_metrics`
description updated to declare the reconciliation source — when
`committed_metrics` is present, that is the contract; when absent,
fall back to the product's current `available_metrics`.
- `docs/media-buy/task-reference/create_media_buy.mdx`: new "Reporting
contract on confirmed packages" subsection documenting the snapshot
semantics, immutability, and v1-optional posture.
- `docs/media-buy/task-reference/get_media_buy_delivery.mdx`: bullet
updated to point at the reconciliation source.

**Design choices spelled out (resolves the three open questions on #3481).**

1. **Optional or required?** Optional. Forcing the snapshot at v1 would
break existing implementations on first deployment. Optional with a
doc note that "buyers SHOULD reconcile against `committed_metrics`
when present and fall back to the product's live state when absent"
lets sellers adopt incrementally. Expected to become required at the
next major.

2. **What snapshots into `committed_metrics`?** The product's full
`reporting_capabilities.available_metrics` at the moment of
`create_media_buy`, NOT the intersection with the buyer's
`required_metrics` filter. The product committed to reporting all
those metrics; reducing to the intersection would silently drop
reporting on metrics the buyer didn't explicitly list but the seller
still has. `requested_metrics` (on `reporting_webhook`) remains the
buyer's payload-optimization filter — a separate concept.

3. **Mutation policy?** Frozen at creation, MUST NOT change post-creation.
`update_media_buy` cannot modify `committed_metrics` or
`committed_vendor_metrics`. If the buyer/seller need to renegotiate,
that's a new buy. This is the cleanest contract; mutability with
audit trail can be added later if real demand emerges.

**Backwards compatibility.** Optional and additive. Sellers without
snapshot infrastructure fall back to the implicit contract (product's
current state) — this matches the v1 behavior of #3472. Buyers can
incrementally upgrade to consume `committed_metrics` when present.

Closes #3481.
9 changes: 9 additions & 0 deletions docs/media-buy/task-reference/create_media_buy.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,15 @@ npx @adcp/client@latest \
| `creative_deadline` | ISO 8601 timestamp for creative upload deadline |
| `packages` | Array of created packages with complete state. Packages may include per-package `creative_deadline` when different from the media buy deadline. |

#### Reporting contract on confirmed packages

Each package in the response MAY carry `committed_metrics` and `committed_vendor_metrics` — frozen snapshots of the product's `reporting_capabilities.available_metrics` and `vendor_metrics` at the moment the buy was created. These become the binding reporting contract for the package's lifetime, independent of subsequent product mutations:

- `missing_metrics` on `get_media_buy_delivery` is computed against `committed_metrics` when present (against the product's current `available_metrics` when absent).
- The fields MUST NOT change post-creation. Sellers MUST reject `update_media_buy` requests that attempt to modify either field with a `validation_error` (suggested code: `IMMUTABLE_FIELD`). Renegotiating the metric contract requires a new buy.
- Optional in v1 — sellers without per-package snapshot infrastructure can adopt incrementally. Absence is conformant, but it carries a known audit gap: without the snapshot, `missing_metrics` reconciles against the product's live state at report time, which may not reflect what was committed at create time. Sellers that omit `committed_metrics` accept this risk; buyers SHOULD treat the absence as "no audit-grade contract" rather than "clean delivery." Expected to become required at the next major.
- Empty array is rejected at the schema level (`minItems: 1`) — absence is the only "no snapshot" signal, eliminating ambiguity between "I committed to nothing" and "I didn't snapshot."

### Error Response

| Field | Description |
Expand Down
2 changes: 1 addition & 1 deletion docs/media-buy/task-reference/get_media_buy_delivery.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -586,7 +586,7 @@ The `by_package` array provides per-package delivery details with these key fiel
- **`pricing_model`**: How the package is billed (cpm, cpcv, cpp, etc.)

**Accountability**:
- **`missing_metrics`**: Metrics the product's `reporting_capabilities.available_metrics` advertised but that are not populated in this report. Empty array (or absent) indicates clean delivery against the declared contract; non-empty signals an accountability breach. Sellers MUST exclude metrics that are not yet measurable for the current `measurement_window` (e.g., post-IVT counts during the live window) — those will appear (or not) when a wider window supersedes this report via `supersedes_window`.
- **`missing_metrics`**: Metrics the binding reporting contract advertised but that are not populated in this report. Reconciled against `package.committed_metrics` (the frozen snapshot stamped at `create_media_buy`) when present; falls back to the product's current `reporting_capabilities.available_metrics` when absent. Empty array (or absent) indicates clean delivery against the contract; non-empty signals an accountability breach. Sellers MUST exclude metrics that are not yet measurable for the current `measurement_window` (e.g., post-IVT counts during the live window) — those will appear (or not) when a wider window supersedes this report via `supersedes_window`.
- **`vendor_metric_values`**: Reported values for vendor-defined metrics that the product's `reporting_capabilities.vendor_metrics` declared (proprietary attention, emissions, panel demographics, brand-lift surveys, etc.). Each entry carries `{ vendor, metric_id, value, unit?, measurable_impressions?, breakdown? }`. The `measurable_impressions` field is the coverage denominator — vendor measurement is rarely 100% of delivery, since vendors only score impressions where their SDK fires or their panel matches. Buyers compute coverage as `measurable_impressions / impressions`. When `measurable_impressions` is absent, coverage is unspecified — buyers MUST NOT compute a coverage rate or assume full coverage. When a declared vendor metric is omitted entirely from this array, infer no measurement happened (no integration).

**Key Distinction**: `paused` reflects buyer control, while `delivery_status` reflects system reality. A package can be not paused but have `delivery_status: "budget_exhausted"`.
Expand Down
34 changes: 34 additions & 0 deletions static/schemas/source/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,40 @@
},
"minItems": 1
},
"committed_metrics": {
"type": "array",
"description": "Frozen snapshot of the product's `reporting_capabilities.available_metrics` at the moment this package was created. Becomes the binding reporting contract for the package's lifetime, independent of subsequent product mutations. The seller stamps this on the `create_media_buy` response; once set, it MUST NOT change for the lifetime of the package. Sellers MUST reject `update_media_buy` requests that attempt to modify `committed_metrics` with a `validation_error` (suggested code: `IMMUTABLE_FIELD`). If the buyer/seller need to renegotiate the metric contract, that is a new buy. The `missing_metrics` field on `get_media_buy_delivery` is computed against `committed_metrics` when present, and against the product's current `available_metrics` when absent. Optional in v1: sellers without per-package snapshot infrastructure can adopt incrementally — absence is conformant, but it carries a known audit gap (without the snapshot, `missing_metrics` reconciles against the product's live state at report time, which may not reflect the contract at create time; sellers that omit `committed_metrics` accept this risk). Expected to become required at the next major once the audit pattern is established. Empty array (`[]`) is a valid frozen snapshot meaning 'this package commits to no metrics' — semantically distinct from omitting the field.",
"items": {
"$ref": "/schemas/enums/available-metric.json"
},
"minItems": 1,
"uniqueItems": true,
"examples": [
["impressions", "spend", "completed_views", "completion_rate"],
["impressions", "spend", "clicks", "ctr", "viewability"]
]
},
"committed_vendor_metrics": {
"type": "array",
"description": "Frozen snapshot of the product's `reporting_capabilities.vendor_metrics` at the moment this package was created. Same contract semantics as `committed_metrics` but for vendor-defined metrics — buyers reconcile delivery against the snapshotted vendor list, not the product's live state. Optional in v1; absent means sellers without snapshot infrastructure fall back to the current `vendor_metrics` declaration (same audit-gap caveat as `committed_metrics`). MUST NOT change post-creation; sellers MUST reject `update_media_buy` attempts to modify with `validation_error`.",
"items": {
"type": "object",
"properties": {
"vendor": {
"$ref": "/schemas/core/brand-ref.json"
},
"metric_id": {
"$ref": "/schemas/core/vendor-metric-id.json"
}
},
"required": ["vendor", "metric_id"],
"additionalProperties": false
},
"minItems": 1,
"examples": [
[{ "vendor": { "domain": "attentionvendor.example" }, "metric_id": "attention_units" }]
]
},
"creative_assignments": {
"type": "array",
"description": "Creative assets assigned to this package",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@
},
"missing_metrics": {
"type": "array",
"description": "Metrics that the product's `reporting_capabilities.available_metrics` declared but that are NOT populated in this report. Empty array (or absent) indicates clean delivery against the declared contract. Non-empty signals an accountability breach — the seller advertised the capability at discovery but did not produce the value here. Sellers MUST exclude metrics that are not yet measurable for the current `measurement_window` (e.g., post-IVT counts during the live window) — those will appear (or not) when a wider window supersedes this report via `supersedes_window`.",
"description": "Metrics that the binding reporting contract declared but that are NOT populated in this report. Reconciliation source: when the package carries `committed_metrics` (frozen snapshot stamped at create_media_buy), `missing_metrics` is computed against THAT — independent of subsequent product mutations. When `committed_metrics` is absent, fall back to the product's current `reporting_capabilities.available_metrics`. Empty array (or absent) indicates clean delivery against the contract. Non-empty signals an accountability breach — the seller committed to the metric but did not produce the value here. Sellers MUST exclude metrics that are not yet measurable for the current `measurement_window` (e.g., post-IVT counts during the live window) — those will appear (or not) when a wider window supersedes this report via `supersedes_window`.",
"items": {
"$ref": "/schemas/enums/available-metric.json"
},
Expand Down
Loading