From b70a5c3eda27d08cdf09129c81cbceaf810c139d Mon Sep 17 00:00:00 2001 From: Sirajmx Date: Thu, 22 Oct 2026 15:56:03 +0530 Subject: [PATCH 1/2] Add linked_execution and requires for linked line items MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A proposal can say its line items must be bought together (linked_execution, § 4.1), and a line item can name the line items or spend that must be in the same commitment (requires, § 5.6.3). A bonus line is a line item with requires and its own price. The § 2 "binding terms" hook now points at a field, and the Summary Representation carries has_requires and linked_execution. Adds a CTV bonus line example (§ 6.5) and linked_execution: none to the proposal example. Refs #23 --- examples/line-item-ctv-bonus.yaml | 29 ++++++++++++++ examples/proposal.yaml | 1 + spec/CHANGELOG.md | 4 ++ spec/openproposal-3.0.md | 63 +++++++++++++++++++++++++++---- 4 files changed, 90 insertions(+), 7 deletions(-) create mode 100644 examples/line-item-ctv-bonus.yaml diff --git a/examples/line-item-ctv-bonus.yaml b/examples/line-item-ctv-bonus.yaml new file mode 100644 index 0000000..6f3acfc --- /dev/null +++ b/examples/line-item-ctv-bonus.yaml @@ -0,0 +1,29 @@ +# OpenProposal 3.0 — Example: Bonus line item (spec § 6.5) +# Illustrative only. A CTV bonus unlocked when the buyer commits to the display +# and CTV line items together, with at least 50,000 USD on display. Its price is +# its own: nothing is inherited from the line items it requires. +line_item_id: "weather-app-ctv-bonus" +status: published +channel: ctv +properties: { available: [{ id: "weather-app" }], included: ["weather-app"] } +environments: { available: ["ctv"], included: ["ctv"] } +formats: { available: ["ctv/instream-15s"], included: ["ctv/instream-15s"] } +creative_delivery: { method: seller_hosted, protocol: vast_4_2, secure_required: true } + +frequency_cap: { max: { min: 1, max: 2 }, period: "day", basis: household } + +requires: + all_of: ["business-daily-display", "weather-app-ctv"] + any_of: [] + min_commitment: + - { line_item_id: "business-daily-display", basis: spend, amount: 50000.00, currency: USD } + +pricing: + - { cost_method: flat, gross_rate: 0.00, + price_valid_until: "2026-12-01T23:59:59Z", currency: USD } + +commitment_bounds: { max_impressions: 500000 } + +buying_route: SSP_PLATFORM +transaction_mechanism: { available: ["PRIVATE_AUCTION"], included: ["PRIVATE_AUCTION"] } +serving_mode: ssai diff --git a/examples/proposal.yaml b/examples/proposal.yaml index 9b51484..94a6922 100644 --- a/examples/proposal.yaml +++ b/examples/proposal.yaml @@ -5,6 +5,7 @@ version: 1 seller_id: "reg:examplepub-digital" seller_name: "Example Publisher US" type: standard +linked_execution: none # line items may be bought separately (§ 4.1) status: published valid_from: "2026-10-01T00:00:00Z" valid_until: "2026-12-31T23:59:59Z" diff --git a/spec/CHANGELOG.md b/spec/CHANGELOG.md index 1dfacef..074ba67 100644 --- a/spec/CHANGELOG.md +++ b/spec/CHANGELOG.md @@ -5,6 +5,10 @@ issue that caused it, so the comment record and the spec history line up. ## [Unreleased] +- § 2, § 4.1, § 5.6 — `linked_execution` on the proposal and `requires` on + line items, for linked execution and bonus line items + ([#23](https://github.com/IABTechLab/OpenProposal/issues/23)). + ## [3.0-draft-1] — 2026-09-22 Initial draft published for public comment. Ported from the working-group diff --git a/spec/openproposal-3.0.md b/spec/openproposal-3.0.md index 6940958..a345e71 100644 --- a/spec/openproposal-3.0.md +++ b/spec/openproposal-3.0.md @@ -65,8 +65,9 @@ item. There is no "overridable unless restated" semantics anywhere in this specification. **Subsettable proposals.** A buyer MAY select and commit a subset of a -proposal's line items, unless the proposal explicitly mandates linked execution -through its binding terms. +proposal's line items, unless the proposal sets `linked_execution` to `all` +(§ 4.1). A line item that declares `requires` (§ 5.6.3) may be committed only +when its conditions are met. ### 2.1 Summary and full representations @@ -79,10 +80,13 @@ item: `line_item_id` · `channel` · `content_detail` · property names · `formats.included[]` · `pricing.cost_method` · `pricing.gross_rate` · `min_spend` · `addressable_scale` · `committed_metrics[]` · -`transaction_mechanism` +`transaction_mechanism` · `has_requires` plus, from the parent proposal: `proposal_id` · `seller_name` · `description` · -`valid_from` · `valid_until`. +`valid_from` · `valid_until` · `linked_execution`. + +`has_requires` is `true` when the line item declares `requires` (§ 5.6.3), so a +screening agent does not recommend a line item that cannot be bought on its own. `catalog_ref` values MUST remain unresolved in a summary. @@ -190,6 +194,7 @@ items. | `valid_from` · `valid_until` | Timestamp | seller-set | ISO-8601 window the offer holds. **This is the offer window, not the campaign flight** | | `brief_ref` | String | supplied | Reference to the buyer's brief. REQUIRED when `type` is `custom` | | `negotiation_history[]` | Array[Object] | derived | Ordered, append-only. `{ version, actor: buyer \| seller, action: proposed \| countered \| accepted \| declined \| withdrawn, fields_changed[], at }`. Entries MUST NOT be edited or removed | +| `linked_execution` | Enum | seller-set | `none` (default, subsettable) \| `all` (every published line item must be committed together) | ### 4.2 Agent comprehension @@ -280,6 +285,7 @@ Each field declares bounds, not a value. A bare value means the field is | `cancellation_policy` | Object | selectable | `available[]` of cancellation rights with `pricing` per option | | `exclusivity` | Object | selectable | `available[]` of exclusivity terms with `pricing` | | `creative_policy` | Object | seller-set | Content and category restrictions on creative | +| `requires` | Object | seller-set | OPTIONAL. Conditions that must hold in the same commitment for this line item to be committable. See 5.6.3 | #### 5.6.1 commitment @@ -313,6 +319,41 @@ Transitions: `none` → `requested` → `held` → (`expired` | `released` | `converted`). `converted` is terminal and occurs when the line item reaches `agreed`. +#### 5.6.3 requires + +| Key | Meaning | +| :--- | :--- | +| `all_of[]` | Line items that MUST all be in the same commitment | +| `any_of[]` | Line items of which at least one MUST be in the same commitment | +| `min_commitment[]` | Purchase-level thresholds: `{ line_item_id, basis, amount, currency }` | + +```yaml +requires: + all_of: ["business-daily-display", "weather-app-ctv"] + any_of: [] + min_commitment: + - { line_item_id: "business-daily-display", basis: spend, amount: 50000.00, currency: USD } +``` + +A bonus line item is a line item with `requires` and its own pricing, such as +a discounted `gross_rate` or `cost_method: flat` at 0. A conditional discount +is modelled as a separate line item that `requires` unlocks, not as a price +that changes according to the other line items selected. Every price remains +self-contained. + +- References in `requires` MUST point to line items in the same proposal and + version. +- A seller MUST reject a commitment that includes a line item whose `requires` + is unmet. This check MUST be deterministic. +- If a line item referenced in `requires` becomes `sold_out` or `withdrawn` + before commitment, the dependent line item becomes non-committable. After + `agreed`, cancellation of a referenced line item is governed by the + dependent line item's `cancellation_policy`. +- A seller MUST reject a proposal at publication if `requires` references + form a cycle. +- When `linked_execution` is `all`, a hold MUST use `hold_scope: proposal` + (§ 5.6.2). + ### 5.7 Guarantees | Field | Type | Mutability | Description | @@ -375,6 +416,12 @@ See [`examples/line-item-ctv.yaml`](../examples/line-item-ctv.yaml). See [`examples/line-item-audio.yaml`](../examples/line-item-audio.yaml). +### 6.5 Bonus line item + +A CTV line item unlocked by buying the display and CTV line items together, +priced on its own. See +[`examples/line-item-ctv-bonus.yaml`](../examples/line-item-ctv-bonus.yaml). + --- ## 7. Why this split @@ -443,6 +490,7 @@ body is normative.** | `valid_from` · `valid_until` | 4.1 Identity and lifecycle | Timestamp | seller-set | | `brief_ref` | 4.1 Identity and lifecycle | String | supplied | | `negotiation_history[]` | 4.1 Identity and lifecycle | Array[Object] | derived | +| `linked_execution` | 4.1 Identity and lifecycle | Enum | seller-set | | `description` | 4.2 Agent comprehension | String | seller-set | | `specifications` | 4.2 Agent comprehension | String/Object | seller-set | | `best_for[]` | 4.2 Agent comprehension | Array[String] | seller-set | @@ -486,6 +534,7 @@ body is normative.** | `cancellation_policy` | 5.6 Commercial terms | Object | selectable | | `exclusivity` | 5.6 Commercial terms | Object | selectable | | `creative_policy` | 5.6 Commercial terms | Object | seller-set | +| `requires` | 5.6 Commercial terms | Object | seller-set | | `committed_metrics[]` | 5.7 Guarantees | Array[Object] | requestable | | `measurement_source` | 5.7 Guarantees | Object | selectable | | `shortfall_remedy` | 5.7 Guarantees | Object | selectable | @@ -512,7 +561,7 @@ body is normative.** | Marker | Count | Meaning | Fields | | :--- | :--- | :--- | :--- | | immutable | 6 | Set once at creation. Neither party may change it. | `proposal_id`, `seller_id`, `seller_name`, `type`, `line_item_id`, `channel` | -| seller-set | 29 | Seller sets and may revise while `draft` or `published`. | `valid_from` · `valid_until`, `description`, `specifications`, `best_for[]`, `not_suitable_for[]`, `seasonality_notes`, `content_detail`, `specs`, `third_party_tags`, `creative_delivery`, `approval`, `pricing[].cost_method`, `pricing[].gross_rate`, `pricing[].seller_rate`, `pricing[].price_valid_until`, `pricing[].currency`, `pricing[].floor`, `commitment_bounds`, `min_spend`, `creative_policy`, `visibility`, `required_credentials[]`, `eligible_bidders[]`, `terms_acceptance_required`, `buying_route`, `serving_mode`, `serving_platform`, `billing`, `supported_order_systems[]` | +| seller-set | 31 | Seller sets and may revise while `draft` or `published`. | `valid_from` · `valid_until`, `linked_execution`, `description`, `specifications`, `best_for[]`, `not_suitable_for[]`, `seasonality_notes`, `content_detail`, `specs`, `third_party_tags`, `creative_delivery`, `approval`, `pricing[].cost_method`, `pricing[].gross_rate`, `pricing[].seller_rate`, `pricing[].price_valid_until`, `pricing[].currency`, `pricing[].floor`, `commitment_bounds`, `min_spend`, `creative_policy`, `requires`, `visibility`, `required_credentials[]`, `eligible_bidders[]`, `terms_acceptance_required`, `buying_route`, `serving_mode`, `serving_platform`, `billing`, `supported_order_systems[]` | | selectable | 12 | Buyer chooses from a declared set. Resolves without assent. | `properties`, `environments`, `audiences`, `activation_route`, `formats`, `production`, `cancellation_policy`, `exclusivity`, `measurement_source`, `shortfall_remedy`, `reporting`, `transaction_mechanism` | | settable | 5 | Buyer sets a value within declared bounds. Resolves without assent. | `geo`, `device`, `daypart`, `frequency_cap`, `commitment` | | supplied | 3 | Buyer provides data or an artifact. | `brief_ref`, `buyer_supplied`, `assets[]` | @@ -522,8 +571,8 @@ body is normative.** ### What the distribution says -Of 71 fields, **23 are touched by the buyer** — 12 `selectable`, 5 `settable`, 3 -`supplied` and 3 `requestable`. The remaining 48 are `seller-set`, `immutable`, +Of 73 fields, **23 are touched by the buyer** — 12 `selectable`, 5 `settable`, 3 +`supplied` and 3 `requestable`. The remaining 50 are `seller-set`, `immutable`, `derived` or `mixed`. Only the 3 `requestable` fields require a round trip. Everything else the buyer From ecec3d74f484dbb4ea4404fb7fe847cbbd94aafe Mon Sep 17 00:00:00 2001 From: Sirajmx Date: Fri, 23 Oct 2026 18:50:47 +0530 Subject: [PATCH 2/2] Keep the hold rule out of the spec text The rule that a hold on a linked_execution: all proposal uses hold_scope: proposal came from a comment on #23, not the change request as filed. It moves to the PR description as a point for the working group, so this PR carries only the filed change. Refs #23 --- spec/openproposal-3.0.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/spec/openproposal-3.0.md b/spec/openproposal-3.0.md index a345e71..7de3385 100644 --- a/spec/openproposal-3.0.md +++ b/spec/openproposal-3.0.md @@ -351,8 +351,6 @@ self-contained. dependent line item's `cancellation_policy`. - A seller MUST reject a proposal at publication if `requires` references form a cycle. -- When `linked_execution` is `all`, a hold MUST use `hold_scope: proposal` - (§ 5.6.2). ### 5.7 Guarantees