diff --git a/.changeset/build-creative-spend-controls.md b/.changeset/build-creative-spend-controls.md
new file mode 100644
index 0000000000..c87f182fe1
--- /dev/null
+++ b/.changeset/build-creative-spend-controls.md
@@ -0,0 +1,14 @@
+---
+"adcontextprotocol": minor
+---
+
+spec(creative): add build_creative spend controls — `max_spend` cap + `mode: "estimate"` dry-run.
+
+Follow-on from the persona/scenario review: fan-out (`max_creatives` × `max_variants`) and refinement produce many independently-billed leaves, and `per_unit` pricing gives a rate but not the unit count in advance — so an autonomous buyer had no protocol brake on spend. Both additions are optional and gated by a new `creative.supports_spend_controls` capability.
+
+- **`mode: "estimate"`** (request) → new `BuildCreativeEstimate` response shape (6th `oneOf` member): a dry run that produces and bills nothing and returns a `cost_low`/`cost_high` band computed against the request's actual inputs, with `basis` (`fixed` exact / `estimated_units` / `cpm_deferred`) and an optional per-leaf breakdown. Advisory/non-binding in this revision.
+- **`max_spend: { amount, currency }`** (request) → a hard per-call ceiling: the agent stops before the next leaf would exceed it and returns the partial `BuildCreativeVariantSuccess` with new `budget_status: "capped"` and an advisory `BUDGET_CAP_REACHED` in `errors[]` (every returned leaf real and billed; `items_returned` < `items_total`). First-leaf-over-cap → terminal `BUDGET_CAP_REACHED`; currency mismatch → `INVALID_REQUEST`.
+- New error code **`BUDGET_CAP_REACHED`** (distinct from `BUDGET_EXCEEDED`/`BUDGET_EXHAUSTED`), in both `enumDescriptions` and `enumMetadata`.
+- New capability **`creative.supports_spend_controls`** (default false).
+
+Deferred to the working group (flagged, not omitted): whether an estimate can be **binding**, and whether a refinement-**loop** bound is a protocol-level session budget vs. a buyer responsibility (documented as buyer-side for now).
diff --git a/.changeset/creative-transformers.md b/.changeset/creative-transformers.md
new file mode 100644
index 0000000000..55f87f6184
--- /dev/null
+++ b/.changeset/creative-transformers.md
@@ -0,0 +1,26 @@
+---
+"adcontextprotocol": minor
+---
+
+spec(creative): add `list_transformers` task + account-scoped creative transformers, and extend `build_creative` for transformer selection and variant/catalog multiplicity.
+
+A **transformer** is the creative analog of a media-buy product: an agent-offered, account-scoped, selectable unit of build capability (a voice, model, style, or director) with a typed configuration surface and per-account pricing. This makes account-specific render configuration — including custom values like cloned voices that exist only for one credential — discoverable from the agent rather than guessed, hung on a global format, or smuggled through `ext`.
+
+Strictly additive. Existing `build_creative` callers are unaffected (all new request fields are optional; the shipped `BuildCreativeSuccess`/`BuildCreativeMultiSuccess` response shapes are unchanged — a new fifth member is added alongside them).
+
+New:
+- `list_transformers` task (creative protocol): account-scoped, brief-filterable, paginated discovery. An `expand_params` mode returns account-scoped enumerable option **values** (e.g. your configured voices) on the same tool — no separate options endpoint.
+- Core schemas `transformer.json` and `transformer-param.json`.
+- `get_adcp_capabilities` → `creative.supports_transformers` discriminator.
+
+`build_creative` extensions:
+- Request: `transformer_id` (select one transformer; target format(s) must be a subset of its `output_format_ids`), `config` (typed bag keyed to the transformer's params — agents MUST reject unknown/out-of-range values), `max_creatives` (catalog/item fan-out: N distinct creatives, one per item, with sampling), `max_variants` + `variant_axis` + `keep_mode` (alternatives per creative).
+- Response: a new `BuildCreativeVariantSuccess` member — `creatives[]` each carrying `variants[]`, with a `build_variant_id` namespace (distinct from preview `preview_id` and served `variant_id`), per-leaf pricing receipt, and `items_total`/`items_returned`. Best-of-N is variants + `recommended`/`rank`. You pay for all produced variants (`per_unit` × N); a kept variant lazily earns a `creative_id` on trafficking, which flows to `report_usage`. Per-format atomic; per-item non-atomic.
+
+`build_variant` lineage + refinement:
+- `build_variant_id` is now the leaf-level lineage anchor (`x-entity: build_variant`): minted per produced variant, distinct from the call-level `build_creative_id`, lazily earning a durable `creative_id` only on trafficking. Untrafficked leaves are billed via the inline per-leaf `vendor_cost` only; `report_usage` reconciliation applies once a leaf earns a `creative_id`.
+- Conversational refinement: `build_creative` gains `refine_from_build_variant_id` — re-build a prior leaf with a natural-language instruction in `message` plus an optional `config` delta, returning new lineage-linked variants (each with `parent_build_variant_id`); never a mutation. Composes with `max_variants`/`variant_axis`, mutually exclusive with `max_creatives`. Gated by the new `get_adcp_capabilities` → `creative.supports_refinement` discriminator (`UNSUPPORTED_FEATURE` when unsupported; `REFERENCE_NOT_FOUND` for an unknown/expired ref).
+
+Pricing rides the existing `per_unit` model + inline receipt + `report_usage` unchanged — transformers carry `pricing_options` (reusing `vendor-pricing-option.json`).
+
+Deprecations (deprecated in 3.1, removed at 4.0; SDKs MUST keep honoring them through 3.1–3.x): `Format.input_format_ids`, `Format.output_format_ids`, `Format.pricing_options`, and the `input_format_ids`/`output_format_ids` discovery filters on `list_creative_formats` — all superseded by `list_transformers`, which carries each transformer's own I/O signature and pricing.
diff --git a/.changeset/generative-encoding-safe-additions.md b/.changeset/generative-encoding-safe-additions.md
new file mode 100644
index 0000000000..39c6ce3ded
--- /dev/null
+++ b/.changeset/generative-encoding-safe-additions.md
@@ -0,0 +1,10 @@
+---
+"adcontextprotocol": minor
+---
+
+spec(creative): generative-encoding safe additions — `free_text` params + per-output transformer pricing.
+
+The additive half of the generative-agent (Veo/Imagen) encodings follow-on. The two *normative* rules it pairs with — generation count is owned by `max_variants`/`max_creatives` (never a config param), and `aspect_ratio` rides the format axis — are intentionally left to the working group; only the safe schema bits land here.
+
+- `transformer-param.json` `value_source` gains **`free_text`** (an open buyer-authored string with no closed set — e.g. a `negative_prompt` or style note; `type` MUST be `string`, the closed-set fields MUST be absent) plus an optional **`max_length`**. The description also states that count/quantity knobs MUST NOT be params (count rides `max_variants`/`max_creatives`).
+- `vendor-pricing-option.json` gains optional **`applies_to_output_format_ids`** so one creative transformer can price different outputs differently (e.g. a multi-publisher template charging per publisher format); an unscoped option is the default. Additive and inert for non-creative vendors (signals/governance) — **flagged for shared-schema owner ack**.
diff --git a/.changeset/transformer-capability-discriminators.md b/.changeset/transformer-capability-discriminators.md
new file mode 100644
index 0000000000..06009905aa
--- /dev/null
+++ b/.changeset/transformer-capability-discriminators.md
@@ -0,0 +1,12 @@
+---
+"adcontextprotocol": minor
+---
+
+spec(creative): add pre-call discriminators for creative-transformer refinement retention and fan-out multiplicity.
+
+Lets a buyer agent know — before sending — what a creative agent supports, instead of probing and handling failures. Additive and optional (all fields default to "unsupported / unbounded"), and the keystone the spend-control and conformance follow-ons build on.
+
+- `get_adcp_capabilities` → `creative.refinable_retention_seconds` (integer): the guaranteed-minimum window a produced `build_variant_id` stays refinable. Replaces the prose-only "agent-defined window" with a machine-readable floor; omit to keep it agent-defined.
+- `get_adcp_capabilities` → `creative.multiplicity` (object): `supports_catalog_fanout` + `max_creatives_limit`, `supports_variants` + `max_variants_limit`, and `variant_dimensions[]`. Over-limit `max_creatives`/`max_variants` are **clamped** to the ceilings (shortfall via `items_returned` < `items_total`), not rejected — consistent with `item_limit`'s "use the lesser" rule. Absent means no fan-out.
+- `transformer.json` → optional `multiplicity` that narrows the agent-level object per transformer (ceilings ≤ agent, `variant_dimensions` ⊆ agent).
+- `build_creative` docs note the clamp behavior on `max_creatives`/`max_variants`.
diff --git a/docs.json b/docs.json
index d20d6835e2..a9b3d7423d 100644
--- a/docs.json
+++ b/docs.json
@@ -93,6 +93,7 @@
"docs/reference/migration/pricing",
"docs/reference/migration/geo-targeting",
"docs/reference/migration/creatives",
+ "docs/reference/migration/creative-transformers",
"docs/reference/migration/catalogs",
"docs/reference/migration/optimization-goals",
"docs/reference/migration/brand-identity",
@@ -359,6 +360,7 @@
"docs/creative/task-reference/preview_creative",
"docs/creative/task-reference/preview_creative-advanced",
"docs/creative/task-reference/list_creative_formats",
+ "docs/creative/task-reference/list_transformers",
"docs/creative/task-reference/list_creatives",
"docs/creative/task-reference/sync_creatives",
"docs/creative/task-reference/get_creative_delivery"
@@ -1298,6 +1300,7 @@
"docs/reference/migration/pricing",
"docs/reference/migration/geo-targeting",
"docs/reference/migration/creatives",
+ "docs/reference/migration/creative-transformers",
"docs/reference/migration/catalogs",
"docs/reference/migration/optimization-goals",
"docs/reference/migration/brand-identity",
@@ -1556,6 +1559,7 @@
"docs/creative/task-reference/preview_creative",
"docs/creative/task-reference/preview_creative-advanced",
"docs/creative/task-reference/list_creative_formats",
+ "docs/creative/task-reference/list_transformers",
"docs/creative/task-reference/list_creatives",
"docs/creative/task-reference/sync_creatives",
"docs/creative/task-reference/get_creative_delivery"
diff --git a/docs/accounts/tasks/report_usage.mdx b/docs/accounts/tasks/report_usage.mdx
index 8a54be96f9..9b7cc8521a 100644
--- a/docs/accounts/tasks/report_usage.mdx
+++ b/docs/accounts/tasks/report_usage.mdx
@@ -35,7 +35,8 @@ Each record requires `account`, `vendor_cost`, and `currency`. Additional fields
| `impressions` | number | Signals: Yes | Impressions delivered |
| `media_spend` | number | percent_of_media: Yes | Media spend for percent-of-media cost verification |
| `signal_agent_segment_id` | string | Signals: Yes | Signal identifier from `get_signals` |
-| `creative_id` | string | Creative: Yes | Creative identifier from `build_creative` or `list_creatives`. Links usage to a specific creative for billing verification. |
+| `creative_id` | string | Creative: Yes | Creative identifier from `build_creative` or `list_creatives`. Links usage to a specific creative for billing verification. A `build_creative` variant leaf earns a `creative_id` only when trafficked/added to the library — discarded best-of-N or fan-out variants are never reported here; their charge is the inline per-leaf `vendor_cost` on the `build_creative` response (the authoritative record for untrafficked leaves). |
+| `build_variant_id` | string | No | When the reported `creative_id` was promoted from a specific `build_creative` variant leaf, the source `build_variant_id` — lets reconciliation link this record back to the exact produced leaf for audit (`pricing_option_id` alone is not unique across leaves). Omit for creatives with no build-variant lineage. |
| `property_list_id` | string | Property lists: Yes | Property list identifier from `list_property_lists`. Links usage to a specific property list for billing verification. |
## Response
diff --git a/docs/creative/specification.mdx b/docs/creative/specification.mdx
index 3fe38f740f..6d4fac5fab 100644
--- a/docs/creative/specification.mdx
+++ b/docs/creative/specification.mdx
@@ -275,6 +275,18 @@ Discover creative formats and their specifications.
- Creative agents MAY include references to other creative agents providing additional formats
- When filtering by `format_ids`, creative agents MUST return only the requested formats
+### list_transformers
+
+**Reference**: [`list_transformers` task](/docs/creative/task-reference/list_transformers)
+
+Discover the account-scoped transformers a creative agent offers — the creative analog of media-buy products: agent-offered, selectable units of build capability (voices, models, styles) that you select with `transformer_id` in `build_creative`. Offered only by agents that declare `creative.supports_transformers: true` in `get_adcp_capabilities`.
+
+**Requirements:**
+- Creative agents that set `creative.supports_transformers: true` MUST implement `list_transformers`
+- Creative agents MUST resolve transformers, their enumerable option values, and pricing for the calling account — including custom values configured for that account (e.g. cloned voices)
+- Creative agents MUST return account-scoped option values inline on `params[].options[]` for each `field` named in `expand_params`, and SHOULD omit them otherwise
+- When `include_pricing` is true, creative agents that charge MUST include `pricing_options` (the `per_unit` model) on each transformer
+
### build_creative
**Reference**: [`build_creative` task](/docs/creative/task-reference/build_creative)
diff --git a/docs/creative/task-reference/build_creative.mdx b/docs/creative/task-reference/build_creative.mdx
index ed836c9908..eaf5659074 100644
--- a/docs/creative/task-reference/build_creative.mdx
+++ b/docs/creative/task-reference/build_creative.mdx
@@ -29,7 +29,16 @@ For information about legacy format IDs and how to reference formats, see [Creat
| `target_format_ids` | array | Conditional | Array of format IDs to generate in a single call. Each element is an object with `agent_url` and `id` fields. For 3.1 canonical creative-agent routing, each `id` is an advertised `creative.supported_formats[].capability_id`; legacy named-format IDs remain accepted during the migration window. Mutually exclusive with `target_format_id` — provide exactly one. Returns one manifest per format. |
| `brand` | object | No | Brand reference with `domain` field. Resolves brand identity via `/.well-known/brand.json`. Provides brand-level context (colors, logos, tone). |
| `quality` | string | No | Quality tier: `"draft"` (fast, lower-fidelity for iteration) or `"production"` (full quality for final delivery). If omitted, the creative agent uses its own default. |
-| `item_limit` | integer | No | Maximum number of catalog items to use when generating. Caps generation cost for catalog-driven formats. |
+| `item_limit` | integer | No | Maximum number of catalog items to use **within one creative**. Caps generation cost for catalog-driven formats (e.g. a 6-card carousel built from a 1,000-product catalog). Distinct from `max_creatives`, which fans out across items. |
+| `transformer_id` | string | No | Select one transformer (discovered via [`list_transformers`](/docs/creative/task-reference/list_transformers)) to perform the build. The requested target format(s) MUST be a subset of that transformer's `output_format_ids`. The transformer's `per_unit` rate is the pricing source for the build. |
+| `config` | object | No | Typed configuration bag keyed to the selected transformer's `params[].field` (e.g. `{ "voice": "isaac", "speaking_rate": 1.1 }`). The creative agent MUST reject unknown keys and out-of-range values with field-attributed errors (strict validation) — vendor-specific knobs that are not declared params go in `ext`. Only meaningful with `transformer_id`. |
+| `max_creatives` | integer | No | Catalog fan-out axis: produce up to N **distinct creatives, one per catalog item** (a sample — e.g. 5 of 150). Distinct from `item_limit`, which caps items used *within* a single creative. Mutually exclusive with `refine_from_build_variant_id`. Triggers the `BuildCreativeVariantSuccess` response shape. Supported only when the agent advertises `creative.multiplicity.supports_catalog_fanout`; values above `max_creatives_limit` are **clamped** (shortfall via `items_returned` < `items_total`), not rejected. |
+| `max_variants` | integer | No | Number of alternative renders to produce **per creative** (best-of-N). Default `1`. You pay for every produced variant; keeping one or more is a separate trafficking step. Supported only when the agent advertises `creative.multiplicity.supports_variants`; values above `max_variants_limit` are clamped. |
+| `variant_axis` | object | No | Describes the dimension along which variants differ. Object with `dimension` (`voice` \| `theme` \| `best_of_n` \| `transformer_config` \| `custom`), optional `values[]` (explicit values to enumerate along the axis), optional `field` (the `config` param to sweep — required when `dimension` is `transformer_config`), and optional `label`. |
+| `keep_mode` | string | No | Advisory hint to the agent on how many variants you intend to keep: `"keep_all"`, `"keep_one"`, or `"keep_some"`. Default `"keep_all"`. Advisory only — you are billed for all produced variants regardless. The response echoes `keep_mode_applied` so you have confirmation the hint was received (audit trail for billing disputes). |
+| `refine_from_build_variant_id` | string | No | Refine a prior produced variant: re-build from its `build_variant_id` applying the NL instruction in `message` plus any `config` delta, returning **new** lineage-linked variants (never a mutation). `transformer_id` and target format(s) are inherited from the parent. Composes with `max_variants`/`variant_axis`; mutually exclusive with `max_creatives`. Requires `creative.supports_refinement` — otherwise `UNSUPPORTED_FEATURE`; an unknown/no-longer-retained ref is `REFERENCE_NOT_FOUND` (`error.field` = `refine_from_build_variant_id`). See [Refinement](#refinement). |
+| `mode` | string | No | `"execute"` (default) produces and bills. `"estimate"` is a **dry run** — produces and bills nothing, returns a `BuildCreativeEstimate` cost band (`cost_low`/`cost_high`) computed against this request's inputs. Requires `creative.supports_spend_controls`. See [Spend controls](#spend-controls). |
+| `max_spend` | object | No | Hard per-call spend ceiling `{ amount, currency }`. The agent produces leaves until the next would exceed `amount`, then stops and returns the partial result with `budget_status: "capped"`. Requires `creative.supports_spend_controls`. Caps one call — bound refinement loops buyer-side. See [Spend controls](#spend-controls). |
| `include_preview` | boolean | No | When true, requests preview renders alongside the manifest. Agents that support this return a `preview` object in the response. Agents that don't simply omit it. |
| `preview_inputs` | array | No | Input sets for preview generation when `include_preview` is true. Each entry has `name` (required), optional `macros`, and optional `context_description`. If omitted, the agent generates a single default preview. Only supported with `target_format_id` (single-format) — ignored for multi-format requests. |
| `preview_quality` | string | No | Render quality for inline previews: `"draft"` or `"production"`. Independent of the build `quality` — you can build at draft and preview at production, or vice versa. Only used when `include_preview` is true. |
@@ -326,6 +335,73 @@ To refine a single format from a multi-format build, call `build_creative` again
Multi-format requests with `include_preview: true` generate one default preview per format. Custom `preview_inputs` are only supported with single-format requests. For multi-format builds where you need context-specific previews (device variants, different contexts), use a separate `preview_creative` batch call after building.
+### Transformers & variants
+
+Select a transformer (discovered via [`list_transformers`](/docs/creative/task-reference/list_transformers)) with `transformer_id`, supply a typed `config` keyed to its params, and ask for alternatives with `max_variants`. The agent returns the [variant response](#variant-response): `creatives[]`, each holding a `variants[]` array. Read `recommended` / `rank` to surface the agent's best-of-N pick; traffic the variant(s) you want by their `build_variant_id`.
+
+Resolutions and quality tiers go on `target_format_ids` (or `quality`), not on `variant_axis` — variants are alternatives for the *same* format.
+
+
+
+```javascript test=false
+import { testAgent } from '@adcp/sdk/testing';
+import { BuildCreativeResponseSchema } from '@adcp/sdk';
+
+const result = await testAgent.buildCreative({
+ account: { account_id: 'acct_acme' },
+ transformer_id: 'audiostack_voiceover',
+ config: { voice: 'isaac', speaking_rate: 1.1, mastering_preset: 'podcast' },
+ target_format_id: { agent_url: 'https://creative.audiostack.example', id: 'audio_vo' },
+ creative_manifest: {
+ format_id: { agent_url: 'https://creative.audiostack.example', id: 'script' },
+ assets: { script: { asset_type: 'text', content: 'Discover the new winter collection.' } },
+ },
+ max_variants: 3,
+ variant_axis: { dimension: 'best_of_n', label: 'Read takes' },
+ keep_mode: 'keep_one',
+ idempotency_key: '0c1d2e3f-4a5b-6c7d-8e9f-a0b1c2d3e4f5',
+});
+
+const parsed = BuildCreativeResponseSchema.parse(result);
+for (const creative of parsed.creatives ?? []) {
+ for (const variant of creative.variants) {
+ console.log(variant.build_variant_id, variant.rank, variant.recommended, variant.vendor_cost);
+ }
+}
+```
+
+```python test=false
+import asyncio
+from adcp.testing import test_agent
+from adcp import BuildCreativeResponse
+
+async def main():
+ result = await test_agent.build_creative(
+ account={"account_id": "acct_acme"},
+ transformer_id="audiostack_voiceover",
+ config={"voice": "isaac", "speaking_rate": 1.1, "mastering_preset": "podcast"},
+ target_format_id={"agent_url": "https://creative.audiostack.example", "id": "audio_vo"},
+ creative_manifest={
+ "format_id": {"agent_url": "https://creative.audiostack.example", "id": "script"},
+ "assets": {"script": {"asset_type": "text", "content": "Discover the new winter collection."}},
+ },
+ max_variants=3,
+ variant_axis={"dimension": "best_of_n", "label": "Read takes"},
+ keep_mode="keep_one",
+ idempotency_key="0c1d2e3f-4a5b-6c7d-8e9f-a0b1c2d3e4f5",
+ )
+ parsed = BuildCreativeResponse.model_validate(result)
+ for creative in parsed.creatives or []:
+ for variant in creative.variants:
+ print(variant.build_variant_id, variant.rank, variant.recommended, variant.vendor_cost)
+
+asyncio.run(main())
+```
+
+
+
+You pay for all three takes (`per_unit` × 3); `keep_mode` is advisory. Keeping is a separate trafficking step on the chosen `build_variant_id` — the kept variant then lazily earns a `creative_id` that flows to [`report_usage`](/docs/accounts/tasks/report_usage).
+
### Paid build with pricing
When the creative agent charges and `account` is provided, the response includes pricing fields. The agent selects the applicable pricing option server-side based on the account's rate card and the work performed — the buyer does not pass `pricing_option_id` in the request.
@@ -444,6 +520,151 @@ When the request uses `target_format_ids`, the response contains an array of cre
}
```
+### Variant response
+
+When the request uses `max_creatives`, `max_variants` > 1, `variant_axis`, or `refine_from_build_variant_id`, the agent returns the `BuildCreativeVariantSuccess` shape — the third success shape (`oneOf` member 3 of 6) alongside the single- and multi-format responses, which are unchanged and still used when you build one creative with one variant.
+
+
+**No fallback.** If you send any of `max_creatives`, `max_variants > 1`, `variant_axis`, or `refine_from_build_variant_id`, you **must** handle `creatives[]` — you will not get `creative_manifest`/`creative_manifests` back. There is no automatic downgrade to the single/multi shapes.
+
+
+```json
+{
+ "$schema": "/schemas/media-buy/build-creative-response.json",
+ "status": "completed",
+ "creatives": [
+ {
+ "build_creative_id": "bc_card_01",
+ "catalog_item_ref": { "catalog_type": "product", "item_id": "sku_winter_parka" },
+ "variants": [
+ {
+ "build_variant_id": "bv_card01_a",
+ "creative_manifest": { "format_id": { "agent_url": "https://creative.example.com", "id": "display_300x250" }, "assets": { "...": "..." } },
+ "variant_axis_value": "studio",
+ "recommended": true,
+ "rank": 1,
+ "pricing_option_id": "po_per_image",
+ "vendor_cost": 0.40,
+ "currency": "USD",
+ "consumption": { "images_generated": 1 }
+ },
+ {
+ "build_variant_id": "bv_card01_b",
+ "creative_manifest": { "format_id": { "agent_url": "https://creative.example.com", "id": "display_300x250" }, "assets": { "...": "..." } },
+ "variant_axis_value": "lifestyle",
+ "recommended": false,
+ "rank": 2,
+ "pricing_option_id": "po_per_image",
+ "vendor_cost": 0.40,
+ "currency": "USD",
+ "consumption": { "images_generated": 1 }
+ }
+ ]
+ }
+ ],
+ "items_total": 150,
+ "items_returned": 5,
+ "vendor_cost": 4.00,
+ "currency": "USD"
+}
+```
+
+- **creatives[]**: One entry per built creative group. With `max_creatives`, there is one entry per sampled catalog item (`catalog_item_ref` identifies which item); without catalog fan-out there is a single entry. Treat this as the produced group set; choose among alternatives inside each group's `variants[]`.
+- **creatives[].build_creative_id**: Identifies a built creative within this response.
+- **creatives[].catalog_item_ref**: Present on catalog fan-out — an object identifying the source catalog item via `item_id` (plus optional `catalog_type`).
+- **creatives[].errors[]**: Present only on a *failed* catalog item — catalog fan-out is non-atomic, so a failed item is returned as a `creatives[]` entry carrying `errors[]` and no `variants[]`, without failing the batch.
+- **creatives[].variants[]**: The choose-among alternatives produced for this creative group. Length is at most `max_variants`. Each variant carries its own complete `creative_manifest`.
+- **variants[].build_variant_id**: Identifies a single variant — the leaf-level **lineage anchor**. This is its **own namespace** — never reuse a `preview_id` (a `preview_creative` render) or a served `variant_id` (a delivery-time identifier) here. You traffic a chosen build by passing its `build_variant_id`.
+- **variants[].parent_build_variant_id**: Present only on a refined variant ([Refinement](#refinement)) — the `build_variant_id` of the source it was refined from. Absent for first-generation builds.
+- **variants[].variant_axis_value**: The value of the `variant_axis` dimension this variant represents (e.g. a voice, a theme).
+- **variants[].recommended** / **variants[].rank**: Best-of-N signals — `recommended` flags the agent's top pick; `rank` orders the variants. These plus `variant_axis` and `keep_mode` are how best-of-N is expressed.
+- **variants[] pricing receipt**: Each variant carries a per-leaf receipt (`pricing_option_id`, `vendor_cost`, `currency`, `consumption`) for the work that produced it.
+- **items_total** / **items_returned**: For catalog fan-out, the total eligible catalog items and how many creatives were returned (e.g. "5 of 150").
+- **leaves_total** / **leaves_returned**: Leaf-granular counts — total leaves the request would produce vs. how many were actually produced/billed. When `budget_status` is `capped`, `leaves_returned` < `leaves_total` is the shortfall (works even for variant-only fan-out with no catalog).
+- **vendor_cost** / **currency**: Aggregate cost across all produced variants in the response.
+- **keep_mode_applied**: Echoes the `keep_mode` the agent applied (present when the request set it). `keep_mode` is advisory and doesn't change what's produced or billed, so this is your confirmation the hint was received — the audit trail for a "I asked for keep_one but was billed for N" dispute.
+- **budget_status**: `complete` (default; absent == complete) or `capped` — a `max_spend` ceiling stopped production early (a successful partial build, not a failure). See [Spend controls](#spend-controls).
+- **errors[]**: Advisory (non-terminal) entries on an otherwise-successful build — e.g. a `BUDGET_CAP_REACHED` notice when `budget_status` is `capped`.
+
+
+You pay for **every produced variant** (`per_unit` × N), not just the ones you keep. Keeping is a separate client act of trafficking the chosen `build_variant_id`(s). A kept variant lazily earns a `creative_id` when it is added to the library or first trafficked; that `creative_id` is what flows to [`report_usage`](/docs/accounts/tasks/report_usage). An unkept variant is still billed but never earns a `creative_id`.
+
+
+
+**Atomicity differs by axis.** Variant production *per format* is atomic — if one variant for a format fails, that format's build fails. Catalog fan-out (`max_creatives`, per item) is **non-atomic**: individual `creatives[]` entries may succeed while others fail. A failed item is returned as a `creatives[]` entry carrying `errors[]` (and no `variants[]`); the batch still succeeds.
+
+**Build-time variants are not delivery variants.** The `build_variant_id` here is a pre-delivery handle for an alternative you produced. It is a different namespace from the served `variant_id` in [`get_creative_delivery`](/docs/creative/task-reference/get_creative_delivery), which identifies a creative *execution that was delivered*. A build-time variant becomes a delivery variant only once you keep and traffic it.
+
+**Resolutions and quality tiers are not variants.** Multiple sizes or quality levels belong on the format axis — pass them as `target_format_ids` (or `quality`), not as `variant_axis` values. Variants are alternatives *for the same format* (different voice, theme, or best-of-N take).
+
+
+### Refinement
+
+Conversational refinement re-builds a prior variant with free-form direction — "make it warmer", "tighten the CTA". The typed `config` surface is closed by design, so free-form intent rides the **open** surface that already exists: the `message` field (whose description is already *"For refinement, this describes the desired changes"*). Refinement is therefore not a new task — it's `build_creative` with one extra input:
+
+- Pass `refine_from_build_variant_id` (a prior leaf's `build_variant_id`) plus your instruction in `message` and any `config` delta.
+- The agent re-builds from that leaf and returns **new** variants, each with `parent_build_variant_id` set to the source. A refinement is **never a mutation** — the parent leaf is unchanged, and the new leaf earns its own `build_variant_id` (and, on trafficking, its own `creative_id`).
+- `transformer_id` and the target format(s) are **inherited** from the parent; you don't repeat them. Passing a `transformer_id` or target format that differs from the parent's is an `INVALID_REQUEST`. `config` is applied as a **delta** over the parent's config.
+- Composes with `max_variants` / `variant_axis` (e.g. "three warmer takes" → three refined leaves), but **not** with `max_creatives` / catalog fan-out — you refine one produced creative, not a catalog.
+- Requires the agent to advertise `creative.supports_refinement: true` (it retains produced leaves for an agent-defined window). Agents that retain nothing return `UNSUPPORTED_FEATURE`; refine a buyer-held manifest instead via the transform path (`creative_manifest` + `message`). An unknown or no-longer-retained ref returns `REFERENCE_NOT_FOUND` with `error.field` set to `refine_from_build_variant_id`.
+
+Any build can be refined, not just variant builds: a single-format `BuildCreativeSuccess` carries an optional `build_variant_id` (present when the agent supports refinement) that you pass as `refine_from_build_variant_id`. Multi-format builds that need per-output refinable leaves should request the variant shape (`max_variants`), since the bare `creative_manifests[]` array carries no leaf ids.
+
+AI-derivative attribution rides the manifest's existing `provenance`; `parent_build_variant_id` carries only the lineage edge (refinements chain into a tree).
+
+```json test=false
+{
+ "refine_from_build_variant_id": "bv_card01_a",
+ "message": "Warmer lighting, and move the logo to the lower-right.",
+ "max_variants": 3,
+ "account": { "account_id": "acct_acme" },
+ "idempotency_key": "7f2a1b3c-4d5e-6f70-8192-a3b4c5d6e7f8"
+}
+```
+
+### Spend controls
+
+Fan-out and refinement can produce many independently-billed leaves (`max_creatives` × `max_variants`), and `per_unit` pricing gives a *rate* but not the *unit count* in advance (a 6-second voiceover and a 60-second one cost 10× at the same rate). Two opt-in controls, both gated by `creative.supports_spend_controls`:
+
+- **Estimate first (`mode: "estimate"`).** A dry run: the agent produces and bills nothing and returns a `BuildCreativeEstimate` with a `cost_low`/`cost_high` band (and `basis`: `fixed` = exact, `estimated_units` = generative projection, `cpm_deferred` = cost accrues at serve). The band is the load-bearing piece — the seller derives it from your actual inputs, so you don't have to guess unit counts.
+- **Cap the call (`max_spend: { amount, currency }`).** A hard stop: the agent produces leaves until the next would push aggregate `vendor_cost` over `amount`, then returns the partial `BuildCreativeVariantSuccess` with `budget_status: "capped"` and an advisory `BUDGET_CAP_REACHED` in `errors[]` — every returned leaf is real and billed, nothing produced is discarded. The leaf-granular shortfall is `leaves_returned` < `leaves_total` (not `items_returned`/`items_total`, which count catalog items and don't capture a variant-only or mid-item cap); the `BUDGET_CAP_REACHED` advisory is the authoritative cap signal. If even the first leaf would exceed the cap, the call fails with a terminal `BUDGET_CAP_REACHED`. `currency` must match the rate card (no FX) or the request is rejected `INVALID_REQUEST` (`error.field: max_spend.currency`). `max_spend` bounds only **build-time** `vendor_cost` — CPM-priced builds (`basis: cpm_deferred`) are 0 at build time and accrue at serve, so the cap never engages; bound a CPM fan-out with `max_creatives` instead.
+
+They compose: estimate to get `cost_high`, then execute with `max_spend` = `cost_high` × a safety margin (CPM builds excepted — their `cost_high` is 0 at build time). `max_spend` caps a **single call**; to bound an autonomous **refinement loop**, track aggregate `vendor_cost` across calls and stop issuing them (a buyer responsibility in this revision — a protocol-level session budget is deferred to the working group).
+
+
+`max_spend` and `mode: "estimate"` require the agent to advertise `creative.supports_spend_controls`; otherwise they're rejected with `UNSUPPORTED_FEATURE`. They're only meaningful with `bills_through_adcp: true` (an out-of-band biller has no AdCP cost to cap against).
+
+
+### Estimate response
+
+A `mode: "estimate"` request returns the `BuildCreativeEstimate` shape (`oneOf` member 4 of 6) — produced and billed nothing, just a projected cost band:
+
+```json test=false
+{
+ "$schema": "/schemas/media-buy/build-creative-response.json",
+ "status": "completed",
+ "mode": "estimate",
+ "estimate": {
+ "items_total": 150,
+ "items_to_produce": 5,
+ "variants_per_item": 3,
+ "leaves_total": 15,
+ "currency": "USD",
+ "cost_low": 6.00,
+ "cost_high": 9.00,
+ "cost_expected": 7.50,
+ "basis": "estimated_units"
+ },
+ "expires_at": "2026-06-01T00:00:00Z"
+}
+```
+
+- **estimate.leaves_total** = `items_to_produce` × `variants_per_item` — the number of billable leaves `mode: "execute"` would produce.
+- **estimate.cost_low / cost_high / cost_expected**: the projected aggregate cost band. The seller derives it from your actual inputs.
+- **estimate.basis**: `fixed` (per-format flat — `cost_low == cost_high`, exact), `estimated_units` (generative `per_unit`; the band reflects unit-count uncertainty), or `cpm_deferred` (CPM — build-time cost 0, accrues at serve, so the band is 0).
+- **estimate.per_leaf** (optional): per-leaf breakdown.
+- Estimates are **advisory / non-binding** in this revision (binding quotes are deferred to the working group).
+
### Field descriptions
- **creative_manifest**: (single-format) The complete creative manifest ready for use with `sync_creatives` or `preview_creative`
diff --git a/docs/creative/task-reference/list_creative_formats.mdx b/docs/creative/task-reference/list_creative_formats.mdx
index 84c620b393..92a9afd432 100644
--- a/docs/creative/task-reference/list_creative_formats.mdx
+++ b/docs/creative/task-reference/list_creative_formats.mdx
@@ -48,8 +48,8 @@ See [list_creative_formats (Sales Agent)](/docs/creative/task-reference/list_cre
| `wcag_level` | string | No | Filter to formats meeting at least this WCAG level: `A`, `AA`, `AAA`. See [Accessibility](/docs/creative/accessibility). |
| `disclosure_positions` | string[] | No | Filter to formats that support all of these disclosure positions. Matches against `disclosure_capabilities` when present, otherwise falls back to `supported_disclosure_positions`. |
| `disclosure_persistence` | string[] | No | Filter to formats whose `disclosure_capabilities` include all of these persistence modes on at least one position. Values: `continuous`, `initial`, `flexible`. |
-| `output_format_ids` | FormatID[] | No | Filter to formats whose `output_format_ids` includes any of these. Returns formats that can produce these outputs — inspect their `input_format_ids` to see what inputs they accept. |
-| `input_format_ids` | FormatID[] | No | Filter to formats whose `input_format_ids` includes any of these. Returns formats that accept these creatives as input — inspect their `output_format_ids` to see what they can produce. |
+| `output_format_ids` | FormatID[] | No | *(deprecated in 3.1)* Filter to formats whose `output_format_ids` includes any of these. Returns formats that can produce these outputs — inspect their `input_format_ids` to see what inputs they accept. Use [`list_transformers`](/docs/creative/task-reference/list_transformers) for build-capability discovery instead. |
+| `input_format_ids` | FormatID[] | No | *(deprecated in 3.1)* Filter to formats whose `input_format_ids` includes any of these. Returns formats that accept these creatives as input — inspect their `output_format_ids` to see what they can produce. Use [`list_transformers`](/docs/creative/task-reference/list_transformers) for build-capability discovery instead. |
| `pagination` | object | No | Pagination: `max_results` (1-100, default 50) and `cursor` (opaque cursor from previous response) |
### Multi-Render Dimension Filtering
@@ -451,6 +451,10 @@ asyncio.run(main())
### Discover Build Capabilities
+
+**Deprecated in 3.1.** Build-capability discovery has moved to [`list_transformers`](/docs/creative/task-reference/list_transformers). A transformer is the account-scoped, agent-offered unit of build capability and carries its own `input_format_ids`, `output_format_ids`, and `pricing_options`. The `input_format_ids` and `output_format_ids` filters on this task — and the matching fields on the [Format object](https://adcontextprotocol.org/schemas/v3/core/format.json) — still work during 3.1–3.x but will be removed in 4.0. Query `list_transformers` to discover what an agent can build and what it costs.
+
+
Some formats declare the output formats they can produce via `output_format_ids`. A creative builder (like a multi-publisher template tool) may accept one asset group and produce many publisher-specific formats. A format transformer may accept an existing creative and reformat it.
The format schema expresses both sides of the relationship:
@@ -579,8 +583,8 @@ Each format includes:
| `type` | *(deprecated)* Format type (audio, video, display, dooh). Use `asset_types` filter instead. |
| `assets` | Array of all assets with `required` boolean indicating mandatory vs optional |
| `renders` | Array of rendered output pieces (dimensions, role) |
-| `input_format_ids` | Creative formats this format accepts as input manifests (omitted for formats that work from raw assets) |
-| `output_format_ids` | Output formats this format can produce (omitted for formats that produce a single fixed output) |
+| `input_format_ids` | *(deprecated in 3.1)* Creative formats this format accepts as input manifests (omitted for formats that work from raw assets). Carried on the transformer instead — see [`list_transformers`](/docs/creative/task-reference/list_transformers). |
+| `output_format_ids` | *(deprecated in 3.1)* Output formats this format can produce (omitted for formats that produce a single fixed output). Carried on the transformer instead — see [`list_transformers`](/docs/creative/task-reference/list_transformers). |
### Asset Roles
diff --git a/docs/creative/task-reference/list_transformers.mdx b/docs/creative/task-reference/list_transformers.mdx
new file mode 100644
index 0000000000..3b381b806e
--- /dev/null
+++ b/docs/creative/task-reference/list_transformers.mdx
@@ -0,0 +1,141 @@
+---
+title: list_transformers
+description: "list_transformers discovers account-scoped creative transformers — the agent-offered, selectable units of build capability (voices, models, styles) used by build_creative."
+"og:title": "AdCP — list_transformers"
+testable: true
+---
+
+
+Discover the **transformers** a creative agent offers for your account. A transformer is the creative analog of a media-buy product: an agent-offered, account-scoped, selectable unit of build capability (a voice, a model, a style, a director) with a typed configuration surface and per-account pricing. You discover transformers here, then select one with `transformer_id` in [`build_creative`](/docs/creative/task-reference/build_creative).
+
+**Response Time**: ~1 second (account-scoped lookup)
+
+**Authentication**: Account-scoped. Transformers, their enumerable option values, and pricing are resolved for the calling credential — including custom values you configured (e.g. cloned voices) that exist only for your account.
+
+**Request Schema**: [`/schemas/v3/creative/list-transformers-request.json`](https://adcontextprotocol.org/schemas/v3/creative/list-transformers-request.json)
+**Response Schema**: [`/schemas/v3/creative/list-transformers-response.json`](https://adcontextprotocol.org/schemas/v3/creative/list-transformers-response.json)
+
+Offered only by agents that set `creative.supports_transformers: true` in [`get_adcp_capabilities`](/docs/protocol/get_adcp_capabilities).
+
+## Why transformers
+
+The set of render knobs a creative agent exposes — and their legal values — is **account-specific and dynamic**. Your configured voices aren't a global enum or a list you hold; the *agent* knows them, and the set changes when you add one. So discovery flows agent → buyer, the same way `get_products` surfaces account-scoped inventory. `list_transformers` is that discovery surface for creative build capability.
+
+The agent chooses granularity: a distinct voice or model may be its own transformer, or a single transformer may expose `voice`/`model` as an enumerable `config` param. Either way you use the same call — list transformers, expand a param if you want its values.
+
+## Request Parameters
+
+| Parameter | Type | Required | Description |
+|-----------|------|----------|-------------|
+| `transformer_ids` | string[] | No | Return only these specific transformer IDs |
+| `input_format_ids` | FormatID[] | No | Filter to transformers that accept any of these formats as input |
+| `output_format_ids` | FormatID[] | No | Filter to transformers that can produce any of these output formats |
+| `name_search` | string | No | Search transformers by name (case-insensitive partial match) |
+| `brief` | string | No | Natural-language brief to rank/filter transformers and their option values (e.g. "warm female Spanish-language voiceover"). Curates to intent rather than returning the full set. |
+| `expand_params` | string[] | No | Param `field` names for which to return the **first page** of account-scoped option **values** inline on `params[].options[]`. Omit for the lean default (descriptors only). |
+| `expand_pagination` | object[] | No | Fetch the **next page** of a specific param's options, scoped per `{ transformer_id, field, options_cursor }` (cursor from a prior response's `params[].options_cursor`). Use once you hold a cursor, instead of `expand_params`. |
+| `include_pricing` | boolean | No | Include `pricing_options` on each transformer. Requires `account`. |
+| `account` | AccountRef | Conditional | Required when `include_pricing` is true. Transformers are account-scoped regardless. |
+| `pagination` | object | No | `max_results` and `cursor` (opaque cursor from previous response) |
+
+### The `expand` mode
+
+By default the response returns each transformer's param **schemas** with small closed enums inlined (e.g. `mastering_preset`). To get the **values** of an account-scoped enumerable param (e.g. your voices), name it in `expand_params` — that returns the **first page** on `params[].options[]`. When a param's values are truncated, its `params[].options_cursor` is set; fetch the next page by passing `{ transformer_id, field, options_cursor }` in **`expand_pagination`** (each `(transformer, param)` pages independently). Values are brief-filtered — "warm Spanish female voice" narrows a 300-voice catalog to a handful instead of dumping all of them. There is no separate options endpoint; value enumeration (and its pagination) is a mode of this one tool.
+
+## Response
+
+| Field | Description |
+|-------|-------------|
+| `transformers` | Array of transformer descriptors (see below) |
+| `errors` | Optional array of task-specific errors and warnings |
+| `pagination` | Pagination cursor when more transformers are available |
+
+Each transformer carries:
+
+| Field | Description |
+|-------|-------------|
+| `transformer_id` | Stable id; pass to `build_creative` `transformer_id` |
+| `name` | Human-readable name |
+| `input_format_ids` / `output_format_ids` | What it accepts and produces. A `build_creative` target MUST be a subset of `output_format_ids`. Empty `input_format_ids` means it builds from a brief (pure generation). |
+| `params` | Config knobs (see [Transformer Param](https://adcontextprotocol.org/schemas/v3/core/transformer-param.json)): `field`, `type`, `value_source` (`inline`/`range`/`enumerable`/`free_text`), `allowed_values`/`minimum`/`maximum`, `options[]`+`options_cursor` (when expanded), `max_length` (for `free_text`), `default`. `free_text` is an open buyer-authored string (e.g. a `negative_prompt`); a param MUST NOT be a generation-count knob — count rides `max_variants`/`max_creatives`. |
+| `pricing_options` | Per-account rate card (when `include_pricing`). Uses the `per_unit` model (e.g. $/generated image, $/second). A pricing option may carry `applies_to_output_format_ids` to price different outputs differently (e.g. a multi-publisher template per publisher format); an unscoped option is the default, and an output matching no option (with no unscoped default) is rejected `UNPRICEABLE_OUTPUT` (no fallback). The applied option is echoed per-leaf on the `build_creative` response and reconciled via `report_usage`. |
+
+See [Transformer schema](https://adcontextprotocol.org/schemas/v3/core/transformer.json) for the complete object.
+
+## Common Scenarios
+
+### Discover voices for a brief, with values
+
+
+
+```javascript test=false
+import { testAgent } from '@adcp/sdk/testing';
+import { ListTransformersResponseSchema } from '@adcp/sdk';
+
+const result = await testAgent.listTransformers({
+ account: { account_id: 'acct_acme' },
+ brief: 'warm female Spanish-language voiceover',
+ output_format_ids: [{ agent_url: 'https://creative.audiostack.example', id: 'audio_vo' }],
+ expand_params: ['voice'],
+ include_pricing: true,
+});
+
+const parsed = ListTransformersResponseSchema.parse(result);
+for (const t of parsed.transformers) {
+ console.log(t.transformer_id, t.name);
+ const voice = t.params?.find((p) => p.field === 'voice');
+ for (const opt of voice?.options ?? []) console.log(' voice:', opt.value, opt.metadata);
+}
+```
+
+```python test=false
+import asyncio
+from adcp.testing import test_agent
+from adcp import ListTransformersResponse
+
+async def main():
+ result = await test_agent.list_transformers(
+ account={"account_id": "acct_acme"},
+ brief="warm female Spanish-language voiceover",
+ output_format_ids=[{"agent_url": "https://creative.audiostack.example", "id": "audio_vo"}],
+ expand_params=["voice"],
+ include_pricing=True,
+ )
+ parsed = ListTransformersResponse.model_validate(result)
+ for t in parsed.transformers:
+ print(t.transformer_id, t.name)
+
+asyncio.run(main())
+```
+
+
+
+### Then build with the selected transformer
+
+Pass the chosen `transformer_id` and a typed `config` (keyed by each param's `field`) to [`build_creative`](/docs/creative/task-reference/build_creative):
+
+```json test=false
+{
+ "transformer_id": "audiostack_voiceover",
+ "config": { "voice": "isaac", "speaking_rate": 1.1, "mastering_preset": "podcast" },
+ "creative_manifest": { "format_id": { "agent_url": "https://creative.audiostack.example", "id": "script" }, "assets": { "script": { "asset_type": "text", "content": "Discover the new winter collection." } } },
+ "target_format_id": { "agent_url": "https://creative.audiostack.example", "id": "audio_vo" },
+ "account": { "account_id": "acct_acme" },
+ "idempotency_key": "0c1d2e3f-4a5b-6c7d-8e9f-a0b1c2d3e4f5"
+}
+```
+
+## Error Handling
+
+| Code | Meaning | Recovery |
+|------|---------|----------|
+| `AUTH_MISSING` | `include_pricing` requested without a resolvable account | Supply `account` / authenticate |
+| `INVALID_REQUEST` | Malformed filter or pagination cursor | Correct the request |
+
+Unknown `expand_params` (a `field` no transformer exposes) are ignored, not an error — the param simply returns no `options`.
+
+## Learn More
+
+- [build_creative](/docs/creative/task-reference/build_creative) — select a transformer and produce creatives (incl. variants)
+- [get_adcp_capabilities](/docs/protocol/get_adcp_capabilities) — `creative.supports_transformers` discriminator
+- [Vendor pricing](https://adcontextprotocol.org/schemas/v3/core/vendor-pricing-option.json) — the `per_unit` rate model
diff --git a/docs/protocol/get_adcp_capabilities.mdx b/docs/protocol/get_adcp_capabilities.mdx
index 18fcfc11b4..1e53f3257d 100644
--- a/docs/protocol/get_adcp_capabilities.mdx
+++ b/docs/protocol/get_adcp_capabilities.mdx
@@ -383,6 +383,11 @@ Creative protocol capabilities. Only present if `creative` is in `supported_prot
| Field | Type | Description |
|-------|------|-------------|
| `supports_compliance` | boolean | When `true`, this creative agent can process briefs with compliance requirements (`required_disclosures`, `prohibited_claims`) and will validate that disclosures can be satisfied by the target format. Use the `disclosure_positions` filter on `list_creative_formats` to find compatible formats. |
+| `supports_transformers` | boolean | When `true`, this creative agent offers account-scoped transformers — the selectable units of build capability (voices, models, styles) discovered via [`list_transformers`](/docs/creative/task-reference/list_transformers) and selected with `transformer_id` (plus the typed `config` bag) on [`build_creative`](/docs/creative/task-reference/build_creative). When `false` or absent, the agent does not expose transformers; `list_transformers` is unavailable and `build_creative` ignores `transformer_id`/`config`. Pre-call discriminator for routing across creative agents. |
+| `supports_refinement` | boolean | When `true`, this creative agent retains produced `build_variant` leaves (for an agent-defined window) and can re-build from one via `refine_from_build_variant_id` on [`build_creative`](/docs/creative/task-reference/build_creative) — applying a natural-language instruction in `message` plus an optional `config` delta, returning new lineage-linked variants. A build-time capability independent of generation/transformation. When `false` or absent, `refine_from_build_variant_id` returns `UNSUPPORTED_FEATURE`; refine via the transform path (`creative_manifest` + `message`) instead. |
+| `refinable_retention_seconds` | integer | When `supports_refinement` is `true`, the **guaranteed-minimum** window (a floor, not a ceiling) during which a produced `build_variant_id` stays refinable via `refine_from_build_variant_id`. A ref within the window SHOULD resolve; the agent MAY retain longer. Omit to leave the window agent-defined (buyers treat refinability as best-effort and handle `REFERENCE_NOT_FOUND`). |
+| `multiplicity` | object | Pre-call fan-out discriminators so a buyer knows before sending `max_creatives`/`max_variants`: `supports_catalog_fanout` + `max_creatives_limit`, `supports_variants` + `max_variants_limit`, and `variant_dimensions[]` (which `variant_axis.dimension` values are supported). Over-limit requests are **clamped** to the ceilings (shortfall shown via `items_returned` < `items_total`), not rejected. Absent means no fan-out — `build_creative` produces a single creative. Individual transformers may narrow this via `transformer.multiplicity`. |
+| `supports_spend_controls` | boolean | When `true`, `build_creative` honors a per-call `max_spend` ceiling (returns a partial paid build with `budget_status: "capped"` + a `BUDGET_CAP_REACHED` advisory rather than overspending) and supports `mode: "estimate"` dry-runs (a projected cost band, producing/billing nothing). When `false` or absent, both are rejected with `UNSUPPORTED_FEATURE`. Meaningful only with `bills_through_adcp: true`. |
| `bills_through_adcp` | boolean | When `true`, this creative agent bills through the AdCP rate-card surface — `list_creatives` returns `pricing_options` (with `include_pricing=true` and an authenticated account), `build_creative` populates `pricing_option_id` and `vendor_cost`, and `report_usage` accepts records against the rate card. When `false` or absent, the agent bills out of band (flat license, SaaS contract, bundled enterprise agreement); buyers should skip pricing fields and tolerate `report_usage` returning `accepted: 0` with `BILLING_OUT_OF_BAND` errors. Pre-call discriminator for routing across creative agents. |
### governance
diff --git a/docs/protocol/required-tasks.mdx b/docs/protocol/required-tasks.mdx
index 8a0a2e7318..b5fe797a3d 100644
--- a/docs/protocol/required-tasks.mdx
+++ b/docs/protocol/required-tasks.mdx
@@ -66,6 +66,7 @@ Orchestrators are not MCP/A2A servers — they call sales agent tasks. Conforman
| Task | Requirement | Notes |
|------|------------|-------|
| `list_creative_formats` | Required | Format discovery with technical specs |
+| `list_transformers` | Conditional | Required when the agent advertises `creative.supports_transformers: true` |
| `build_creative` | Optional | Generation, transformation, or library retrieval |
| `preview_creative` | Optional | Preview rendering |
| `list_creatives` | Conditional | Required when `has_creative_library: true` |
diff --git a/docs/reference/migration/creative-transformers.mdx b/docs/reference/migration/creative-transformers.mdx
new file mode 100644
index 0000000000..f57b30c7b3
--- /dev/null
+++ b/docs/reference/migration/creative-transformers.mdx
@@ -0,0 +1,86 @@
+---
+title: "Migrating to creative transformers (3.1)"
+description: "Move build-capability discovery and creative pricing off Format fields onto transformers (list_transformers). Additive in 3.1; deprecated Format fields removed at 4.0."
+"og:title": "AdCP — Migrating to creative transformers"
+---
+
+# Migrating to creative transformers (3.1)
+
+AdCP 3.1 introduces **transformers** — the creative analog of a media-buy product: an agent-offered, account-scoped, selectable unit of build capability (a voice, model, style, or director) with a typed configuration surface and per-account pricing, discovered via [`list_transformers`](/docs/creative/task-reference/list_transformers) and selected with `transformer_id` on [`build_creative`](/docs/creative/task-reference/build_creative).
+
+Because a transformer carries **its own input/output formats and its own pricing**, three things that 3.0 hung on the *format* are now redundant and are deprecated:
+
+- `Format.input_format_ids` / `Format.output_format_ids`
+- `Format.pricing_options`
+- the `input_format_ids` / `output_format_ids` discovery **filters** on `list_creative_formats`
+
+**Nothing breaks if you do nothing in 3.1.** All four are `deprecated: true` but still validate and still function through the 3.1–3.x line. Act before 4.0. The hazards below are about *silent degradation* over the window, not hard breaks.
+
+## What changed
+
+| Surface | 3.0 | 3.1 | Removed |
+|---|---|---|---|
+| Build-capability discovery | filter `list_creative_formats` on `input_format_ids` / `output_format_ids`; read `Format.input_format_ids` / `output_format_ids` | discover via `list_transformers` (each transformer declares its own I/O signature) | 4.0 |
+| Creative/transform pricing | `Format.pricing_options` (per format) | `transformer.pricing_options` (per transformer; `include_pricing` + `account` on `list_transformers`) | 4.0 |
+| `list_creative_formats` `input_format_ids` / `output_format_ids` filters | supported | `deprecated: true` — use `list_transformers` filters + `brief` | 4.0 |
+
+## Migrate
+
+### Format I/O signature → transformer
+
+A transformer declares the formats it accepts and produces directly, so build capability is a property of the *selectable unit*, not a relationship hung on a format.
+
+**Before (3.0)** — a transform format declares what it consumes/produces:
+```json test=false
+{
+ "format_id": { "agent_url": "https://creative.example", "id": "resize_to_meta" },
+ "input_format_ids": [{ "agent_url": "https://creative.example", "id": "display_master" }],
+ "output_format_ids": [{ "agent_url": "https://creative.example", "id": "meta_reels_9x16" }]
+}
+```
+
+**After (3.1)** — declare a transformer (discovered via `list_transformers`):
+```json test=false
+{
+ "transformer_id": "resize_to_meta",
+ "name": "Resize to Meta Reels",
+ "input_format_ids": [{ "agent_url": "https://creative.example", "id": "display_master" }],
+ "output_format_ids": [{ "agent_url": "https://creative.example", "id": "meta_reels_9x16" }],
+ "params": [],
+ "pricing_options": [ { "pricing_option_id": "per_format", "model": "per_unit", "unit": "format", "unit_price": 0.10, "currency": "USD" } ]
+}
+```
+
+### Format pricing → transformer pricing
+
+`Format.pricing_options` moves to `transformer.pricing_options` (same `vendor-pricing-option` shape; `per_unit` is typical). The applied option is echoed per-leaf on the `build_creative` response and reconciled via `report_usage`, unchanged.
+
+> **Per-output pricing does not survive the move unchanged.** `Format.pricing_options` could price each output format differently; `transformer.pricing_options` is one rate card for the whole transformer with no edge to a specific `output_format_id`. A multi-publisher template that priced outputs differently must **split into one transformer per price point** (e.g. one transformer per publisher), not a single transformer with many outputs.
+
+### `list_creative_formats` discovery filters → `list_transformers`
+
+Stop filtering `list_creative_formats` on `input_format_ids` / `output_format_ids` to find "what can build X". Use `list_transformers` — filter on its `input_format_ids` / `output_format_ids`, narrow with `brief`, and expand account-scoped option values with `expand_params`.
+
+## What breaks silently if you do nothing
+
+These do not throw — which is exactly why they are the reason this guide exists.
+
+1. **Discovery-read degradation (buyers).** If your buyer/storyboard learns build capability *only* by reading `Format.input_format_ids` / `output_format_ids` or by filtering `list_creative_formats` on them, you keep working against a 3.1 seller but see **progressively emptier results** as sellers stop emitting the deprecated fields and move capability onto transformers. No error — just fewer and fewer options. **The field outlives its data.** Move discovery to `list_transformers`.
+
+2. **Per-output pricing loss (multi-publisher template sellers).** See the pricing note above — a template that priced outputs differently silently mis-states pricing after a naïve `Format.pricing_options` → `transformer.pricing_options` lift. Split into per-price-point transformers.
+
+3. **Fan-out / best-of-N spend under-counting (billing pipelines).** With `max_variants` / `max_creatives`, every produced variant is billed, but only a **trafficked** leaf lazily earns a `creative_id`. Discarded best-of-N leaves are billed via the **inline per-leaf `vendor_cost`** on the `build_creative` response *only* — they never earn a `creative_id` and so never appear in `report_usage`. A pipeline that reconciles spend solely from `report_usage` (the 3.0 invariant "every billed unit reconciles via `creative_id`") will **under-count** by every discarded variant. Ingest the inline per-leaf receipts as the authoritative record for untrafficked leaves.
+
+## SDK behavior and timeline
+
+| Version | Deprecated fields |
+|---|---|
+| 3.1 | `deprecated: true`; still emitted and honored. SDKs MUST continue to read them. |
+| 3.1–3.x | Honored throughout. New code SHOULD NOT emit them. |
+| 4.0+ | SDKs MAY reject. Removed. |
+
+## See also
+
+- [`list_transformers`](/docs/creative/task-reference/list_transformers) — discover transformers, their params, option values, and pricing
+- [`build_creative`](/docs/creative/task-reference/build_creative) — select a transformer, configure, multiply, refine
+- [What's new in 3.1](/docs/reference/whats-new-in-3-1)
diff --git a/docs/reference/whats-new-in-3-1.mdx b/docs/reference/whats-new-in-3-1.mdx
index ff7dba93cc..d1688c7d4c 100644
--- a/docs/reference/whats-new-in-3-1.mdx
+++ b/docs/reference/whats-new-in-3-1.mdx
@@ -29,6 +29,7 @@ This page is the curated adopter overview for the 3.1 minor release. Need the fu
- **You can pin a release and stop fighting drift.** Release-precision `adcp_version` (`"3.1"` at GA, exact pre-releases such as `"3.1-rc.4"` during validation) on every request; sellers advertise their full `supported_versions` set and echo what they actually served. SDK constructor pins are now a real thing.
- **Sub-brands self-publish.** A brand can publish its own canonical `brand.json` on its own domain while the corporate house declares ownership via a portfolio pointer — same reciprocal pattern as IAB's `ads.txt` / `sellers.json`.
- **Creative formats have a canonical vocabulary.** 12 canonical `format_kind` values + a publisher catalog discovery surface + a projection-ref mechanism. Creative agents also advertise buildable canonical outputs through `creative.supported_formats[]`; targetable entries SHOULD include stable `capability_id` values for `build_creative` routing.
+- **You can discover and select creative transformers.** A new `list_transformers` task surfaces the account-scoped, agent-offered build units — voices, models, styles — the creative analog of media-buy products, with an `expand_params` mode that returns the enumerable option values (e.g. your configured voices) on the same tool. `build_creative` selects one with `transformer_id`, configures it with a typed `config` bag (strict validation — unknown keys and out-of-range values are rejected with field-attributed errors; vendor knobs go in `ext`), and fans out across catalog items (`max_creatives`) and alternatives (`max_variants` + `variant_axis`, with `keep_mode` advisory). A new `BuildCreativeVariantSuccess` response member carries per-variant manifests, recommendation/rank, and a per-leaf pricing receipt. Pricing moves onto the transformer (`pricing_options` `per_unit`), settled via `report_usage`.
- **Video inventory can declare placement semantics.** Products and placements can declare OpenRTB-aligned `video_placement_types` so buyers can distinguish instream, accompanying-content, interstitial, and standalone video.
- **Verification badges are version-scoped.** Public 3.1 badge issuance can run alongside active 3.0 badges; buyers pinned to a release read the matching badge version.
- **Action discovery and proposals are mechanical.** Products advertise `allowed_actions[]`; media buys carry `available_actions[]`; proposals use `proposal_status` to say whether finalize is still required before `create_media_buy(proposal_id)`.
@@ -52,6 +53,7 @@ Plus a long tail of error-code clarity, auth tightening, idempotency rules, TMP
| **Package signal targeting** | No grouped buy-time surface for seller-offered named signals; storefronts overloaded audience fields or brief text | `targeting_overlay.signal_targeting_groups`: top-level `operator: "all"` with child `any` include groups and `none` exclusion groups. Product `signal_targeting_rules` declares selection mode, direct vs seller-planned resolution, and group limits |
| **Wholesale feed webhooks** | None | Account-level webhooks registered via `sync_accounts.accounts[].notification_configs[]` carry `product.*` / `signal.*` / `wholesale_feed.bulk_change` change payloads with `applies_to.scope`; standard webhook signing and SSRF guards apply |
| **Creative formats** | Format-by-name with per-publisher variants | 12 canonical `format_kind` values + publisher catalog discovery (`adagents.json formats[]`) + `v1_format_ref` for dual emission + size flexibility (fixed / multi-size / responsive); creative agents advertise buildable outputs in `creative.supported_formats[]` and SHOULD include `capability_id` on targetable entries |
+| **Creative transformers** | `build_creative` built to a target format; render knobs implicit; format-attached `Format.input_format_ids` / `output_format_ids` / `pricing_options` | `list_transformers` discovers account-scoped build units (voices/models/styles) with an `expand_params` option-enumeration mode; `build_creative` selects one via `transformer_id`, takes a strictly-validated typed `config`, and fans out with `max_creatives` (per-catalog-item) + `max_variants`/`variant_axis` + advisory `keep_mode`; new `BuildCreativeVariantSuccess` member returns per-variant manifests, recommendation/`rank`, and per-leaf pricing receipts; rate lives on `transformer.pricing_options` (`per_unit`), settled via `report_usage`. The shipped `BuildCreativeSuccess` / `BuildCreativeMultiSuccess` are unchanged |
| **Version negotiation** | Integer `adcp_major_version` per request | Release-precision `adcp_version` (e.g. `"3.1"` at GA or exact prereleases like `"3.1-rc.4"`) + `adcp.supported_versions` advertisement + envelope echo. Integer field remains as backwards-compatible legacy |
| **Optimization goals** | `event` + `metric` kinds, vendor-agnostic | New `vendor_metric` kind — bind goals to vendor-attested metrics; `vendor_metric_optimization` per-product capability; three-precondition rejection rule |
| **Capability declarations** | Per-protocol basics | New: `media_buy.buying_modes` for wholesale products, `signals.discovery_modes` for wholesale signals, `wholesale_feed_versioning`, `wholesale_feed_webhooks`, `supported_optimization_metrics`, `supported_target_kinds`, `media_buy.frequency_capping`, `media_buy.propagation_surfaces`, `creative.bills_through_adcp`, `capabilities.idempotency.in_flight_max_seconds` |
@@ -159,6 +161,28 @@ Live in 3.1, additive over 3.0. Products carry `format_options[]`: a list of `Pr
→ Spec: [Canonical formats](/docs/creative/canonical-formats) · PRs [#3307](https://github.com/adcontextprotocol/adcp/pull/3307), [#4770](https://github.com/adcontextprotocol/adcp/pull/4770)
+### Creative transformers — discover build capability, select, fan out, variant
+
+3.1 introduces **transformers**: the creative analog of media-buy products. A transformer is an agent-offered, account-scoped, selectable unit of build capability — a voice, a model, a style, a director — with a typed configuration surface and per-account pricing. The set is account-specific and dynamic (your configured voices aren't a global enum), so discovery flows agent → buyer the same way `get_products` surfaces account-scoped inventory.
+
+- **`list_transformers`** is the new discovery surface. It returns the transformers a creative agent offers for your account, each with `input_format_ids` / `output_format_ids`, a typed param schema, and (with `include_pricing`) a `per_unit` rate card. Its `expand_params` mode returns the account-scoped enumerable option values for a param — your actual configured voices, for example — on the same tool, instead of forcing you to hold a stale local list. Offered only by agents that set `creative.supports_transformers: true` in `get_adcp_capabilities`.
+
+- **`build_creative` selects and configures a transformer.** Pass `transformer_id` to pick one — the target format(s) MUST be a subset of its `output_format_ids` — and a typed `config` bag keyed to the transformer's params. Validation is strict: the agent MUST reject unknown keys and out-of-range values with field-attributed errors; vendor-specific knobs go in `ext`.
+
+- **Two fan-out axes.** `max_creatives` is the item/catalog axis: N distinct creatives, one per catalog item ("5 of 150" sampling) — distinct from `item_limit`, which caps the items used *within* one creative. `max_variants` (default 1) produces alternatives per creative along a `variant_axis` (`voice` | `theme` | `best_of_n` | `transformer_config` | `custom`, with optional `values[]` and `label`). `keep_mode` (`keep_all` | `keep_one` | `keep_some`) is advisory. Resolutions and quality levels are the **format** axis (`target_format_ids`), not variants.
+
+- **New `BuildCreativeVariantSuccess` response member** (the 5th success shape) carries `creatives[]`, each `{ build_creative_id, catalog_item_ref?, variants[] }`; each variant is `{ build_variant_id, creative_manifest, variant_axis_value?, recommended, rank?, ... }` with a per-leaf pricing receipt (`pricing_option_id` + `vendor_cost` + `currency` + `consumption`). Top-level `items_total` / `items_returned` plus an aggregate `vendor_cost`. The shipped `BuildCreativeSuccess` / `BuildCreativeMultiSuccess` are **unchanged**.
+
+- **Best-of-N is variants + `keep_mode` + `recommended`/`rank`.** `build_variant_id` is its own namespace — never reuse `preview_id` (preview renders) or served `variant_id` (delivery). You **pay for all produced variants** (`per_unit` × N); keeping is a client act of trafficking the chosen `build_variant_id`(s). A kept variant lazily earns a `creative_id` (added to the library / first trafficked) which flows to `report_usage`. Per-**format** generation is atomic; per-**item** (catalog fan-out) is non-atomic.
+
+- **Pricing moves onto the transformer.** The rate lives on `transformer.pricing_options` (`per_unit`), echoed inline as a per-leaf receipt on `build_creative`, and settled via `report_usage`. `Format.pricing_options` is **deprecated** in favor of `transformer.pricing_options`.
+
+
+**Deprecations (3.1, removed in 4.0).** `Format.input_format_ids`, `Format.output_format_ids`, and `Format.pricing_options`, plus the `input_format_ids` / `output_format_ids` filters on `list_creative_formats`, are deprecated and all redirect to `list_transformers`. SDKs honor them through 3.1–3.x; they are removed at 4.0. Migrate format-attached input/output/pricing reads to `list_transformers`. Full migration (incl. the discovery-degradation, per-output-pricing, and best-of-N spend hazards): [Migration › Creative transformers](/docs/reference/migration/creative-transformers).
+
+
+→ Spec: [`list_transformers`](/docs/creative/task-reference/list_transformers) · [`build_creative`](/docs/creative/task-reference/build_creative) · [`get_adcp_capabilities` § `creative.supports_transformers`](/docs/protocol/get_adcp_capabilities)
+
### Release-precision version negotiation — pin your release
Every request and response now carries `adcp_version` (release-precision: `"3.1"` at GA, exact pre-releases like `"3.1-rc.4"` during validation); sellers advertise their full `supported_versions` set on `get_adcp_capabilities` and echo the release they actually served at the envelope root. SDKs pin via a constructor option (`adcpVersion: "3.1"` JS, `adcp_version="3.1"` Python, `WithAdcpVersion("3.1")` Go at GA; exact prerelease strings before GA) and emit both the new string and the integer `adcp_major_version` mirror for compatibility with sellers that only read the legacy field. The integer remains functional through all of 3.x — additive ship, no required changes for 3.0-conformant agents. `VERSION_UNSUPPORTED` is typed with `error.data.supported_versions[]` echoed so retry doesn't require an out-of-band lookup.
@@ -278,6 +302,7 @@ Normative tightenings landed as the spec settled through beta and RC. Mostly low
| A sales / signals agent serving wholesale feed mirrors at scale | Declare `wholesale_feed_webhooks.supported: true` and support webhook registration through `sync_accounts.accounts[].notification_configs[]`, with standard account-level webhook signing, endpoint proof-of-control before activation, SSRF guards, account/caller authorization checks, and the notification-config fan-out cap. Keep `event_types[]` consistent with declared repair reads: `product.*` requires wholesale `get_products`, `signal.*` requires wholesale `get_signals`. The webhook emitter MUST include the actual changed product/signal payload or bulk-change summary and apply the same per-caller scope filter as your wholesale task at event-emission time — multi-tenant agents that can't reliably scope events per-principal MUST NOT declare the capability. |
| A measurement vendor (attention, brand lift, emissions, retail) | Publish your experimental `measurement.metrics[]` catalog at your AdCP agent and declare `experimental_features: ["measurement.core"]`. Sellers declaring `vendor_metric_optimization` per product can now bind optimization goals to your `(vendor, metric_id)` pairs. |
| A creative agent | Declare `capabilities.creative.bills_through_adcp` honestly. If you bill out of band, reject `report_usage` calls with `BILLING_OUT_OF_BAND` rather than silently accepting. |
+| A creative agent offering transformers | Declare `creative.supports_transformers: true` and serve `list_transformers` (including the `expand_params` option-enumeration mode). On `build_creative`, validate the typed `config` strictly — reject unknown keys and out-of-range values with field-attributed errors, route vendor knobs through `ext`, and verify each target format is a subset of the transformer's `output_format_ids`. Return `BuildCreativeVariantSuccess` with per-leaf pricing receipts when fanning out across catalog items or variants. Move rate cards from `Format.pricing_options` to `transformer.pricing_options` and settle through `report_usage`. |
| Maintaining SDK fixtures, compliance mirrors, or release packaging | Keep compliance vector and test-kit references inside the packaged `/compliance/{version}/` tree. Treat missing bundle-relative references as release blockers. |
| An SDK author | Pin `published_version` to the published 3.1 artifact you bundle; emit `adcp_version` (release-precision string) plus `adcp_major_version` (integer mirror); normalize semver values to release-precision before wire emission (`"3.1.0-rc.4"` → `"3.1-rc.4"`); surface `VERSION_UNSUPPORTED` as a typed error rather than auto-downshifting. |
diff --git a/scripts/error-code-drift-dispositions.json b/scripts/error-code-drift-dispositions.json
index 341ef37ce1..097f2b01bc 100644
--- a/scripts/error-code-drift-dispositions.json
+++ b/scripts/error-code-drift-dispositions.json
@@ -1,6 +1,16 @@
{
"$comment": "Disposition registry for error codes that are present in the current source enum but absent from the 3.0.x maintenance branch. Read by scripts/lint-error-code-drift.cjs. Every code in the AHEAD set (main \\ 3.0.x) MUST have an entry here; codes missing from this file fail the lint. Policy: 3.0.x is wire-stable. Adding a new enum value is a wire change (a 3.0.x receiver decoding a 3.1 sender's error.code cannot match against an enum it doesn't carry, and JSON Schema enum is closed by default), so the default disposition for any new code is 'held-for-next-minor' with target_version='3.1'. 'held-for-next-major' (target_version='4.0') is for codes that should defer past 3.x \u2014 typically because the surrounding subsystem is being reworked. 'backport-pending' is reserved for prose-only or doc-comment-only changes to a code that already exists on 3.0.x; the lint rejects backport-pending entries whose code is not on 3.0.x. Valid `disposition` values: 'backport-pending', 'held-for-next-minor', 'held-for-next-major', 'unclassified'. `target_version` (required for held-*) is a 'MAJOR.MINOR' string. When a code lands on 3.0.x its entry SHOULD be removed; the lint flags stale entries.",
"dispositions": {
+ "BUDGET_CAP_REACHED": {
+ "disposition": "held-for-next-minor",
+ "target_version": "3.1",
+ "note": "build_creative spend controls — emitted when a per-call max_spend ceiling stops production early (advisory on a partial BuildCreativeVariantSuccess, or terminal when even the first leaf exceeds the cap). New 3.1 creative-transformer surface; wire change — held for 3.1."
+ },
+ "UNPRICEABLE_OUTPUT": {
+ "disposition": "held-for-next-minor",
+ "target_version": "3.1",
+ "note": "Creative transformers per-output pricing (transformer.pricing_options applies_to_output_format_ids) — emitted when a build targets an output no pricing option covers and none is unscoped. New 3.1 creative-transformer surface; wire change — held for 3.1."
+ },
"ACTION_NOT_ALLOWED": {
"disposition": "held-for-next-minor",
"target_version": "3.1",
diff --git a/scripts/oneof-discriminators.baseline.json b/scripts/oneof-discriminators.baseline.json
index 8f76cac6eb..b5ad990fd0 100644
--- a/scripts/oneof-discriminators.baseline.json
+++ b/scripts/oneof-discriminators.baseline.json
@@ -84,7 +84,7 @@
"brand/verify-brand-claim-response.json##/oneOf": {
"kind": "narrowable",
"variants": 2,
- "note": "0:[claim_type,verification_status] | 1:[errors]"
+ "note": "0:[claim_type,verification_status,signed_response] | 1:[errors]"
},
"brand/verify-brand-claims-response.json##/definitions/result_entry/oneOf": {
"kind": "narrowable",
@@ -94,7 +94,7 @@
"brand/verify-brand-claims-response.json##/oneOf": {
"kind": "narrowable",
"variants": 2,
- "note": "0:[results] | 1:[errors]"
+ "note": "0:[results,signed_response] | 1:[errors]"
},
"compliance/comply-test-controller-response.json##/oneOf": {
"kind": "dangerous",
@@ -253,13 +253,13 @@
},
"media-buy/build-creative-response.json##/oneOf": {
"kind": "narrowable",
- "variants": 4,
- "note": "0:[creative_manifest] | 1:[creative_manifests] | 2:[errors] | 3:[status,task_id]"
+ "variants": 6,
+ "note": "0:[creative_manifest] | 1:[creative_manifests] | 2:[creatives] | 3:[mode,estimate] | 4:[errors] | 5:[status,task_id]"
},
"media-buy/create-media-buy-response.json##/oneOf": {
"kind": "narrowable",
"variants": 3,
- "note": "0:[media_buy_id,packages] | 1:[errors] | 2:[status,task_id]"
+ "note": "0:[media_buy_id,confirmed_at,revision,packages] | 1:[errors] | 2:[status,task_id]"
},
"media-buy/get-media-buy-delivery-request.json##/properties/reporting_dimensions/properties/geo/properties/system/oneOf": {
"kind": "dangerous",
@@ -309,7 +309,7 @@
"media-buy/update-media-buy-response.json##/oneOf": {
"kind": "narrowable",
"variants": 3,
- "note": "0:[media_buy_id] | 1:[errors] | 2:[status,task_id]"
+ "note": "0:[media_buy_id,revision] | 1:[errors] | 2:[status,task_id]"
},
"pricing-options/cpv-option.json##/properties/parameters/properties/view_threshold/oneOf": {
"kind": "dangerous",
diff --git a/server/src/services/adcp-taxonomy.ts b/server/src/services/adcp-taxonomy.ts
index eb25b5a599..640a09842e 100644
--- a/server/src/services/adcp-taxonomy.ts
+++ b/server/src/services/adcp-taxonomy.ts
@@ -94,6 +94,7 @@ export type AdcpSpecialism =
| 'creative-ad-server'
| 'creative-generative'
| 'creative-template'
+ | 'creative-transformers'
| 'governance-aware-seller'
| 'governance-delivery-monitor'
| 'governance-spend-authority'
@@ -117,6 +118,7 @@ export const ADCP_SPECIALISMS: readonly AdcpSpecialism[] = [
'creative-ad-server',
'creative-generative',
'creative-template',
+ 'creative-transformers',
'governance-aware-seller',
'governance-delivery-monitor',
'governance-spend-authority',
diff --git a/server/src/training-agent/task-handlers.ts b/server/src/training-agent/task-handlers.ts
index 4b1594d465..8879873ff5 100644
--- a/server/src/training-agent/task-handlers.ts
+++ b/server/src/training-agent/task-handlers.ts
@@ -3176,6 +3176,258 @@ export async function handleListCreativeFormats(args: ToolArgs, _ctx: TrainingCo
};
}
+// ── Transformers (list_transformers + build_creative transformer path) ──────
+//
+// A transformer is the creative analog of a media-buy product: an
+// agent-offered, account-scoped, selectable unit of build capability
+// (a voice, a model, a style) with a typed config surface. The reference
+// agent exposes one static transformer; real agents resolve account-scoped
+// option values (e.g. cloned voices) per credential. Enumerable option VALUES
+// are returned only when the param's field is named in expand_params.
+
+interface TransformerParamOption {
+ value: unknown;
+ label?: string;
+ metadata?: Record;
+}
+
+interface TransformerParam {
+ field: string;
+ type: 'string' | 'number' | 'integer' | 'boolean';
+ value_source: 'inline' | 'range' | 'enumerable' | 'free_text';
+ allowed_values?: unknown[];
+ minimum?: number;
+ maximum?: number;
+ max_length?: number;
+ default?: unknown;
+ required?: boolean;
+ description?: string;
+}
+
+interface TrainingTransformer {
+ transformer_id: string;
+ name: string;
+ description?: string;
+ metadata?: Record;
+ input_format_ids?: FormatID[];
+ output_format_ids: FormatID[];
+ params: TransformerParam[];
+ pricing_options?: Array>;
+ multiplicity?: Record;
+ // Full account-scoped option pool for enumerable params, surfaced on
+ // params[].options[] only when expand_params names the field.
+ enumerableOptions?: Record;
+}
+
+// The agent advertises this as multiplicity.max_variants_limit and enforces it
+// on build_creative — a variant request above it is clamped (not rejected),
+// signalling the shortfall via leaves_returned < leaves_total.
+const TRANSFORMER_MAX_VARIANTS_LIMIT = 10;
+
+function getTransformers(): TrainingTransformer[] {
+ const agentUrl = getAgentUrl();
+ return [
+ {
+ transformer_id: 'audiostack_voiceover',
+ name: 'Voiceover',
+ description: 'Script-to-audio voiceover with account-configured voices.',
+ metadata: { provider: 'audiostack', modality: 'audio' },
+ input_format_ids: [{ agent_url: agentUrl, id: 'script' }],
+ output_format_ids: [{ agent_url: agentUrl, id: 'audio_vo' }],
+ params: [
+ { field: 'voice', type: 'string', value_source: 'enumerable', default: 'sara', description: 'Narration voice, incl. account-specific custom/cloned voices.' },
+ { field: 'mastering_preset', type: 'string', value_source: 'inline', allowed_values: ['broadcast', 'podcast', 'music'], default: 'broadcast', description: 'Audio mastering profile applied to the final mix.' },
+ { field: 'speaking_rate', type: 'number', value_source: 'range', minimum: 0.5, maximum: 2.0, default: 1.0, description: 'Narration speed multiplier.' },
+ { field: 'pronunciation_note', type: 'string', value_source: 'free_text', max_length: 280, description: 'Optional free-text pronunciation/style guidance.' },
+ ],
+ pricing_options: [
+ { pricing_option_id: 'vo_per_second_standard', model: 'per_unit', unit: 'second', unit_price: 0.05, currency: 'USD' },
+ ],
+ multiplicity: { supports_catalog_fanout: false, supports_variants: true, max_variants_limit: TRANSFORMER_MAX_VARIANTS_LIMIT, variant_dimensions: ['voice', 'best_of_n', 'transformer_config'] },
+ enumerableOptions: {
+ voice: [
+ { value: 'sara', label: 'Sara', metadata: { language: 'en-US', gender: 'female', provider: 'audiostack' } },
+ { value: 'isaac', label: 'Isaac', metadata: { language: 'en-US', gender: 'male', provider: 'audiostack' } },
+ { value: 'mateo', label: 'Mateo', metadata: { language: 'es-ES', gender: 'male', provider: 'audiostack' } },
+ { value: 'ceo_clone_2026', label: 'CEO (custom)', metadata: { language: 'en-US', custom: true } },
+ ],
+ },
+ },
+ ];
+}
+
+interface ListTransformersArgs {
+ transformer_ids?: string[];
+ input_format_ids?: FormatID[];
+ output_format_ids?: FormatID[];
+ name_search?: string;
+ brief?: string;
+ expand_params?: string[];
+ expand_pagination?: Array<{ transformer_id?: string; field?: string; options_cursor?: string }>;
+ include_pricing?: boolean;
+ account?: unknown;
+ pagination?: { max_results?: number; cursor?: string };
+}
+
+export async function handleListTransformers(args: ToolArgs, _ctx: TrainingContext): Promise