Motivation
Every seller adapter (GAM, Kevel, Xandr, prebid-server, …) carries seller-side configuration on each Product that the buyer doesn't see and isn't part of the AdCP wire schema — line item template, ad unit IDs, key-value targeting templates, default placements, etc. AdCP's Product.ext (extra='allow') is the wire surface, but every adopter stores it under Product.implementation_config by convention (salesagent, the prebid example, the GAM reference adapter all do it the same way).
The pattern on create_media_buy is universal: enumerate the requested products, look up implementation_config per product_id, hand it to the adapter alongside the wire CreateMediaBuyRequest. Today every adopter writes this loop. The SDK can ship the helper.
Parent tracker: #491.
Current state
Salesagent: does this inline at src/core/tools/media_buy_create.py:2423-2466:
- Looks up products via
get_product_catalog(tenant_id=…) then filters by req.get_product_ids() (lines 2425-2429)
- For GAM specifically, auto-generates a default
implementation_config if missing (lines 2438-2455)
- Validates the config via
GAMProductConfigService.validate_config (lines 2468-2491)
The model field: src/core/database/models.py:256 (implementation_config: Mapped[dict | None] = mapped_column(JSONType)) and src/core/database/models.py:428-448 (effective_implementation_config accessor).
SDK: no helper. MediaBuyHandler.create_media_buy (src/adcp/decisioning/handler.py:1035-1052) hands the raw CreateMediaBuyRequest to the adopter; the adopter has to do the lookup itself. No implementation_config references anywhere in src/adcp/decisioning/.
Proposed API
A pluggable lookup protocol the adopter wires once, then framework injects the resolved configs into the platform method via a typed wrapper around the request:
# src/adcp/decisioning/implementation_config.py (new)
class ProductConfigStore(Protocol):
\"\"\"Adopter-supplied lookup. Framework calls once per create_media_buy
with the product_ids from the request; adopter returns the configs.\"\"\"
async def lookup_implementation_configs(
self,
product_ids: list[str],
ctx: RequestContext,
) -> dict[str, dict[str, Any]]:
...
# SalesPlatform method signature gains an optional configs kwarg the
# framework populates before calling — adopters that don't wire a
# ProductConfigStore see configs={} (current behavior).
class SalesPlatform(Protocol):
def create_media_buy(
self,
req: CreateMediaBuyRequest,
ctx: RequestContext,
configs: dict[str, dict[str, Any]] = ..., # framework-injected
) -> SalesResult[CreateMediaBuySuccessResponse]:
...
Acceptance criteria
Out of scope
- Auto-generation of default configs (salesagent has GAM-specific defaults at
media_buy_create.py:2440-2455 — that's adapter-specific, not framework-portable)
- Config validation (also adapter-specific — salesagent uses
GAMProductConfigService.validate_config)
- Persisting configs back to a store on creation (write-side; only reading is universal)
- Threading config to other tools (
update_media_buy, sync_creatives) — open question, possibly follow-up
Cross-references
Open question (for the implementer)
Should ProductConfigStore be a standalone protocol or fold into a broader inventory_store primitive? Brian's note in the original triage prompt flagged signals/inventory store as an architectural question. Recommendation: ship narrow first (ProductConfigStore only, scoped to create_media_buy); generalize when a second tool needs the same lookup.
Motivation
Every seller adapter (GAM, Kevel, Xandr, prebid-server, …) carries seller-side configuration on each
Productthat the buyer doesn't see and isn't part of the AdCP wire schema — line item template, ad unit IDs, key-value targeting templates, default placements, etc. AdCP'sProduct.ext(extra='allow') is the wire surface, but every adopter stores it underProduct.implementation_configby convention (salesagent, the prebid example, the GAM reference adapter all do it the same way).The pattern on
create_media_buyis universal: enumerate the requested products, look upimplementation_configper product_id, hand it to the adapter alongside the wireCreateMediaBuyRequest. Today every adopter writes this loop. The SDK can ship the helper.Parent tracker: #491.
Current state
Salesagent: does this inline at
src/core/tools/media_buy_create.py:2423-2466:get_product_catalog(tenant_id=…)then filters byreq.get_product_ids()(lines 2425-2429)implementation_configif missing (lines 2438-2455)GAMProductConfigService.validate_config(lines 2468-2491)The model field:
src/core/database/models.py:256(implementation_config: Mapped[dict | None] = mapped_column(JSONType)) andsrc/core/database/models.py:428-448(effective_implementation_configaccessor).SDK: no helper.
MediaBuyHandler.create_media_buy(src/adcp/decisioning/handler.py:1035-1052) hands the rawCreateMediaBuyRequestto the adopter; the adopter has to do the lookup itself. Noimplementation_configreferences anywhere insrc/adcp/decisioning/.Proposed API
A pluggable lookup protocol the adopter wires once, then framework injects the resolved configs into the platform method via a typed wrapper around the request:
Acceptance criteria
ProductConfigStoreis optional; when not wired, framework injectsconfigs={}(back-compat)lookup_implementation_configswith deduped product_ids extracted fromreq.packages[*].product_id(or wherever they live in the v3 wire shape — verify against currentCreateMediaBuyRequest)AdcpError(IMPLEMENTATION_CONFIG_LOOKUP_FAILED, recovery='transient')validate_platformwarns when adopter declares the sales-* specialism but doesn't wire aProductConfigStoreAND usesconfigskwargconfigs={pid1: {...}, pid2: {...}, pid3: {...}}IMPLEMENTATION_CONFIG_LOOKUP_FAILEDOut of scope
media_buy_create.py:2440-2455— that's adapter-specific, not framework-portable)GAMProductConfigService.validate_config)update_media_buy,sync_creatives) — open question, possibly follow-upCross-references
src/core/tools/media_buy_create.py:2423-2491src/core/database/models.py:256,:428-448Product.ext:src/adcp/types/generated_poc/core/product.py:486(ExtensionObject)CreateMediaBuyRequestwire shape — verify product_id location before implementingOpen question (for the implementer)
Should
ProductConfigStorebe a standalone protocol or fold into a broaderinventory_storeprimitive? Brian's note in the original triage prompt flagged signals/inventory store as an architectural question. Recommendation: ship narrow first (ProductConfigStoreonly, scoped tocreate_media_buy); generalize when a second tool needs the same lookup.