ask for commercial nuance. Sellers validate the complete request before mutation, then return one independently classified result for each source proposal.
This guide uses one scenario throughout: Sam, a buyer agent acting for Pinnacle Agency, is negotiating an Acme Outdoor campaign with the StreamHaus sales agent.
Lifecycle
Every transition creates a newproposal_id; the source snapshot remains unchanged. Every successor returned by refine_proposals carries parent_proposal_id equal to the source ID. A buyer can therefore reconstruct negotiation and amendment history by walking the parent chain.
draft terms are indicative and do not reserve inventory. finalize copies an unchanged draft into a committed snapshot and creates a hold until expires_at. accept_proposal consumes that hold and creates, updates, or cancels the MediaBuy according to proposal_kind.
Discover what the seller supports
Readget_adcp_capabilities.media_buy.lifecycle_tools before selecting the compact lifecycle. A negotiating seller includes refine_proposals; it may also publish media_buy.proposal_refinement:
An explicit
supported_dimensions: [] means ask-only refinement. An omitted proposal_refinement block means typed support is unknown, so the buyer must tolerate per-result partial or unable outcomes.
When an explicit list omits a dimension, the buyer SHOULD remove or translate that field before sending. If it sends the field anyway, the seller MUST reject the entire task with UNSUPPORTED_FEATURE before creating any successor. Free-text interpretation is competence, not a capability dimension; ask remains available even when the list is empty.
When the seller interprets hard targeting found only in ask and that interpretation materially affects eligibility, pricing, or forecasting, inspect the result’s targeting_resolution.brief_targeting. Structured criteria.targeting_overlay is not echoed when accepted exactly; any product-specific departure remains a sparse Product.targeting_resolution.modifications proposal that the buyer must review.
Two failure planes
Do not flatten task errors and negotiation outcomes into one exception type.constraint_unsatisfiable means the seller accepted the dimension but did not satisfy it. unsupported_dimension is used per proposal only when capabilities were unknown; an explicitly omitted dimension is a task-level UNSUPPORTED_FEATURE. commercially_declined applies only to ask, never to a typed constraint.
For example, if StreamHaus explicitly omits criteria, the task fails before any proposal is created:
Buyer implementation
1. Build typed-first requests
Sam requests a USD 50,000 ceiling, a CPM ceiling, US/Canada criteria, a product change, and three distinct alternatives. The prose asks for subjective reach optimization only:get_products.refine[].budget_range shape without changing its meaning: legacy budget_range is a soft discovery preference, while constraints.total_budget forces partial or unable when missed. Legacy more_like_this has no typed compact equivalent and maps only lossily to ask.
A fully satisfied revision returns new immutable drafts and no failure prose:
2. Branch on the discriminated result
Handleoutcome before reading proposal fields:
A valid counteroffer with only two of the three requested alternatives is
partial:
3. Verify terms independently
For every returned proposal:- Recompute
sha256:base64url(SHA-256(JCS(commercial_terms)))using RFC 8785 JSON Canonicalization Scheme bytes. - Compare the recomputed digest byte-for-byte with
terms_digest. - Verify
parent_proposal_idequalssource_proposal_id. - Re-evaluate budgets, fixed rates, impression totals, flight bounds, product membership, and structured criteria from
commercial_terms. - Verify any
targeting_resolution.brief_targetingagainst the hard targeting inask, and review every sparse producttargeting_resolution.modificationsdeparture before selecting that configured product. - On
partial, enforce the partial invariant: every requirement absent from the unsatisfied sets still passes. - Treat
reason,description,suggestions, andaskas non-contractual prose.
commercial_terms are distinct after canonicalization; a seller cannot create fake diversity by changing only IDs, names, descriptions, or digest strings.
4. Retry safely
An exact retry uses the same tool name, payload, andidempotency_key; the seller returns the original response with replayed: true. Any payload change requires a new key. Never use a new key merely to extend a committed hold: finalizing the same draft while its hold is active is INVALID_STATE, not a second reservation.
A seller may return status: "submitted" with a task_id when a complex multi-publisher revision or inventory hold needs upstream pricing or manual re-underwriting. Poll get_task_status or consume the requested completion notification; do not treat submission as a proposal outcome or retry it under a new key while the task is pending.
5. Finalize, then accept before expiry
Finalization changes no terms:hold_unavailable identifies the source that could not be reserved; otherwise eligible siblings use batch_aborted.
The successful result is singular and committed; its commercial terms and digest are unchanged:
expires_at:
PROPOSAL_EXPIRED; the buyer requests and finalizes fresh terms rather than retrying acceptance against a lapsed hold.
6. Amend, cancel, or decline
Fork an accepted snapshot withchange_kind: "amendment":
change_kind: "cancellation":
control_media_buy cancellation is reserved for rights already granted by the accepted terms. When Sam stops pursuing a draft instead, decline_proposals records terminal feedback; it does not mutate the proposal into another state.
Seller implementation
Preflight the whole request
Before loading an LLM, pricing inventory, or writing state:- Validate the request schema, the maximum of 25 refinements, unique source IDs, and finalize exclusivity.
- Authenticate and authorize every source without disclosing cross-account existence.
- Compare every typed field with the explicit capability declaration.
- Enforce the protocol alternatives ceiling of 10 and any lower advertised
max_alternatives. - Look up the idempotency key and reject reuse with a changed payload.
- If any task-level check fails, create no successor and no hold.
constraint_unsatisfiable result, not UNSUPPORTED_FEATURE.
Keep protocol validation separate from policy
The protocol layer owns schema validation, capability gates, result cardinality, lineage, digest computation, response validation, and transaction boundaries. Application policy owns pricing floors, available product mixes, targeting feasibility, alternative generation, underwriting, and hold duration. Fenceask from pricing and authorization authority. It is attacker-controlled free text and may influence candidate generation only through bounded application policy. Only the typed commercial_terms object becomes contractual.
Stage immutable successors
For revision batches:- Load each immutable source snapshot.
- Build candidate
commercial_termswithout mutating the source. - Evaluate every typed constraint and product action against every candidate.
- Classify the ordered result as
revised,partial, orunable. - Generate new proposal IDs, set
parent_proposal_id, compute JCS digests, and validate the full response. - Commit staged successors only after response validation succeeds.
partial, include every failed constraint key and failed product action. Every unlisted requirement must pass for every returned draft. constraint_unsatisfiable takes precedence when it coexists with an alternatives shortfall or uninterpreted prose.
For finalize batches, acquire holds inside one transaction or equivalent staged operation. If any hold fails, roll back every sibling. A seller may decline a hold, apply its normal credit and relationship checks, and cap concurrent unexpired holds per buyer to resist inventory squatting. Choose expires_at long enough for acceptance but short enough to prevent free inventory options. Return submitted when those checks require asynchronous upstream pricing or manual re-underwriting, preserving the original idempotency scope through terminal completion.
Log for audit without leaking terms
Record the authenticated buyer, source IDs, successor IDs, idempotency fingerprint, capability decision, outcome, unsatisfied keys, hold transaction, and digest. Do not place rawask, full commercial terms, credentials, or cross-buyer pricing in broadly accessible logs. Rate-limit repeated unheld constraint probes: deterministic failures can otherwise become a price oracle.
Conformance and SDK status
The canonical compliance scenario ismedia_buy_seller/typed_proposal_negotiation. It exercises capability gating, satisfied and unsatisfied constraints, alternatives, immutable lineage, finalize atomicity, exact replay, acceptance, amendment, cancellation, double-finalize rejection, and multi-source ordering.
The deterministic training profiles use the proposal-negotiation primitives published in @adcp/sdk 13.0.0-rc.26. The ordinary /sales/mcp route remains the backward-compatible ask-only seller. Three capability-distinct routes are available to local tests and will become public with the 3.2 beta deployment:
The constrained route is the canonical three-to-two exercise: request a USD 50,000 cap, include and omit products, apply US/Canada criteria, and request three alternatives. Retry the returned
partial / alternatives_unavailable result with two alternatives before selecting and finalizing one. Exact retries reuse the same idempotency key; a changed alternatives count is a new logical request and needs a new key.
For reason-code labs, the semantic ask markers [commercially-declined] and [unsupported-dimension] select those bounded fixture outcomes. An ordinary untyped ask reaches uninterpreted; impossible typed terms reach constraint_unsatisfiable; an unknown source reaches source_unavailable. The training seller caps each principal at three concurrent unexpired holds.
Keep the following boundaries in your adapter:
- capability preflight is separate from mutation;
- exact replay is separate from changed-request retry;
- task errors are separate from per-proposal outcomes;
- commercial policy is separate from protocol verification;
- finalization is separate from acceptance;
- immutable terms are separate from explanatory prose.
Implementation checklist
- Discover
lifecycle_tools,supported_dimensions, andmax_alternatives. - Enforce 25 refinements, 10 alternatives, seller limits, unique sources, and finalize-only batches before mutation.
- Preserve task errors separately from
revised/partial/unable/finalized. - Verify typed constraints, product actions, partial unsatisfied subsets, parent lineage, and JCS digests.
- Reuse a key only for the exact same request; use a new key for changed payloads.
- Stage successors and validate responses before commit.
- Create finalize holds atomically and enforce expiry during acceptance.
- Fork accepted snapshots for amendments and negotiated cancellation.
- Fence
ask, authorize every source, rate-limit probing, and avoid sensitive logs. - Run the typed proposal negotiation storyboard against the implementation.