Problem
Every seed_* scenario of comply_test_controller shares one contract: the runner picks the entity ID, and the seller must store it verbatim, surface it through its read tools, and accept it back in buyer calls — most controller storyboards reference the seeded literal directly (e.g. governance-delivery-monitor seeds outdoor_display_q2, then sends packages[].product_id: "outdoor_display_q2" to create_media_buy). SeedSuccess is message-only, so the seller has no channel to answer with its own ID.
We operate a multi-tenant ad-management platform (goTom) with an AdCP sales agent on top, and for sellers like us this contract is unimplementable — for every seeded entity type, not just products:
- Our entities are not rows in a fixture store. A product is a valid, priced combination derived at read time from an underlying inventory model (rate card, publisher, placement, pricing option, …). A media buy is the outcome of a real booking transaction; an account is a real customer record.
- Our IDs are derived keys, not free-form strings.
create_media_buy decodes the product ID and resolves its components against real entities before booking. A runner-chosen string is not "a product we haven't created yet" — it is unparseable. The same holds for pricing_option_id, format IDs, media_buy_id, account_id.
- Inventory is publisher-owned. Publishers provision it through the platform; the AdCP agent deliberately has no write path into the catalog.
The only workaround is an alias table plus interception of every read and write path to translate runner IDs into real ones — a parallel, test-only code path. That is exactly the teach-to-test trap the docs warn against, and it means the graded surface is the alias layer, not the production pipeline. (TestControllerBridge doesn't close the gap either: a fixture merged into a read response still cannot be bought.)
Opting out is not viable when the goal is full coverage: UNKNOWN_SCENARIO on a required seed grades the whole storyboard not_applicable, the media_buy_seller baseline itself declares controller_seeding: true, and Stateful compliance requires seed_product + seed_pricing_option for sales-* specialisms.
Proposal: requirement-based fixture satisfaction
Keep the storyboard fixtures: blocks exactly as they are, but define them as requirements ("a non-guaranteed display product with a CPM/USD pricing option must be available") rather than rows to inject. The runner satisfies each requirement through an ordered strategy ladder and binds the fixture's ID as a session-scoped handle to whatever entity satisfied it — using the substitution machinery that already exists (context_outputs / $context.*). Storyboard YAML does not change.
| Strategy |
Applies to |
Mechanism |
| 1. Seed (today's behavior) |
Sellers advertising the matching seed_* scenario |
Push as today; handle = seeded ID verbatim. Zero change for existing implementers. |
| 2. Discover |
Catalog entities: products, pricing options, creative formats |
Enumerate the normal read path (get_products under the sandbox account, list_creative_formats), select the first entity satisfying the requirement fields in spec-defined order, bind the handle to its real IDs |
| 3. Construct |
Lifecycle entities: creatives, media buys |
Drive the real buyer flow (sync_creatives; create_media_buy against an already-bound product), then reach the required state via force_*; bind the handle to the seller-minted ID |
| 4. Coverage gap |
Anything unsatisfied |
New skip reason (e.g. fixture_unsatisfied) graded not_applicable — same honest-coverage philosophy as UNKNOWN_SCENARIO today |
For a platform seller the fixture surface becomes the platform itself: we pre-provision deterministic test inventory in the sandbox tenant through our own provisioning path, and the runner finds it the way any buyer would. The only controller surface still needed is the one that genuinely requires a control channel — force_* / simulate_* — which we are implementing regardless (force_media_buy_status, simulate_delivery, simulate_budget_spend, force_create_media_buy_arm / force_task_completion).
Determinism. Requirement fields are exactly the fields fixtures carry today; selection order is spec-defined; within a session a handle stays pinned to the same entity. Storyboards that need exact seeded values (not shapes) keep strategy 1 as their only path and otherwise grade not_applicable — accurate coverage reporting, same philosophy as requires_capability gates.
Anti-gaming gets stronger. Under strategies 2–3 the graded flows exercise the seller's production catalog and booking pipeline end-to-end, and there are no magic ID literals a seller could special-case, because the seller never sees them.
This is not new machinery. The suite and SDK already use every ingredient: pagination_integrity_list_accounts bootstraps via sync_accounts; the content-standards / collection-lists / property-lists pagination storyboards bootstrap via real create_* calls; sales-non-guaranteed already binds create_media_buy to a discovered $context.first_product_id; and the @adcp/sdk conformance fuzzer's auto-seed tier discovers seller IDs instead of pushing them ("Seeded IDs are agent-generated and differ per run"). Meanwhile media buys and creative formats are seeded by ID — entities that could bootstrap exactly like accounts and content standards do today.
Alternative mechanics we'd also accept
- Seller-resolved seed IDs (smaller runner change): keep the seed push, but let the seed response carry the seller's real ID and have the runner bind to it (
SeedSuccess variants are additionalProperties: true, so this is additive). Works for us too, but pushes per-entity matching logic into every platform seller instead of once into the runner.
- Statically declared fixtures in
get_adcp_capabilities / adagents.json: simple, but covers only catalog entities and risks drift against per-version fixture shapes.
What we are NOT asking to change
force_* / simulate_* / query_* — unchanged; strategy 3 depends on them, and we are implementing them.
seed_* — unchanged; it remains strategy 1 for sellers with a fixture store.
- Sandbox gating, the production ban,
comply-controller-mode-gate, observational-mode grading — unchanged.
- Storyboard YAML — unchanged; fixture IDs are reinterpreted as session-scoped handles.
Affected areas
- Storyboard runner — discover/construct strategies and handle→entity binding (reuses existing
$context machinery); fixture_unsatisfied skip reason in the runner output contract
- Docs — L3 seeding semantics; a platform-seller row in the test-surfaces table; Stateful-compliance wording ("satisfiable fixtures + full
force_*/simulate_*" rather than seed_* specifically)
- No wire-schema change is required for the lead proposal
Related: #2584 (seed scenarios spec), #2743 (fixtures blocks), #4226 (UNKNOWN_SCENARIO grading), #4379 ((Sandbox) framing), #5168 (open scenario strings).
Offer
We are building this seller now and are happy to prototype against a spec draft and contribute — in particular the discovery/selection logic, which the SDK's conformance seeder already implements in miniature.
Problem
Every
seed_*scenario ofcomply_test_controllershares one contract: the runner picks the entity ID, and the seller must store it verbatim, surface it through its read tools, and accept it back in buyer calls — most controller storyboards reference the seeded literal directly (e.g.governance-delivery-monitorseedsoutdoor_display_q2, then sendspackages[].product_id: "outdoor_display_q2"tocreate_media_buy).SeedSuccessis message-only, so the seller has no channel to answer with its own ID.We operate a multi-tenant ad-management platform (goTom) with an AdCP sales agent on top, and for sellers like us this contract is unimplementable — for every seeded entity type, not just products:
create_media_buydecodes the product ID and resolves its components against real entities before booking. A runner-chosen string is not "a product we haven't created yet" — it is unparseable. The same holds forpricing_option_id, format IDs,media_buy_id,account_id.The only workaround is an alias table plus interception of every read and write path to translate runner IDs into real ones — a parallel, test-only code path. That is exactly the teach-to-test trap the docs warn against, and it means the graded surface is the alias layer, not the production pipeline. (
TestControllerBridgedoesn't close the gap either: a fixture merged into a read response still cannot be bought.)Opting out is not viable when the goal is full coverage:
UNKNOWN_SCENARIOon a required seed grades the whole storyboardnot_applicable, themedia_buy_sellerbaseline itself declarescontroller_seeding: true, and Stateful compliance requiresseed_product+seed_pricing_optionforsales-*specialisms.Proposal: requirement-based fixture satisfaction
Keep the storyboard
fixtures:blocks exactly as they are, but define them as requirements ("a non-guaranteed display product with a CPM/USD pricing option must be available") rather than rows to inject. The runner satisfies each requirement through an ordered strategy ladder and binds the fixture's ID as a session-scoped handle to whatever entity satisfied it — using the substitution machinery that already exists (context_outputs/$context.*). Storyboard YAML does not change.seed_*scenarioget_productsunder the sandbox account,list_creative_formats), select the first entity satisfying the requirement fields in spec-defined order, bind the handle to its real IDssync_creatives;create_media_buyagainst an already-bound product), then reach the required state viaforce_*; bind the handle to the seller-minted IDfixture_unsatisfied) gradednot_applicable— same honest-coverage philosophy asUNKNOWN_SCENARIOtodayFor a platform seller the fixture surface becomes the platform itself: we pre-provision deterministic test inventory in the sandbox tenant through our own provisioning path, and the runner finds it the way any buyer would. The only controller surface still needed is the one that genuinely requires a control channel —
force_*/simulate_*— which we are implementing regardless (force_media_buy_status,simulate_delivery,simulate_budget_spend,force_create_media_buy_arm/force_task_completion).Determinism. Requirement fields are exactly the fields fixtures carry today; selection order is spec-defined; within a session a handle stays pinned to the same entity. Storyboards that need exact seeded values (not shapes) keep strategy 1 as their only path and otherwise grade
not_applicable— accurate coverage reporting, same philosophy asrequires_capabilitygates.Anti-gaming gets stronger. Under strategies 2–3 the graded flows exercise the seller's production catalog and booking pipeline end-to-end, and there are no magic ID literals a seller could special-case, because the seller never sees them.
This is not new machinery. The suite and SDK already use every ingredient:
pagination_integrity_list_accountsbootstraps viasync_accounts; the content-standards / collection-lists / property-lists pagination storyboards bootstrap via realcreate_*calls;sales-non-guaranteedalready bindscreate_media_buyto a discovered$context.first_product_id; and the@adcp/sdkconformance fuzzer's auto-seed tier discovers seller IDs instead of pushing them ("Seeded IDs are agent-generated and differ per run"). Meanwhile media buys and creative formats are seeded by ID — entities that could bootstrap exactly like accounts and content standards do today.Alternative mechanics we'd also accept
SeedSuccessvariants areadditionalProperties: true, so this is additive). Works for us too, but pushes per-entity matching logic into every platform seller instead of once into the runner.get_adcp_capabilities/adagents.json: simple, but covers only catalog entities and risks drift against per-version fixture shapes.What we are NOT asking to change
force_*/simulate_*/query_*— unchanged; strategy 3 depends on them, and we are implementing them.seed_*— unchanged; it remains strategy 1 for sellers with a fixture store.comply-controller-mode-gate, observational-mode grading — unchanged.Affected areas
$contextmachinery);fixture_unsatisfiedskip reason in the runner output contractforce_*/simulate_*" rather thanseed_*specifically)Related: #2584 (seed scenarios spec), #2743 (fixtures blocks), #4226 (
UNKNOWN_SCENARIOgrading), #4379 ((Sandbox) framing), #5168 (open scenario strings).Offer
We are building this seller now and are happy to prototype against a spec draft and contribute — in particular the discovery/selection logic, which the SDK's conformance seeder already implements in miniature.