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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/preserve-tmp-context-provider-attribution.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"adcontextprotocol": patch
---

Preserve Trusted Match Context targeting key-values in router-authored, provider-attributed buckets.
3 changes: 3 additions & 0 deletions .github/workflows/build-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ jobs:
- name: Validate schemas
run: npm run test:schemas && npm run test:json-schema && npm run test:adagents-catalog-only && npm run test:extension-schemas && npm run test:composed

- name: Trusted Match Context merge conformance vectors
run: npm run test:tmp-context-merge

- name: Validate MCP schema projection
run: npm run test:mcp-schema-projection

Expand Down
1 change: 1 addition & 0 deletions docs/trusted-match/ai-mediation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ Priya chose full recommendation for StreamHaus's assistant:

```json
{
"status": "completed",
"type": "context_match_response",
"request_id": "ctx-trail-shoes-01",
"offers": [
Expand Down
3 changes: 2 additions & 1 deletion docs/trusted-match/buyer-guide.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,9 @@ The router sends you page context. You evaluate your active packages against tha
3. Return offers for packages that match, each with a creative manifest

```json
// Your response
// Your provider→router response (provider-context-match-response.json)
{
"status": "completed",
"type": "context_match_response",
"request_id": "ctx-8f3a2b",
"offers": [
Expand Down
9 changes: 5 additions & 4 deletions docs/trusted-match/context-and-identity.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ The buyer agent evaluates each package against the context. Package B is homepag

```json
{
"status": "completed",
"type": "context_match_response",
"request_id": "ctx-7f3a",
"offers": [
Expand Down Expand Up @@ -100,7 +101,7 @@ The buyer agent evaluates each package against the context. Package B is homepag
}
```

The buyer returns an offer per matched package. Each offer carries a `package_id` and optionally a `brand`, `price`, `summary`, `creative_manifest`, and `macros`. The `summary` gives the publisher enough to judge relevance. When the creative manifest is present inline, the publisher has everything needed to render. For large creatives (e.g., VAST video), the manifest references external assets via URLs rather than embedding them. The response also includes enrichment signals — audience segments and targeting key-values — that the publisher can pass to their ad server.
The buyer returns an offer per matched package. Each offer carries a `package_id` and optionally a `brand`, `price`, `summary`, `creative_manifest`, and `macros`. The `summary` gives the publisher enough to judge relevance. When the creative manifest is present inline, the publisher has everything needed to render. For large creatives (e.g., VAST video), the manifest references external assets via URLs rather than embedding them. This provider-hop response validates against `provider-context-match-response.json`. Targeting pairs use `signals.targeting_kvs`; the router preserves them unchanged under `signals_by_provider[provider_id].targeting_kvs` in the `context-match-response.json` shape returned to the publisher.

### What Context Match never carries

Expand Down Expand Up @@ -186,7 +187,7 @@ For each offer from Context Match:
→ Skip this offer for this user.

Result: Accept Package A.
Set targeting KVs: category_affinity=beverages, seasonal_relevance=high.
Resolve the provider-attributed targeting tuples through the publisher's local mapping, then set the mapped ad-server keys. Drop any tuple without a mapping.
```

The publisher already knows how to render Package A — it maps to a sponsored carousel slot. The inline creative manifest carries the catalog items and assets needed. The publisher handled the rest.
Expand All @@ -195,7 +196,7 @@ The publisher already knows how to render Package A — it maps to a sponsored c

TMP's response model is the offer. An offer carries a `package_id` (required) and optional fields: `brand`, `price`, `summary`, `creative_manifest`, and `macros` (a key-value map for dynamic values).

**Simple case (GAM/Prebid)**: The offer carries `package_id`. The publisher flows `package_id` via `targeting_kvs` signals to GAM for line item matching. The `macros` map carries dynamic values (e.g., sponsor labels, promo text) that GAM can insert into the creative at render time.
**Simple case (GAM/Prebid)**: The offer carries `package_id`. The provider may return a provider-local package targeting pair in `signals.targeting_kvs`; the router attributes it in `signals_by_provider`, and the publisher maps `(provider_id, key)` to its GAM key for line item matching. The `macros` map carries dynamic values (e.g., sponsor labels, promo text) that GAM can insert into the creative at render time.

**Rich case (AI assistants, dynamic retail)**: The offer includes a `summary` ("50% off cold brew — recipe integration") so the publisher can judge relevance, and an inline `creative_manifest` with everything needed to render. For large creatives (e.g., VAST video), the manifest references external assets via URLs rather than embedding the full payload.

Expand All @@ -212,7 +213,7 @@ Per-user exposure tracking flows through the TMPX macro — an encrypted token f
Context Match responses can include enrichment signals alongside package activation:

- **Segments**: Audience or contextual segments (e.g., "coffee_enthusiast", "high_purchase_intent") that flow into the publisher's ad server as targeting signals.
- **Targeting key-values**: Arbitrary key-value pairs (e.g., `category_affinity=beverages`) that the publisher can use for line item targeting, reporting breakdowns, or real-time decisioning.
- **Targeting key-values**: Provider-local key-value pairs (e.g., `category_affinity=beverages`). The router attributes them under the provider's registered `provider_id`; the publisher maps `(provider_id, key)` to its local destination before using the value for line item targeting, reporting breakdowns, or real-time decisioning. Unmapped tuples are dropped.

Enrichment signals are additive — they are not tied to specific packages. A buyer agent might return enrichment signals even when it activates no packages, providing value as a data provider rather than a demand source.

Expand Down
1 change: 1 addition & 0 deletions docs/trusted-match/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ Response from Sam's buyer agent:

```json
{
"status": "completed",
"type": "context_match_response",
"request_id": "ctx-8f3a2b",
"offers": [
Expand Down
8 changes: 5 additions & 3 deletions docs/trusted-match/router-architecture.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -136,12 +136,14 @@ Identity tokens come from existing providers (ID5, LiveRamp, UID2, etc.) that ar
When the publisher sends a Context Match request:

1. The router identifies all providers configured for the request's `property_rid` with `context_match` capability.
2. It sends the request to all matching providers in parallel over HTTP/2.
2. It sends the request to all matching providers in parallel over HTTP/2 and validates their responses against `provider-context-match-response.json`. That provider-hop schema permits `signals.targeting_kvs` but forbids `signals_by_provider`.
3. It waits for responses up to the latency budget (default: 50ms).
4. It merges responses:
- **Offers** are collected from all providers. If two providers return offers for the same `package_id` (uncommon — packages are typically provider-specific), the router keeps the offer from the higher-priority provider (lower `priority` value). Equal priorities are broken by first response received. Duplicate `package_id` across providers is a configuration error; the router SHOULD log a warning that identifies both providers and the selected winner.
- **Enrichment signals** are concatenated. Segments from all providers are combined into a single list. Targeting key-values from different providers are namespaced to prevent collisions.
5. It returns the merged response to the publisher.
- **Enrichment signals** are merged by kind. Segments from all providers are combined into `signals.segments`. Targeting key-values retain provider attribution: for every provider that returns a non-empty `signals.targeting_kvs` list, the router copies the list unchanged into `signals_by_provider[provider_id].targeting_kvs`, using the publisher-assigned `provider_id` from that provider's registration. The router MUST derive this key itself and MUST ignore or reject any provider-supplied `signals_by_provider` map. A provider with no targeting pairs is omitted.
5. It validates the merged response against `context-match-response.json` and returns it to the publisher. That router-hop schema permits `signals_by_provider` and forbids flattened `signals.targeting_kvs` even when no attributed bucket is present.

The publisher resolves each `(provider_id, key)` tuple in `signals_by_provider` to the ad-server destination configured for that provider on the local surface. This mapping is publisher-owned deployment configuration and does not travel between provider and router. Tuples without a local mapping MUST be dropped; the publisher MUST NOT fall back to treating an unrecognized provider-local key as a global ad-server key. Distinct providers may therefore use the same local key without collision or cross-provider name capture.

### Identity Match fan-out

Expand Down
36 changes: 30 additions & 6 deletions docs/trusted-match/specification.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -126,15 +126,31 @@ Geographic context for the impression opportunity. Publisher controls granularit

### ContextMatchResponse

Returned by the buyer agent. Contains offers for matched packages and optional response-level targeting signals.
Contains offers for matched packages and optional response-level signals. Two schemas carry this message across the two hops of a Context Match fan-out so the provider boundary cannot claim router-authored attribution and the publisher boundary cannot receive flattened provider targeting keys.

**Provider → router** (`provider-context-match-response.json`):

| Field | Type | Required | Description |
|---|---|---|---|
| `type` | string | Yes | `"context_match_response"`. Message type discriminator for deserialization. |
| `request_id` | string | Yes | Echo of the request's `request_id`. |
| `offers` | List\<Offer\> | Yes | Offers from the buyer, one per activated package. Empty list means no packages matched. |
| `cache_ttl` | integer | No | Provider override (in seconds) for the router's default Context Match response cache TTL. When present, routers MUST use this value instead of their default. `0` disables caching (e.g., when targeting configuration has just changed); schema-enforced maximum is 86400 seconds. See [Caching](#caching). |
| `signals` | Signals | No | Response-level targeting signals for ad server pass-through. Not per-offer — applies to the response as a whole. In the GAM case, these carry the key-value pairs that trigger line items. |
| `signals` | Signals | No | Provider-origin response-level signals. Targeting pairs use the provider's local vocabulary in `signals.targeting_kvs`; the provider MUST NOT author `signals_by_provider`. |

**Router → publisher** (`context-match-response.json`):

| Field | Type | Required | Description |
|---|---|---|---|
| `type` | string | Yes | `"context_match_response"`. Message type discriminator for deserialization. |
| `request_id` | string | Yes | Echo of the request's `request_id`. |
| `offers` | List\<Offer\> | Yes | Offers collected across the provider fan-out. Empty list means no packages matched. |
| `signals` | Signals | No | Merged non-keyed response-level signals such as `segments`. The router-facing schema forbids flattened `signals.targeting_kvs`, even when `signals_by_provider` is absent. |
| `signals_by_provider` | Map\<provider_id, ProviderSignals\> | No | Router-authored targeting pairs grouped under the publisher-assigned `provider_id` from provider registration. For every provider response containing a non-empty `signals.targeting_kvs` list, the router copies the complete list unchanged into `signals_by_provider[provider_id].targeting_kvs`. The router derives the key from registration and MUST ignore or reject provider-supplied buckets. Providers with no targeting pairs are omitted. Map keys match the registered `provider_id` charset (`^[A-Za-z0-9_]+$`, 1–64 chars). |

The hop-specific fields are disjoint. `provider-context-match-response.json` permits `signals.targeting_kvs` and forbids `signals_by_provider`; `context-match-response.json` permits `signals_by_provider` and forbids `signals.targeting_kvs` whether or not the attributed map is present. Both forbid `context` and `ext` so extension data cannot cross the context privacy boundary.

`signals_by_provider` is the attribution boundary for merged targeting signals. The router MUST NOT invent a namespace by rewriting provider keys, accept a provider's claimed bucket name, or flatten attributed pairs back into `signals.targeting_kvs`. The publisher resolves each `(provider_id, key)` tuple through publisher-owned deployment configuration to a local ad-server destination. A tuple without a local mapping MUST be dropped; its provider-local `key` MUST NOT be treated as a global or publisher-owned targeting key. The mapping format is deployment-specific in 3.2 and is not carried on either protocol hop.

#### Offer

Expand All @@ -160,19 +176,27 @@ A buyer's response for a single package.

#### Signals

Response-level targeting signals for ad server pass-through.
Response-level context signals. Providers may emit both fields; routers combine `segments` but move targeting pairs to `signals_by_provider` rather than passing provider-local keys through as flattened publisher targeting.

| Field | Type | Required | Description |
|---|---|---|---|
| `segments` | List\<string\> | No | Audience or contextual segment IDs. |
| `targeting_kvs` | List\<KeyValuePair\> | No | Key-value pairs for ad server targeting. |
| `targeting_kvs` | List\<KeyValuePair\> | No | Provider-hop only. Provider-local key-value pairs for ad-server targeting. A router preserves the complete list unchanged in the corresponding `signals_by_provider` bucket; this field is forbidden on router-to-publisher responses. |

#### ProviderSignals

Router-authored bucket preserving one provider's targeting pairs.

| Field | Type | Required | Description |
|---|---|---|---|
| `targeting_kvs` | List\<KeyValuePair\> | Yes | Non-empty exact copy of the provider's `signals.targeting_kvs` list. Providers with an absent or empty list have no bucket. |

#### KeyValuePair

| Field | Type | Required | Description |
|---|---|---|---|
| `key` | string | Yes | Targeting key. |
| `value` | string | Yes | Targeting value. |
| `key` | string | Yes | Provider-local targeting key. Publishers resolve it together with `provider_id`; it is not globally unique or itself a publisher ad-server destination. |
| `value` | string | Yes | Targeting value, preserved unchanged by the router. |

### IdentityMatchRequest

Expand Down
3 changes: 3 additions & 0 deletions docs/trusted-match/surfaces/ai-assistants.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ The buyer agent responds with an offer:

```json
{
"status": "completed",
"type": "context_match_response",
"request_id": "ctx-a1b2c3d4",
"offers": [
Expand Down Expand Up @@ -84,6 +85,8 @@ The buyer agent responds with an offer:
}
```

This buyer response uses the provider-hop `provider-context-match-response.json` shape, where `signals.targeting_kvs` contains provider-local keys. The router moves those pairs unchanged into its router-authored `signals_by_provider` buckets; it never forwards this flattened list to the publisher.

The buyer's offer includes `package_id` (required) along with optional fields: `brand`, `price`, `summary`, `creative_manifest`, and `macros`. For an AI assistant, the creative manifest is small enough to send inline in the real-time path. The manifest carries the text the platform can weave into the conversation and the catalog items to reference. The `summary` helps the platform judge relevance before deciding whether to incorporate the sponsored content.

## Identity Match
Expand Down
1 change: 1 addition & 0 deletions docs/trusted-match/surfaces/ctv.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ Each buyer agent evaluates the content context and responds with offers for pack

```json
{
"status": "completed",
"type": "context_match_response",
"request_id": "ctx-9f3a-e7b2",
"offers": [
Expand Down
2 changes: 2 additions & 0 deletions docs/trusted-match/surfaces/mobile.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ No package list is sent per request. The provider evaluates all eligible package

```json
{
"status": "completed",
"type": "context_match_response",
"request_id": "ctx-mob-7f3a91",
"offers": [
Expand Down Expand Up @@ -107,6 +108,7 @@ A recipe app shows sponsored content cards in its recipe feed:

```json
{
"status": "completed",
"type": "context_match_response",
"request_id": "ctx-mob-b2c419",
"offers": [
Expand Down
1 change: 1 addition & 0 deletions docs/trusted-match/surfaces/retail-media.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ The buyer responds with an offer:

```json
{
"status": "completed",
"type": "context_match_response",
"request_id": "ctx-retail-8f3a",
"offers": [
Expand Down
23 changes: 14 additions & 9 deletions docs/trusted-match/surfaces/web.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -46,20 +46,25 @@ The router fans out to each buyer agent and merges the responses. Each buyer ret

```json
{
"status": "completed",
"type": "context_match_response",
"request_id": "ctx-7f2a-oakwood-91b3",
"offers": [
{ "package_id": "pkg-display-0041" },
{ "package_id": "pkg-native-0078" }
],
"signals": {
"segments": ["sustainability", "home_cooking"],
"targeting_kvs": [
{ "key": "adcp_seg", "value": "sustainability" },
{ "key": "adcp_seg", "value": "home_cooking" },
{ "key": "adcp_pkg", "value": "pkg-display-0041" },
{ "key": "adcp_pkg", "value": "pkg-native-0078" }
]
"segments": ["sustainability", "home_cooking"]
},
"signals_by_provider": {
"outdoor_media": {
"targeting_kvs": [
{ "key": "segment", "value": "sustainability" },
{ "key": "segment", "value": "home_cooking" },
{ "key": "package", "value": "pkg-display-0041" },
{ "key": "package", "value": "pkg-native-0078" }
]
}
}
}
```
Expand All @@ -68,7 +73,7 @@ Key points:

- `offers` contains one entry per activated package. The provider's synced set for this placement included three packages — these two matched the kitchen/sustainability context, the third (`pkg-display-0103`) did not and is absent.
- For web/GAM activation, offers are simple — just `package_id`. Richer fields (`brand`, `price`, `summary`, `creative_manifest`, `macros`) are available for integrations that need them but are not required.
- `signals.targeting_kvs` are the key-value pairs that the Prebid module sets on the GAM ad request. GAM line items are configured to match on these keys.
- `signals_by_provider` preserves which provider emitted each targeting pair. The Prebid module resolves each `(provider_id, key)` through publisher-owned configuration before setting the resulting GAM key. Unmapped tuples are dropped.

## Identity Match

Expand Down Expand Up @@ -148,7 +153,7 @@ Only two packages survive: `pkg-display-0041` and `pkg-native-0078`.

### Step 2: Set GAM Targeting

The Prebid module takes the context match `signals.targeting_kvs` and sets them on the GAM ad request as key-value pairs:
The Prebid module resolves the context match `signals_by_provider` tuples through the publisher's local mapping. For example, mapping `(outdoor_media, segment)` to `adcp_seg` and `(outdoor_media, package)` to `adcp_pkg` produces:

```
adcp_seg = sustainability, home_cooking
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
"typecheck": "tsc --project server/tsconfig.json --noEmit",
"test:schemas": "node tests/schema-validation.test.cjs && node --test tests/schema-deprecation-metadata.test.cjs",
"test:dist-schema-version-ids": "node --test --test-force-exit --test-timeout=30000 tests/dist-schema-version-ids.test.cjs",
"test:examples": "node tests/example-validation-simple.test.cjs",
"test:examples": "node tests/example-validation-simple.test.cjs && npm run test:tmp-context-merge",
"test:tmp-context-merge": "node --test --test-force-exit --test-timeout=30000 tests/trusted-match-context-merge.test.cjs",
"test:extensions": "node tests/extension-fields.test.cjs",
"test:extension-schemas": "node tests/extension-schemas.test.cjs",
"test:snippets": "node tests/snippet-validation.test.cjs",
Expand Down
Loading
Loading