> ## Documentation Index
> Fetch the complete documentation index at: https://docs.adcontextprotocol.org/llms.txt
> Use this file to discover all available pages before exploring further.

# TMP Specification

> Authoritative message type definitions, field tables, privacy requirements, and conformance levels for the Trusted Match Protocol.

# Trusted Match Protocol Specification

<Note>
  **Experimental.** The Trusted Match Protocol is part of AdCP 3.0 as an experimental surface — it may change between 3.x releases with at least 6 weeks' notice. Sellers implementing TMP MUST declare `trusted_match.core` in `experimental_features`. See [experimental status](/dist/docs/3.2.0-beta.0/reference/experimental-status) for the full contract. Fields on this surface are not subject to deprecation cycles until 3.0.0 GA.
</Note>

This is the authoritative reference for the Trusted Match Protocol (TMP). For conceptual introductions, see the [overview](/dist/docs/3.2.0-beta.0/trusted-match/) and [core concepts](/dist/docs/3.2.0-beta.0/trusted-match/context-and-identity).

Specific areas expected to evolve include TMPX exposure tokens, country-partitioned identity, and Offer creative data — see the [3.1.0 roadmap](https://github.com/adcontextprotocol/adcp/issues/2201) for planned changes.

## Definitions

| Term                       | Definition                                                                                                                                                                                                                                  |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Context Match**          | TMP operation that evaluates available packages against content context. Carries no user identity.                                                                                                                                          |
| **Identity Match**         | TMP operation that evaluates user eligibility against package criteria. Carries no page context.                                                                                                                                            |
| **TMP Router**             | Infrastructure that fans out TMP requests to buyer agents and merges responses. A single binary that handles both context and identity requests, with structurally separate code paths.                                                     |
| **Offer**                  | A buyer's response to a context match request. Ranges from simple activation (package\_id only) to rich proposals with brand, price, summary, and creative manifest.                                                                        |
| **Available package**      | A package from an active media buy that is eligible for evaluation on a given placement. Package metadata — including the originating seller agent — is synced at media buy time. See [Package Sync](#package-sync).                        |
| **Seller agent**           | The buyer-side agent that sold the package into a publisher. Identified by the agent URL declared in the publisher's `adagents.json` `authorized_agents[].url`. Every `AvailablePackage` is bound to exactly one seller agent at sync time. |
| **Eligibility**            | List of eligible package IDs returned by Identity Match, plus a serve-window throttle. The buyer computes eligibility from frequency caps, audience membership, and other signals; the reasons are opaque to the publisher.                 |
| **Artifact**               | A typed content reference associated with a publisher property (article URL, episode EIDR, show Gracenote ID, music ISRC, product GTIN, conversation turn). Each artifact has a `type` and `value`. Referenced in context match requests.   |
| **Temporal decorrelation** | Random delay and random ordering between Context Match and Identity Match requests, preventing timing- and order-based correlation.                                                                                                         |

## Message Types

All TMP message types include a `type` field that identifies the message for deserialization. Routers and agents use this field to select the correct schema for parsing the JSON body.

| Message                 | `type` value              |
| ----------------------- | ------------------------- |
| Context Match request   | `context_match_request`   |
| Context Match response  | `context_match_response`  |
| Identity Match request  | `identity_match_request`  |
| Identity Match response | `identity_match_response` |
| Error response          | `error`                   |

### ContextMatchRequest

Sent by the publisher (via router) to buyer agents. Contains content context. MUST NOT contain user identity.

| Field              | Type               | Required | Description                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| ------------------ | ------------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type`             | string             | Yes      | `"context_match_request"`. Message type discriminator for deserialization.                                                                                                                                                                                                                                                                                                                                                                           |
| `protocol_version` | string             | No       | TMP protocol version. Default: `1.0`. Allows receivers to handle semantic differences across versions.                                                                                                                                                                                                                                                                                                                                               |
| `request_id`       | string             | Yes      | Unique request identifier for logging. MUST NOT correlate with any Identity Match request\_id.                                                                                                                                                                                                                                                                                                                                                       |
| `property_rid`     | UUID               | Yes      | Property catalog UUID (v7). Globally unique, stable.                                                                                                                                                                                                                                                                                                                                                                                                 |
| `property_id`      | string             | No       | Publisher's human-readable slug. Optional when `property_rid` is present.                                                                                                                                                                                                                                                                                                                                                                            |
| `property_type`    | enum               | Yes      | One of: `website`, `mobile_app`, `ctv_app`, `desktop_app`, `dooh`, `podcast`, `radio`, `linear_tv`, `streaming_audio`, `ai_assistant`. See `property-type` enum.                                                                                                                                                                                                                                                                                     |
| `placement_id`     | string             | Yes      | Placement identifier from the publisher's placement registry in `adagents.json`. One placement per request.                                                                                                                                                                                                                                                                                                                                          |
| `seller_agent_url` | string (URI)       | Yes      | API endpoint URL of the seller agent issuing this request. The provider resolves the active package set it has synced for this seller against it; an unsynced seller MUST yield an empty offer set, never a fall-back to another seller's set. A single per-placement value carrying no user identity. Compared with AdCP URL canonicalization. Consistent with `seller_agent_url` on the Identity Match request and `agent_url` in `adagents.json`. |
| `artifact`         | Artifact           | No       | Full content artifact adjacent to this ad opportunity. Same schema as content standards evaluation. The publisher sends the full artifact when they want the buyer to evaluate the actual content. Contractual protections govern buyer use. TEE deployment upgrades contractual trust to cryptographic verification.                                                                                                                                |
| `artifact_refs`    | List\<ArtifactRef> | No       | Public content references the buyer can resolve independently. Each has a `type` (one of: `url`, `url_hash`, `eidr`, `gracenote`, `isrc`, `gtin`, `rss_guid`, `isbn`, `custom`) and a `value`. For URL-addressable content, the buyer may have pre-classified these. Use `url_hash` when the publisher prefers not to reveal the URL (contextual clean room).                                                                                        |
| `context_signals`  | ContextSignals     | No       | Pre-computed classifier outputs for the content environment. Use when content is ephemeral (conversation turns, search queries) or to supplement artifact-based matching. Can replace `artifact_refs` entirely. Raw content MUST NOT be included — only classified outputs. The publisher is the classifier boundary.                                                                                                                                |
| `geo`              | Geo                | No       | Coarse geographic location of the viewer. Publisher controls granularity — country for regulatory compliance, region/metro for campaign targeting and valuation. No postcode or coordinates — coarsened to prevent user identification.                                                                                                                                                                                                              |
| `package_ids`      | List\<string>      | No       | Restrict evaluation to specific packages. When omitted, the provider evaluates all eligible packages for this placement (the common case). Package metadata (formats, catalogs) is synced at media buy time — not sent per request.                                                                                                                                                                                                                  |

#### ContextSignals

Pre-computed classifier outputs for the content environment. MUST NOT contain raw content (conversation text, article body, URLs). Only classified outputs. The publisher is the classifier boundary.

| Field              | Type          | Required | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| ------------------ | ------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `topics`           | List\<string> | No       | Content topic identifiers. Use IAB Content Taxonomy 3.0 IDs when `taxonomy_id` is 7 (default), or human-readable strings for custom taxonomies.                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `taxonomy_source`  | enum          | No       | Organization that defines the topic taxonomy. Default: `iab`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `taxonomy_id`      | integer       | No       | Taxonomy version within the source. For IAB, follows the AdCOM cattax enum: `7` = Content Taxonomy 3.0 (CC-BY-3.0). Default: `7`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `sentiment`        | enum          | No       | Content sentiment: `positive`, `negative`, `neutral`, `mixed`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `keywords`         | List\<string> | No       | Content keywords extracted by the publisher's classifier.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `language`         | string        | No       | ISO 639-1 language code.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `content_policies` | List\<string> | No       | Policy IDs from the [AdCP Policy Registry](/dist/docs/3.2.0-beta.0/governance/policy-registry) that this content satisfies. Routers populate this from the publisher's property governance configuration or content metadata. Buyers filter on policies they require via `required_policies` on packages. This is a **pre-filtering optimization** — contexts missing required policies are excluded before reaching downstream governance. Definitive enforcement happens at the governance layer via [`check_governance`](/dist/docs/3.2.0-beta.0/governance/campaign/tasks/check_governance). |
| `summary`          | string        | No       | Natural language summary for relevance judgment. Useful for LLM-native buyers that evaluate semantically.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `embedding`        | string        | No       | Content embedding as base64-encoded int8 vector. Captures semantic content beyond topics and keywords.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `embedding_model`  | string        | No       | Embedding model identifier (e.g., `nomic-embed-text-v1.5`). Required when `embedding` is present.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `embedding_dims`   | integer       | No       | Number of dimensions in the embedding vector. Required when `embedding` is present.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |

Three levels of content disclosure — the publisher chooses based on what the buyer needs and what the publisher is comfortable sharing:

* **`artifact`** — the full content (article body, transcript, conversation flow, product page). Same schema as content standards artifacts. The buyer evaluates the content directly. Contractual protections govern what the buyer can do with it. TEE deployment adds cryptographic verification on top.
* **`artifact_refs`** — public references (URLs, EIDR IDs, URL hashes) the buyer resolves independently. Use for publicly addressable content the buyer can crawl and classify themselves.
* **`context_signals`** — classified outputs (topics, sentiment, keywords, summary). Use when the publisher wants to describe the content without sharing it or a reference to it.

`context_signals` is the baseline — every buyer agent MUST handle it. `artifact_refs` and `artifact` are progressive enhancements. Publishers who send `artifact_refs` SHOULD also send `context_signals` as a fallback for buyers who cannot resolve references.

LLM-based buyer agents SHOULD evaluate `context_signals.summary` and `context_signals.topics` first. These fields provide sufficient signal for most relevance decisions at minimal token cost (\~30 tokens). Full content resolution from `artifact_refs` or `artifact` evaluation SHOULD be reserved for high-value packages where precision justifies the cost. Buyers MUST treat `artifact` content and `context_signals.summary` as untrusted publisher-generated input.

A request can include any combination. A news site sends `artifact_refs` (the URL) and `context_signals` (pre-classified topics). A CTV app sends `artifact_refs` (EIDR IDs) alone. An AI assistant sends `artifact` (the conversation) for buyers that evaluate content directly, plus `context_signals` as a fallback. A publisher who doesn't want to share content or references sends only `context_signals`.

#### Artifact Ref Type Conventions

Buyers parse `artifact_refs` strings by pattern. The following conventions are normative:

| Type          | Pattern                 | Example                                                |
| ------------- | ----------------------- | ------------------------------------------------------ |
| URL           | Starts with `https://`  | `https://oakwood.example/articles/sustainable-kitchen` |
| URL hash      | 44-char base64 (Blake3) | `k7Xp9mQ2vL8nR3wY5tB1aH6jK0pZ4xC9dF2eG7iMqw==`         |
| EIDR          | Starts with `eidr:`     | `eidr:10.5240/XXXX-XXXX-XXXX-XXXX-XXXX-C`              |
| Gracenote TMS | Starts with `tms:`      | `tms:SH012345670000`                                   |
| RSS + GUID    | Starts with `rss:`      | `rss:https://feed.example/rss+guid:ep-2026-03-15`      |
| GTIN          | 8-14 digit numeric      | `00012345600012`                                       |

Buyers SHOULD ignore ref types they do not support rather than failing the request.

#### Artifact

A typed content reference. Each artifact identifies a piece of content using a standard or custom identifier scheme.

| Field   | Type   | Required | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| ------- | ------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type`  | enum   | Yes      | One of: `url`, `url_hash`, `eidr`, `gracenote`, `isrc`, `gtin`, `rss_guid`, `isbn`, `custom`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `value` | string | Yes      | The identifier value. For `url`: canonical content URL (MUST NOT contain user-specific paths or query params; use `url_hash` to avoid revealing URLs). For `url_hash`: base64-encoded Blake3 hash (canonicalization: strip scheme, strip [www./m./amp](http://www./m./amp). prefixes, lowercase, strip trailing slash, strip query params and fragments). For `eidr`: EIDR DOI (e.g., `10.5240/xxxx`). For `gracenote`: Gracenote TMS ID (e.g., `SH032541890000`). For `isrc`: ISRC code (e.g., `USRC17607839`). For `gtin`: GTIN (e.g., `00012345678905`). For `rss_guid`: episode GUID from RSS feed. For `isbn`: ISBN (e.g., `978-0-123456-78-9`). For `custom`: publisher-defined string. |

#### Geo

Geographic context for the impression opportunity. Publisher controls granularity.

| Field     | Type   | Required | Description                                            |
| --------- | ------ | -------- | ------------------------------------------------------ |
| `country` | string | No       | ISO 3166-1 alpha-2 country code (e.g., `US`, `GB`).    |
| `region`  | string | No       | ISO 3166-2 subdivision code (e.g., `US-CA`, `GB-SCT`). |
| `metro`   | Metro  | No       | Metro area using AdCP's metro classification systems.  |

### ContextMatchResponse

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       | 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 its [`targeting_kv_mapping`](#publisher-targeting-kv-mapping) 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 is deployment configuration and is not carried on either protocol hop.

#### Offer

A buyer's response for a single package.

| Field               | Type                 | Required | Description                                                                                                                                                                                                                                                                                       |
| ------------------- | -------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `package_id`        | string               | Yes      | Package identifier from the media buy.                                                                                                                                                                                                                                                            |
| `seller_agent`      | SellerAgentRef       | No       | Optional echo of the package's seller agent for publisher-side observability. Non-authoritative — the binding on the cached AvailablePackage is source of truth. Routers MAY stamp this field from the cached package→seller map when omitted by the provider. See [Package Sync](#package-sync). |
| `brand`             | BrandRef             | No       | Brand for this offer. Required when the product allows dynamic brands. For single-brand packages, already known from the media buy.                                                                                                                                                               |
| `price`             | OfferPrice           | No       | Variable price for this offer. Only present when the product supports variable pricing.                                                                                                                                                                                                           |
| `summary`           | string               | No       | Buyer-generated description of the offer for the publisher to judge relevance. E.g., "50% off Goldenfield mayo — recipe integration".                                                                                                                                                             |
| `creative_manifest` | CreativeManifest     | No       | Full creative details, inline. When present, the publisher has everything needed to render. For large creatives (VAST, video), the manifest references external assets via URLs.                                                                                                                  |
| `creative_data`     | Map\<string, string> | No       | Optional free-form string enhancements for dynamic creative rendering, such as sponsor labels and promotion codes. This is not ad-server macro substitution or attribution tracking; tracker URLs belong in `creative_manifest` assets and exposure tracking uses TMPX.                           |

`creative_data` deliberately has no protocol-level key registry and declares no
required keys. Key meanings are package-local. Receivers MUST ignore unknown
keys, and a missing key MUST NOT make an otherwise renderable offer fail. Any
content required to render an inline creative belongs in the declared
`creative_manifest` assets; `creative_data` may complement those assets but MUST
NOT be their only carrier. This keeps a manifest self-contained while allowing
optional publisher-specific presentation enhancements without creating a second
format schema.

#### OfferPrice

| Field      | Type   | Required | Description                                  |
| ---------- | ------ | -------- | -------------------------------------------- |
| `amount`   | number | Yes      | Price amount in the specified currency.      |
| `currency` | string | No       | ISO 4217 currency code. Default: `USD`.      |
| `model`    | enum   | Yes      | One of: `cpm`, `cpc`, `cpcv`, `cpa`, `flat`. |

#### Signals

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       | 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      | 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

Sent by the publisher (via router) to buyer agents. Contains the seller agent URL, one or more opaque identity tokens, and an optional package ID list. MUST NOT contain page context.

| Field                | Type                    | Required | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| -------------------- | ----------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type`               | string                  | Yes      | `"identity_match_request"`. Message type discriminator for deserialization.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `protocol_version`   | string                  | No       | TMP protocol version. Default: `1.0`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `request_id`         | string                  | Yes      | Unique request identifier. MUST NOT correlate with any Context Match request\_id.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `seller_agent_url`   | string (URI)            | Yes      | API endpoint URL of the seller agent issuing this request. The buyer's identity-match service uses this to resolve the active package set it has registered for this seller; when `package_ids` is omitted, evaluation occurs against that full set. Compared using the [AdCP URL canonicalization rules](/dist/docs/3.2.0-beta.0/reference/url-canonicalization), not byte-equality. Consistent with `seller_agent.agent_url` on `AvailablePackage` and `agent_url` in `adagents.json`.                                                                                                                                   |
| `identities`         | List\<Identity>         | Yes      | One or more identity tokens for the user. Publishers SHOULD include every token they have available — the buyer resolves on whichever graph matches, maximizing match rate. Each entry is an independent identifier for the same user; buyers MUST NOT treat the combination as a new correlated identity.                                                                                                                                                                                                                                                                                                                 |
| `consent`            | Consent                 | No       | Privacy consent signals. Buyers in regulated jurisdictions MUST NOT process identity tokens without consent information.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `package_ids`        | List\<string>           | No       | When omitted, the buyer evaluates eligibility against the full set of active packages it has registered for `seller_agent_url`. When provided, composition MUST be statistically independent of the current placement. Two acceptable modes: **all-active** (every active package for this buyer at this publisher) or **fuzzed** (a random sample of active packages, optionally padded with synthetic non-existent IDs, drawn from a distribution that does not depend on the current placement). The page-specific subset is forbidden — it would let the buyer correlate with Context Match by comparing package sets. |
| `country`            | string                  | No       | ISO 3166-1 alpha-2 country code. Routing directive — the router uses this to select the correct regional provider. The router MUST strip this field before forwarding to the buyer agent. Not an identity signal.                                                                                                                                                                                                                                                                                                                                                                                                          |
| `sealed_credentials` | List\<SealedCredential> | No       | **Experimental (`trusted_match.verified_identity`).** HPKE-sealed verified-identity credentials addressed to specific audiences — the network-as-RP carrier. Opaque pass-through for the publisher. See [Verified Identity Attestation](#verified-identity-attestation).                                                                                                                                                                                                                                                                                                                                                   |

Each entry in `identities` is a `{user_token, uid_type, attestation?}` triple:

| Field         | Type        | Required | Description                                                                                                                                                                                                                                                                                               |
| ------------- | ----------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `user_token`  | string      | Yes      | Opaque token from an identity provider (ID5, LiveRamp, UID2) or publisher-generated. Buyer may map to internal identity graph but cannot reverse to PII.                                                                                                                                                  |
| `uid_type`    | enum        | Yes      | Type of user identifier: `uid2`, `rampid`, `id5`, `euid`, `pairid`, `maid`, `hashed_email`, `publisher_first_party`, `world_id_nullifier`, `other`. Tells the buyer which identity graph to resolve against. See `uid-type` enum.                                                                         |
| `attestation` | Attestation | No       | **Experimental (`trusted_match.verified_identity`).** Verifiable proof *about* this identity (proof-of-personhood and/or age). The receiver MUST verify it and treat an unverifiable attestation as absent — never as asserted-true. See [Verified Identity Attestation](#verified-identity-attestation). |

### IdentityMatchResponse

Returned by the buyer agent. A list of eligible package IDs with a serve-window throttle. Two shapes carry this message across the two hops of an Identity Match fan-out — the schemas are split so each hop's obligations are expressible in JSON Schema and the untrusted provider boundary never accepts a publisher-local destination name.

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

| Field                  | Type             | Required | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| ---------------------- | ---------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type`                 | string           | Yes      | `"identity_match_response"`. Message type discriminator for deserialization.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `request_id`           | string           | Yes      | Echo of the request's `request_id`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `eligible_package_ids` | List\<string>    | Yes      | Package IDs the user is eligible for. Packages not listed are ineligible.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `serve_window_sec`     | integer          | Yes      | Per-package single-shot fcap window, in seconds. Range: 1–300. Default: 60.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `tmpx_chunks`          | List\<TmpxChunk> | No       | Ordered TMPX chunks minted by this identity agent. Each entry pairs a provider-local `slot_id` (from the provider's registered `tmpx_slots` on provider-registration.json) with an opaque URL-safe `value`. Slot IDs are provider-local opaque tokens — the agent does NOT emit publisher-local ad-server names. Ordered-prefix invariant: a response that carries fewer chunks than the provider registered MUST emit an ordered prefix of the registered `tmpx_slots` (never shifted or sparse). Capped at 2 chunks in v1; the cap MAY rise without a shape change. Omitted when the agent mints no TMPX for this request. |

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

| Field                  | Type                                              | Required | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| ---------------------- | ------------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type`                 | string                                            | Yes      | `"identity_match_response"`. Message type discriminator for deserialization.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `request_id`           | string                                            | Yes      | Echo of the request's `request_id`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `eligible_package_ids` | List\<string>                                     | Yes      | Union of eligible package IDs across the fan-out.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `serve_window_sec`     | integer                                           | Yes      | Per-package single-shot fcap window, in seconds. Range: 1–300. Default: 60. After serving the user one impression on each eligible package within this window, the publisher MUST re-query Identity Match before serving from those packages again. This is **not** a router response cache TTL — it is a buyer-asserted serve throttle. Multi-impression frequency caps are handled separately by the buyer's impression tracker, which writes cap-fire events to the IdentityMatch cap-state store at the boundary regardless of this window — see [Frequency-Cap Data Flow](/dist/docs/3.2.0-beta.0/trusted-match/identity-match-implementation).                                                                                                                                |
| `tmpx_providers`       | Map\<provider\_id, \{ chunks: List\<TmpxChunk> }> | No       | Router-populated. Ordered TMPX chunks grouped by the originating identity provider's `provider_id`. Each entry's `chunks[]` is a copy of the provider's emitted `tmpx_chunks` list in the same order. Each chunk carries `{slot_id, value}` — the publisher's deployment configuration (see [Publisher TMPX macro mapping](#publisher-tmpx-macro-mapping)) resolves each `(provider_id, slot_id)` pair to a local destination on this surface. Required by router conformance when any identity provider emitted TMPX in this request; collapsing per-provider tokens into a single string loses attribution and breaks per-provider impression accounting. Map keys MUST match the `provider_id` charset registered in provider-registration.json (`^[A-Za-z0-9_]+$`, 1–64 chars). |
| `tmpx`                 | string                                            | No       | DEPRECATED in favor of `tmpx_providers`. Single HPKE-encrypted exposure token. Routers MAY continue to populate this field for back-compat with consumers that only know the single-token shape; when both fields are present, `tmpx_providers` is authoritative. Wire format: `kid.base64url_nopad(ciphertext)` (unpadded, no `=` characters). Removed in 4.0.                                                                                                                                                                                                                                                                                                                                                                                                                     |

#### TmpxChunk

| Field     | Type   | Required | Description                                                                                                                                                                                                                                                                                                            |
| --------- | ------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `slot_id` | string | Yes      | Provider-local slot identifier from the emitting provider's registered `tmpx_slots` (provider-registration.json). Opaque provider-namespaced token; publishers map `(provider_id, slot_id)` → local destination via `tmpx_macro_mapping`. Pattern: `^[a-zA-Z][a-zA-Z0-9_]*$`, 1–64 chars. NOT an ad-server macro name. |
| `value`   | string | Yes      | Opaque, URL-safe wire string the publisher substitutes verbatim into the destination the publisher's mapping resolves for this `(provider_id, slot_id)` pair. Length capped at 1024 characters. Publishers MUST NOT parse, decode, or transform this value.                                                            |

Each TMPX `value` is an HPKE-encrypted exposure token that flows through creative tracking URLs to the buyer's impression pixel, enabling real-time per-user frequency state updates without exposing user identity to the publisher. The buyer computes eligibility from whatever identity signals they have (frequency caps, audience membership, purchase history) and returns only the packages that pass. The publisher does not need to know why a package was excluded — just which packages are eligible.

**TMPX destinations are publisher-owned.** Naming an ad-server macro, targeting key, VAST substitution, or DOOH play-log field is a deployment concern owned by the publisher, not a protocol identifier. Providers declare their own slot contract (`tmpx_slots` on provider-registration.json) using opaque provider-local IDs; identity-match responses carry ordered `{slot_id, value}` chunks; the router preserves the chunks and their slot IDs under the emitting `provider_id`; the publisher's [Publisher TMPX macro mapping](#publisher-tmpx-macro-mapping) resolves each `(provider_id, slot_id)` to the local destination on that surface. Slot IDs and destination names never mix on the wire — the router only ever sees provider-local slot tokens, never a publisher's ad-server namespace. Ordered multi-chunk support lets a single TMPX exceed one macro slot (capped at 2 chunks per provider in v1 and MAY be raised without a shape change). This keeps the same identity provider mappable to different destinations across surfaces (GAM key-values vs. VAST macros vs. DOOH play-log fields) via publisher-side configuration, and closes the cross-provider name-hijack surface that existed while destination names travelled on the wire — the router never accepts a destination name from an untrusted provider.

**Startup validation and drift detection.** Because each chunk carries its `slot_id`, publishers can validate `tmpx_macro_mapping` at deployment startup: every `slot_id` a provider registered SHOULD have a corresponding entry in `tmpx_macro_mapping[provider_id]`, and every response chunk's `slot_id` MUST already be known — a chunk with an unrecognized `slot_id` for a provider present in the mapping is treated as a configuration error (see the fail-closed rule under [Publisher TMPX macro mapping](#publisher-tmpx-macro-mapping)). Publishers detect provider slot-contract drift (a provider adding, removing, or renaming a slot) at startup rather than at serve time.

`tmpx_providers` keys ordered chunk lists by `provider_id` so the router preserves attribution when its fan-out reaches multiple identity providers. The legacy `tmpx` field remains supported through 3.x for consumers that haven't migrated; when both fields are present, `tmpx_providers` is authoritative and the singular field SHOULD reflect one provider's first-slot value solely as a transitional convenience.

The `serve_window_sec` field is a **per-package single-shot fcap**, not a router cache TTL. The buyer is saying: "After you serve the user one impression on each eligible package, re-query me before serving from those packages again." The router MAY still cache the response for an internal deduplication/cost-saving window, but the binding contract on the publisher side is "one impression per eligible package per window." Multi-impression frequency caps (5 per day per campaign, 100 per month per advertiser, etc.) live in the buyer's impression tracker and surface to the IdentityMatch service as cap-fire events at the boundary regardless of `serve_window_sec`.

The publisher enforces allocation rules (competitive separation, pod composition) using the eligibility list as input. This eliminates the need for pod-specific or batch-specific protocol semantics — the publisher allocates across whatever placements exist during the serve window (a CTV ad pod, a web page with 20 slots, a single pre-roll), honoring the one-impression-per-package contract.

#### Conformance invariants for IdentityMatch eligibility

A conformant IdentityMatch service MUST compute `eligible_package_ids` such that, for each `package_id ∈ request.package_ids`, the package is included in `eligible_package_ids` if and only if **all** of the following hold:

1. **Audience eligibility.** Either the package has no audience requirement, OR there exists at least one audience identifier `a` such that `a` is in the package's required audience set AND `a` is in the audience-membership of at least one identity `i ∈ request.identities` (the union across the user's resolved identities intersects the package's required audiences).
2. **Frequency cap eligibility.** No `(identity, package)` cap-state entry exists for any identity `i ∈ request.identities` against the package. Cap-state entries are written by the buyer's impression tracker when it determines an impression has exhausted a cap and carry an expiration timestamp; an entry is "present" until that timestamp. The protocol does not constrain how the impression tracker counts impressions, evaluates windows, or decides when a cap fires — only the boundary contract (cap-fire entries flow into the cap-state store; the IdentityMatch service checks presence at query time). See [Frequency-Cap Data Flow](/dist/docs/3.2.0-beta.0/trusted-match/identity-match-implementation) for the boundary contract.
3. **Active state.** Packages or policies marked inactive MUST be treated as if absent.
4. **Audience freshness.** If the buyer's audience pipeline publishes a freshness deadline and the current time is past it, that audience-membership entry MUST NOT contribute to (1).
5. **Age eligibility** (experimental — applies only when `trusted_match.verified_identity` is in effect). Either the package requires no age policy, OR some identity `i ∈ request.identities` carries a **verified** `attestation` (per the [Verified Identity Attestation](#verified-identity-attestation) conformance rules) with an age claim ≥ the threshold resolved from `(the package's required age policy, request geo)`. An unverified or absent attestation does not satisfy this clause. When the feature is not in effect this clause is vacuously true, so core conformance is unchanged.

The TMPX returned with the response MUST encode the resolved identities so the out-of-band impression tracker can update fcap policy state and signal cap-fire events to the IdentityMatch cap-state store — see § TMPX tokens and [Frequency-Cap Data Flow](/dist/docs/3.2.0-beta.0/trusted-match/identity-match-implementation).

Storage backend (valkey, Aerospike, DynamoDB, in-memory, anything) is implementation. Two services with different storage backends that satisfy these invariants for the same inputs MUST return the same eligibility output.

#### Consent

Privacy consent signals for the identity match. Publishers MUST include consent information when operating in regulated jurisdictions (EU/EEA, California, etc.). Buyers MUST NOT process user tokens without consent information when required by applicable law.

| Field         | Type   | Required | Description                                                                 |
| ------------- | ------ | -------- | --------------------------------------------------------------------------- |
| `gdpr`        | bool   | No       | Whether GDPR applies to this request.                                       |
| `tcf_consent` | string | No       | IAB TCF v2.2 consent string. Present when `gdpr` is true.                   |
| `gpp`         | string | No       | IAB Global Privacy Platform string.                                         |
| `us_privacy`  | string | No       | US Privacy string (CCPA). Deprecated in favor of GPP but still widely used. |

### Verified Identity Attestation

**Experimental — declare `trusted_match.verified_identity` in `experimental_features`** (separate from `trusted_match.core`, so buyers can detect support independently). Lets a publisher — or a network/issuer acting as the relying party — carry a **verifiable** proof *about* a user (proof-of-personhood and/or age) so the buyer verifies the claim cryptographically rather than trusting an assertion. Issuer-agnostic: World ID is the first scheme; mDL / VC-style issuers use the same shape. Design rationale: `specs/tmp-verified-identity-attestation.md`.

This widens `identity-match-request.json`, which is otherwise `additionalProperties: false`. The widening is deliberate: an attestation is proof on the **identity** side of the privacy boundary — not page context — so it does not breach the boundary the strict schema protects.

#### Topologies

| Topology                     | Relying party                   | Carrier                                                            |
| ---------------------------- | ------------------------------- | ------------------------------------------------------------------ |
| Publisher-as-RP              | Publisher                       | `attestation` on an `identities[]` entry (per-publisher pseudonym) |
| Network-as-RP / issuer-as-RP | A network, or the issuer itself | `sealed_credentials[]` (HPKE-sealed to the audience's key)         |

#### Attestation

Optional object on each `identities[]` entry.

| Field                | Type               | Required | Description                                                                                                                                                                                                                                              |
| -------------------- | ------------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `issuer`             | BrandRef           | Yes      | Identity issuer / attestation authority as a vendor BrandRef (e.g. `{"domain": "world.org"}`) — the same shape AdCP uses for measurement/signals vendors. Domain is the anchor; `brand.json` hosting is optional. `scheme` selects the verifier version. |
| `scheme`             | string             | Yes      | Proof scheme and version, e.g. `world_id_v4`.                                                                                                                                                                                                            |
| `relying_party_id`   | string             | No       | Relying-party id the proof was minted for. Checked against the `relying_party_id` owner's published `identity_relying_parties[]` in `brand.json` (provenance).                                                                                           |
| `action`             | string             | No       | Issuer action/scope the proof was bound to.                                                                                                                                                                                                              |
| `claims`             | List\<enum>        | Yes      | Closed, issuer-agnostic set: `unique_human`, `age_over_13`, `age_over_16`, `age_over_18`, `age_over_21`. See `attestation-claim` enum.                                                                                                                   |
| `verification_level` | enum               | No       | `orb` \| `device` \| `document`. Credential strength.                                                                                                                                                                                                    |
| `signal_binding`     | string             | No       | Hash of the signal the proof commits to (replay defense).                                                                                                                                                                                                |
| `proof`              | object             | Yes      | Scheme-specific verifiable proof material; opaque to this schema.                                                                                                                                                                                        |
| `expires_at`         | string (date-time) | No       | Validity horizon; receivers MUST reject when past.                                                                                                                                                                                                       |

#### SealedCredential

Entry in the top-level `sealed_credentials[]`.

| Field          | Type   | Required | Description                                                                                                                   |
| -------------- | ------ | -------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `audience_kid` | string | Yes      | Key id of the recipient (network / relying party) whose HPKE private key opens `payload`.                                     |
| `payload`      | string | Yes      | HPKE-sealed attestation in the TMPX envelope format `kid.base64url_nopad(ciphertext)`. Opaque pass-through for the publisher. |

#### Conformance invariants (verified attestation)

A receiver that accepts attestations MUST:

1. **Verify before trust.** Verify the proof for every `scheme` it accepts. An attestation that fails verification — or `signal_binding`, or `relying_party_id` provenance, or `expires_at` — MUST be treated as **absent** (no attestation), never as an asserted-true claim.
2. **No silent upgrade.** Never treat an unverified or unverifiable attestation as `true`.
3. **relying\_party\_id provenance.** Confirm the attestation's `relying_party_id` belongs to the entity claiming the traffic before trusting it. The owner's `brand.json` `identity_relying_parties[]` is the v1 discovery surface, but it is self-published over HTTPS — the authoritative root is the issuer's own relying-party registry (e.g. World ID's on-chain registry), against which `brand.json` is a cross-check. A receiver MUST NOT trust a `relying_party_id` solely because some `brand.json` lists it absent an issuer-side anchor; the bidirectional issuer-metadata cross-check is a tracked open item.
4. **Sealed credentials.** Decrypt only `sealed_credentials[]` entries whose `audience_kid` the receiver holds a key for; ignore the rest.
5. **Bound resources.** Bound attestation and sealed-credential count and size to prevent DoS.

**Replay (v1 limitation).** Verifying a proof establishes that *a* credential holder produced it, not that it was produced for *this* impression. Without an enforced `signal_binding` freshness window and nullifier-reuse tracking — both left to the verifier in v1 (the freshness policy is WG-open) — this surface provides at most daily-epoch replay resistance (via `request_id` dedup). Receivers SHOULD bind `signal_binding` to a fresh, receiver-checkable value and track nullifier reuse, and MUST NOT over-trust an attestation as a per-impression liveness signal until they do.

#### Router handling of `sealed_credentials[]`

The router MUST forward each `sealed_credentials[]` entry only to the provider that owns its `audience_kid` (route-by-audience, not broadcast). The tamper-evidence and cache-partitioning rules are defined canonically in [Identity Match signed fields](#identity-match-signed-fields) and [Caching](#caching): `sealed_credentials` is folded into the per-provider re-signature canonical bytes (so an injected or swapped blob breaks the request signature) and `sealed_credentials_hash` is part of the dedup cache key (so a network-credential change repartitions the cache rather than serving a stale response). The same canonical-identities bytes already cover any per-identity `attestation`.

#### Age as eligibility

A verified age claim (`age_over_N`) resolves to `eligible_package_ids` — it is **never** carried as a cleartext age or date of birth. The verifying party maps `(required age policy, geo) → required threshold claim` and includes a package only when the attestation carries a claim ≥ the required threshold. The jurisdiction → threshold tables are maintained in the [AdCP Policy Registry](/dist/docs/3.2.0-beta.0/governance/policy-registry) (a package requires an age policy via `required_policies`); sub-country (e.g. US state) resolution happens where finer geo is available, since the Identity Match `country` field is coarse and stripped before forwarding.

### Error Response

Returned by a provider or router when a request cannot be processed. Distinct from an empty result — an empty `offers` array or empty `eligible_package_ids` list is a valid response meaning no matches, not an error.

| Field        | Type   | Required | Description                                                                                                                                                                                                                                                                                                                                           |
| ------------ | ------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type`       | string | Yes      | `"error"`. Message type discriminator for deserialization.                                                                                                                                                                                                                                                                                            |
| `request_id` | string | Yes      | Echo of the original request's `request_id`.                                                                                                                                                                                                                                                                                                          |
| `code`       | enum   | Yes      | Machine-readable error code: `invalid_request`, `unknown_package`, `seller_not_authorized`, `rate_limited`, `timeout`, `internal_error`, `provider_unavailable`. `seller_not_authorized` is returned at [package sync](#package-sync) time when an AvailablePackage declares a `seller_agent.agent_url` not present in the publisher's adagents.json. |
| `message`    | string | No       | Human-readable error description for debugging.                                                                                                                                                                                                                                                                                                       |

The router SHOULD exclude providers that return errors from the merged response for that request. The router MAY track error rates per provider and preemptively skip providers with sustained errors.

## Provider Registration

TMP providers are registered with the router via publisher configuration. The publisher specifies which providers the router should call, along with each provider's endpoint and supported capabilities. This is an operational relationship — the publisher trusts the provider to run code in their ad decisioning path.

The standard registration path is **static configuration** — the publisher declares providers in their Prebid module config, router YAML, or equivalent surface-specific configuration. Dynamic registration (API-driven, database-backed) is an equally valid variant for publishers who manage many providers or need runtime updates. Both approaches use the same provider registration schema (`/schemas/trusted-match/provider-registration.json`).

| Setting          | Type          | Required    | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| ---------------- | ------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `provider_id`    | string        | Yes         | Stable identifier for this provider registration. Used in logs, metrics, cache keys, as the key in `signals_by_provider` and publisher [`targeting_kv_mapping`](#publisher-targeting-kv-mapping) for Context Match, and as the key in `tmpx_providers` and publisher [`tmpx_macro_mapping`](#publisher-tmpx-macro-mapping) for Identity Match. Pattern: `^[A-Za-z0-9_]+$`, max length 64 — a safe alphanumeric/underscore charset so the value can appear in operational surfaces (logs, metrics, dashboards) without quoting.                                                                                                                                                                                                                                                                                                                                                                                                     |
| `endpoint`       | URL           | Yes         | Provider's base URL. The router appends `/context` or `/identity` when dispatching.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `context_match`  | bool          | No          | Provider handles Context Match requests. At least one of `context_match` or `identity_match` must be true.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `identity_match` | bool          | No          | Provider handles Identity Match requests. At least one of `context_match` or `identity_match` must be true.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `countries`      | List\<string> | Conditional | ISO 3166-1 alpha-2 country codes this provider serves. MUST be present and non-empty when `identity_match` is true.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `uid_types`      | List\<string> | Conditional | Identity types this provider can resolve (from `uid-type` enum). The router selects providers whose `uid_types` overlaps with any `uid_type` in the request's `identities` array, and filters the forwarded `identities` to the intersection — providers MUST NOT receive tokens for types they did not declare. MUST be present and non-empty when `identity_match` is true.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `properties`     | List\<UUID>   | No          | Property RIDs this provider serves. When absent, the provider serves all properties.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `timeout_ms`     | integer       | No          | Per-provider timeout in milliseconds. Must be ≤ the router's overall `latency_budget_ms`. Default: 50.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `priority`       | integer       | No          | Provider ordering for Context Match offer conflicts. Lower values = higher priority; equal priorities are broken by first response received. Default: 0. Identity eligibility remains a responder-scoped union because silent omission is not a negative vote.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `tmpx_slots`     | List\<string> | No          | Stable provider-local slot identifiers for the ordered TMPX chunks this provider mints — e.g. `["primary","secondary"]`. Slot IDs are opaque provider-namespaced tokens, NOT ad-server macro names; publishers map `(provider_id, slot_id)` → local destination via [`tmpx_macro_mapping`](#publisher-tmpx-macro-mapping), so the destination namespace stays publisher-owned. Distinct providers MAY reuse the same slot\_id without collision (publisher lookup keys on `(provider_id, slot_id)`). Ordering carries the ordered-prefix invariant: a provider that emits fewer chunks than it registered MUST emit an ordered prefix — chunks are never shifted, sparse, or reordered. Pattern: `^[a-zA-Z][a-zA-Z0-9_]*$`, 1–64 chars, `uniqueItems`. Cap of 2 slots in v1; the cap MAY rise without a shape change. A provider that emits TMPX (populates `tmpx_chunks` on its identity-match response) MUST register this list. |
| `status`         | enum          | No          | Provider lifecycle status: `active`, `inactive`, or `draining`. Default: `active`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |

At least one of `context_match` or `identity_match` must be true — a provider that handles neither operation is invalid. When `identity_match` is true, `countries` and `uid_types` are **required** — the router cannot perform country-partitioned identity routing without them. The schema enforces both constraints.

Providers MAY support any combination of `context_match` and `identity_match`. A provider that supports only `context_match` is a pure enrichment or contextual targeting provider. A provider that supports only `identity_match` is a frequency capping provider — the publisher evaluates context locally from the media buy's targeting rules and calls the buyer only for identity checks.

### Provider lifecycle

Providers have three lifecycle states:

* **Active**: Provider receives requests normally.
* **Draining**: Provider stops receiving new requests. In-flight requests complete normally. Use this when taking a provider offline for maintenance — the router finishes current work without starting new fan-outs to this provider.
* **Inactive**: Provider is skipped entirely. Use this to disable a provider without removing its configuration.

State transitions are immediate in static configuration (reload the config) and take effect within one refresh cycle in dynamic registration.

### Publisher targeting KV mapping

Context providers author targeting pairs in their own local vocabulary. The router preserves those pairs under the emitting provider's registered `provider_id` in `signals_by_provider`; the publisher then maps each `(provider_id, key)` tuple to the ad-server targeting destination it owns on that surface. The mapping never travels between provider and router, so neither party can select or overwrite a publisher-local destination name.

The shape is captured by `/schemas/trusted-match/publisher-targeting-kv-config.json` and is authored alongside the publisher's surface-specific adapter configuration:

```yaml theme={null}
targeting_kv_mapping:
  outdoor_media:
    segment: adcp_seg
    package: adcp_pkg
  commerce_media:
    segment: commerce_seg
```

Outer-map keys are publisher-assigned `provider_id` values. Inner-map keys are exact provider-local targeting keys; values are publisher-local ad-server destinations. Distinct providers MAY reuse the same local key, as `outdoor_media` and `commerce_media` do above, because lookup uses the full `(provider_id, key)` tuple. A publisher may map the same provider to different destinations across surfaces by carrying one instance of this configuration per surface.

**Exact, own-property lookup.** Implementations MUST NOT add a prefix, separator, or case normalization before lookup. They MUST test both the provider and key as own properties of their respective mapping objects, rather than following an object prototype. This keeps legal object-like identifiers such as `constructor` from resolving an inherited property in JavaScript-family implementations.

**Unmapped tuples are dropped independently.** If the mapping omits the provider or the exact provider-local key, the publisher MUST drop that tuple and MUST NOT fall back to using the provider-local key as an ad-server destination. Other mapped tuples from the same provider remain usable. Publishers SHOULD warn at startup when `targeting_kv_mapping` names an unregistered provider because that entry is dead configuration.

**Destination aliases are multi-valued.** Multiple `(provider_id, key)` tuples MAY intentionally map to the same publisher destination. Every mapped input tuple contributes its value; the publisher MUST NOT silently select a first or last value, and exact duplicate values are preserved. The relative order of values contributed by different provider buckets is not significant because `signals_by_provider` is a map. An adapter serializes all contributed values using the surface's native multi-value mechanism, just as it does when one provider emits the same local key more than once.

### Publisher TMPX macro mapping

Where an identity provider's TMPX chunks land in the ad server is a deployment concern owned by the publisher. Providers declare their own slot contract as `tmpx_slots` on their registration entry (opaque provider-local IDs — see [Provider Registration](#provider-registration)); the identity-match response carries each chunk as a `{slot_id, value}` pair. The publisher maps each `(provider_id, slot_id)` to the ad-server macro name, GAM key-value, VAST URL macro, DOOH play-log field, or equivalent local destination for that surface. The mapping never travels between provider and router, so the router never accepts a destination name from an untrusted provider.

The shape is captured by `/schemas/trusted-match/publisher-tmpx-config.json` and is authored by the publisher — the same operator who trafficks the corresponding ad-server line items — alongside the router configuration for that surface:

```yaml theme={null}
tmpx_macro_mapping:
  provider_a:
    primary: PIN_TMPX_1
    secondary: PIN_TMPX_2
  provider_b:
    primary: NOVA_TMPX_1
```

Inner-map keys are the provider's registered `slot_id`s; values are the local ad-server destination strings. Slot IDs are provider-local, so distinct providers MAY reuse the same slot\_id (as `provider_a` and `provider_b` do above) with no collision — the publisher lookup keys on `(provider_id, slot_id)`. The same `provider_id` may map to different destinations across surfaces (GAM key-values on web, VAST URL macros on CTV, play-log fields on DOOH) by carrying one instance of this configuration per surface. Outer-map key charset matches `provider_id`; inner-map key charset matches `slot_id` — both enforced by `propertyNames` on the schema.

**Startup validation.** Publishers use `tmpx_slots` from each provider's registration to validate the mapping at startup:

* Every `slot_id` a provider registered SHOULD have a corresponding entry in that provider's inner map. A missing entry is an operator misconfiguration — the provider is expected to emit that slot at serve time, and once it does the serve-time fail-closed rule below will suppress the whole provider's chunks on that impression. Publishers SHOULD surface missing entries as a startup warning so operators can add them before serve-time responses begin carrying the unmapped slot.
* Every provider present in `tmpx_macro_mapping` SHOULD have a registration; a mapping entry for an unregistered provider is dead configuration.

**Serve-time fail-closed rule.** When a response carries a `slot_id` for a provider that this mapping has no entry for — the mapping is missing the whole provider, OR the provider is present but the specific `slot_id` is unknown to this surface — the publisher's adapter MUST fail closed for that provider on that impression: none of that provider's chunks are fired into the ad-serving path and the adapter logs a configuration error. Other providers on the same response are unaffected. A `provider_id` absent from the mapping entirely (e.g. a newly onboarded provider not yet trafficked on this surface) is the same case — the adapter drops that provider's chunks on this surface. This is a single rule with two detection stages: startup validation gives the operator a warning before responses begin carrying the unmapped slot; if the warning is not acted on, the serve-time rule catches it.

Ordered position is preserved on the wire (chunks are an ordered prefix of the provider's registered `tmpx_slots`) but is not the identity key — `slot_id` is. That distinction is what makes this proposal not "macro name → slot id" rename: slot IDs stay provider-local and opaque, publisher destinations stay publisher-local, and the mapping between them lives only on the publisher side. Adopters gain startup drift detection and self-describing chunks; the router still never sees a destination name.

### Provider registration security

**Endpoint URL validation (SSRF).** Both static configuration and dynamic registration MUST validate provider `endpoint` URLs against the canonical [Webhook URL validation (SSRF)](/dist/docs/3.2.0-beta.0/building/by-layer/L1/security#webhook-url-validation-ssrf) rules — HTTPS only in production, reserved IPv4 and IPv6 ranges rejected (including `::ffff:0:0/96` IPv4-mapped bypasses and the `169.254.169.254` / `fd00:ec2::254` cloud metadata addresses), no redirects. Because a router calls the provider on every request, DNS rebinding is the primary risk: routers MUST either pin the TCP connection to the IP that passed validation or re-validate the socket's post-handshake peer address before sending the request body. Re-resolving DNS without pinning is not sufficient.

**Dynamic registration authentication.** The dynamic registration API is a privileged surface; unauthenticated registration lets an attacker point publisher traffic at arbitrary HTTPS endpoints. Routers exposing dynamic registration MUST authenticate callers (mTLS or short-lived OAuth 2.0 tokens; static API keys only with IP allow-listing) and SHOULD apply per-agent rate limits on mutations and a cap on total registered providers per publisher to bound registration-storm abuse.

**Router-to-provider auth.** TMP does not define a credential profile for router-to-provider calls. Deployments MUST authenticate every provider request, and production providers MUST NOT accept anonymous calls. Static bearer tokens MAY be used only with IP allow-listing.

**`/health` endpoint.** The `/health` endpoint providers are encouraged to expose for router liveness MAY be unauthenticated, but the response MUST NOT leak internal state. Providers SHOULD return `200` with body `{"status": "ok"}` when ready and `503` when not ready; other status codes are bugs. Providers MUST NOT differentiate status codes or response bodies by internal subsystem (for example, a distinct code when the database is down versus when the identity cache is down is a side-channel that maps external probing onto internal topology). Version strings, build hashes, internal hostnames, and dependency statuses MUST NOT appear in the body. Rate-limit the endpoint (recommended: 1 req/sec per source IP) so it doesn't become a DoS amplifier.

## Product Integration

Publishers declare TMP support on their products via the `trusted_match` field. Buyers see this on `get_products` and know what TMP capabilities are available.

| Field            | Type                 | Required | Description                                                                                                                                                                                                                                                                                                                                         |
| ---------------- | -------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `context_match`  | bool                 | Yes      | Product supports Context Match requests.                                                                                                                                                                                                                                                                                                            |
| `identity_match` | bool                 | No       | Product supports Identity Match requests. Default: false.                                                                                                                                                                                                                                                                                           |
| `response_types` | List\<string>        | No       | What the publisher can accept back: `activation` (default), `catalog_items`, `creative`, `deal`.                                                                                                                                                                                                                                                    |
| `dynamic_brands` | bool                 | No       | Whether the buyer can select a brand at match time. When false (default), brand must be on the media buy. When true, the buyer's offer can include any brand — the publisher applies approval rules at match time. Enables multi-brand agreements.                                                                                                  |
| `providers`      | List\<ProviderEntry> | No       | TMP providers integrated with this product's inventory. Each entry identifies a provider by `agent_url` (from the registry) and declares what match types it supports. The product-level `context_match` and `identity_match` booleans declare overall support; per-provider booleans declare which provider handles each. Enables buyer discovery. |

#### ProviderEntry

| Field            | Type          | Required | Description                                                                                                                                                                                                                                                                                                                                              |
| ---------------- | ------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `agent_url`      | string (URI)  | Yes      | Provider's agent URL from the registry. Canonical identifier for this TMP provider. Compared against the router's provider registry using the [AdCP URL canonicalization rules](/dist/docs/3.2.0-beta.0/reference/url-canonicalization), not byte-equality.                                                                                              |
| `context_match`  | bool          | No       | Whether this provider handles context match for this product. Default: false.                                                                                                                                                                                                                                                                            |
| `identity_match` | bool          | No       | Whether this provider handles identity match for this product. Default: false.                                                                                                                                                                                                                                                                           |
| `countries`      | List\<string> | No       | ISO 3166-1 alpha-2 country codes this provider serves. The router filters providers by the request's `country` field. Required when `identity_match` is true.                                                                                                                                                                                            |
| `uid_types`      | List\<string> | No       | Identity types this provider can resolve (from `uid-type` enum). The router selects providers whose `uid_types` overlaps with any `uid_type` in the request's `identities` array, and filters the forwarded `identities` to the intersection — providers MUST NOT receive tokens for types they did not declare. Required when `identity_match` is true. |

## Package Sync

Package metadata is synced from seller agents to TMP providers at media buy creation time and whenever the media buy materially changes. Providers cache the `AvailablePackage` set per placement and use it at request time — no package metadata flows through `context_match_request` or `identity_match_request`. The sync transport, authentication, and batch-error shape are deployment-specific; this section defines the payload contract and the obligations each participant inherits.

<Note>
  `seller_agent` on `AvailablePackage` is required under the experimental `trusted_match.core` surface. Sellers running TMP in existing deployments must update their sync payloads to populate it — see the [experimental feature contract](/dist/docs/3.2.0-beta.0/reference/experimental-status) for the 3.x-to-3.x evolution policy.
</Note>

### AvailablePackage

| Field            | Type                        | Required | Description                                                                                                                                                                                                                           |
| ---------------- | --------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `package_id`     | string                      | Yes      | Unique identifier for the package.                                                                                                                                                                                                    |
| `media_buy_id`   | string                      | Yes      | Media buy this package belongs to.                                                                                                                                                                                                    |
| `seller_agent`   | SellerAgentRef              | Yes      | Seller agent that owns this package. `agent_url` MUST match one of `authorized_agents[].url` in the adagents.json authoritative for every property this package may serve. See [Seller Agent Attribution](#seller-agent-attribution). |
| `format_options` | ProductFormatDeclaration\[] | No       | Canonical creative contracts eligible for this package.                                                                                                                                                                               |
| `format_ids`     | FormatId\[]                 | No       | Deprecated 3.x named-format compatibility projection.                                                                                                                                                                                 |
| `catalogs`       | List\<Catalog>              | No       | Buyer catalogs attached to this package, with selectors scoping which items are in play. Referenced by `catalog_id` against separately-synced catalog data.                                                                           |

#### SellerAgentRef

| Field       | Type         | Required | Description                                                                                                                                                                |
| ----------- | ------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `agent_url` | string (URI) | Yes      | The seller agent's API endpoint URL, exactly as declared in the property publisher's adagents.json `authorized_agents[].url`. HTTPS in production.                         |
| `id`        | string       | No       | Reserved for a future registry-assigned stable seller identifier. Not used today; included so the reference can absorb an opaque ID layer later without a breaking rename. |

### Seller Agent Attribution

TMP providers cache packages from many seller agents against many publishers. The `seller_agent` field makes that provenance explicit on each cached `AvailablePackage` so providers — which have no access to a media-buy store — can attribute offers, apply per-seller observability, and resolve disputes without out-of-band lookups.

The canonical seller identity in AdCP is the agent URL declared in the property publisher's [adagents.json](https://adcontextprotocol.org/schemas/3.2.0-beta.0/adagents.json) `authorized_agents[].url` entry. TMP reuses that URL directly rather than introducing a parallel identifier space. The `id` slot on `SellerAgentRef` is reserved for a future registry-assigned opaque identifier and is not used today.

**Placement rationale.** Both `context_match_request` and `identity_match_request` carry `seller_agent_url`. It is the key the receiving agent uses to resolve the active package set it has registered for the asking seller: the provider on the context path, the buyer agent on the identity path. When `package_ids` is omitted, evaluation runs against that seller's full active set; a `seller_agent_url` the receiver has not synced packages for MUST yield an empty result rather than a fall-back to another seller's set.

`seller_agent` also lives on the cached `AvailablePackage` (sync time) for attribution — that binding is the source of truth for which seller owns a package, and the offer echo below reads from it. The two carry different jobs: the request-side `seller_agent_url` selects which seller's set to evaluate; the package-side `seller_agent` attributes an individual package. Sync time remains when a provider first learns about a package; that binding is established once and reused for every subsequent evaluation.

`seller_agent_url` does not interact with the [Package set decorrelation](#package-set-decorrelation) guarantee. That guarantee constrains data that varies per user — chiefly `package_ids`, whose composition MUST be independent of the current placement. `seller_agent_url` is a single stable value identifying the asking seller; it is identical for every user on a given placement and carries no user identity, so it adds no per-user signal on which context and identity requests could be correlated. This is receiver-side active-set scoping, not a per-user filter.

**Offer echo.** `seller_agent` MAY appear on `offer.json` as an echo from the cached package, for publisher-side log pipelines that want one-hop attribution without rejoining to the media-buy store. The echo is non-authoritative — the cached `AvailablePackage` binding is source of truth. Providers and routers MUST overwrite a disagreeing echo with the cached binding before logging, forwarding, or emitting the offer downstream, and MUST NOT use the echo value in any field consumed for billing, reporting, or dispute resolution. A disagreement SHOULD be counted to a `seller_agent_echo_mismatch` metric for anomaly detection. Routers MAY stamp `seller_agent` on merge when providers omit it.

### Sync-Time Validation

Providers SHOULD validate `seller_agent.agent_url` against the property publisher's adagents.json at sync time:

1. For each property the package may serve, fetch `/.well-known/adagents.json` on the property domain. If the file contains an `authoritative_location` pointer, follow it at most one hop to a same-scheme HTTPS URL; do not chain further. Both the initial fetch and the `authoritative_location` hop MUST apply the [Webhook URL validation (SSRF)](/dist/docs/3.2.0-beta.0/building/by-layer/L1/security#webhook-url-validation-ssrf) rules — HTTPS-only, reserved IPv4 and IPv6 ranges rejected (including `::ffff:0:0/96` IPv4-mapped bypasses and the `169.254.169.254` / `fd00:ec2::254` cloud metadata addresses), no transparent redirects, and the TCP connection pinned to the validated IP. The inbound-fetch rules referenced for [Provider registration security](#provider-registration-security) apply equally to this outbound fetch.
2. Confirm that `seller_agent.agent_url` appears in `authorized_agents[].url` and that any `property_ids`, `collections`, `placement_ids`, `placement_tags`, `countries`, `effective_from`, and `effective_until` constraints on that entry permit the package's scope. The URL comparison uses the [AdCP URL canonicalization rules](/dist/docs/3.2.0-beta.0/reference/url-canonicalization) — canonicalize both values before matching, never byte-equality. Reject `seller_agent.agent_url` values that do not use the `https://` scheme with `seller_not_authorized`; non-HTTPS seller URLs have no transport-integrity guarantee and cannot be trusted as an authorization key.
3. On mismatch, reject the sync operation for that `AvailablePackage` with an `error` response using `code: seller_not_authorized`. Other packages in the same sync batch are unaffected. The exact wire shape of the sync error is deployment-specific; `code` is the machine-readable reason.

**Cache and re-validation.** The validation result SHOULD be cached for at most 5 minutes, matching the recommended adagents.json cache window. Providers MUST re-validate on cache expiry and SHOULD surface sustained `seller_not_authorized` rejections to publisher operations — repeated failures usually indicate the publisher's adagents.json and the seller's sync pipeline have diverged. When an authorization's `effective_until` passes or the seller is removed from `authorized_agents`, providers MUST treat previously-cached packages from that seller as `unknown_package` at request time until they are re-synced and re-validated.

**Fetch failures.** When validation cannot complete (fetch error, timeout, cert failure, malformed file), providers SHOULD reject the sync with `seller_not_authorized` rather than cache an unvalidated binding. Providers that fail-open MUST bound the unvalidated window to the 5-minute cache TTL and re-validate at the next opportunity; a binding that has never successfully validated MUST NOT remain cached past that window.

**Bypass for pre-attested relationships.** Providers MAY skip the adagents.json check only when they can verify the same `agent_url → authorized_agents[].url` binding through an out-of-band onboarding process (e.g., a mutually authenticated provider-seller enrollment that carries the publisher's attestation), and SHOULD publish their enforcement mode (`enforcing` / `advisory`) in their conformance self-report so publishers can gate onboarding. This escape hatch is permitted for `trusted_match.core` v1 and will be removed in the first non-experimental TMP release, at which point the validation becomes MUST.

### Participant Responsibilities

| Actor            | Sync time                                                                                                                                                                                                                       | Request time                                                                                                                                                                                                                                                                                                       |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Seller agent** | Include its own adagents-registered `agent_url` as `seller_agent.agent_url` on every `AvailablePackage` it syncs. MUST be the URL the publisher has attested in `authorized_agents[].url`.                                      | No new behavior. The offer MAY echo `seller_agent`; omitting it is fine — the router can stamp from cache.                                                                                                                                                                                                         |
| **Publisher**    | Keep `authorized_agents` in adagents.json accurate, including scope constraints (`property_ids`, `placement_ids`, `countries`, effective windows).                                                                              | No new behavior.                                                                                                                                                                                                                                                                                                   |
| **Router**       | No new behavior.                                                                                                                                                                                                                | MAY stamp `seller_agent` on offers that arrive without it, using its cached package→seller binding. Both `context_match_request` and `identity_match_request` carry `seller_agent_url` directly per their schemas; the router MUST forward it unchanged and MUST NOT use it to filter or re-route the request.     |
| **Provider**     | Validate `seller_agent.agent_url` against the property's adagents.json per [Sync-Time Validation](#sync-time-validation). Reject unauthorized packages with `seller_not_authorized`. Store the binding with the cached package. | Echo `seller_agent` on offers it produces. The cached binding is immutable for an in-flight decision, but providers MUST treat packages whose authorization has expired (removal from `authorized_agents` or `effective_until` passed) as `unknown_package` on subsequent requests until the package is re-synced. |

### What This Is Not

* **Not a per-user filter.** `seller_agent_url` selects which seller's registered active set the receiver evaluates — it is the same value for every user on a placement and carries no user identity. Publishers, routers, and providers MUST NOT use it as a per-user or per-request filter, nor to re-route the request; doing so would reintroduce the per-user variation [Package set decorrelation](#package-set-decorrelation) exists to prevent. The package-side `seller_agent` attribution likewise MUST NOT be used to scope or filter a request — it exists for offer attribution only.
* **Not a sellers.json bridge.** IAB sellers.json `seller_id` and TAG-IDs serve a distinct financial-audit identity space. They remain on `adagents.json` `contact` — TMP does not duplicate them.
* **Not a cryptographic attestation.** The binding is publisher-attested via adagents.json over HTTPS. Signed TMP seller claims are a future enhancement and can layer onto `SellerAgentRef` through the reserved `id` slot or an `ext` field without breaking changes. When a future release populates both `agent_url` and `id`, `agent_url` remains authoritative and `id` is advisory — a disagreement between the two MUST NOT be used to upgrade trust above what the URL's adagents.json binding provides.

## Privacy Requirements

The following requirements use RFC 2119 keywords (MUST, SHOULD, MAY).

### Structural separation

* Context Match requests MUST NOT contain user identity data (user tokens, device IDs, IP addresses, session tokens, or any data that could identify a specific user).
* Identity Match requests MUST NOT contain page context data (URLs, content hashes, topic IDs, content signals, or any data that could identify what the user is viewing).
* The TMP Router MUST process Context Match and Identity Match in structurally separate code paths with no shared state.
* Context Match and Identity Match request IDs MUST NOT be correlated or derivable from each other.

### Package set decorrelation

* Context Match MUST NOT be filtered by user identity or audience. The provider evaluates its synced package set for the placement — the same packages for every user. No per-request package list is sent, so the publisher cannot accidentally leak identity through package filtering.
* The publisher SHOULD omit `package_ids` from Identity Match and let the buyer evaluate against the full active set it has registered for `seller_agent_url`. When `package_ids` IS provided, its composition MUST be statistically independent of the current placement — sending only the page-specific subset would allow a buyer to correlate Identity Match with Context Match by comparing package sets. Two acceptable modes:
  * **All-active.** Include every active package this buyer has at this publisher.
  * **Fuzzed.** Include a random sample of active packages, optionally padded with synthetic non-existent IDs, drawn from a distribution that does not depend on the current placement. The silent-drop rule below makes synthetic-ID padding safe — unknown IDs do not affect response shape and cannot leak registry membership.
* When both `seller_agent_url` and `package_ids` are present, the buyer evaluates against the intersection of its registered active set and `package_ids`; unknown IDs in `package_ids` MUST be silently ignored (not error-surfaced) so the response does not leak registry membership back to the publisher.
* A publisher that maintains a cached list of all active package IDs per buyer MAY send the full set on every Identity Match request as a defense in depth, but the buyer-side resolution from `seller_agent_url` is the primary mechanism.
* The publisher performs the intersection of context match offers and identity match eligibility locally, after both responses arrive.

### Temporal decorrelation

* Publishers SHOULD introduce a random delay between Context Match and Identity Match requests. Recommended: 100-2000ms, uniformly distributed.
* Publishers SHOULD also randomize the order of Context Match and Identity Match: each opportunity SHOULD have a roughly equal probability of Context Match being sent first or Identity Match being sent first. A fixed order — e.g., Identity Match always after Context Match — leaks the pairing through ordering even when the delay is randomized.
* Publishers MAY batch Identity Match requests across multiple page views.
* Publishers MAY route Context Match and Identity Match through different network paths.

### TEE attestation

* The TMP Router SHOULD provide TEE attestation when available, proving the deployed binary matches the published source.
* Attestation documents SHOULD be available on request to publishers and auditors.
* Attestation SHOULD include measurements confirming service code integrity and isolation.

### Consent handling

* When `consent` is omitted from an Identity Match request, buyers MUST treat this as "consent status unknown" rather than "consent not required."
* Buyers in jurisdictions where consent is required MUST reject Identity Match requests that omit `consent`, not assume consent.

### User token requirements

* User tokens MUST be opaque to buyer agents. Tokens may originate from identity providers (ID5, LiveRamp, UID2) or be publisher-generated.
* User tokens MUST NOT contain PII or be reversible to PII by the buyer agent.

## Request Authentication

TMP requests carry a signature to prove the request originated from an authorized router. This prevents unauthorized parties from sending forged requests to providers to probe targeting logic, extract sponsored content, or manipulate frequency state.

### Signing model

The router signs all requests using Ed25519. Both Context Match and Identity Match requests are signed, but with different signed fields reflecting their different contents and caching characteristics.

Signatures bind the request to a specific provider. The router signs a separate signature per fan-out target using the provider's endpoint URL from the router's provider registration. Providers MUST verify that the signed `provider_endpoint_url` matches their own advertised endpoint and reject the request otherwise. This prevents a captured signature from being replayed against a different provider in the registry within the epoch.

Signatures also bind to `seller_agent_url`, the asking seller's agent URL. `seller_agent_url` selects which seller's registered active package set the receiver evaluates against (see [Seller Agent Attribution](#seller-agent-attribution)) and is the lookup key receivers use to resolve the publisher's signing keys for verification. Including it in the signed bytes prevents a captured signature from being replayed under a different seller's identity to read another seller's offers or active package set.

The daily epoch provides replay protection — a captured signature is valid for at most \~48 hours (current + previous epoch accepted by verifiers).

### Signature envelope

The signature is transmitted via HTTP headers alongside the JSON body.

| Header             | Value                                                    |
| ------------------ | -------------------------------------------------------- |
| `X-AdCP-Signature` | Base64-encoded (URL-safe, no padding) Ed25519 signature  |
| `X-AdCP-Key-Id`    | Key identifier from the agent's `agent-signing-key.json` |

#### Context Match signed fields

Concatenated in this order, UTF-8, newline-separated:

| Field                   | Source                                                                                                                                                                                                                                                       |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `type`                  | `context_match_request`                                                                                                                                                                                                                                      |
| `seller_agent_url`      | From the request body, compared using the [AdCP URL canonicalization rules](/dist/docs/3.2.0-beta.0/reference/url-canonicalization). Binds the signature to the asking seller — reusing a signature under a different `seller_agent_url` fails verification. |
| `property_rid`          | From the request body                                                                                                                                                                                                                                        |
| `placement_id`          | From the request body                                                                                                                                                                                                                                        |
| `package_ids`           | Sorted, comma-separated list of active package IDs                                                                                                                                                                                                           |
| `provider_endpoint_url` | Provider's registered endpoint URL (exact string match with provider registration, no trailing slash)                                                                                                                                                        |
| `daily_epoch`           | `floor(unix_timestamp / 86400)`                                                                                                                                                                                                                              |

When `package_ids` is absent from the request, the signature MUST use an empty string for that field in the signed payload.

Because the signed fields are static per seller per placement per provider, the same signature can be cached and reused for all requests to the same `(seller_agent_url, placement_id, provider_endpoint_url)` triple within a 24-hour epoch. Cache keys MUST include `seller_agent_url` and `provider_endpoint_url` — reusing a signature across sellers or providers violates the binding and will fail verification.

#### Identity Match signed fields

The signed input is the hex-encoded SHA-256 of the [RFC 8785 JCS](https://datatracker.ietf.org/doc/html/rfc8785) serialization of the following canonical object. Using JCS eliminates delimiter-injection risk — raw `tcf_consent` / `gpp` / `us_privacy` / `package_id` values may contain any byte, but JSON string escaping in JCS renders any framing byte harmless.

| Field                     | Source                                                                                                                                                                                                                                                       |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `type`                    | `"identity_match_request"`                                                                                                                                                                                                                                   |
| `request_id`              | From the request body                                                                                                                                                                                                                                        |
| `seller_agent_url`        | From the request body, compared using the [AdCP URL canonicalization rules](/dist/docs/3.2.0-beta.0/reference/url-canonicalization). Binds the signature to the asking seller — reusing a signature under a different `seller_agent_url` fails verification. |
| `identities_hash`         | Hex-encoded SHA-256 of the canonical `identities` bytes (see below)                                                                                                                                                                                          |
| `consent`                 | The request's `consent` object verbatim, or `null` when absent                                                                                                                                                                                               |
| `package_ids`             | The request's `package_ids` sorted lexicographically by UTF-8 byte order                                                                                                                                                                                     |
| `sealed_credentials_hash` | Hex-encoded SHA-256 of the canonical `sealed_credentials` bytes (see below), or `null` when absent. **Experimental (`trusted_match.verified_identity`).**                                                                                                    |
| `provider_endpoint_url`   | Provider's registered endpoint URL (exact string match with provider registration, no trailing slash). Binds the signature to a specific provider — reusing a signature across providers fails verification.                                                 |
| `daily_epoch`             | `floor(unix_timestamp / 86400)` as a JSON integer                                                                                                                                                                                                            |

**Canonical `identities` bytes:** deduplicate on `(uid_type, user_token)` using byte-exact match (no case folding, no trimming) — this collapses duplicate tokens only — sort entries by `uid_type` (UTF-8 byte order), then by `user_token` (UTF-8 byte order), then serialize the resulting array of **complete identity objects** as [RFC 8785 JCS](https://datatracker.ietf.org/doc/html/rfc8785). Each object is serialized in full, **including any `attestation`** — so the signature covers the attestation, and a stripped, swapped, or injected per-identity attestation breaks signature verification (the dedup key on `(uid_type, user_token)` is for collapsing duplicate tokens, not a statement that `attestation` is excluded from the hash). SHA-256 the UTF-8 bytes; hex-encode for the signed input, use raw bytes for cache keys (both conventions hash the same preimage).

**Canonical `sealed_credentials` bytes** (experimental, `trusted_match.verified_identity`): sort entries by `audience_kid` (UTF-8 byte order), serialize the resulting array as [RFC 8785 JCS](https://datatracker.ietf.org/doc/html/rfc8785), SHA-256 the UTF-8 bytes. When the request carries no `sealed_credentials`, `sealed_credentials_hash` is `null`. Because `sealed_credentials` is part of the signed input, an injected or swapped sealed blob breaks signature verification; because `sealed_credentials_hash` is part of the dedup cache key (see [Caching](#caching)), a network-credential change repartitions the cache so stale eligibility is not served. The router re-signs over each provider's forwarded set after [route-by-`audience_kid`](#verified-identity-attestation) filtering.

The router filters `identities` per provider before forwarding (see [Identity Match fan-out](/dist/docs/3.2.0-beta.0/trusted-match/router-architecture#identity-match-fan-out)). The signature is computed over each provider's filtered `identities` set — the router re-signs per outbound forward. The same filtered `identities_hash` value is used in the cache key (see [Caching](#caching)), so each provider has its own cache partition keyed on the subset it actually received.

Identity Match signatures include `request_id` and `identities_hash`, so they are unique per request and cannot be cached. This is intentional — Identity Match responses affect buyer-side frequency state and must be idempotent. Buyers MUST deduplicate Identity Match requests by `request_id` within the daily epoch window. A repeated `request_id` MUST return the same response without updating frequency state. Buyers SHOULD dedupe by `hash(request_id)` rather than retaining the raw identifier.

### Signature verification

Each TMP router deployment **MUST** authenticate publisher requests before the router signs or fans them out. TMP does not define the publisher credential profile, publisher-facing endpoint URLs, or enforcement point; authentication may be enforced by the router, an API gateway, a service mesh, or another trusted ingress. When authentication is absent or fails at the actual publisher-facing HTTP endpoint for either Context Match or Identity Match, that endpoint **MUST** return `401` with a `WWW-Authenticate` challenge. TMP does not constrain the challenge scheme. A separate probe-only rejection path does not satisfy this requirement. A deployment whose admission control can only terminate the connection before an HTTP response (for example, strict mTLS with no HTTP authentication gate) also does not satisfy it. Because the rejection may occur before TMP request parsing, a TMP error body is not required. This prevents unauthenticated callers from using the router as a signing oracle.

The router signs requests before fanning out. Providers verify signatures using the publisher's public key, obtained from the property registry. This proves the request originated from an authorized router — not a third party probing the provider's targeting logic. Providers SHOULD sample-verify rather than verify every request — Ed25519 verification adds \~30μs per request, which is small compared to the full pipeline but adds up at volume. A 5% sample rate detects fraudulent publishers within seconds while adding negligible overhead.

On verification failure, the provider SHOULD suppress the property for a configurable period (recommended: 24 hours) and alert operations.

### Key rotation

Agents publish new signing keys via `agent-signing-key.json` at their well-known agent URL. Routers SHOULD cache keys with a 5-minute TTL. When a signature fails verification, the router SHOULD re-fetch the key before rejecting — the agent may have rotated.

### Key revocation

Rotation replaces a key; revocation kills one. Revocation is for the compromise case where a private key is known or suspected to have leaked.

Publishers mark a compromised key by setting `revoked_at` (ISO 8601 timestamp) on the key entry in `agent-signing-key.json` and leaving the key in the trust anchor during a grace period so stale caches still find it. Verifiers MUST:

* Reject any signature produced with a key where `revoked_at` is present and the signing epoch falls at or after the revocation timestamp.
* Re-fetch `agent-signing-key.json` on verification failure before rejecting the request, to pick up a revocation that propagated after the last cache refresh.
* Treat revocation as non-retryable for the offending request — there is no "maybe it'll work on retry" state.

Keys may be removed from the trust anchor entirely once the cache TTL (recommended: 5 minutes) has elapsed across all verifiers. Removing the key earlier can produce a window where verifiers with a cached copy accept signatures that a fresh fetch would reject — keep the `revoked_at` marker in place until cache propagation completes.

**Revocation limits.** Revocation propagates within 5 minutes (cache TTL) but does not retroactively invalidate signatures already accepted by providers before the revocation was observed. Captured signatures with a signing epoch before `revoked_at` remain verifiable for the remainder of the 48-hour replay window. Operators who suspect compromise SHOULD:

* Rotate proactively on any suspicion, not only on confirmed leak.
* Keep signing keys in HSM or KMS rather than on disk to minimize compromise likelihood in the first place.
* Accept that a leaked key grants up to \~48 hours of forgery capability until daily-epoch rollover retires the signed payloads that contained it. Shorter custom epoch windows are a deployment option when this window is unacceptable.

### Key distribution

Publisher public keys are distributed via the property registry. Each property record includes the publisher's Ed25519 public key. Providers download the registry at startup and keep it current via incremental sync.

## Wire Format

Content type: `application/json`

All TMP messages use JSON encoding. Field names and types follow the JSON Schema definitions in this specification. Implementations MUST support JSON.

TMP messages are small (200-600 bytes per request/response). At these sizes, serialization format is less than 1% of total latency — the protocol's performance comes from smaller messages and structural separation, not encoding efficiency. JSON is universal, debuggable, and supported by every language and tool.

## Country-Partitioned Identity

Identity data is logically partitioned by country (ISO 3166-1 alpha-2). The protocol carries the country code on every Identity Match request and in every TMPX token (the buyer's read replica includes the country in the encrypted plaintext for data residency routing — this is buyer-internal and not visible to the publisher or router). How countries are physically grouped into database clusters is a deployment decision — the protocol does not constrain it.

Publishers include a `country` field on Identity Match requests as a routing directive. The router uses it to select providers whose `countries` list includes that country code, then strips the field before forwarding. The buyer agent never sees the country — it is not an identity signal.

Each provider entry declares which countries and identity types it serves via `countries` and `uid_types` fields. A multi-country buyer operates separate provider entries per cluster (e.g., one for US, one for EU countries). This enables buyers to comply with data residency requirements, subscribe publishers to only the countries they serve, and move countries between clusters without protocol changes.

## TMPX Exposure Tokens

TMP uses encrypted exposure tokens (TMPX) to close the frequency capping loop. The Identity Match read replica encrypts resolved identity tokens into an opaque TMPX macro that flows through creative tracking URLs. The buyer's impression pixel decrypts the token and logs per-user exposures.

### Encryption

TMPX uses HPKE (RFC 9180) with `mode_base`:

| Parameter | Value                                                  |
| --------- | ------------------------------------------------------ |
| Mode      | `mode_base` — encrypt with recipient's public key only |
| KEM       | DHKEM(X25519, HKDF-SHA256)                             |
| KDF       | HKDF-SHA256                                            |
| AEAD      | ChaCha20-Poly1305                                      |

The buyer's cluster master holds the recipient private key. Read replicas encrypt using the master's public key. Only the master can decrypt. Forging a TMPX token requires both the master's public key (published) and realistic identity tokens from identity providers (UID2, ID5, RampID) — fabricating these is harder than the fraud itself. Systematic frequency manipulation is detected at the IVT (Invalid Traffic) layer, not the encryption layer.

### Binary format

The TMPX plaintext is a compact binary structure. The type ID implicitly defines the token length — no length prefix is needed.

**Header (16 bytes):**

| Field     | Size    | Description                                         |
| --------- | ------- | --------------------------------------------------- |
| Version   | 1 byte  | Format version (`0x01`)                             |
| Timestamp | 4 bytes | uint32 Unix seconds — token creation time           |
| Country   | 2 bytes | ISO 3166-1 alpha-2, ASCII — identity data residency |
| Nonce     | 8 bytes | Random — replay deduplication at the master         |
| Count     | 1 byte  | Number of identity entries                          |

**Entries (repeated, buyer-configured priority order):**

| Field   | Size    | Description                                            |
| ------- | ------- | ------------------------------------------------------ |
| Type ID | 1 byte  | Fixed integer, defines token binary size               |
| Token   | N bytes | Raw binary identity token (size determined by Type ID) |

**Type ID registry:**

| ID | Token Type              | Binary size                                                 |
| -- | ----------------------- | ----------------------------------------------------------- |
| 1  | uid2                    | 32 bytes                                                    |
| 2  | euid                    | 32 bytes                                                    |
| 3  | id5                     | 32 bytes                                                    |
| 4  | rampid                  | 32 bytes                                                    |
| 5  | rampid\_derived         | 48 bytes                                                    |
| 6  | maid                    | 16 bytes                                                    |
| 7  | pairid                  | 32 bytes                                                    |
| 8  | hashed\_email           | 32 bytes                                                    |
| 9  | publisher\_first\_party | 32 bytes                                                    |
| 10 | world\_id\_nullifier    | 48 bytes (16-byte relying-party digest + 32-byte nullifier) |

Type IDs are stable — new types append, existing IDs never change. Tokens are stored in binary (UUIDs as 16 bytes, base64-encoded tokens decoded). RampID has two entries because maintained (XY, 32 bytes) and derived (Xi, 48 bytes) forms differ in size.

`world_id_nullifier` is **relying-party-scoped**: its token is the 16-byte SHA-256 digest of the proof's `relying_party_id` followed by the 32-byte nullifier. This entry is produced only from a **verified** attestation — the verifier-derived nullifier under the receiver-confirmed `rp_id` (see [Verified Identity Attestation](#verified-identity-attestation)). A sender-asserted `world_id_nullifier` in `identities[]` carries no trust and is never resolved or sealed; it cannot even form this token, since it lacks a verified `rp_id`. Unlike `uid2`/`id5`/`rampid`, the entry is not a graph identifier the buyer resolves — it is a proof-of-personhood pseudonym, valid only with an accompanying verified attestation. A World ID nullifier is meaningful only within the `rp_id` it was minted for, and that `rp_id` rides the request-side `attestation`, which does not round-trip into the token. Carrying the `rp_id` digest in the token lets the out-of-band impression tracker attribute the nullifier to its relying party — by matching the digest against the relying parties it accepts — and key frequency state on the `(rp_id, nullifier)` pair, so a nullifier under one relying party never shares cap state with another. The token carries no `rp_id` cleartext, and the digest width is a working-group open item.

If a parser encounters an unknown Type ID, it MUST stop parsing and treat the remaining entries as absent. The header Count field indicates total entries, but implementations MUST NOT assume all entries are parseable — forward compatibility requires graceful degradation when newer Type IDs are present.

### Wire format

The TMPX macro value uses the format `<kid>.<base64url_ciphertext>`. The ciphertext MUST use unpadded base64url encoding (RFC 4648 section 5, no `=` padding characters). Padding characters break URL query parameters where `=` is the key-value delimiter. The `kid` (key identifier, max 8 characters) is opaque — it MUST NOT encode geographic or deployment information. It maps to a cluster master private key internally.

**Size budget (255-char GAM macro limit):** After HPKE overhead (48 bytes) and header (16 bytes), \~120 bytes remain for identity entries. Three 32-byte tokens = 99 bytes — fits comfortably. When the buyer resolves more identities than fit the budget, the TMPX plaintext is truncated to the highest-priority entries according to buyer deployment configuration. The priority order is a buyer-side configuration concern (not protocol-level), typically ranking deterministic graphs (UID2, RampID) above probabilistic or publisher-scoped identifiers. Buyers MUST configure an explicit priority list — the default implementation MUST NOT truncate arbitrarily — and the list SHOULD be documented in the buyer's operational runbook.

### Key management

One X25519 keypair per cluster:

* The cluster master holds the **private key** for decryption.
* The **public key** is published in `encryption_keys` on agent authorization entries in `adagents.json`.
* Read replicas use the public key to encrypt. No per-replica key management.

Key rotation follows the same pattern as TMP signing keys: 5-minute cache TTL, kid prefix for versioning, 30-day grace period for old master keys.

### Replay protection

The 8-byte random nonce enables deduplication at the master. The master stores nonces for a configurable window (recommended: 7 days) and rejects duplicates. The nonce is inside the AEAD-protected ciphertext — intermediaries cannot observe it.

### Caching behavior

The TMPX token is generated once per Identity Match evaluation and accompanies the eligibility response for the `serve_window_sec` window. All impressions on eligible packages within that window share the same TMPX value (same nonce, same tokens).

The buyer's master MUST NOT deduplicate by TMPX value or nonce within a serve window — each pixel fire is one impression. Multiple ads served to the same user in a CTV pod or a web page with multiple ad units all produce distinct pixel fires with the same TMPX token. The nonce deduplication only prevents replay of the same TMPX token *after* the serve window expires — if the same nonce appears outside its original window, it is a replay and MUST be rejected.

### Publisher obligations

Publishers MUST NOT parse, decrypt, or make decisions based on TMPX values. The token is opaque pass-through data substituted into creative tracking URLs exactly like other macros. For DOOH inventory, the player MAY include the opaque TMPX value in play log records transmitted to the buyer for exposure reconciliation — the publisher MUST NOT retain TMPX values beyond the transmission window.

### Inventory-specific behavior

For each terminal surface the publisher trafficks the destinations named in its [`tmpx_macro_mapping`](#publisher-tmpx-macro-mapping) — those names (macros, GAM key-values, VAST URL macros, DOOH play-log fields) are the operational contract the ad-server line items target. At serve time, the publisher walks `tmpx_providers[provider_id].chunks[]` from the response and, for each chunk, substitutes `chunk.value` into `tmpx_macro_mapping[provider_id][chunk.slot_id]`. The legacy `{TMPX}` macro (from the deprecated singular `tmpx` field) remains supported for consumers that haven't migrated.

* **Web, mobile, CTV (SSAI), audio (DAI):** Standard impression pixel — for each `chunk ∈ tmpx_providers[provider_id].chunks`, substitute `chunk.value` into the destination the publisher's mapping resolves for `(provider_id, chunk.slot_id)`.
* **CTV (client-side VAST):** Publisher performs the same per-chunk substitution before the VAST document reaches the player.
* **DOOH:** Play-log-based — TMPX values are logged by the DOOH player into the play-log fields the publisher's mapping resolves for `(provider_id, chunk.slot_id)` rather than pixel URLs. Play log records MUST carry `provider_id` alongside each `slot_id`/`value` pair so the buyer can route to the correct decryption master.

## Transport

* JSON over HTTP/2 POST. All implementations MUST use this transport.
* Each provider exposes two path-based endpoints under its base URL: `POST /context` for Context Match and `POST /identity` for Identity Match. The router dispatches requests by path, not by inspecting the message body.
* Each provider SHOULD expose `GET /health` at its base URL. The endpoint returns HTTP `200` with a JSON body `{"status": "ok"}` when the provider is ready to accept requests. Any non-`200` response or connection failure means the provider is not ready. Routers and operators use this for pre-flight checks and monitoring — it is not called in the request hot path.
* The `type` field in each message identifies the message for deserialization — routers and agents use it to select the correct schema, not for routing. Agents MUST validate that the `type` field matches the endpoint: `context_match_request` on `/context`, `identity_match_request` on `/identity`. A mismatch MUST be rejected with HTTP `400`.
* Connections SHOULD be reused via HTTP/2 multiplexing.
* The router SHOULD maintain a connection pool to each buyer agent.
* The [adcp-go](https://github.com/adcontextprotocol/adcp-go) SDK provides the reference client and server implementation. Conformance tests validate compatibility — implementations in other languages MUST pass the same test suite.

### HTTP Status Codes

TMP uses HTTP status codes for transport-level errors only. Application-level results (including TMP error responses) are always returned with HTTP `200`:

| HTTP Status | Meaning                                                                                                                                                                                                                                                               |
| ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `200`       | Request processed. Body contains the TMP response (success, empty result, or TMP error).                                                                                                                                                                              |
| `400`       | Malformed request (invalid JSON, missing `type` field). Not a TMP response — no body to parse.                                                                                                                                                                        |
| `401`       | Publisher authentication was absent or failed. The response MUST include `WWW-Authenticate`; TMP does not constrain the challenge scheme. No TMP error body is required because the deployment authentication gate may reject the request before TMP request parsing. |
| `503`       | Provider temporarily unavailable. The router should retry or skip.                                                                                                                                                                                                    |

This means a `200` with an empty `offers` array is a valid "no matches" response, and a `200` with a TMP error body (`"type": "error"`) is a valid application error. The router handles one response format regardless of outcome.

## Latency

* TMP targets sub-50ms end-to-end latency (publisher → router → agents → router → publisher).
* The router SHOULD apply adaptive per-agent timeouts based on observed latency percentiles.
* Agents that exceed the timeout are excluded from the merged response for that request.
* The router MAY preemptively skip agents whose p95 latency consistently exceeds the budget.

## Caching

Context Match responses are cacheable because the same packages are evaluated for every user on a given placement. The recommended cache key is `{property_rid, placement_id, provider_id}`.

* Routers SHOULD cache Context Match responses with a TTL of **5 minutes**.
* Providers MAY include a `cache_ttl` field (integer, seconds) in Context Match responses to override the default. Routers MUST respect this value when present.
* Identity Match responses are bound by `serve_window_sec` (per-package single-shot fcap, max 300s, default 60s). Routers MAY apply an internal deduplication cache keyed on `{identities_hash, provider_id, package_ids_hash, consent_hash, sealed_credentials_hash}`, where `identities_hash` is the SHA-256 of the canonical `identities` bytes defined in [Identity Match signed fields](#identity-match-signed-fields) (computed over the per-provider filtered subset); `package_ids_hash` is SHA-256 over the JCS serialization of the sorted `package_ids` array; `consent_hash` is SHA-256 over the JCS serialization of the request's `consent` object (or JCS `null` when the field is absent — this distinguishes "consent unknown" from an explicit-empty consent object); and `sealed_credentials_hash` (experimental, `trusted_match.verified_identity`) is the SHA-256 of the canonical `sealed_credentials` bytes defined in [Identity Match signed fields](#identity-match-signed-fields), or `null` when absent — so a network-credential change that shifts eligibility repartitions the cache rather than serving a stale response. JCS framing prevents delimiter-injection: raw consent strings or package IDs containing `|`, `,`, or `\n` cannot collide two distinct inputs. Including the identity set ensures that adding or removing tokens produces a distinct cache entry. Including the package list hash ensures cached responses are invalidated when the active package set changes (e.g., a new media buy activates). Including the consent hash prevents eligibility decisions taken under one consent state from being served under another. The publisher's binding contract is the serve-window throttle, not the router's internal cache window.
* When a provider's targeting configuration changes (new packages, updated targeting rules), the provider SHOULD return `"cache_ttl": 0` (Context Match) or `"serve_window_sec": 1` (Identity Match) until the change has propagated, then resume normal values.
* `cache_ttl` (Context Match) has a schema-enforced maximum of 86400 seconds. `serve_window_sec` is bounded at 300 seconds — longer windows make per-package fcap too coarse for typical campaigns, shorter than the IdentityMatch round-trip wastes the throttle.

## Conformance Levels

### TMP Buyer Agent (Basic)

* Supports `context_match` capability
* Responds to ContextMatchRequest with valid ContextMatchResponse
* Meets latency budget (p95 \< 30ms for agent-side processing)
* Respects privacy constraints (does not log or correlate requests with identity data from other sources)

### TMP Buyer Agent (Full)

All of Basic, plus:

* Supports `identity_match` capability
* Responds to IdentityMatchRequest with valid IdentityMatchResponse (eligible package IDs + TTL)
* Supports rich offers (brand, price, summary, creative manifest) when the product declares matching `response_types`

### TMP Router (Basic)

* Configured with provider endpoints
* Fans out Context Match and Identity Match to authorized providers
* Merges responses
* Meets end-to-end latency budget (p95 \< 50ms)
* Its actual publisher-facing Context Match and Identity Match endpoints return `401` with `WWW-Authenticate` when publisher authentication is absent or fails
* For each Context Match request in a multi-publisher deployment, binds the authenticated publisher principal to its allowed `property_rid` values and rejects unauthorized values before signing or fan-out; single-publisher deployments satisfy this requirement trivially
* Operator documentation identifies the authentication mechanism and where it is enforced (router, gateway, service mesh, or other trusted ingress)

### TMP Router (Trusted)

All of Basic, plus:

* Runs in a TEE-attested environment (e.g., AWS Nitro Enclaves)
* Provides attestation documents on request, proving the deployed binary matches the published source
* Structural separation between context and identity code paths is verifiable via attestation measurements
