Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .changeset/2881-2882-creative-billing-schema.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
"adcontextprotocol": minor
---

spec(creative): add `bills_through_adcp` capability + `BILLING_OUT_OF_BAND` error.

PR #2879 softened the creative-ad-server conformance so ad servers that bill out of band (flat license, SaaS contract, bundled enterprise — CM360 is the canonical case) stay spec-valid without returning `pricing_options`. Two follow-ups close that loop on the wire:

- `capabilities.creative.bills_through_adcp` (boolean, default false/absent) on the `get_adcp_capabilities` response — a pre-call discriminator so buyer agents can pre-filter creative agents across a portfolio before establishing an account just to probe pricing. When `true`, buyers can expect `pricing_options` on `list_creatives`, `pricing_option_id`/`vendor_cost` on `build_creative`, and `report_usage` that accepts records against the rate card.
- `BILLING_OUT_OF_BAND` (recovery: terminal) on the error-code enum — the standard code for a per-record `report_usage` rejection where the record is well-formed but the account bills via a non-AdCP channel. Distinct from `BILLING_NOT_SUPPORTED` (media-buy `billing`-value rejection) and `BILLING_NOT_PERMITTED_FOR_AGENT` (per-buyer-agent commercial gate) — signals that the entire billing surface is offline for this account, not that a specific value or caller is rejected. The code itself is the discriminator; no `error.details` shape is defined (mirroring `CONFIGURATION_ERROR`).

Strictly additive — no existing agents break. Agents that don't declare `bills_through_adcp` remain in the probe-to-discover mode buyers already tolerate. Both follow `held-for-next-minor` / 3.1 on the drift registry.

Closes #2881, #2882. Builds on #2879.

Files:
- `static/schemas/source/protocol/get-adcp-capabilities-response.json` — `bills_through_adcp` added to the `creative` block alongside `has_creative_library` / `supports_generation` / `supports_transformation` / `supports_compliance`.
- `static/schemas/source/enums/error-code.json` — `BILLING_OUT_OF_BAND` in enum, `enumDescriptions`, and `enumMetadata`.
- `scripts/error-code-drift-dispositions.json` — `held-for-next-minor` / `3.1` entry.
- `specs/creative-agent-pricing.md` — pre-account-discovery and capabilities-change sections updated.
- `static/compliance/source/specialisms/creative-ad-server/index.yaml` — `report_usage` narrative references the standard code (replaces "vendor codes are fine today" placeholder).
- `docs/protocol/get_adcp_capabilities.mdx` — capability table row + example.
4 changes: 3 additions & 1 deletion docs/protocol/get_adcp_capabilities.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,7 @@ 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. |
| `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

Expand Down Expand Up @@ -925,7 +926,8 @@ An agent can implement multiple protocols from a single endpoint. This is common
"has_creative_library": true,
"supports_generation": true,
"supports_transformation": false,
"supports_compliance": false
"supports_compliance": false,
"bills_through_adcp": true
}
}
```
Expand Down
5 changes: 5 additions & 0 deletions scripts/error-code-drift-dispositions.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@
"target_version": "3.1",
"note": "From sync_accounts billing coverage (#3831). Adjacent to but distinct from 3.0.x's ACCOUNT_PAYMENT_REQUIRED (which signals payment due on an existing account); BILLING_NOT_SUPPORTED signals the seller doesn't support the requested billing arrangement. Not a rename. Held for 3.1."
},
"BILLING_OUT_OF_BAND": {
"disposition": "held-for-next-minor",
"target_version": "3.1",
"note": "Creative-agent report_usage rejection for accounts that bill via a non-AdCP channel (flat license, SaaS contract, bundled enterprise). Pairs with capabilities.creative.bills_through_adcp pre-call discriminator (#2881). Distinct from BILLING_NOT_SUPPORTED (per-value rejection on a billable account) — this signals the whole billing surface is offline. Wire change — held for 3.1."
},
"BRAND_REQUIRED": {
"disposition": "held-for-next-minor",
"target_version": "3.1",
Expand Down
21 changes: 15 additions & 6 deletions specs/creative-agent-pricing.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,12 @@ agent's products and pricing.
A buyer agent that has not established an account with a creative agent cannot see pricing.
This is by design — pricing is contractual, and there is no contract without an account.

Creative agents that charge SHOULD indicate this in their agent description (MCP tool
annotations or A2A Agent Card) so buyer agents can know an account is required before
calling `list_creatives`. The protocol does not define a standard field for this in v1.
Creative agents that charge SHOULD set `capabilities.creative.bills_through_adcp = true`
in their `get_adcp_capabilities` response so buyer agents can pre-filter before establishing
an account. Agents that bill out of band (flat license, SaaS contract, bundled enterprise
agreement) leave the flag absent or set it to `false`; buyer agents then know to skip
`include_pricing` on `list_creatives` and tolerate `report_usage` returning
`accepted: 0` with `BILLING_OUT_OF_BAND` errors.

### Consumption details

Expand Down Expand Up @@ -348,9 +351,15 @@ Structured consumption details for `build_creative` responses:

## Capabilities change

No new capability flag needed. An agent that charges has pricing on its creatives. An
agent that doesn't, doesn't. The buyer discovers pricing by its presence in
`list_creatives` responses and `build_creative` responses.
Add `bills_through_adcp` (boolean, default false/absent) to the `creative` block in
`get-adcp-capabilities-response.json`. When `true`, the buyer can expect `pricing_options`
on `list_creatives` (with `include_pricing=true` and an authenticated account),
`pricing_option_id` and `vendor_cost` on `build_creative`, and `report_usage` that
accepts records against the rate card. When `false` or absent, the agent bills out
of band: `pricing_options` is omitted, build responses carry no pricing fields, and
`report_usage` returns `accepted: 0` with `BILLING_OUT_OF_BAND` errors per-record. The
flag is a pre-call discriminator for routing — buyers can pre-filter creative agents
before establishing an account.

The spec language around Accounts needs updating as described above.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -343,12 +343,14 @@ phases:

Agents that bill through AdCP verify the rate and return accepted: 1
with empty errors. Agents that bill out of band return accepted: 0 and
populate errors with a field pointing at the offending record (e.g.
`usage[0].pricing_option_id`) and a human-readable message. Don't
fake-accept records you won't bill on — silent acceptance breaks
reconciliation for buyers who trust the response. A standard error
code for "billing is handled out of band" is not yet defined in the
spec; vendor codes are fine today.
populate errors with code `BILLING_OUT_OF_BAND`, a `field` pointing at
the offending record (e.g. `usage[0]` or `usage[0].pricing_option_id`),
and a human-readable message. Don't fake-accept records you won't bill
on — silent acceptance breaks reconciliation for buyers who trust the
response. Buyers SHOULD pre-filter by reading
`capabilities.creative.bills_through_adcp` from `get_adcp_capabilities`
before calling `report_usage`; agents that have not yet declared the
capability stay in the probe-to-discover mode.
task: report_usage
schema_ref: "account/report-usage-request.json"
response_schema_ref: "account/report-usage-response.json"
Expand Down
6 changes: 6 additions & 0 deletions static/schemas/source/enums/error-code.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"PROVENANCE_CLAIM_CONTRADICTED",
"BILLING_NOT_SUPPORTED",
"BILLING_NOT_PERMITTED_FOR_AGENT",
"BILLING_OUT_OF_BAND",
"PAYMENT_TERMS_NOT_SUPPORTED",
"BRAND_REQUIRED",
"AGENT_SUSPENDED",
Expand Down Expand Up @@ -134,6 +135,7 @@
"PROVENANCE_CLAIM_CONTRADICTED": "Seller invoked a governance agent from `creative_policy.accepted_verifiers` via `get_creative_features` and the verifier's result contradicts the buyer's provenance claim - e.g., buyer claims `digital_source_type: digital_capture` but the AI-detection feature returns `ai_generated: true` above the seller's confidence threshold. Distinct from the `PROVENANCE_*_MISSING` family (structural absence) by being an active refutation. `error.details` SHOULD be limited to the audit-safe allowlist `{ agent_url, feature_id, claimed_value, observed_value, confidence }`; sellers MUST NOT forward arbitrary verifier extension fields, `detail_url`, or any verifier response shape that may carry cross-tenant or PII data. When the seller calls a different on-list agent than the buyer nominated (the seller is the verifier-of-record), `error.details.agent_url` is the agent the seller actually called and `error.details.substituted_for` SHOULD carry the buyer's nominated `agent_url` so the buyer can reconcile. Recovery: correctable - buyer revises the provenance claim to match reality (or replaces the creative); auto-retry without correction will not pass.",
"BILLING_NOT_SUPPORTED": "The seller declines the requested `billing` value either at the seller-wide capability level (`supported_billing` does not include the value) or at the per-account-relationship level (e.g., the seller accepts `operator` billing in general but has no direct billing relationship with the operator on this specific account). The default reject code for billing-value mismatches; `error.details` SHOULD conform to `error-details/billing-not-supported.json` (`scope` ∈ `{\"capability\", \"account\"}` plus optional `supported_billing` echo for the `\"capability\"` scope) so callers can dispatch without parsing prose. Distinct from `BILLING_NOT_PERMITTED_FOR_AGENT`, which is narrowly scoped to the calling buyer agent's commercial relationship with the seller (passthrough-only vs agent-billable) rather than to the seller's capability or per-account state. Sellers MUST emit `BILLING_NOT_PERMITTED_FOR_AGENT` only when agent identity has been established via signed-request derivation or a credential-to-agent mapping in the seller's onboarding record; in all other cases (unauthenticated callers and bearer credentials not mapped to a specific agent record) sellers MUST return `BILLING_NOT_SUPPORTED` and MUST omit `error.details.scope` — emitting the per-agent code or the `\"account\"`-scope hint without established identity is a cross-tenant onboarding oracle (same uniform-response shape required by the `*_NOT_FOUND` family). Recovery: correctable (check `get_adcp_capabilities` for `supported_billing` and resubmit with a value the seller supports, or omit `billing` to accept the seller's default).",
"BILLING_NOT_PERMITTED_FOR_AGENT": "The seller's `supported_billing` capability accepts the requested model, but the calling buyer agent's commercial relationship with the seller does not — e.g., the agent is onboarded as passthrough-only (no payments relationship — only the operator can be invoiced) and `billing: 'agent'` or `billing: 'advertiser'` is rejected even though the seller supports both at the capability level. Distinct from `BILLING_NOT_SUPPORTED` (seller-wide capability) by being narrowly per-buyer-agent: the gate is the seller's onboarding record for this caller, not the seller's global wire capability. Sellers MUST emit this code only after agent identity has been established via signed-request derivation or a credential-to-agent mapping in the seller's onboarding record; callers without established identity MUST receive `BILLING_NOT_SUPPORTED` instead, to prevent the distinct code from acting as an onboarding oracle. The recovery shape is deliberately minimal — `error.details` MUST conform to `error-details/billing-not-permitted-for-agent.json` (`rejected_billing` plus an optional single `suggested_billing` retry value, typically `operator`) and MUST NOT carry the agent's full permitted-billing subset, rate cards, payment terms, credit limit, billing entity, or any other per-agent commercial state. Recovery: correctable (retry with `error.details.suggested_billing` when present; when absent, surface to a human at the buyer — the agent cannot unilaterally extend its commercial relationship and MUST NOT auto-retry, since payments-relationship onboarding with the seller is offline).",
"BILLING_OUT_OF_BAND": "A creative-agent billing-loop operation (`report_usage` is the canonical case) received a well-formed record that the agent will not bill on because this account bills via a non-AdCP channel — flat license, SaaS contract, bundled enterprise agreement, or any other out-of-band arrangement. The agent returns `accepted: 0` with the offending record(s) listed in `errors[]` carrying this code; the request itself is valid and silent acceptance would break buyer-side reconciliation. Distinct from `BILLING_NOT_SUPPORTED` (the seller declines a specific `billing` value on a media-buy account where AdCP billing is otherwise in scope) and `BILLING_NOT_PERMITTED_FOR_AGENT` (per-buyer-agent commercial gate on an otherwise-billable surface) by signaling that the entire billing surface is offline for this account, not that a specific value or caller is rejected. Buyers SHOULD pre-filter by reading `capabilities.creative.bills_through_adcp` from `get_adcp_capabilities` before issuing `report_usage`; agents that have not yet declared the capability remain in the probe-to-discover mode. The error is returned per-record (in the `report_usage` response `errors[]` array with `field` pointing at `usage[N]` or a specific record subpath), not at the envelope level. The code itself is the discriminator; no `error.details` shape is defined for this code (mirroring `CONFIGURATION_ERROR`'s discriminator-by-code pattern). Recovery: terminal — auto-retry will not change the outcome; the buyer SHOULD flag the account in its routing table so reconciliation falls back to the ad server's native billing export (or whatever offline channel the agent uses) rather than expecting AdCP-side cost truth.",
"PAYMENT_TERMS_NOT_SUPPORTED": "The seller does not accept the requested `payment_terms` value for this account. Payment terms are never silently remapped — sellers either accept or reject. Distinct from `BILLING_NOT_SUPPORTED` (the `billing` enum) by being narrowly about the `payment_terms` enum on the same account. Recovery: correctable (omit `payment_terms` to accept the seller's default, retry with a different value the seller supports, or negotiate offline).",
"BRAND_REQUIRED": "A billable operation was attempted without a brand reference. Every billable operation requires either a seller-assigned `account_id` or a natural key including `brand`. Recovery: correctable (include `brand` — `domain` plus optional `brand_id` — on the request).",
"AGENT_SUSPENDED": "The calling buyer agent's commercial relationship with the seller is temporarily paused — the agent is onboarded but currently suspended. Sibling to `ACCOUNT_SUSPENDED` (account-wide) and `CAMPAIGN_SUSPENDED` (per-plan) but scoped to the agent-relationship axis (orthogonal to any specific account on that agent). The code itself is the discriminator — it does NOT carry an `error.details` payload (mirroring `BILLING_NOT_PERMITTED_FOR_AGENT`'s discriminator-by-code pattern), and MUST NOT carry per-agent commercial state (rate cards, payment terms, credit limit, billing entity, contact channels) since full disclosure of per-agent state in a single probe is a per-agent oracle. Cross-tenant onboarding oracle clamp + channel-coverage requirements (response shape, HTTP/A2A/MCP status, headers, side effects, observability, latency parity, retry-counter side channel) are normative in error-handling.mdx Per-Agent Authorization Gate; this description does not restate them to avoid drift. Recovery: terminal (re-onboarding may resolve the suspension; the agent MUST surface to a human at the buyer rather than auto-retrying — the agent cannot unilaterally lift a suspension, and re-attempts only reinforce the gate).",
Expand Down Expand Up @@ -386,6 +388,10 @@
"recovery": "correctable",
"suggestion": "retry with error.details.suggested_billing (typically 'operator') when present; when absent, surface to a human at the buyer — the agent cannot unilaterally extend its commercial relationship and MUST NOT auto-retry"
},
"BILLING_OUT_OF_BAND": {
"recovery": "terminal",
"suggestion": "do not retry — billing for this account runs through a non-AdCP channel. Pre-filter via capabilities.creative.bills_through_adcp before sending report_usage; flag the account in the buyer's routing table and fall back to the ad server's native billing export for cost reconciliation"
},
"PAYMENT_TERMS_NOT_SUPPORTED": {
"recovery": "correctable",
"suggestion": "omit payment_terms to accept the seller's default, retry with a different supported value, or negotiate offline"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -935,6 +935,11 @@
"type": "boolean",
"description": "When true, this agent can transform or resize existing manifests via build_creative. The buyer provides a creative_manifest and a target_format_id, and the agent adapts the creative to the new format.",
"default": false
},
"bills_through_adcp": {
"type": "boolean",
"description": "When true, this creative agent bills through the AdCP rate-card surface: list_creatives returns pricing_options when include_pricing=true with an authenticated account, build_creative populates pricing_option_id and vendor_cost on the response, 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) and buyers should skip pricing fields and tolerate report_usage returning accepted: 0 with errors carrying BILLING_OUT_OF_BAND. A pre-call discriminator so buyer agents can route across many creative agents without first establishing an account to probe pricing.",
"default": false
}
},
"additionalProperties": true
Expand Down
Loading