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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 16 additions & 11 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@ require (
github.com/IABTechLab/adscert v0.34.0
github.com/NYTimes/gziphandler v1.1.1
github.com/WURFL/golang-wurfl v1.30.3
github.com/adcontextprotocol/adcp-go/tmproto v0.1.0
github.com/alitto/pond v1.8.3
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d
github.com/benbjohnson/clock v1.3.0
github.com/buger/jsonparser v1.1.2
github.com/chasex/glog v0.0.0-20160217080310-c62392af379c
github.com/coocood/freecache v1.2.1
github.com/docker/go-units v0.4.0
github.com/docker/go-units v0.5.0
github.com/go-sql-driver/mysql v1.6.0
github.com/gofrs/uuid v4.2.0+incompatible
github.com/golang/glog v1.2.5
Expand All @@ -28,13 +29,13 @@ require (
github.com/prebid/go-gdpr v1.12.0
github.com/prebid/go-gpp v0.2.0
github.com/prebid/openrtb/v20 v20.3.0
github.com/prometheus/client_golang v1.12.1
github.com/prometheus/client_model v0.2.0
github.com/prometheus/client_golang v1.23.2
github.com/prometheus/client_model v0.6.2
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475
github.com/rs/cors v1.11.0
github.com/spf13/cast v1.5.0
github.com/spf13/viper v1.12.0
github.com/stretchr/testify v1.8.4
github.com/stretchr/testify v1.11.1
github.com/tidwall/gjson v1.17.1
github.com/tidwall/sjson v1.2.5
github.com/vrischmann/go-metrics-influxdb v0.1.1
Expand All @@ -48,39 +49,43 @@ require (
)

require (
github.com/adcontextprotocol/adcp-go/urlcanon v0.1.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fsnotify/fsnotify v1.5.4 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/magiconair/properties v1.8.10 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/common v0.32.1 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.20.1 // indirect
github.com/sergi/go-diff v1.2.0 // indirect
github.com/spf13/afero v1.8.2 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/objx v0.5.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/subosito/gotenv v1.3.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect
github.com/yudai/pp v2.0.1+incompatible // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
golang.org/x/crypto v0.52.0 // indirect
golang.org/x/sys v0.45.0 // indirect
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
google.golang.org/protobuf v1.36.10 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/ini.v1 v1.66.4 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
lukechampine.com/blake3 v1.4.1 // indirect
)
63 changes: 44 additions & 19 deletions go.sum

Large diffs are not rendered by default.

227 changes: 227 additions & 0 deletions modules/adcontextprotocol/tmp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
# AdContextProtocol TMP Module

This module implements the [Trusted Match Protocol (TMP)](https://github.com/adcontextprotocol/adcp)
router role inside Prebid Server:

- It converts each incoming OpenRTB bid request into a TMP `context_match_request`
and, when identity tokens are present, a TMP `identity_match_request`.
- It fans out to one or more TMP providers in parallel, signing every outbound
call with Ed25519 (`X-AdCP-Signature`, `X-AdCP-Key-Id`) per the TMP spec.
- It joins each provider's context offers with its identity eligibility set
locally and surfaces the surviving package IDs plus response-level signals on
the bid response.

TMP wire types, signing and URL canonicalization come from
[`github.com/adcontextprotocol/adcp-go`](https://github.com/adcontextprotocol/adcp-go);
this module builds the OpenRTB→TMP mapping and the property registry client
on top.

## Configuration

```yaml
hooks:
enabled: true
modules:
adcontextprotocol:
tmp:
enabled: true
seller_agent_url: https://seller.example.com
signing:
key_id: kid-1
# PEM (PKCS#8) Ed25519 private key. Substitute from environment in
# your deployment YAML.
private_key_pem: ${ADCP_TMP_SIGNING_KEY_PEM}
property_registry:
# POST /api/registry/resolve — adcp catalog-openapi.ts
# ResolveRequest/ResolveResponse. The module sends
# {identifiers:[{type,value}], provenance, mode} and reads
# resolved[0].property_rid.
endpoint: https://agenticadvertising.org/api/registry/resolve
# "resolve" (default) contributes the identifier to the catalog
# and requires auth_bearer. "lookup" is a pure read with no auth
# and returns null property_rid for unknown identifiers.
mode: resolve
auth_bearer: ${ADCP_REGISTRY_TOKEN}
# FactProvenance.type — how the catalog attributes this request.
# See adcp catalog-openapi.ts FactProvenance for the enum.
provenance_type: member_assertion
provenance_context: prebid-server
cache_ttl_seconds: 3600
negative_cache_ttl_seconds: 300
cache_size: 4096
timeout_ms: 500
providers:
- name: example
identity_url: https://tmp.example.com/identity
context_url: https://tmp.example.com/context
timeout_ms: 200
# `tmpx_slots` mirrors the provider's registered
# `tmpx_slots` list from adcp provider-registration.json.
# Order is significant: the module enforces the ordered-
# prefix invariant (adcp#5971) on incoming responses —
# any provider whose emitted `tmpx_chunks[].slot_id`
# sequence deviates (reordered, sparse, unregistered,
# over-cap) has its chunks dropped atomically. Required
# only when the provider emits TMPX. Capped at 2 per
# adcp v1.
tmpx_slots:
- primary
- secondary
# Publisher-owned deployment configuration that resolves each
# provider's ordered TMPX chunks (provider-local {slot_id,
# value} pairs, per adcp publisher-tmpx-config.json) to local
# ad-server macro names on this Prebid Server surface. Outer
# key MUST match one of the `providers[].name` above (used as
# `provider_id` in the adcp spec); inner key MUST be a
# `slot_id` the provider declared in `tmpx_slots`; value is
# the publisher-local destination (GAM key, VAST URL macro,
# DOOH play-log field). Providers absent from this map emit
# no TMPX targeting. Chunks with an unmapped slot cause the
# whole provider's chunks to be dropped for that impression
# (fail-closed).
tmpx_macro_mapping:
example:
primary: TMPX_1
secondary: TMPX_2
timeout_ms: 300
# Set to a positive value to jitter the second of a provider's context /
# identity outbound calls by a random [0, N] ms, breaking timing
# correlation at a passive observer. Order of the two calls is always
# randomized regardless.
decorrelation_max_delay_ms: 0
targeting_key: adcp
add_to_targeting: false
# Caps on the segment set surfaced onto the response ext. Guards
# against a misbehaving or hostile provider bloating the bid
# response.
max_segments: 128
max_segment_value_len: 256
# Masking gates optional finer-grained fields into the context
# payload (zip / city / lat-long) and controls which EID sources
# / mobile IDs flow into the identity payload. Defaults are
# strict: only country / region / metro on the context path;
# a small hardcoded EID whitelist on the identity path unless
# `enabled: true` and `preserve_eids` narrows or widens it.
masking:
enabled: true
geo:
preserve_metro: true
preserve_zip: false
preserve_city: false
lat_long_precision: 0
user:
preserve_eids:
- liveramp.com
- uidapi.com
- id5-sync.com
device:
preserve_mobile_ids: false

host_execution_plan:
endpoints:
/openrtb2/auction:
stages:
auction_processed:
groups:
- timeout: 500
hook_sequence:
- module_code: "adcontextprotocol.tmp"
hook_impl_code: "HandleProcessedAuctionHook"
auction_response:
groups:
- timeout: 500
hook_sequence:
- module_code: "adcontextprotocol.tmp"
hook_impl_code: "HandleAuctionResponseHook"
```

### Required fields

| Field | Notes |
|-------|-------|
| `seller_agent_url` | Publicly reachable URL identifying this Prebid Server deployment as a seller agent. Must appear as one of `authorized_agents[].url` in the publisher's `adagents.json` (compared under AdCP URL canonicalization). |
| `signing.key_id` | Sent in `X-AdCP-Key-Id`. Verifiers use it to look up the matching Ed25519 public key. |
| `signing.private_key_pem` | PEM-encoded PKCS#8 Ed25519 private key. |
| `property_registry.endpoint` | Resolves `site.domain` / `app.bundle` → `property_rid` via `POST /api/registry/resolve` (adcp `ResolveRequest`/`ResolveResponse`). Defaults to `https://agenticadvertising.org/api/registry/resolve` when omitted. |
| `property_registry.mode` | `resolve` (default) contributes to the catalog and requires `auth_bearer`; `lookup` is an unauthenticated pure read. |
| `property_registry.provenance_type` | Enum from adcp `FactProvenance.type`. Default `member_assertion`. `crawl` is reserved for server-side pipelines and rejected. |
| `providers[].name` | Stable provider identifier (adcp `provider_id`). Appears verbatim in logs, metrics, and as the outer key of `tmpx_macro_mapping`. Charset matches the adcp spec: `^[A-Za-z0-9_]{1,64}$`. |
| `providers[].identity_url` or `providers[].context_url` | At least one is required per provider. |
| `providers[].tmpx_slots` | Optional. Ordered list of `slot_id`s the provider registered in adcp `provider-registration.json`. Required when the provider emits TMPX. The module drops any provider response whose emitted slot sequence is not a non-empty ordered prefix of this list. |
| `tmpx_macro_mapping` | Optional. Publisher-owned map of `provider_id → slot_id → ad-server macro name` used to route each provider's TMPX chunks. Omit to disable TMPX targeting. Missing entries for a provider's registered slots produce a startup warning; unmapped slots seen at serve time fail closed. |

### Providers

Each entry describes one downstream TMP provider. A provider may expose only
an identity endpoint, only a context endpoint, or both:

- If only `context_url` is set, no identity match is performed for that
provider and all offers pass through unfiltered.
- If only `identity_url` is set, no offers are produced (eligibility with no
context is not useful on its own — the module drops that combination).
- If both are set, offers are intersected with the identity eligibility set.

Providers are called in parallel; per-provider `timeout_ms` overrides the
module-level `timeout_ms`.

### Property registry

`site.domain` (or `app.bundle` when no site is present) is resolved to a
`property_rid` via the configured registry endpoint. Successful and negative
answers are cached in an in-memory LRU (`cache_size`, `cache_ttl_seconds`,
`negative_cache_ttl_seconds`). The first request from a cold domain may miss
its auction's timeout budget — that is expected; subsequent requests hit the
cache.

## Response surface

Merged targeting is written to the auction response `ext` under the configured
`targeting_key` (default `adcp`) as a flat list of `key=value` strings. Four
surfaces are covered per the adcp TMP spec:

- **Package IDs** eligible under identity, comma-joined under
`package_targeting_key` (default `adcp_package_id`).
- **Response-level context signals** — the identity-agent-neutral
`ContextMatchResponse.signals` map, one `key=value` per scalar entry.
- **Per-offer creative macros** — `Offer.macros` for offers that survived
the identity eligibility gate.
- **Identity TMPX chunks** resolved through `tmpx_macro_mapping`. Providers
emit `{slot_id, value}` pairs against their registered `tmpx_slots`; the
publisher's mapping decides the ad-server destination for each pair on
this surface. Chunks with unmapped slots are dropped atomically for that
provider (fail-closed).

```json
{
"ext": {
"adcp": {
"segments": [
"adcp_package_id=pkg-fall-2026,pkg-holiday",
"iab_cat=IAB1",
"brand=Acme",
"TMPX_1=opaque-chunk-value"
]
}
}
}
```

When `add_to_targeting: true`, each `key=value` pair is also mirrored into
`ext.prebid.targeting` so downstream ad servers (e.g. Google Ad Manager) can
consume them without a custom bridge.

## Privacy

- The TMP wire is decorrelated by design: context requests carry no identity
tokens, identity requests carry no page context. This module never mixes the
two payloads.
- Identity token count is capped at three, matching the TMP HPKE budget.
- Masking is applied to the context path (geo coarsening, EID allowlist)
before requests leave the process. Identity requests never carry the masked
fields to begin with.

## References

- TMP spec: [`adcontextprotocol/adcp`](https://github.com/adcontextprotocol/adcp) — `docs/trusted-match/specification.mdx`
- Go SDK: [`adcontextprotocol/adcp-go`](https://github.com/adcontextprotocol/adcp-go) — `tmproto`, `urlcanon`
- Property registry: [agenticadvertising.org](https://agenticadvertising.org)
Loading
Loading