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
5 changes: 5 additions & 0 deletions .changeset/selection-dependent-price-adjustments.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"adcontextprotocol": minor
---

Add optional `price_adjustments` to binding fixed-price pricing options (`cpm`, `vcpm`, `cpc`, `cpcv`, `cpv`, `cpp`, `flat_rate`) (#7659). The surface is experimental under the feature id `media_buy.price_adjustments`. A table is optional and has equal standing with a fully priced seller offer: whether a table is present tells the buyer if it can compute the price locally, and without one the price applies only to what was quoted; yield-managed sellers are not expected to publish a table. A seller whose rate depends on buyer-selected values or on the delivery date declares an ordered table of `fee`/`discount` rows, each with a `rate` (a proportion of the running total; a fee rate may be 1 or more, so a Dutch index-card factor `f` is a fee rate of `f - 1` or a discount rate of `1 - f`) or a fixed `amount`. Rows are keyed by `format_option` (a `format_option_ref`, which also carries spot length), `placement_selection` (a `placement_ref`, requiring `overlay_support.placement_selection`), or `always`, and may carry a `valid_from`/`valid_until` window evaluated at the purchase `start_time`; a flight crossing a window boundary is rejected. Resolution is normative: selection rows fire at most once per dimension (first applicable row in declared order), then each firing row applies to one running total in declared order (a 10% fee then a 10% fee is x1.21), every step rounds half away from zero to the option's price precision (the greatest of the currency minor-unit exponent, 4 decimals for per-unit models, and the decimals needed to represent declared values exactly, by numeric value), and the result is never negative. The `price_breakdown` rounding sentence is amended to match. A table is bound to its quote: it applies only to the account it was returned for and expires with the proposal or offer `expires_at` or the discovery response's validity. The accepted purchase's `pricing.fixed_price` is the resolved rate and `price_breakdown` records each fired row in declared order (both existing fields), so a verifier can re-resolve it. The table is not embedded in the accepted snapshot, because the canonical schemas are closed and a minor release does not add properties to closed schemas (embedding it is a 4.0 item, #8501): any change to the table mints a new `pricing_option_id`, so the accepted `pricing_option_id` (covered by `terms_digest`) identifies exactly one immutable table, both parties retain it from discovery, and the seller MUST price changes the agreement already permits from it, not from a later table; the table grants no change rights (those come only from `change_terms[]`). A buyer-supplied `pricing` that differs from the resolved result is rejected with `INVALID_REQUEST` carrying the resolved price and breakdown (`error-details/price-adjustments-mismatch.json`). Sellers must not return an option carrying the table to callers pinned below 3.3. The uniform-price language in `Product.pricing_options`, `overlay_support`, and the product, targeting, and pricing docs is amended to except declared adjustments. v1 dimensions are placement and format; buyer-owned property and collection list references have no seller-side anchor and are out, as are auction floors, activation method, measurement vendor, reporting tier, and the `cpa`/`time` models. Adds resolution vectors, including a fictional Dutch-style TV rate card, under `test-vectors/price-adjustments`.
2 changes: 1 addition & 1 deletion docs/building/by-layer/L3/error-handling.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -532,7 +532,7 @@ The wire-level `recovery: "correctable"` on the sandbox-only path is the registe
| [`AUDIENCE_TOO_SMALL`](/docs/building/verification/compliance-catalog#error-code-audience-too-small) | correctable | Audience segment below minimum size | Broaden targeting or upload more audience members |

{/* /schemas/latest/ link: schema unreleased; switch to the v3 alias after the next release. */}
For `UNSUPPORTED_FEATURE` rejecting an unsupported or removed targeting dimension, details SHOULD follow [`error-details/unsupported-targeting-dimension.json`](https://adcontextprotocol.org/schemas/latest/error-details/unsupported-targeting-dimension.json); see [Rejected targeting dimensions](/docs/media-buy/advanced-topics/targeting#rejected-targeting-dimensions). For `REQUOTE_REQUIRED`, details SHOULD follow [`error-details/requote-required.json`](https://adcontextprotocol.org/schemas/v3/error-details/requote-required.json). For [`ACTION_NOT_ALLOWED`](/docs/building/verification/compliance-catalog#error-code-action-not-allowed) with `reason: "not_supported_on_buy"`, the existing [`error-details/action-not-allowed.json`](https://adcontextprotocol.org/schemas/v3/error-details/action-not-allowed.json) shape also permits `decline_reason`. Sellers SHOULD emit the coarse reason when they have a structured seller-policy classification and disclosure does not expose private thresholds or enforcement controls. Buyers MUST tolerate its absence and MUST NOT infer that an undisclosed policy does not exist. Structural action denials SHOULD omit it.
For `UNSUPPORTED_FEATURE` rejecting an unsupported or removed targeting dimension, details SHOULD follow [`error-details/unsupported-targeting-dimension.json`](https://adcontextprotocol.org/schemas/latest/error-details/unsupported-targeting-dimension.json); see [Rejected targeting dimensions](/docs/media-buy/advanced-topics/targeting#rejected-targeting-dimensions). For an `INVALID_REQUEST` that rejects a buyer-supplied `pricing` differing from the price a `price_adjustments` table resolves to, details SHOULD follow [`error-details/price-adjustments-mismatch.json`](https://adcontextprotocol.org/schemas/v3/error-details/price-adjustments-mismatch.json). For `REQUOTE_REQUIRED`, details SHOULD follow [`error-details/requote-required.json`](https://adcontextprotocol.org/schemas/v3/error-details/requote-required.json). For [`ACTION_NOT_ALLOWED`](/docs/building/verification/compliance-catalog#error-code-action-not-allowed) with `reason: "not_supported_on_buy"`, the existing [`error-details/action-not-allowed.json`](https://adcontextprotocol.org/schemas/v3/error-details/action-not-allowed.json) shape also permits `decline_reason`. Sellers SHOULD emit the coarse reason when they have a structured seller-policy classification and disclosure does not expose private thresholds or enforcement controls. Buyers MUST tolerate its absence and MUST NOT infer that an undisclosed policy does not exist. Structural action denials SHOULD omit it.

**Implementation note (non-normative).** Repeated out-of-envelope controls can be used to map a seller's exact share-of-voice, notice-window, frequency, or commitment thresholds. Consider rate-limiting these deterministic probes using the same posture as [`refine_proposals`](/docs/media-buy/product-discovery/proposal-negotiation#log-for-audit-without-leaking-terms), and omit `decline_reason` when even the coarse category would disclose sensitive enforcement logic.

Expand Down
3 changes: 1 addition & 2 deletions docs/creative/channels/broadcast.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -227,10 +227,9 @@ This works today with four gaps. Read them before building on it:

- **The demo is bound on the forecast, not on the price.** A CPM pricing option has no demographic parameters, unlike a CPP option (`parameters.demographic_system`, `demographic`). `measurement_terms.billing_measurement` binds the vendor, the window, and the variance, but has no demographic field.
- **The guaranteed volume has no typed definition.** `purchases[].impressions` is the contracted volume, and a buyer can require a floor on it with `refine_proposals` `constraints.impressions` (a hard minimum summed across purchases). The schema does not say that number counts measured audience, in which demo, from which vendor, or on which window. Those facts sit on the forecast and `billing_measurement`, and the forecast is outside `commercial_terms`, so it is not covered by `terms_digest`. Treat the demo as a term agreed in the human contract outside AdCP, not one a protocol agent can verify.
- **:30 equivalization is not expressible.** Nothing declares whether a :15 impression counts as half a :30 unit or the factor to apply. A CPM prices impressions regardless of unit length, so on a buy that mixes :15 and :30 spots the equivalization rule is a term to settle outside the protocol.
- **:30 equivalization is only partly expressible.** Rate-side spot-length indexing is expressible through [`price_adjustments`](/docs/media-buy/advanced-topics/pricing-models#selection-dependent-price-adjustments) (a rate row keyed to the spot's format option). Impression-side :30 equivalization and demo-bound CPM remain open and are tracked for 3.4 in [#8001](https://github.com/adcontextprotocol/adcp/issues/8001). Nothing declares whether a :15 impression counts as half a :30 unit or the factor to apply, so on a buy that mixes :15 and :30 spots the impression-side rule is a term to settle outside the protocol.
- **No under-delivery trigger, and the schedule on the wire is experimental.** `makegood_policy` does not name a volume shortfall as a trigger. A seller may report the planned schedule as `scheduled` rows in `by_spot`, but that field is experimental and seller-declared, so a buyer cannot assume one. See [Makegoods](#makegoods).

Demo-bound CPM and :30 equivalization are tracked for 3.4 in [#8001](https://github.com/adcontextprotocol/adcp/issues/8001).

### 1. The seller publishes the product

Expand Down
Loading
Loading