npx @adcp/sdk@latest storyboard run.”
The three surfaces the runner needs
The runner drives your agent through the same public tools a buyer would call, plus one sandbox-only tool for fixture setup. Three surfaces must be in place:
You ship these three surfaces. The runner owns storyboard selection, fixture ordering, and response comparison.
Step 1 — Declare capabilities honestly
get_adcp_capabilities is how the runner picks which storyboards apply to you. It is also the conformance contract: you are promising to pass every storyboard that matches what you declare.
The example below is for a full-service guaranteed seller (proposal lifecycle enabled). A direct-buy guaranteed seller would set media_buy.supports_proposals: false (or omit it). A broadcast-TV seller would claim sales-broadcast-tv; a creative-only agent would claim the creative protocol with creative-ad-server or creative-generative specialisms; a signals provider would claim signals. The pattern is the same: declare only what you actually implement.
supported_protocols— Pulls in the matching protocol storyboards from/compliance/{version}/protocols/.specialisms— Pulls in opt-in specialism storyboards (see the Compliance Catalog for the full enumeration).account.sandbox: true— Signals that you honor sandbox semantics (no real spend, no production side effects).account.require_operator_auth— Determines your sandbox bootstrap path (step 2).
Step 2 — Pick your sandbox bootstrap path
The runner must obtain a sandbox account before it can do anything. Yourrequire_operator_auth flag chooses the path:
Buyer-declared accounts (require_operator_auth: false). Your agent accepts sync_accounts from any authenticated buyer. The runner calls sync_accounts with sandbox: true to mint a test account on demand. Most new sales agents start here.
Account-id namespaces (require_operator_auth: true). Accounts must be pre-provisioned by a human on your side or by an upstream platform. The runner calls list_accounts with a sandbox filter to discover pre-existing test accounts. If your credential is bound to exactly one sandbox account, return that singleton. Publish a short note telling operators how to request one — include the contact, the expected turnaround, and what credentials they’ll receive.
Full details and examples: Sandbox mode.
Step 3 — Implement the compliance test controller
Without a compliance test controller, the runner tests only buyer-initiated flows (observational mode) — schema conformance, auth rejection, happy-path buyer calls. That is enough for early integration work and production-path sandbox smoke tests, but it produces partial coverage whenever controller-seeded or controller-forced scenarios are in scope. Conformance treats deterministic mode — full lifecycle walks enabled by the controller — as the bar for complete specialism coverage. In practice, sellers usually run both:
The first run answers “does the production buyer-visible path tolerate sandbox traffic?” The second answers “can the runner prove every lifecycle path for the declared specialisms?” Do not collapse those into one signal. A zero-failure partial run is useful, but buyers need the skip list to know exactly which lifecycle assertions were not graded.
comply_test_controller is a single sandbox-only tool with a scenario parameter covering three families:
See the Compliance test controller reference for scenario-by-scenario parameters and response shapes, and the Compliance Catalog for which scenarios each specialism requires.
Wiring the SDK scaffold
@adcp/sdk (6.x is the production GA on AdCP 3.0) ships createComplyController so you wire your data layer to the controller without reimplementing tool registration, param validation, error envelopes, or re-seed idempotency.
The scaffold is TypeScript/JavaScript. Python, Go, and Java sellers implement the tool directly against the schema — the contract below (adapters, error codes, idempotency semantics) applies the same way. SDKs for other languages are tracked in Choose your SDK.
- Tool registration and schema.
controller.toolDefinitionstays in sync with the published spec version. - Dispatch and
UNKNOWN_SCENARIO. Scenarios you do not register returnUNKNOWN_SCENARIOautomatically — never a schema error. - Param validation. Invalid params produce
INVALID_PARAMSwith a readableerror_detailwithout reaching your adapter. - Seed idempotency. Calling
seed_producttwice with the sameproduct_idand an equivalentfixturereturnsprevious_state: "existing"; a divergentfixturereturnsINVALID_PARAMS. Your adapter is only invoked on the first seed. - Typed error envelopes. Throw
TestControllerError(code, message, currentState?)withcodefrom the controller error-code table. Common adapter codes are'INVALID_TRANSITION' | 'NOT_FOUND' | 'FORBIDDEN' | 'INVALID_PARAMS'; digest-modequery_upstream_trafficimplementations may also returnJCS_NON_FINITE_NUMBERwhen RFC 8785/JCS canonicalization encounters a non-finite numeric value.
Two layers of sandbox gating
The scaffold supports two gates. Ship both in any deployment that serves both sandbox and production traffic from the same process:- Registration gate (primary). Wrap
controller.register(server)in an environment check. This is what keepscomply_test_controllerout of productiontools/listentirely. Without it, a leaked sandbox credential on a production endpoint exposes seller-side state-forcing. - Per-request gate (defense-in-depth). Pass a
sandboxGate: (input) => booleantocreateComplyController. The scaffold calls it on every request and returnsFORBIDDENwhen it returnsfalse. Use this on shared-process deployments where the tool IS registered but some requests might still reference a production account.
sandboxGate receives the raw tool input (Record<string, unknown>). The SDK does not plumb auth context onto it — you decide what to inspect. A typical pattern is to pull the referenced entity ID out of params and verify it belongs to a sandbox account in your own data layer:
For custom MCP wrappers — AsyncLocalStorage for per-request auth, transport-level sandbox gating, session-backed stores — compose the lower-level
handleTestControllerRequest, toMcpResponse, and TOOL_INPUT_SHAPE from @adcp/sdk/server directly.Step 4 — Run the storyboard runner
Once the three surfaces are in place, the runner takes over:Avoiding the teach-to-test trap
Storyboards hardcode fixture IDs —"test-product", "campaign_hero_video", "acmeoutdoor.example". A controller that special-cases those strings passes the suite while silently failing on every real buyer. That is the exact industry cost conformance is trying to prevent: every post-conformance integration failure burns seller reputation, inflates buyer agent skepticism, and slows protocol adoption.
The SDK scaffold already points you in the right direction: adapters receive product_id, creative_id, etc. as values, not as conditions. If your adapter contains a switch on product_id === "test-product", you have regressed.
Two rules of thumb:
- Implement seed scenarios generically.
seed_productaccepts anyproduct_idand persists a product with that ID in your sandbox data layer. Your adapter is a thin wrapper over a real upsert against your sandbox store. - The
fixtureobject is the contract, the ID is not. Storyboard authors setfixtureto the minimum shape the test needs. Everything beyond that — discovery, filtering, authorization — is your normal code path, exercised on fixture-seeded data the same way it runs on production data.
Readiness checklist
Before your first full storyboard sweep:-
get_adcp_capabilitiesreturns only protocols and specialisms you actually implement -
account.sandbox: trueis declared and honored — sandbox requests produce no real spend, no production platform calls, no persisted production state -
sync_accounts(implicit) orlist_accounts(explicit) handles sandbox requests per step 2 -
comply_test_controlleris absent fromtools/liston any production endpoint - Requests that reference a non-sandbox account are rejected with
FORBIDDEN - Every seed scenario your claimed storyboards depend on persists fixtures generically, with no ID special-cases
- Every force scenario uses the same state-transition rules as production, returning typed errors on invalid transitions
- A full storyboard sweep still passes when fixture IDs are swapped for random UUIDs
What’s next
- Validate Your Agent — CLI, Addie workflows, and multi-instance verification
- Compliance test controller reference — Full scenario-by-scenario spec
- Sandbox mode — The two account model paths in depth
- Conformance — What “conformant” and “verified” mean once your runs pass