From 01b5cb679a323741f82e73fb9b3cc537752b68ca Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Fri, 14 Aug 2026 22:18:37 +0200 Subject: [PATCH 1/5] feat(schema): audience activation method declarations (#4324) Products declare how buyer audience data can reach them via audience_activation.methods; the seller-level union surfaces as audience_targeting.supported_activation_methods, and buyers filter discovery with filters.audience_activation_methods. Co-Authored-By: Claude Fable 5 --- .changeset/audience-activation-methods.md | 5 + .../product-discovery/media-products.mdx | 48 ++++++ .../media-buy/task-reference/get_products.mdx | 1 + .../task-reference/sync_audiences.mdx | 2 + docs/protocol/get_adcp_capabilities.mdx | 19 ++- .../core/audience-activation-method.json | 161 ++++++++++++++++++ .../schemas/source/core/product-filters.json | 55 ++++++ static/schemas/source/core/product.json | 24 +++ .../get-adcp-capabilities-response.json | 8 + 9 files changed, 322 insertions(+), 1 deletion(-) create mode 100644 .changeset/audience-activation-methods.md create mode 100644 static/schemas/source/core/audience-activation-method.json diff --git a/.changeset/audience-activation-methods.md b/.changeset/audience-activation-methods.md new file mode 100644 index 0000000000..fc239b7469 --- /dev/null +++ b/.changeset/audience-activation-methods.md @@ -0,0 +1,5 @@ +--- +"adcontextprotocol": minor +--- + +Add audience activation method declarations (#4324). Products declare how buyer audience data can reach them via `audience_activation.methods` — `sync_audiences`, `tmp_identity_match`, `file_transfer`, `dataset_query`, `clean_room`, or `platform_distribution` — with vendor identity as a BrandRef domain. The seller-level union surfaces as `media_buy.audience_targeting.supported_activation_methods` in `get_adcp_capabilities` for fast-fail discovery, and buyers filter products with `filters.audience_activation_methods` (OR across entries, AND within an entry, omitted fields as wildcards). Dataset entries may publish `consumer_identities[]` (cloud/region-qualified principals to grant); grant-based paths are in-protocol only when the vendor flow is grantee-identified. diff --git a/docs/media-buy/product-discovery/media-products.mdx b/docs/media-buy/product-discovery/media-products.mdx index f829c76327..2d878d693e 100644 --- a/docs/media-buy/product-discovery/media-products.mdx +++ b/docs/media-buy/product-discovery/media-products.mdx @@ -150,6 +150,54 @@ At buy time, a package can repeat `audience_evidence_requirements` and can carry When an attestation affected either decision, readback preserves the exact reference and its `AttestationEvaluation` as one pair. That pair is valid only when the reference is published on the selected snapshot, its claim and issuer satisfy both buyer and seller policy, `reference_digest` covers that same reference, the outcome is `verified`, and both the subject digest and action-binding digest equal the evidence `content_digest`. An evaluation for one reference cannot verify a different reference. Sellers retain the full immutable snapshot and evaluations for at least as long as the package remains readable. Buyers should cache the evidence returned during discovery; AdCP 3.2 does not introduce a buyer-controlled snapshot fetch URL. Sellers may embed the full snapshot in readback, but need not duplicate it on every product or package. +### Audience activation + +The [`audience_targeting`](/docs/protocol/get_adcp_capabilities#audience_targeting) capability describes *what identifiers* a seller matches on; `audience_activation` describes *how the data moves*. Two parties can both support audiences and remain practically incompatible — a buyer built around Snowflake sharing and a seller that only accepts API pushes discover that today at integration time, or worse, at activation time. Products that accept buyer audiences declare their activation paths so that mismatch surfaces during discovery. + +```json test=false +"audience_activation": { + "methods": [ + { "pattern": "sync_audiences" }, + { + "pattern": "dataset_query", + "vendor": { "domain": "snowflake.com" }, + "consumer_identities": [ + { "cloud": "aws", "region": "us-east-1", "identity": "SELLERORG.ADCP_INGEST" } + ] + }, + { + "pattern": "platform_distribution", + "vendor": { "domain": "liveramp.com" }, + "destination_ref": "dest_12345", + "bind_expiry_days": 14 + } + ], + "preferred_method": { "pattern": "sync_audiences" }, + "notes": "Dataset ingestion runs daily at 06:00 UTC." +} +``` + +`methods` is an unordered set of independent options. `preferred_method` (optional) names the seller's preference and MUST also appear in `methods`. Absence of `audience_activation` means undeclared — treat it as needs-clarification rather than non-support, except under an `audience_activation_methods` filter, where sellers MUST exclude undeclared products. + +Schema: [`core/audience-activation-method.json`](https://adcontextprotocol.org/schemas/v3/core/audience-activation-method.json) + +#### Activation method patterns + +Each entry declares a `pattern` plus pattern-specific fields. Vendor identity is a [BrandRef](/docs/media-buy/product-discovery/media-products) domain — new platforms declare a domain, no enum change required. + +| Pattern | Required fields | Meaning | +|---------|----------------|---------| +| `sync_audiences` | — | Audience pushes and external source references via the AdCP [`sync_audiences`](/docs/media-buy/task-reference/sync_audiences) task. Any AdCP-speaking buyer can use it. | +| `tmp_identity_match` | `buyer_agent.agent_url` | The seller has wired TMP identity-match calls to this specific buyer agent. Entries MUST be consistent with `trusted_match.providers[]` where both are present. | +| `file_transfer` | `transport`, `vendor` | Bucket-addressed file exchange (`s3`, `gcs`, `azure_blob`). Optional `directions` (`buyer_to_seller`, `seller_to_buyer`); absent means unspecified, not neither. | +| `dataset_query` | `vendor` | The seller reads a dataset the buyer shares through the vendor's grantee-identified sharing mechanism (Snowflake Secure Data Sharing, Databricks Delta Sharing, BigQuery authorized views). Optional `consumer_identities[]` lists the principals to grant, qualified by cloud/region — routing and cost hints, not grant-reachability or compliance boundaries. | +| `clean_room` | `vendor` | Privacy-preserving match in the named vendor's room. Informational: room setup is bilateral onboarding; where the room can export output, the output binds through a `dataset` or `platform_segment` source on `sync_audiences`. | +| `platform_distribution` | `vendor`, `destination_ref` | The vendor's rails deliver segments to the seller's declared destination. The buyer initiates distribution in the vendor's system, then binds the arriving segment via `sync_audiences`. Optional `bind_expiry_days` declares when an unfulfilled bind may expire. | + +A grant-based path is in-protocol only when the vendor flow is **grantee-identified** — the grant names a principal and no secret is conveyed. Token-bearer flows (presigned URLs, bearer-token activation links) remain out-of-band integrations. + +Buyers filter discovery with [`filters.audience_activation_methods`](/docs/media-buy/task-reference/get_products): OR across requested entries, AND within an entry, omitted optional fields are wildcards, `directions` matches on non-empty intersection. + ### Pricing Models Publishers declare which pricing models they support for each product. Buyers select from the available options when creating a media buy. This approach supports: diff --git a/docs/media-buy/task-reference/get_products.mdx b/docs/media-buy/task-reference/get_products.mdx index 680c034507..d585c6dc7d 100644 --- a/docs/media-buy/task-reference/get_products.mdx +++ b/docs/media-buy/task-reference/get_products.mdx @@ -343,6 +343,7 @@ targeting filter and the legacy top-level `property_list`. | `required_metrics` | string[] ([metric vocabulary](/docs/media-buy/media-buys/optimization-reporting)) | Filter to products whose `reporting_capabilities.available_metrics` is a superset of these metrics — i.e., products that commit to reporting all listed metrics in delivery. Use for capability discovery (e.g., `["completed_views"]` for a CTV CPCV buy). Sellers MUST silently exclude products that cannot meet the list — filter-not-fail; do not return an error. The product's declared `available_metrics` becomes the binding reporting contract carried into the resulting media buy. | | `required_vendor_metrics` | object[] | Filter to products whose `reporting_capabilities.vendor_metrics` covers vendor-defined metrics (proprietary attention, emissions, panel demographics, brand-lift surveys, etc.). Each entry pins `vendor` (BrandRef) and/or `metric_id` — at least one. Cross-vendor discovery (e.g., "any attention measurement") is the buyer agent's responsibility: resolve which vendors offer a category via the vendors' `brand.json` records, then enumerate them as filter entries. Same filter-not-fail semantics as `required_metrics`. | | `audience_evidence_requirements` | AudienceEvidenceRequirements | Apply buyer-authored admissibility or ranking policy to `Product.audience_evidence`. `requirement_mode` distinguishes hard filtering from preference; `evidence_presence` distinguishes evidence-must-exist from apply-when-published. | +| `audience_activation_methods` | object[] | Filter to products whose [`audience_activation.methods`](/docs/media-buy/product-discovery/media-products#audience-activation) matches at least one requested entry (OR across entries). Within an entry every specified field must match (AND); omitted optional fields are wildcards; `directions` matches on non-empty intersection. Each entry requires `pattern` (`sync_audiences`, `tmp_identity_match`, `file_transfer`, `dataset_query`, `clean_room`, `platform_distribution`) plus optional `vendor`, `transport`, `directions`, `buyer_agent`. Sellers MUST exclude products with no `audience_activation` declaration when this filter is present, reported via `filter_diagnostics.excluded_by`. | ### Audience evidence requirements diff --git a/docs/media-buy/task-reference/sync_audiences.mdx b/docs/media-buy/task-reference/sync_audiences.mdx index 0babbc8519..82e02a99e7 100644 --- a/docs/media-buy/task-reference/sync_audiences.mdx +++ b/docs/media-buy/task-reference/sync_audiences.mdx @@ -146,6 +146,8 @@ Providing multiple identifiers for the same person improves match rates. Composi **Identifier support varies by seller**: Check [`get_adcp_capabilities`](/docs/protocol/get_adcp_capabilities) → `media_buy.audience_targeting.supported_identifier_types` and `media_buy.audience_targeting.supported_uid_types` before sending. MAID support is not universal (LinkedIn does not accept MAIDs; iOS IDFA requires App Tracking Transparency consent). The `media_buy.audience_targeting.matching_latency_hours` range and `media_buy.audience_targeting.minimum_audience_size` in capabilities are also seller-specific. +**Activation paths beyond inline push**: Inline members are one activation method. Sellers declare which paths they support — AdCP push, dataset sharing, clean rooms, platform distribution — via [`audience_activation`](/docs/media-buy/product-discovery/media-products#audience-activation) on products and `media_buy.audience_targeting.supported_activation_methods` in [`get_adcp_capabilities`](/docs/protocol/get_adcp_capabilities). Filter discovery with `filters.audience_activation_methods` on [`get_products`](/docs/media-buy/task-reference/get_products) to find inventory compatible with your data pipeline. + **Size limit**: Payloads are limited to 100,000 members per call across all audiences. For larger lists, chunk into sequential calls using `add` deltas. **Concurrency**: Ensure that calls made to `sync_audience` are independent of eachother. They may be processed out-of-order. If you need sequential execution, wait for the callback to your configured webhook before making another call. diff --git a/docs/protocol/get_adcp_capabilities.mdx b/docs/protocol/get_adcp_capabilities.mdx index 1bd4c58177..9a87f0b33f 100644 --- a/docs/protocol/get_adcp_capabilities.mdx +++ b/docs/protocol/get_adcp_capabilities.mdx @@ -696,6 +696,7 @@ Audience targeting capabilities. Presence of this object indicates the seller su | `supports_platform_customer_id` | boolean | | When `true`, the seller accepts the buyer's CRM/loyalty ID as a matchable identifier. Only applicable when the seller operates a closed ecosystem with a shared ID namespace (e.g., a retailer matching against their loyalty program). Buyers can include `platform_customer_id` values in `AudienceMember.identifiers`. Reporting on matched IDs typically requires a clean room or the seller's own reporting surface. | | `supported_uid_types` | string[] | | Universal ID types accepted for audience matching (MAIDs, RampID, UID2, etc.). MAID support varies significantly by platform — check this field before sending `uids` with `type: maid`. | | `matching_latency_hours` | object | | Expected matching latency range in hours after upload. Use to calibrate polling cadence and set appropriate expectations before configuring `push_notification_config`. Shape: `{ min: integer, max: integer }`. | +| `supported_activation_methods` | object[] | | Union of per-product [`audience_activation.methods`](/docs/media-buy/product-discovery/media-products#audience-activation) across the seller's catalog. Fast-fail discovery: read this once and skip the catalog walk when nothing overlaps your pipeline. Per-product declarations are the source of truth; sellers MUST keep this consistent with the catalog. Each entry is an [activation method](/docs/media-buy/product-discovery/media-products#activation-method-patterns). | #### conversion_tracking @@ -1466,7 +1467,23 @@ const buy = await client.createMediaBuy({ "supports_platform_customer_id": false, "supported_uid_types": ["uid2", "rampid"], "minimum_audience_size": 500, - "matching_latency_hours": { "min": 1, "max": 24 } + "matching_latency_hours": { "min": 1, "max": 24 }, + "supported_activation_methods": [ + { "pattern": "sync_audiences" }, + { + "pattern": "dataset_query", + "vendor": { "domain": "snowflake.com" }, + "consumer_identities": [ + { "cloud": "aws", "region": "us-east-1", "identity": "SELLERORG.ADCP_INGEST" } + ] + }, + { + "pattern": "platform_distribution", + "vendor": { "domain": "liveramp.com" }, + "destination_ref": "dest_12345", + "bind_expiry_days": 14 + } + ] }, "conversion_tracking": { "multi_source_event_dedup": false, diff --git a/static/schemas/source/core/audience-activation-method.json b/static/schemas/source/core/audience-activation-method.json new file mode 100644 index 0000000000..41e543011a --- /dev/null +++ b/static/schemas/source/core/audience-activation-method.json @@ -0,0 +1,161 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/core/audience-activation-method.json", + "title": "Audience Activation Method", + "description": "One way buyer audience data can reach a seller for targeting. The pattern field discriminates the entry; each pattern carries only its own fields. Two patterns are AdCP-canonical (sync_audiences, tmp_identity_match); the rest describe integration paths that move data outside the protocol while remaining discoverable through it. Vendor identity is a BrandRef domain so new platforms declare a domain rather than waiting on an enum change.", + "type": "object", + "oneOf": [ + { + "title": "AdCP audience sync", + "description": "The seller accepts audience pushes and external source references via the AdCP sync_audiences task. Any AdCP-speaking buyer can use this path with no additional integration.", + "type": "object", + "properties": { + "pattern": { "const": "sync_audiences" } + }, + "required": ["pattern"], + "additionalProperties": false + }, + { + "title": "TMP identity match", + "description": "The seller has configured Trusted Match Protocol identity-match calls to a specific buyer agent. Declarations are per buyer agent because the integration is not generic. Entries MUST be consistent with trusted_match.providers[] where both are present on the product.", + "type": "object", + "properties": { + "pattern": { "const": "tmp_identity_match" }, + "buyer_agent": { + "type": "object", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "Buyer agent's registered agent URL. Canonical identifier, mirroring trusted_match.providers[].agent_url." + } + }, + "required": ["agent_url"], + "additionalProperties": false + } + }, + "required": ["pattern", "buyer_agent"], + "additionalProperties": false + }, + { + "title": "File transfer", + "description": "Bucket-addressed file exchange over a cloud storage protocol. The buyer moves the bytes; direction declares who hosts the bucket.", + "type": "object", + "properties": { + "pattern": { "const": "file_transfer" }, + "transport": { + "$ref": "/schemas/enums/cloud-storage-protocol.json", + "description": "Storage protocol for the exchange." + }, + "directions": { + "type": "array", + "description": "Supported transfer directions. buyer_to_seller: buyer writes to a seller-hosted bucket. seller_to_buyer: seller reads from a buyer-hosted bucket. Absent means unspecified (resolve during account setup), not neither.", + "items": { + "type": "string", + "enum": ["buyer_to_seller", "seller_to_buyer"] + }, + "minItems": 1, + "uniqueItems": true + }, + "vendor": { + "$ref": "/schemas/core/brand-ref.json", + "description": "Platform providing the storage primitive (e.g., the cloud provider's domain)." + } + }, + "required": ["pattern", "transport", "vendor"], + "additionalProperties": false + }, + { + "title": "Dataset query", + "description": "Vendor-mediated query access: the seller reads a dataset the buyer shares through the vendor's grantee-identified sharing mechanism (e.g., Snowflake Secure Data Sharing, Databricks Delta Sharing, BigQuery authorized views). Admissible only when the grant names a principal and no secret is conveyed; token-bearer flows remain out-of-band.", + "type": "object", + "properties": { + "pattern": { "const": "dataset_query" }, + "vendor": { + "$ref": "/schemas/core/brand-ref.json", + "description": "Data-sharing platform the seller can consume from." + }, + "consumer_identities": { + "type": "array", + "description": "Principals the buyer grants access to in the vendor's system, qualified by cloud and region. Qualifiers are routing and cost hints (pick the near/cheap identity) — not grant-reachability boundaries and not compliance boundaries; data-transfer assessments key on the recipient entity's jurisdiction, not the grantee account's region. Optional: sellers MAY instead communicate identities during account setup.", + "items": { + "type": "object", + "properties": { + "cloud": { + "type": "string", + "enum": ["aws", "azure", "gcp"], + "description": "Cloud family hosting this identity's account." + }, + "region": { + "type": "string", + "description": "Vendor/cloud region identifier. Format follows the cloud family's region naming; not constrained by this schema." + }, + "identity": { + "type": "string", + "description": "Principal to grant, interpreted relative to the enclosing entry's vendor.domain. Format is vendor-specific and opaque — Snowflake: orgname.accountname; Databricks: sharing recipient identifier; BigQuery: IAM principal." + } + }, + "required": ["cloud", "region", "identity"], + "additionalProperties": false + }, + "minItems": 1 + } + }, + "required": ["pattern", "vendor"], + "additionalProperties": false + }, + { + "title": "Clean room", + "description": "Privacy-preserving match in the named vendor's clean room; neither party sees the other's raw rows. Informational declaration: room setup is bilateral onboarding, and where the room can export output, the output binds through a dataset or platform_segment source on sync_audiences.", + "type": "object", + "properties": { + "pattern": { "const": "clean_room" }, + "vendor": { + "$ref": "/schemas/core/brand-ref.json", + "description": "Clean-room product's operating domain." + } + }, + "required": ["pattern", "vendor"], + "additionalProperties": false + }, + { + "title": "Platform distribution", + "description": "The vendor's rails deliver segments to the seller's declared destination; the buyer initiates distribution in the vendor's system and binds the arriving segment via sync_audiences.", + "type": "object", + "properties": { + "pattern": { "const": "platform_distribution" }, + "vendor": { + "$ref": "/schemas/core/brand-ref.json", + "description": "Distribution platform's operating domain." + }, + "destination_ref": { + "type": "string", + "maxLength": 256, + "description": "Seller's destination identifier in the vendor's system, as the buyer needs it to initiate distribution. A seller-declared configuration reference, not a buyer-invoked key." + }, + "bind_expiry_days": { + "type": "integer", + "minimum": 1, + "description": "Window after which the seller MAY expire an unfulfilled segment bind (action: failed). Initial vendor distribution is days-scale; windows shorter than the vendor's documented distribution latency are non-conformant." + } + }, + "required": ["pattern", "vendor", "destination_ref"], + "additionalProperties": false + } + ], + "examples": [ + { "pattern": "sync_audiences" }, + { "pattern": "tmp_identity_match", "buyer_agent": { "agent_url": "https://buyer.example-agent.com" } }, + { "pattern": "file_transfer", "transport": "s3", "directions": ["buyer_to_seller"], "vendor": { "domain": "amazon.com" } }, + { + "pattern": "dataset_query", + "vendor": { "domain": "snowflake.com" }, + "consumer_identities": [ + { "cloud": "aws", "region": "us-east-1", "identity": "SELLERORG.ADCP_INGEST" }, + { "cloud": "aws", "region": "eu-central-1", "identity": "SELLERORG.ADCP_INGEST_EU" } + ] + }, + { "pattern": "clean_room", "vendor": { "domain": "liveramp.com" } }, + { "pattern": "platform_distribution", "vendor": { "domain": "liveramp.com" }, "destination_ref": "dest_12345", "bind_expiry_days": 14 } + ] +} diff --git a/static/schemas/source/core/product-filters.json b/static/schemas/source/core/product-filters.json index 645ec206b9..e86a16ae7e 100644 --- a/static/schemas/source/core/product-filters.json +++ b/static/schemas/source/core/product-filters.json @@ -252,6 +252,61 @@ }, "additionalProperties": false }, + "audience_activation_methods": { + "type": "array", + "description": "Filter to products whose audience_activation.methods matches at least one requested entry (OR across entries). Within an entry every specified field must match (AND); omitted optional fields are wildcards; directions matches on non-empty intersection; vendor matches on domain, plus brand_id when specified. Sellers MUST exclude products with no audience_activation declaration when this filter is present, reported via filter_diagnostics.excluded_by.", + "items": { + "type": "object", + "properties": { + "pattern": { + "type": "string", + "enum": [ + "sync_audiences", + "tmp_identity_match", + "file_transfer", + "dataset_query", + "clean_room", + "platform_distribution" + ], + "description": "Activation pattern to match." + }, + "vendor": { + "$ref": "/schemas/core/brand-ref.json", + "description": "Require the method's vendor to match this reference." + }, + "transport": { + "$ref": "/schemas/enums/cloud-storage-protocol.json", + "description": "Require this storage protocol (file_transfer only)." + }, + "directions": { + "type": "array", + "description": "Require a non-empty intersection with the method's declared directions (file_transfer only).", + "items": { + "type": "string", + "enum": ["buyer_to_seller", "seller_to_buyer"] + }, + "minItems": 1, + "uniqueItems": true + }, + "buyer_agent": { + "type": "object", + "description": "Require this buyer agent on tmp_identity_match entries. Buyers typically filter on their own agent_url.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri" + } + }, + "required": ["agent_url"], + "additionalProperties": false + } + }, + "required": ["pattern"], + "additionalProperties": false + }, + "minItems": 1, + "uniqueItems": true + }, "required_features": { "$ref": "/schemas/core/media-buy-features.json", "description": "Filter to products from sellers supporting specific protocol features. Only features set to true are used for filtering." diff --git a/static/schemas/source/core/product.json b/static/schemas/source/core/product.json index 77227d89aa..db797c20ce 100644 --- a/static/schemas/source/core/product.json +++ b/static/schemas/source/core/product.json @@ -592,6 +592,30 @@ "required": ["context_match"], "additionalProperties": true }, + "audience_activation": { + "type": "object", + "description": "How buyer audience data can reach this product's targeting. Absence means undeclared — buyers SHOULD treat it as needs-clarification rather than non-support, except under an audience_activation_methods filter, where sellers MUST exclude undeclared products. Declare when the product accepts buyer audiences.", + "properties": { + "methods": { + "type": "array", + "description": "Activation paths available on this product, as an unordered set of independent options.", + "items": { + "$ref": "/schemas/core/audience-activation-method.json" + }, + "minItems": 1 + }, + "preferred_method": { + "$ref": "/schemas/core/audience-activation-method.json", + "description": "The seller's preferred path when the buyer supports several. MUST also appear in methods." + }, + "notes": { + "type": "string", + "description": "Free-text caveats (onboarding lead times, data-format constraints, regional restrictions)." + } + }, + "required": ["methods"], + "additionalProperties": false + }, "material_submission": { "type": "object", "description": "Instructions for submitting physical creative materials (print, static OOH, cinema). Present only for products requiring physical delivery outside the digital creative assignment flow. Buyer agents MUST validate url and email domains against the seller's known domains (from adagents.json) before submitting materials. Never auto-submit without human confirmation.", diff --git a/static/schemas/source/protocol/get-adcp-capabilities-response.json b/static/schemas/source/protocol/get-adcp-capabilities-response.json index 086dadee1f..df3ff95a96 100644 --- a/static/schemas/source/protocol/get-adcp-capabilities-response.json +++ b/static/schemas/source/protocol/get-adcp-capabilities-response.json @@ -1188,6 +1188,14 @@ "description": "Minimum matched audience size required for targeting. Audiences below this threshold will have status: too_small. Varies by platform (100–1000 is typical).", "minimum": 1 }, + "supported_activation_methods": { + "type": "array", + "description": "Union of audience_activation.methods across the seller's products. Fast-fail discovery: a buyer reads this once and skips the catalog walk when nothing overlaps its pipeline. Per-product declarations are the source of truth; sellers MUST keep this consistent with the catalog.", + "items": { + "$ref": "/schemas/core/audience-activation-method.json" + }, + "minItems": 1 + }, "matching_latency_hours": { "type": "object", "description": "Expected matching latency range in hours after upload. Use to calibrate polling cadence and set appropriate expectations before configuring push_notification_config.", From 0276f93efd6db1c0f9c7e09c39b15aef9a9ba71d Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sat, 15 Aug 2026 07:15:48 +0200 Subject: [PATCH 2/5] docs: regenerate compliance symbol links in get_products Co-Authored-By: Claude Fable 5 --- docs/media-buy/task-reference/get_products.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/media-buy/task-reference/get_products.mdx b/docs/media-buy/task-reference/get_products.mdx index d585c6dc7d..a936bb60a7 100644 --- a/docs/media-buy/task-reference/get_products.mdx +++ b/docs/media-buy/task-reference/get_products.mdx @@ -343,7 +343,7 @@ targeting filter and the legacy top-level `property_list`. | `required_metrics` | string[] ([metric vocabulary](/docs/media-buy/media-buys/optimization-reporting)) | Filter to products whose `reporting_capabilities.available_metrics` is a superset of these metrics — i.e., products that commit to reporting all listed metrics in delivery. Use for capability discovery (e.g., `["completed_views"]` for a CTV CPCV buy). Sellers MUST silently exclude products that cannot meet the list — filter-not-fail; do not return an error. The product's declared `available_metrics` becomes the binding reporting contract carried into the resulting media buy. | | `required_vendor_metrics` | object[] | Filter to products whose `reporting_capabilities.vendor_metrics` covers vendor-defined metrics (proprietary attention, emissions, panel demographics, brand-lift surveys, etc.). Each entry pins `vendor` (BrandRef) and/or `metric_id` — at least one. Cross-vendor discovery (e.g., "any attention measurement") is the buyer agent's responsibility: resolve which vendors offer a category via the vendors' `brand.json` records, then enumerate them as filter entries. Same filter-not-fail semantics as `required_metrics`. | | `audience_evidence_requirements` | AudienceEvidenceRequirements | Apply buyer-authored admissibility or ranking policy to `Product.audience_evidence`. `requirement_mode` distinguishes hard filtering from preference; `evidence_presence` distinguishes evidence-must-exist from apply-when-published. | -| `audience_activation_methods` | object[] | Filter to products whose [`audience_activation.methods`](/docs/media-buy/product-discovery/media-products#audience-activation) matches at least one requested entry (OR across entries). Within an entry every specified field must match (AND); omitted optional fields are wildcards; `directions` matches on non-empty intersection. Each entry requires `pattern` (`sync_audiences`, `tmp_identity_match`, `file_transfer`, `dataset_query`, `clean_room`, `platform_distribution`) plus optional `vendor`, `transport`, `directions`, `buyer_agent`. Sellers MUST exclude products with no `audience_activation` declaration when this filter is present, reported via `filter_diagnostics.excluded_by`. | +| `audience_activation_methods` | object[] | Filter to products whose [`audience_activation.methods`](/docs/media-buy/product-discovery/media-products#audience-activation) matches at least one requested entry (OR across entries). Within an entry every specified field must match (AND); omitted optional fields are wildcards; `directions` matches on non-empty intersection. Each entry requires `pattern` ([`sync_audiences`](/docs/media-buy/task-reference/sync_audiences), `tmp_identity_match`, `file_transfer`, `dataset_query`, `clean_room`, `platform_distribution`) plus optional `vendor`, `transport`, `directions`, `buyer_agent`. Sellers MUST exclude products with no `audience_activation` declaration when this filter is present, reported via `filter_diagnostics.excluded_by`. | ### Audience evidence requirements From 29b637c832e15c3f382d9fe2bfc816a6194f9c79 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sun, 16 Aug 2026 17:59:54 +0200 Subject: [PATCH 3/5] fix(schema): align audience activation with vendor workflows --- .changeset/audience-activation-methods.md | 2 +- .../product-discovery/media-products.mdx | 16 ++-- docs/protocol/get_adcp_capabilities.mdx | 6 +- package.json | 3 +- .../core/audience-activation-method.json | 40 ++++---- static/schemas/source/core/product.json | 2 +- .../get-adcp-capabilities-response.json | 2 +- tests/audience-activation-methods.test.cjs | 96 +++++++++++++++++++ 8 files changed, 136 insertions(+), 31 deletions(-) create mode 100644 tests/audience-activation-methods.test.cjs diff --git a/.changeset/audience-activation-methods.md b/.changeset/audience-activation-methods.md index fc239b7469..1137c7f84d 100644 --- a/.changeset/audience-activation-methods.md +++ b/.changeset/audience-activation-methods.md @@ -2,4 +2,4 @@ "adcontextprotocol": minor --- -Add audience activation method declarations (#4324). Products declare how buyer audience data can reach them via `audience_activation.methods` — `sync_audiences`, `tmp_identity_match`, `file_transfer`, `dataset_query`, `clean_room`, or `platform_distribution` — with vendor identity as a BrandRef domain. The seller-level union surfaces as `media_buy.audience_targeting.supported_activation_methods` in `get_adcp_capabilities` for fast-fail discovery, and buyers filter products with `filters.audience_activation_methods` (OR across entries, AND within an entry, omitted fields as wildcards). Dataset entries may publish `consumer_identities[]` (cloud/region-qualified principals to grant); grant-based paths are in-protocol only when the vendor flow is grantee-identified. +Add audience activation method declarations (#4324). Products declare how buyer audience data can reach them via `audience_activation.methods` — `sync_audiences`, `tmp_identity_match`, `file_transfer`, `dataset_query`, `clean_room`, or `platform_distribution` — with vendor identity as a BrandRef domain. The seller-level union surfaces as `media_buy.audience_targeting.supported_activation_methods` in `get_adcp_capabilities` for fast-fail discovery, and buyers filter products with `filters.audience_activation_methods` (OR across entries, AND within an entry, omitted fields as wildcards). Dataset entries may publish `consumer_identities[]` with an opaque principal and optional paired cloud/region deployment metadata. Platform destinations are optional account-scoped coordinates. Clean-room declarations cover only collaborations that produce targetable audiences and compose with the declared dataset or distribution rail; analytics-only rooms do not imply audience activation. Grant-based paths are in-protocol only when the vendor flow is grantee-identified. diff --git a/docs/media-buy/product-discovery/media-products.mdx b/docs/media-buy/product-discovery/media-products.mdx index 2d878d693e..7f8a4e3f2d 100644 --- a/docs/media-buy/product-discovery/media-products.mdx +++ b/docs/media-buy/product-discovery/media-products.mdx @@ -154,20 +154,20 @@ When an attestation affected either decision, readback preserves the exact refer The [`audience_targeting`](/docs/protocol/get_adcp_capabilities#audience_targeting) capability describes *what identifiers* a seller matches on; `audience_activation` describes *how the data moves*. Two parties can both support audiences and remain practically incompatible — a buyer built around Snowflake sharing and a seller that only accepts API pushes discover that today at integration time, or worse, at activation time. Products that accept buyer audiences declare their activation paths so that mismatch surfaces during discovery. -```json test=false +```json "audience_activation": { "methods": [ { "pattern": "sync_audiences" }, { "pattern": "dataset_query", - "vendor": { "domain": "snowflake.com" }, + "vendor": { "domain": "data-cloud.example" }, "consumer_identities": [ { "cloud": "aws", "region": "us-east-1", "identity": "SELLERORG.ADCP_INGEST" } ] }, { "pattern": "platform_distribution", - "vendor": { "domain": "liveramp.com" }, + "vendor": { "domain": "activation-hub.example" }, "destination_ref": "dest_12345", "bind_expiry_days": 14 } @@ -177,7 +177,7 @@ The [`audience_targeting`](/docs/protocol/get_adcp_capabilities#audience_targeti } ``` -`methods` is an unordered set of independent options. `preferred_method` (optional) names the seller's preference and MUST also appear in `methods`. Absence of `audience_activation` means undeclared — treat it as needs-clarification rather than non-support, except under an `audience_activation_methods` filter, where sellers MUST exclude undeclared products. +`methods` is an unordered set of activation capabilities. Entries are usually independent options; `clean_room` may compose with `dataset_query` or `platform_distribution` to declare the output rail. `preferred_method` (optional) names the seller's preference and MUST also appear in `methods`. Absence of `audience_activation` means undeclared — treat it as needs-clarification rather than non-support, except under an `audience_activation_methods` filter, where sellers MUST exclude undeclared products. Schema: [`core/audience-activation-method.json`](https://adcontextprotocol.org/schemas/v3/core/audience-activation-method.json) @@ -190,9 +190,11 @@ Each entry declares a `pattern` plus pattern-specific fields. Vendor identity is | `sync_audiences` | — | Audience pushes and external source references via the AdCP [`sync_audiences`](/docs/media-buy/task-reference/sync_audiences) task. Any AdCP-speaking buyer can use it. | | `tmp_identity_match` | `buyer_agent.agent_url` | The seller has wired TMP identity-match calls to this specific buyer agent. Entries MUST be consistent with `trusted_match.providers[]` where both are present. | | `file_transfer` | `transport`, `vendor` | Bucket-addressed file exchange (`s3`, `gcs`, `azure_blob`). Optional `directions` (`buyer_to_seller`, `seller_to_buyer`); absent means unspecified, not neither. | -| `dataset_query` | `vendor` | The seller reads a dataset the buyer shares through the vendor's grantee-identified sharing mechanism (Snowflake Secure Data Sharing, Databricks Delta Sharing, BigQuery authorized views). Optional `consumer_identities[]` lists the principals to grant, qualified by cloud/region — routing and cost hints, not grant-reachability or compliance boundaries. | -| `clean_room` | `vendor` | Privacy-preserving match in the named vendor's room. Informational: room setup is bilateral onboarding; where the room can export output, the output binds through a `dataset` or `platform_segment` source on `sync_audiences`. | -| `platform_distribution` | `vendor`, `destination_ref` | The vendor's rails deliver segments to the seller's declared destination. The buyer initiates distribution in the vendor's system, then binds the arriving segment via `sync_audiences`. Optional `bind_expiry_days` declares when an unfulfilled bind may expire. | +| `dataset_query` | `vendor` | The seller reads a dataset the buyer shares through the vendor's grantee-identified sharing mechanism (Snowflake Secure Data Sharing, Databricks Delta Sharing, BigQuery authorized views). Optional `consumer_identities[]` lists the principals to grant. `identity` is required; paired `cloud` and `region` metadata is optional for deployment-scoped identities and omitted for global principals. Location semantics are vendor-specific: they can constrain direct-share reachability, select fulfillment, or provide routing/cost hints, but are not compliance boundaries. | +| `clean_room` | `vendor` | Privacy-preserving match in the named vendor's room. Declare this under audience activation only when the collaboration can produce an audience targetable on this product; analytics-only or measurement-only room access does not qualify. The declaration does not promise portable output. Room setup remains bilateral. | +| `platform_distribution` | `vendor` | The vendor's rails deliver segments to the seller's destination. The buyer initiates distribution in the vendor's system, then binds the arriving segment via `sync_audiences`. Optional `destination_ref` is an opaque account-scoped destination or seat reference; omit it until bilateral setup establishes the destination. Optional `bind_expiry_days` declares when an unfulfilled bind may expire. | + +Clean-room activation composes with the output rail rather than assuming every room behaves alike. Platform-native and partner-distributed audiences pair `clean_room` with `platform_distribution`; queryable outputs pair it with `dataset_query`. A room that only returns aggregate insights belongs in measurement or reporting capabilities, not `audience_activation`. This covers walled-garden rooms, independent collaboration providers, and warehouse-native rooms without a vendor allowlist or a false promise that outputs can move between them. A grant-based path is in-protocol only when the vendor flow is **grantee-identified** — the grant names a principal and no secret is conveyed. Token-bearer flows (presigned URLs, bearer-token activation links) remain out-of-band integrations. diff --git a/docs/protocol/get_adcp_capabilities.mdx b/docs/protocol/get_adcp_capabilities.mdx index 9a87f0b33f..043e0e434f 100644 --- a/docs/protocol/get_adcp_capabilities.mdx +++ b/docs/protocol/get_adcp_capabilities.mdx @@ -696,7 +696,7 @@ Audience targeting capabilities. Presence of this object indicates the seller su | `supports_platform_customer_id` | boolean | | When `true`, the seller accepts the buyer's CRM/loyalty ID as a matchable identifier. Only applicable when the seller operates a closed ecosystem with a shared ID namespace (e.g., a retailer matching against their loyalty program). Buyers can include `platform_customer_id` values in `AudienceMember.identifiers`. Reporting on matched IDs typically requires a clean room or the seller's own reporting surface. | | `supported_uid_types` | string[] | | Universal ID types accepted for audience matching (MAIDs, RampID, UID2, etc.). MAID support varies significantly by platform — check this field before sending `uids` with `type: maid`. | | `matching_latency_hours` | object | | Expected matching latency range in hours after upload. Use to calibrate polling cadence and set appropriate expectations before configuring `push_notification_config`. Shape: `{ min: integer, max: integer }`. | -| `supported_activation_methods` | object[] | | Union of per-product [`audience_activation.methods`](/docs/media-buy/product-discovery/media-products#audience-activation) across the seller's catalog. Fast-fail discovery: read this once and skip the catalog walk when nothing overlaps your pipeline. Per-product declarations are the source of truth; sellers MUST keep this consistent with the catalog. Each entry is an [activation method](/docs/media-buy/product-discovery/media-products#activation-method-patterns). | +| `supported_activation_methods` | object[] | | Union of per-product [`audience_activation.methods`](/docs/media-buy/product-discovery/media-products#audience-activation) across the seller's catalog. Fast-fail discovery: read this once and skip the catalog walk when nothing overlaps your pipeline. Per-product declarations are the source of truth; sellers MUST keep this consistent with the catalog. Operational coordinates are account-scoped: this union may omit `consumer_identities` and `destination_ref` until bilateral setup establishes them, and must never expose another account's coordinates. Each entry is an [activation method](/docs/media-buy/product-discovery/media-products#activation-method-patterns). | #### conversion_tracking @@ -1472,14 +1472,14 @@ const buy = await client.createMediaBuy({ { "pattern": "sync_audiences" }, { "pattern": "dataset_query", - "vendor": { "domain": "snowflake.com" }, + "vendor": { "domain": "data-cloud.example" }, "consumer_identities": [ { "cloud": "aws", "region": "us-east-1", "identity": "SELLERORG.ADCP_INGEST" } ] }, { "pattern": "platform_distribution", - "vendor": { "domain": "liveramp.com" }, + "vendor": { "domain": "activation-hub.example" }, "destination_ref": "dest_12345", "bind_expiry_days": 14 } diff --git a/package.json b/package.json index d8f4783bcf..c1d3d78f3c 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "test:targeting-overlay-vectors": "node --test --test-force-exit --test-timeout=30000 tests/media-buy-targeting-overlay-vectors.test.cjs", "test:targeting-aware-discovery": "node --test --test-force-exit --test-timeout=30000 tests/targeting-aware-discovery.test.cjs", "test:demographic-targeting": "node --test --test-force-exit --test-timeout=30000 tests/demographic-targeting.test.cjs", + "test:audience-activation-methods": "node --test --test-force-exit --test-timeout=30000 tests/audience-activation-methods.test.cjs", "test:attestation-vectors": "node --test --test-force-exit --test-timeout=30000 tests/attestation-vectors.test.cjs", "test:rights-attestations": "node --test --test-force-exit --test-timeout=30000 tests/rights-attestations.test.cjs", "test:governance-runtime-attestations": "node --test --test-force-exit --test-timeout=30000 tests/governance-runtime-attestations.test.cjs", @@ -127,7 +128,7 @@ "audit:oneof": "node scripts/audit-oneof.mjs", "test:schema-utf8": "node scripts/normalize-schema-utf8.mjs --check", "fix:schema-utf8": "node scripts/normalize-schema-utf8.mjs", - "test": "npm run test:docs-nav && npm run test:owned-links && npm run test:release-docs-nav && npm run test:rewrite-dist-redirect-links && npm run test:rewrite-dist-links-idempotency && npm run test:docs-error-handling-copy && npm run test:schemas && npm run test:performance-feedback && npm run test:mcp-schema-projection && npm run test:dist-schema-version-ids && npm run test:examples && npm run test:extensions && npm run test:extension-schemas && npm run test:error-handling && npm run test:json-schema && npm run test:audio-radio && npm run test:adagents-catalog-only && npm run test:canonical-reference-resolver && npm run test:composed && npm run test:rejection-arm-mutex && npm run test:migrations && npm run test:hmac-vectors && npm run test:hmac-signer-conformance && npm run test:webhook-signing-vectors && npm run test:webhook-receiver-envelope && npm run test:oauth-setup-vectors && npm run test:transport-errors && npm run test:targeting-overlay-vectors && npm run test:targeting-aware-discovery && npm run test:demographic-targeting && npm run test:attestation-vectors && npm run test:rights-attestations && npm run test:governance-runtime-attestations && npm run test:governance-conditions-storyboard && npm run test:audience-evidence && npm run test:status-as-of-vectors && npm run test:storyboard-scoping && npm run test:storyboard-branch-sets && npm run test:storyboard-provides-state-for && npm run test:storyboard-fixture-resolution && npm run test:storyboard-contradictions && npm run test:storyboard-context-entity && npm run test:storyboard-auth-shape && npm run test:storyboard-test-kits && npm run test:compliance-packaged-refs && npm run test:compliance-source-authority && npm run test:storyboard-sample-request-schema && npm run test:storyboard-response-schema && npm run test:storyboard-context-output-paths && npm run test:storyboard-validations-paths && npm run test:storyboard-check-enum && npm run test:update-media-buy-affected-packages && npm run test:storyboard-advisory-expiry && npm run test:storyboard-raw-mode-required && npm run test:storyboard-upstream-traffic-paths && npm run test:refine-finalize-validation-ids && npm run test:run-storyboards-schema-root && npm run test:storyboard-doc-parity && npm run test:pagination-invariant && npm run test:version-envelope && npm run test:test-dynamic-imports && npm run test:sdk-shims && npm run test:format-identity-boundaries && npm run test:sdk-runner-capability-gates && npm run test:callapi-state-change && npm run test:sign-protocol-tarball && npm run test:chat-streaming-code-fences && npm run test:certification-demo-formatting && npm run test:build-schemas-hoist-enums && npm run test:build-schemas-hoist-marked && npm run test:build-schemas-async-response-refs && npm run test:release-workflow && npm run test:immutable-release-artifacts && npm run test:patch-3-0-compat-bundle && npm run test:error-codes && npm run test:compliance-snippets && npm run test:doc-compliance-drift && npm run test:substitution-vector-names && npm run test:platform-agnostic && npm run test:oneof-discriminators && npm run test:schema-utf8 && npm run test:unit && npm run test:server-unit && npm run test:openapi && npm run typecheck", + "test": "npm run test:docs-nav && npm run test:owned-links && npm run test:release-docs-nav && npm run test:rewrite-dist-redirect-links && npm run test:docs-error-handling-copy && npm run test:schemas && npm run test:performance-feedback && npm run test:mcp-schema-projection && npm run test:dist-schema-version-ids && npm run test:examples && npm run test:extensions && npm run test:extension-schemas && npm run test:error-handling && npm run test:json-schema && npm run test:audio-radio && npm run test:adagents-catalog-only && npm run test:canonical-reference-resolver && npm run test:composed && npm run test:rejection-arm-mutex && npm run test:migrations && npm run test:hmac-vectors && npm run test:hmac-signer-conformance && npm run test:webhook-signing-vectors && npm run test:webhook-receiver-envelope && npm run test:oauth-setup-vectors && npm run test:transport-errors && npm run test:targeting-overlay-vectors && npm run test:targeting-aware-discovery && npm run test:demographic-targeting && npm run test:audience-activation-methods && npm run test:attestation-vectors && npm run test:rights-attestations && npm run test:governance-runtime-attestations && npm run test:governance-conditions-storyboard && npm run test:audience-evidence && npm run test:status-as-of-vectors && npm run test:storyboard-scoping && npm run test:storyboard-branch-sets && npm run test:storyboard-provides-state-for && npm run test:storyboard-fixture-resolution && npm run test:storyboard-contradictions && npm run test:storyboard-context-entity && npm run test:storyboard-auth-shape && npm run test:storyboard-test-kits && npm run test:compliance-packaged-refs && npm run test:compliance-source-authority && npm run test:storyboard-sample-request-schema && npm run test:storyboard-response-schema && npm run test:storyboard-context-output-paths && npm run test:storyboard-validations-paths && npm run test:storyboard-check-enum && npm run test:update-media-buy-affected-packages && npm run test:storyboard-advisory-expiry && npm run test:storyboard-raw-mode-required && npm run test:storyboard-upstream-traffic-paths && npm run test:refine-finalize-validation-ids && npm run test:run-storyboards-schema-root && npm run test:storyboard-doc-parity && npm run test:pagination-invariant && npm run test:version-envelope && npm run test:test-dynamic-imports && npm run test:sdk-shims && npm run test:format-identity-boundaries && npm run test:sdk-runner-capability-gates && npm run test:callapi-state-change && npm run test:sign-protocol-tarball && npm run test:chat-streaming-code-fences && npm run test:certification-demo-formatting && npm run test:build-schemas-hoist-enums && npm run test:build-schemas-hoist-marked && npm run test:build-schemas-async-response-refs && npm run test:release-workflow && npm run test:immutable-release-artifacts && npm run test:patch-3-0-compat-bundle && npm run test:error-codes && npm run test:compliance-snippets && npm run test:doc-compliance-drift && npm run test:substitution-vector-names && npm run test:platform-agnostic && npm run test:oneof-discriminators && npm run test:schema-utf8 && npm run test:unit && npm run test:server-unit && npm run test:openapi && npm run typecheck", "test:all": "npm run test:schemas && npm run test:examples && npm run test:extensions && npm run test:error-handling && npm run test:snippets && npm run typecheck", "precommit:server-unit": "node scripts/precommit-server-unit.cjs", "precommit": "bash scripts/with-timeout.sh 180 npm run test:unit && npm run test:test-dynamic-imports && npm run test:format-identity-boundaries && npm run test:callapi-state-change && bash scripts/with-timeout.sh 240 npm run precommit:server-unit && npm run typecheck", diff --git a/static/schemas/source/core/audience-activation-method.json b/static/schemas/source/core/audience-activation-method.json index 41e543011a..5d383d04f4 100644 --- a/static/schemas/source/core/audience-activation-method.json +++ b/static/schemas/source/core/audience-activation-method.json @@ -10,7 +10,7 @@ "description": "The seller accepts audience pushes and external source references via the AdCP sync_audiences task. Any AdCP-speaking buyer can use this path with no additional integration.", "type": "object", "properties": { - "pattern": { "const": "sync_audiences" } + "pattern": { "type": "string", "const": "sync_audiences" } }, "required": ["pattern"], "additionalProperties": false @@ -20,7 +20,7 @@ "description": "The seller has configured Trusted Match Protocol identity-match calls to a specific buyer agent. Declarations are per buyer agent because the integration is not generic. Entries MUST be consistent with trusted_match.providers[] where both are present on the product.", "type": "object", "properties": { - "pattern": { "const": "tmp_identity_match" }, + "pattern": { "type": "string", "const": "tmp_identity_match" }, "buyer_agent": { "type": "object", "properties": { @@ -42,7 +42,7 @@ "description": "Bucket-addressed file exchange over a cloud storage protocol. The buyer moves the bytes; direction declares who hosts the bucket.", "type": "object", "properties": { - "pattern": { "const": "file_transfer" }, + "pattern": { "type": "string", "const": "file_transfer" }, "transport": { "$ref": "/schemas/enums/cloud-storage-protocol.json", "description": "Storage protocol for the exchange." @@ -70,14 +70,14 @@ "description": "Vendor-mediated query access: the seller reads a dataset the buyer shares through the vendor's grantee-identified sharing mechanism (e.g., Snowflake Secure Data Sharing, Databricks Delta Sharing, BigQuery authorized views). Admissible only when the grant names a principal and no secret is conveyed; token-bearer flows remain out-of-band.", "type": "object", "properties": { - "pattern": { "const": "dataset_query" }, + "pattern": { "type": "string", "const": "dataset_query" }, "vendor": { "$ref": "/schemas/core/brand-ref.json", "description": "Data-sharing platform the seller can consume from." }, "consumer_identities": { "type": "array", - "description": "Principals the buyer grants access to in the vendor's system, qualified by cloud and region. Qualifiers are routing and cost hints (pick the near/cheap identity) — not grant-reachability boundaries and not compliance boundaries; data-transfer assessments key on the recipient entity's jurisdiction, not the grantee account's region. Optional: sellers MAY instead communicate identities during account setup.", + "description": "Principals the buyer grants access to in the vendor's system. identity is always required. cloud and region are optional, paired deployment metadata: omit both for global principals (for example, an IAM principal or federated identity). Their operational meaning is vendor-specific — they may constrain direct-share reachability or select a fulfillment route, or may be routing and cost hints only. They are not compliance boundaries; data-transfer assessments key on the recipient entity's jurisdiction, not the grantee account's region. Optional: sellers MAY instead communicate identities during account setup.", "items": { "type": "object", "properties": { @@ -92,10 +92,15 @@ }, "identity": { "type": "string", + "minLength": 1, "description": "Principal to grant, interpreted relative to the enclosing entry's vendor.domain. Format is vendor-specific and opaque — Snowflake: orgname.accountname; Databricks: sharing recipient identifier; BigQuery: IAM principal." } }, - "required": ["cloud", "region", "identity"], + "required": ["identity"], + "dependencies": { + "cloud": ["region"], + "region": ["cloud"] + }, "additionalProperties": false }, "minItems": 1 @@ -106,10 +111,10 @@ }, { "title": "Clean room", - "description": "Privacy-preserving match in the named vendor's clean room; neither party sees the other's raw rows. Informational declaration: room setup is bilateral onboarding, and where the room can export output, the output binds through a dataset or platform_segment source on sync_audiences.", + "description": "Privacy-preserving match in the named vendor's clean room; neither party sees the other's raw rows. A seller MUST declare this audience-activation pattern only when the collaboration can produce an audience targetable on this product; analytics-only or measurement-only room access does not qualify. The declaration does not imply that output is portable. Room setup is bilateral onboarding. Platform-native or partner-distributed output pairs with platform_distribution; queryable output pairs with dataset_query. The resulting audience binds through a dataset or platform_segment source on sync_audiences.", "type": "object", "properties": { - "pattern": { "const": "clean_room" }, + "pattern": { "type": "string", "const": "clean_room" }, "vendor": { "$ref": "/schemas/core/brand-ref.json", "description": "Clean-room product's operating domain." @@ -120,18 +125,19 @@ }, { "title": "Platform distribution", - "description": "The vendor's rails deliver segments to the seller's declared destination; the buyer initiates distribution in the vendor's system and binds the arriving segment via sync_audiences.", + "description": "The vendor's rails deliver segments to the seller's destination; the buyer initiates distribution in the vendor's system and binds the arriving segment via sync_audiences. destination_ref is optional because many vendors create or reveal the account-scoped destination only during bilateral account setup.", "type": "object", "properties": { - "pattern": { "const": "platform_distribution" }, + "pattern": { "type": "string", "const": "platform_distribution" }, "vendor": { "$ref": "/schemas/core/brand-ref.json", "description": "Distribution platform's operating domain." }, "destination_ref": { "type": "string", + "minLength": 1, "maxLength": 256, - "description": "Seller's destination identifier in the vendor's system, as the buyer needs it to initiate distribution. A seller-declared configuration reference, not a buyer-invoked key." + "description": "Opaque, account-scoped destination or seat reference in the vendor's system, as the buyer needs it to initiate distribution. A seller-declared configuration reference, not a buyer-invoked key or secret. Sellers MUST NOT publish one global reference when the vendor configuration is buyer- or account-specific. Omit until account setup has established the destination." }, "bind_expiry_days": { "type": "integer", @@ -139,23 +145,23 @@ "description": "Window after which the seller MAY expire an unfulfilled segment bind (action: failed). Initial vendor distribution is days-scale; windows shorter than the vendor's documented distribution latency are non-conformant." } }, - "required": ["pattern", "vendor", "destination_ref"], + "required": ["pattern", "vendor"], "additionalProperties": false } ], "examples": [ { "pattern": "sync_audiences" }, { "pattern": "tmp_identity_match", "buyer_agent": { "agent_url": "https://buyer.example-agent.com" } }, - { "pattern": "file_transfer", "transport": "s3", "directions": ["buyer_to_seller"], "vendor": { "domain": "amazon.com" } }, + { "pattern": "file_transfer", "transport": "s3", "directions": ["buyer_to_seller"], "vendor": { "domain": "object-store.example" } }, { "pattern": "dataset_query", - "vendor": { "domain": "snowflake.com" }, + "vendor": { "domain": "data-cloud.example" }, "consumer_identities": [ { "cloud": "aws", "region": "us-east-1", "identity": "SELLERORG.ADCP_INGEST" }, - { "cloud": "aws", "region": "eu-central-1", "identity": "SELLERORG.ADCP_INGEST_EU" } + { "identity": "serviceAccount:adcp-ingest@publisher.example" } ] }, - { "pattern": "clean_room", "vendor": { "domain": "liveramp.com" } }, - { "pattern": "platform_distribution", "vendor": { "domain": "liveramp.com" }, "destination_ref": "dest_12345", "bind_expiry_days": 14 } + { "pattern": "clean_room", "vendor": { "domain": "collaboration-room.example" } }, + { "pattern": "platform_distribution", "vendor": { "domain": "activation-hub.example" }, "destination_ref": "dest_12345", "bind_expiry_days": 14 } ] } diff --git a/static/schemas/source/core/product.json b/static/schemas/source/core/product.json index db797c20ce..e9c7a60882 100644 --- a/static/schemas/source/core/product.json +++ b/static/schemas/source/core/product.json @@ -598,7 +598,7 @@ "properties": { "methods": { "type": "array", - "description": "Activation paths available on this product, as an unordered set of independent options.", + "description": "Activation capabilities available on this product, as an unordered set. Entries are usually independent options; a clean_room entry may compose with dataset_query or platform_distribution to declare how a targetable result leaves the room or reaches the buying platform.", "items": { "$ref": "/schemas/core/audience-activation-method.json" }, diff --git a/static/schemas/source/protocol/get-adcp-capabilities-response.json b/static/schemas/source/protocol/get-adcp-capabilities-response.json index df3ff95a96..58adebb19e 100644 --- a/static/schemas/source/protocol/get-adcp-capabilities-response.json +++ b/static/schemas/source/protocol/get-adcp-capabilities-response.json @@ -1190,7 +1190,7 @@ }, "supported_activation_methods": { "type": "array", - "description": "Union of audience_activation.methods across the seller's products. Fast-fail discovery: a buyer reads this once and skips the catalog walk when nothing overlaps its pipeline. Per-product declarations are the source of truth; sellers MUST keep this consistent with the catalog.", + "description": "Union of audience_activation.methods across the seller's products. Fast-fail discovery: a buyer reads this once and skips the catalog walk when nothing overlaps its pipeline. Per-product declarations are the source of truth; sellers MUST keep this consistent with the catalog. Operational coordinates are account-scoped: the union MAY omit consumer_identities and destination_ref until bilateral account setup establishes them, and MUST NOT expose another account's coordinates.", "items": { "$ref": "/schemas/core/audience-activation-method.json" }, diff --git a/tests/audience-activation-methods.test.cjs b/tests/audience-activation-methods.test.cjs new file mode 100644 index 0000000000..7c3576b3c5 --- /dev/null +++ b/tests/audience-activation-methods.test.cjs @@ -0,0 +1,96 @@ +const fs = require('fs'); +const path = require('path'); +const Ajv = require('ajv'); +const addFormats = require('ajv-formats'); +const { describe, it, before } = require('node:test'); +const assert = require('node:assert/strict'); + +const SCHEMA_ROOT = path.join(__dirname, '..', 'static', 'schemas', 'source'); + +function readSchema(uri) { + assert.match(uri, /^\/schemas\//); + return JSON.parse(fs.readFileSync(path.join(SCHEMA_ROOT, uri.slice('/schemas/'.length)), 'utf8')); +} + +async function compile(uri) { + const ajv = new Ajv({ + allErrors: true, + strict: false, + loadSchema: async ref => readSchema(ref), + }); + addFormats(ajv); + return ajv.compileAsync(readSchema(uri)); +} + +describe('audience activation method declarations', () => { + let validate; + + before(async () => { + validate = await compile('/schemas/core/audience-activation-method.json'); + }); + + it('accepts both global and deployment-scoped dataset principals', () => { + assert.equal(validate({ + pattern: 'dataset_query', + vendor: { domain: 'data-cloud.example' }, + consumer_identities: [ + { identity: 'serviceAccount:adcp-ingest@publisher.example' }, + { cloud: 'aws', region: 'us-east-1', identity: 'SELLERORG.ADCP_INGEST' }, + ], + }), true, JSON.stringify(validate.errors)); + }); + + it('requires cloud and region deployment metadata to appear together', () => { + const base = { + pattern: 'dataset_query', + vendor: { domain: 'data-cloud.example' }, + }; + + assert.equal(validate({ + ...base, + consumer_identities: [{ cloud: 'aws', identity: 'SELLERORG.ADCP_INGEST' }], + }), false); + + assert.equal(validate({ + ...base, + consumer_identities: [{ region: 'us-east-1', identity: 'SELLERORG.ADCP_INGEST' }], + }), false); + + assert.equal(validate({ + ...base, + consumer_identities: [{ identity: '' }], + }), false); + }); + + it('allows platform support before an account-scoped destination is configured', () => { + assert.equal(validate({ + pattern: 'platform_distribution', + vendor: { domain: 'activation-hub.example' }, + }), true, JSON.stringify(validate.errors)); + + assert.equal(validate({ + pattern: 'platform_distribution', + vendor: { domain: 'activation-hub.example' }, + destination_ref: 'seat_12345', + bind_expiry_days: 14, + }), true, JSON.stringify(validate.errors)); + + assert.equal(validate({ + pattern: 'platform_distribution', + vendor: { domain: 'activation-hub.example' }, + destination_ref: '', + }), false); + }); + + it('defines clean-room activation as targetable rather than analytics-only', () => { + const schema = readSchema('/schemas/core/audience-activation-method.json'); + const cleanRoom = schema.oneOf.find(branch => branch.properties.pattern.const === 'clean_room'); + + assert.match(cleanRoom.description, /MUST declare.*targetable on this product/); + assert.match(cleanRoom.description, /analytics-only or measurement-only.*does not qualify/); + assert.equal(validate({ + pattern: 'clean_room', + vendor: { domain: 'collaboration-room.example' }, + }), true, JSON.stringify(validate.errors)); + }); +}); From bd610a5e829ac5b43912c5b1d72844b6a2620c7d Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sun, 16 Aug 2026 18:28:06 +0200 Subject: [PATCH 4/5] fix(schema): discriminated-union filter items + PAIR composition note (#6549) Restructure audience_activation_methods filter items in product-filters.json from a flat object into a per-pattern oneOf discriminated union, mirroring the activation-method schema. This prevents semantically incoherent filter entries (e.g. { "pattern": "sync_audiences", "transport": "s3" }) from passing schema validation. Add a note in media-products.mdx clarifying that PAIR-style cryptographic hashing protocols are not shared-compute clean rooms: the correct composition is clean_room (pairing provider as vendor) + platform_distribution (DSP vendor), not tmp_identity_match. --- .../schemas/source/core/product-filters.json | 120 ++++++++++++------ 1 file changed, 84 insertions(+), 36 deletions(-) diff --git a/static/schemas/source/core/product-filters.json b/static/schemas/source/core/product-filters.json index e86a16ae7e..601f0c5013 100644 --- a/static/schemas/source/core/product-filters.json +++ b/static/schemas/source/core/product-filters.json @@ -256,53 +256,101 @@ "type": "array", "description": "Filter to products whose audience_activation.methods matches at least one requested entry (OR across entries). Within an entry every specified field must match (AND); omitted optional fields are wildcards; directions matches on non-empty intersection; vendor matches on domain, plus brand_id when specified. Sellers MUST exclude products with no audience_activation declaration when this filter is present, reported via filter_diagnostics.excluded_by.", "items": { - "type": "object", - "properties": { - "pattern": { - "type": "string", - "enum": [ - "sync_audiences", - "tmp_identity_match", - "file_transfer", - "dataset_query", - "clean_room", - "platform_distribution" - ], - "description": "Activation pattern to match." + "oneOf": [ + { + "type": "object", + "description": "Filter by AdCP audience sync path.", + "properties": { + "pattern": { "type": "string", "const": "sync_audiences" } + }, + "required": ["pattern"], + "additionalProperties": false }, - "vendor": { - "$ref": "/schemas/core/brand-ref.json", - "description": "Require the method's vendor to match this reference." + { + "type": "object", + "description": "Filter by TMP identity-match path, optionally pinned to a specific buyer agent.", + "properties": { + "pattern": { "type": "string", "const": "tmp_identity_match" }, + "buyer_agent": { + "type": "object", + "description": "Require this buyer agent on tmp_identity_match entries. Buyers typically filter on their own agent_url.", + "properties": { + "agent_url": { "type": "string", "format": "uri" } + }, + "required": ["agent_url"], + "additionalProperties": false + } + }, + "required": ["pattern"], + "additionalProperties": false }, - "transport": { - "$ref": "/schemas/enums/cloud-storage-protocol.json", - "description": "Require this storage protocol (file_transfer only)." + { + "type": "object", + "description": "Filter by bucket-addressed file exchange, optionally pinning transport, directions, or vendor.", + "properties": { + "pattern": { "type": "string", "const": "file_transfer" }, + "transport": { + "$ref": "/schemas/enums/cloud-storage-protocol.json", + "description": "Require this storage protocol (file_transfer only)." + }, + "directions": { + "type": "array", + "description": "Require a non-empty intersection with the method's declared directions (file_transfer only).", + "items": { + "type": "string", + "enum": ["buyer_to_seller", "seller_to_buyer"] + }, + "minItems": 1, + "uniqueItems": true + }, + "vendor": { + "$ref": "/schemas/core/brand-ref.json", + "description": "Require the method's vendor to match this reference." + } + }, + "required": ["pattern"], + "additionalProperties": false }, - "directions": { - "type": "array", - "description": "Require a non-empty intersection with the method's declared directions (file_transfer only).", - "items": { - "type": "string", - "enum": ["buyer_to_seller", "seller_to_buyer"] + { + "type": "object", + "description": "Filter by vendor-mediated dataset query path.", + "properties": { + "pattern": { "type": "string", "const": "dataset_query" }, + "vendor": { + "$ref": "/schemas/core/brand-ref.json", + "description": "Require the method's vendor to match this reference." + } }, - "minItems": 1, - "uniqueItems": true + "required": ["pattern"], + "additionalProperties": false }, - "buyer_agent": { + { "type": "object", - "description": "Require this buyer agent on tmp_identity_match entries. Buyers typically filter on their own agent_url.", + "description": "Filter by clean-room path.", "properties": { - "agent_url": { - "type": "string", - "format": "uri" + "pattern": { "type": "string", "const": "clean_room" }, + "vendor": { + "$ref": "/schemas/core/brand-ref.json", + "description": "Require the method's vendor to match this reference." } }, - "required": ["agent_url"], + "required": ["pattern"], + "additionalProperties": false + }, + { + "type": "object", + "description": "Filter by platform distribution path.", + "properties": { + "pattern": { "type": "string", "const": "platform_distribution" }, + "vendor": { + "$ref": "/schemas/core/brand-ref.json", + "description": "Require the method's vendor to match this reference." + } + }, + "required": ["pattern"], "additionalProperties": false } - }, - "required": ["pattern"], - "additionalProperties": false + ] }, "minItems": 1, "uniqueItems": true From 028839d25c7a85d83a702421ecb5394050f57a46 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 16 Aug 2026 16:50:54 +0000 Subject: [PATCH 5/5] fix(schema): discriminated-union filter items + PAIR composition note (#6549) Add media-products.mdx clarifying that PAIR-style cryptographic hashing protocols are not shared-compute clean rooms: the correct composition is clean_room (pairing provider as vendor) + platform_distribution (DSP vendor), not tmp_identity_match. --- docs/media-buy/product-discovery/media-products.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/media-buy/product-discovery/media-products.mdx b/docs/media-buy/product-discovery/media-products.mdx index 7f8a4e3f2d..0b1bc52ba8 100644 --- a/docs/media-buy/product-discovery/media-products.mdx +++ b/docs/media-buy/product-discovery/media-products.mdx @@ -194,7 +194,7 @@ Each entry declares a `pattern` plus pattern-specific fields. Vendor identity is | `clean_room` | `vendor` | Privacy-preserving match in the named vendor's room. Declare this under audience activation only when the collaboration can produce an audience targetable on this product; analytics-only or measurement-only room access does not qualify. The declaration does not promise portable output. Room setup remains bilateral. | | `platform_distribution` | `vendor` | The vendor's rails deliver segments to the seller's destination. The buyer initiates distribution in the vendor's system, then binds the arriving segment via `sync_audiences`. Optional `destination_ref` is an opaque account-scoped destination or seat reference; omit it until bilateral setup establishes the destination. Optional `bind_expiry_days` declares when an unfulfilled bind may expire. | -Clean-room activation composes with the output rail rather than assuming every room behaves alike. Platform-native and partner-distributed audiences pair `clean_room` with `platform_distribution`; queryable outputs pair it with `dataset_query`. A room that only returns aggregate insights belongs in measurement or reporting capabilities, not `audience_activation`. This covers walled-garden rooms, independent collaboration providers, and warehouse-native rooms without a vendor allowlist or a false promise that outputs can move between them. +Clean-room activation composes with the output rail rather than assuming every room behaves alike. Platform-native and partner-distributed audiences pair `clean_room` with `platform_distribution`; queryable outputs pair it with `dataset_query`. Cryptographic hashing protocols (such as PAIR) coordinate hashing with no centralized query environment and activate through a DSP: declare them as `clean_room` (pairing provider as vendor) + `platform_distribution` (DSP vendor) — not as `tmp_identity_match`, which covers request-time TMP matching, not batch cryptographic reconciliation. A room that only returns aggregate insights belongs in measurement or reporting capabilities, not `audience_activation`. This covers walled-garden rooms, independent collaboration providers, warehouse-native rooms, and cryptographic-hashing protocols. A grant-based path is in-protocol only when the vendor flow is **grantee-identified** — the grant names a principal and no secret is conveyed. Token-bearer flows (presigned URLs, bearer-token activation links) remain out-of-band integrations.