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.filters.budget_range into constraints.total_budget: the legacy field is a hard product-discovery filter, while the compact field validates the negotiated proposal’s commercial_terms.total_budget and forces partial or unable when missed. Legacy more_like_this has no typed compact equivalent and maps only lossily to ask.
Legacy budget compatibility (normative)
Budget compatibility has two distinct rules:- Discovery range: Initial
request_proposalshas nocriteria.constraints.total_budgetfield. Its budget field isrequest_proposals.criteria.offer_filters.budget_range. A compatibility adapter MAY project that field toget_products.filters.budget_rangebecause both are hard product-discovery filters. The adapter MUST copy every supplied numeric bound and the three-letter currency unchanged, MUST preserve inclusiveminandmaxboundaries, MUST enforcemin <= max, and MUST NOT perform currency conversion. If the legacy seller cannot apply the exact currency and bounds, the adapter MUST return the pre-dispatch rejection below rather than approximate the filter. - Proposal-term constraint: An adapter MUST NOT project
refine_proposals.refinements[].constraints.total_budgettoget_products.filters.budget_range. The compact constraint is evaluated after proposal generation against each successor’scommercial_terms.total_budget; the legacy field filters product candidates and does not guarantee any returned proposal’s aggregate total. Identical{min,max,currency}syntax does not make those evaluation targets equivalent. Dropping the constraint, converting currency, changing a boundary, placing it inask, or returning an out-of-range proposal with a warning would all weaken the buyer’s hard requirement.
request_proposals compatibility call. On a revision, a present criteria.offer_filters replaces that entire criterion from the source proposal; it is not a subfield delta. The adapter MUST project the complete replacement object to legacy absolute filters, or reject before dispatch if any field cannot be represented exactly. When criteria.offer_filters is omitted, the adapter MUST also omit legacy filters, preserving each referenced proposal’s bound discovery constraints. This discovery mapping is not a substitute for constraints.total_budget on action: "revise".
Compact action: "finalize" accepts no constraints and copies the draft’s complete commercial_terms unchanged into a committed snapshot. Legacy finalize asks for firm pricing and therefore does not, by itself, prove unchanged terms. An adapter MAY project finalize only when it can guarantee that the legacy operation preserves the complete commercial terms, not merely the total budget. Otherwise it MUST reject before dispatch rather than compare or repair terms after mutation.
Every unsupported compact-to-legacy case above returns the same typed local compatibility result:
/refinements/0/constraints/total_budget; an unsafe finalize uses /refinements/0/action. unsupported_fields MUST contain every incompatibility detected during preflight, and any entry rejects the whole operation before a network request or mutation. PRE_MUTATION_UNSUPPORTED is not an AdCP wire error code. SDKs MAY express the type idiomatically in each language, but MUST preserve these discriminator values, JSON Pointer paths, lost-guarantee identifiers, recovery choices, and dispatched: false. An explicit caller opt-in to lossy projection does not make the mapping conformant.
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 expose the exact 3.2-beta.2 proposal-negotiation wire contract. The ordinary /sales/mcp route remains the backward-compatible ask-only seller. Use the public test token with https://test-agent.adcontextprotocol.org and one of these capability-distinct routes:
Pin the beta exactly on every call. A stable
3.2 selector intentionally negotiates to the seller’s stable compatibility response instead of silently opting into beta behavior. The bundled ADCPMultiAgentClient.simple() proposal path cannot yet target a different exact beta ordinal. Until adcp-client#2609 ships, construct a lower-level client with wireAdcpVersion: "3.2-beta.2" or send raw calls as below.
Run the constrained profile
This live example proves the three-to-two counteroffer and the changed-request retry rule against the public seller. It first requests three alternatives and receives a deterministicpartial result containing two. It then changes the count to two, uses a new idempotency key, and receives revised.
test=true integration requires-env=ADCP_AUTH_TOKEN
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.