diff --git a/.changeset/audience-activation-methods.md b/.changeset/audience-activation-methods.md new file mode 100644 index 0000000000..1137c7f84d --- /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[]` 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 f829c76327..0b1bc52ba8 100644 --- a/docs/media-buy/product-discovery/media-products.mdx +++ b/docs/media-buy/product-discovery/media-products.mdx @@ -150,6 +150,56 @@ 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 +"audience_activation": { + "methods": [ + { "pattern": "sync_audiences" }, + { + "pattern": "dataset_query", + "vendor": { "domain": "data-cloud.example" }, + "consumer_identities": [ + { "cloud": "aws", "region": "us-east-1", "identity": "SELLERORG.ADCP_INGEST" } + ] + }, + { + "pattern": "platform_distribution", + "vendor": { "domain": "activation-hub.example" }, + "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 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) + +#### 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. `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`. 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. + +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 4881d954fa..18a0d7d344 100644 --- a/docs/media-buy/task-reference/get_products.mdx +++ b/docs/media-buy/task-reference/get_products.mdx @@ -360,6 +360,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`](/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 diff --git a/docs/media-buy/task-reference/sync_audiences.mdx b/docs/media-buy/task-reference/sync_audiences.mdx index 601ae5c33c..9195866567 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 f346922e8c..778898f9cb 100644 --- a/docs/protocol/get_adcp_capabilities.mdx +++ b/docs/protocol/get_adcp_capabilities.mdx @@ -857,6 +857,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. 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 @@ -1672,7 +1673,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": "data-cloud.example" }, + "consumer_identities": [ + { "cloud": "aws", "region": "us-east-1", "identity": "SELLERORG.ADCP_INGEST" } + ] + }, + { + "pattern": "platform_distribution", + "vendor": { "domain": "activation-hub.example" }, + "destination_ref": "dest_12345", + "bind_expiry_days": 14 + } + ] }, "conversion_tracking": { "multi_source_event_dedup": false, diff --git a/package.json b/package.json index afffee8974..21d8667a98 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "test:targeting-aware-discovery": "node --test --test-force-exit --test-timeout=30000 tests/targeting-aware-discovery.test.cjs", "test:geo-region-targeting": "node --test --test-force-exit --test-timeout=30000 tests/geo-region-targeting.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", @@ -130,7 +131,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:signal-governance-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: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: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:signal-governance-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 new file mode 100644 index 0000000000..5d383d04f4 --- /dev/null +++ b/static/schemas/source/core/audience-activation-method.json @@ -0,0 +1,167 @@ +{ + "$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": { "type": "string", "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": { "type": "string", "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": { "type": "string", "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": { "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. 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": { + "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", + "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": ["identity"], + "dependencies": { + "cloud": ["region"], + "region": ["cloud"] + }, + "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. 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": { "type": "string", "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 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": { "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": "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", + "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"], + "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": "object-store.example" } }, + { + "pattern": "dataset_query", + "vendor": { "domain": "data-cloud.example" }, + "consumer_identities": [ + { "cloud": "aws", "region": "us-east-1", "identity": "SELLERORG.ADCP_INGEST" }, + { "identity": "serviceAccount:adcp-ingest@publisher.example" } + ] + }, + { "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-filters.json b/static/schemas/source/core/product-filters.json index 645ec206b9..601f0c5013 100644 --- a/static/schemas/source/core/product-filters.json +++ b/static/schemas/source/core/product-filters.json @@ -252,6 +252,109 @@ }, "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": { + "oneOf": [ + { + "type": "object", + "description": "Filter by AdCP audience sync path.", + "properties": { + "pattern": { "type": "string", "const": "sync_audiences" } + }, + "required": ["pattern"], + "additionalProperties": false + }, + { + "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 + }, + { + "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 + }, + { + "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." + } + }, + "required": ["pattern"], + "additionalProperties": false + }, + { + "type": "object", + "description": "Filter by clean-room path.", + "properties": { + "pattern": { "type": "string", "const": "clean_room" }, + "vendor": { + "$ref": "/schemas/core/brand-ref.json", + "description": "Require the method's vendor to match this reference." + } + }, + "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 + } + ] + }, + "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..e9c7a60882 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 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" + }, + "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 377f0e94c7..23b10a4f31 100644 --- a/static/schemas/source/protocol/get-adcp-capabilities-response.json +++ b/static/schemas/source/protocol/get-adcp-capabilities-response.json @@ -1289,6 +1289,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. 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" + }, + "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.", 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)); + }); +});