git:20260503.79c2835 to git:20260503.91c1ff8

37 added, 545 removed. Audit A to B.

---
name: build-signals-agent
- description: Use when building an AdCP signals agent, creating an audience data server, or standing up a data provider agent that serves targeting segments to buyers.
+ description: Use when building an AdCP signals agent — a marketplace data provider, identity provider, CDP, or any system that serves audience or contextual signals to buyers.
---
# Build a Signals Agent
- ## Overview
-
- A signals agent serves audience segments to buyers for campaign targeting. Two tools: `get_signals` (discovery) and `activate_signal` (push to DSPs or sales agents). The business model — marketplace vs owned data — shapes every implementation decision. Determine that first.
-
- > **Common shape gotchas:** `signal_ids[]` in `get_signals` is `signal_id[]` (provenance objects with `source`/`data_provider_domain`/`id`), not bare strings. `activate_signal` returns deployments with `ActivationKey` `oneOf` — for `type: 'key_value'` the `key` and `value` fields sit at the TOP level of `activation_key`, NOT nested under a `key_value` sub-field. See [SHAPE-GOTCHAS.md](../SHAPE-GOTCHAS.md) for the patterns adopters consistently get wrong on first pass.
-
- ## When to Use
-
- - User wants to build an agent that serves audience/targeting data
- - User mentions signals, segments, audiences, data provider, or CDP in the context of AdCP
- - User references `get_signals`, `activate_signal`, or the signals protocol
-
- **Not this skill:**
-
- - Selling ad inventory (products, packages, media buys) → `skills/build-seller-agent/`
- - Rendering creatives from briefs → that's a creative agent
- - Building a client that _calls_ a signals agent → see `docs/getting-started.md`
-
- **Often claimed alongside:** [`sales-social`](../build-seller-agent/SKILL.md) (walled-garden audience push), [`sales-non-guaranteed`](../build-seller-agent/SKILL.md) (DSP data marketplace as a side surface), [`audience-sync`](../build-seller-agent/SKILL.md) (identity-provider owned-graph activation). See [Common multi-specialism bundles](../../examples/README.md#common-multi-specialism-bundles).
-
- ## Specialisms This Skill Covers
-
- | Specialism | Status | Delta | See |
- | -------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
- | `signal-marketplace` | stable | `signal_id.source: 'catalog'` + resolvable `data_provider_domain`; span ≥2 providers in demos; platform activations are **async** (`is_live: false` → poll to live) | [§ signal-marketplace](#specialism-signal-marketplace) |
- | `signal-owned` | stable | `signal_id.source: 'agent'` + your `agent_url`; `value_type` constraints (`allowed_values` for categorical, `min`/`max` for numeric); `deployed_at` on deployments | [§ signal-owned](#specialism-signal-owned) |
-
- ## Protocol-Wide Requirements
-
- Full treatment lives in `skills/build-seller-agent/SKILL.md` §Protocol-Wide Requirements and §Composing. Minimum viable pointers for a signals agent:
-
- - **`idempotency_key`** on every mutating request (`activate_signal`, and any future mutating signals tools). Pass `createIdempotencyStore` to `createAdcpServerFromPlatform(platform, { idempotency })`.
- - **Authentication** via `serve({ authenticate })` with `verifyApiKey`/`verifyBearer` from `@adcp/sdk/server`. Unauthenticated agents fail the universal `security_baseline` storyboard.
- - **Signature-header transparency**: accept requests with `Signature-Input`/`Signature` headers even if you don't claim `signed-requests`.
-
- ## Before Writing Code
-
- Determine these four things. Ask the user — don't guess.
-
- ### 1. Marketplace or Owned?
-
- These are fundamentally different businesses.
-
- **Marketplace** — aggregates third-party data providers (LiveRamp, Oracle Data Cloud, Lotame). Each signal traces to a `data_provider_domain` that buyers can verify via `adagents.json`. `signal_type: "marketplace"`, `signal_id.source: "catalog"`.
-
- **Owned** — first-party data (retailer CDP, publisher contextual, CRM). Buyers trust your agent directly. `signal_type: "owned"` or `"custom"`, `signal_id.source: "agent"`.
-
- ### 2. What Segments?
-
- Get specifics: names, definitions, what each represents. Push for 3-5 segments with variety. Each needs:
-
- - Clear behavioral/demographic definition
- - Realistic `coverage_percentage` (typically 5-30%)
- - Value type: `binary` (in/out), `categorical` (tier levels — define the categories), or `numeric` (score range — define min/max)
-
- ### 3. Pricing
-
- At least one pricing option per signal. Signals use `VendorPricingOption` (field: `model`), distinct from product `PricingOption` (field: `pricing_model`).
-
- - `cpm` — `{ pricing_option_id: "po_cpm", model: "cpm", cpm: 2.50, currency: "USD" }`
- - `percent_of_media` — `{ pricing_option_id: "po_pom", model: "percent_of_media", percent: 15, currency: "USD" }`
- - `flat_fee` — `{ pricing_option_id: "po_flat", model: "flat_fee", amount: 5000, period: "monthly", currency: "USD" }`
-
- ### 4. Activation Destinations
-
- If implementing `activate_signal`:
-
- - **Platform** (DSP): `type: "platform"`, returns `activation_key: { type: "segment_id", segment_id: "..." }`
- - **Agent** (sales agent): `type: "agent"`, returns `activation_key: { type: "key_value", key: "...", value: "..." }`
-
- ## Tools and Required Response Shapes
-
- > **Before writing any handler's return statement, fetch [`docs/llms.txt`](../../docs/llms.txt) and grep for `#### \`<tool_name>\``(e.g.`#### \`get_signals\``) to read the exact required + optional field list.** The schema-derived contract lives there; this skill covers patterns, gotchas, and domain-specific examples. Strict response validation is on by default in dev — it will tell you the exact field path if you drift, so write the obvious thing and trust the contract.
- >
- > **Cross-cutting pitfalls matrix runs keep catching:**
- >
- > - **Declare `capabilities.specialisms: ['signal-marketplace'] as const` (or `'signal-owned'`) on the `DecisioningPlatform` you pass to `createAdcpServerFromPlatform`.** Value is `string[]` of enum ids (not `[{id, version}]`). Agents that don't declare their specialism fail the grader with "No applicable tracks found" even if every tool works — tracks are gated on the specialism claim.
-
- **`get_adcp_capabilities`** — auto-generated by `createAdcpServerFromPlatform` from your typed `DecisioningPlatform`. Do not register manually.
-
- **`get_signals`** — handled by `signals.getSignals`
-
- Two discovery modes — support both:
-
- 1. `signal_spec` — natural language. Match against segment names and descriptions.
- 2. `signal_ids` — exact lookup by `{ source, data_provider_domain, id }` or `{ source, agent_url, id }`.
-
- Plus filtering via `filters.catalog_types`, `filters.max_cpm`, `filters.min_coverage_percentage`, and `max_results`.
-
- ```
- getSignalsResponse({
- signals: [{
- signal_agent_segment_id: string, // required - key for activate_signal
- name: string, // required
- description: string, // required
- signal_type: 'marketplace' | 'owned' | 'custom', // required
- data_provider: string, // required - your company name
- coverage_percentage: number, // required - 0 to 100
- deployments: [], // required - empty array (not live until activated)
- pricing_options: [{ // required - at least one
- pricing_option_id: string, // required
- model: 'cpm', // required - discriminator
- cpm: number, // required for cpm model
- currency: 'USD', // required
- }],
- // signal_id is critical — shape depends on marketplace vs owned:
- signal_id: {
- source: 'catalog', // marketplace
- data_provider_domain: string, // marketplace — domain for provenance verification
- id: string, // unique segment ID
- },
- // OR for owned:
- signal_id: {
- source: 'agent', // owned
- agent_url: string, // your agent URL
- id: string,
- },
- value_type: 'binary' | 'categorical' | 'numeric', // optional but recommended
- }],
- sandbox: true, // for mock data
- })
- ```
-
- **`activate_signal`** — handled by `signals.activateSignal`
-
- Look up by `signal_agent_segment_id`. Validate `pricing_option_id`. Return deployments matching the requested destinations. **Platform activation is async; agent activation is sync** — different shape per destination type, driven by the compliance contract:
-
- ```
- activateSignalResponse({
- deployments: [
- // Platform (DSP) — ASYNC. First response returns is_live:false plus
- // an ETA AND the planned activation_key. Buyer re-sends activate_signal
- // to poll until is_live:true; final response adds deployed_at.
- {
- type: 'platform',
- platform: string,
- account: string | null, // echo from request
- is_live: false, // flips true on completion
- estimated_activation_duration_minutes: number, // present while activating
- activation_key: { type: 'segment_id', segment_id: string }, // committed up front
- deployed_at: string, // ISO; present when is_live:true
- },
- // Agent (sales-agent) — SYNC. First response is the final response.
- {
- type: 'agent',
- agent_url: string,
- is_live: true,
- activation_key: { type: 'key_value', key: string, value: string },
- deployed_at: string, // ISO timestamp
- },
- ],
- sandbox: true,
- })
- ```
-
- ### Context and Ext Passthrough
-
- The framework auto-echoes the request's `context` into every response — **do not set `context` yourself in your handler return values.** It's injected post-handler only when the field isn't already present.
-
- **Crucial:** `context` is schema-typed as an object. If your handler hand-sets a string or narrative description, validation fails with `/context: must be object` and the framework does not overwrite. Leave the field out entirely; the framework handles it.
-
- Some schemas also define an `ext` field for vendor-namespaced extensions. If your request schema includes `ext`, accept it without error. Tools with explicit `ext` support: `activate_signal`.
-
- ## SDK Quick Reference
-
- | SDK piece | Usage |
- | ----------------------------------------------------- | ------------------------------------------------------------------------------ |
- | `createAdcpServerFromPlatform(platform, opts)` | Create server from a typed `DecisioningPlatform` — compile-time specialism enforcement, auto-capabilities |
- | `createAdcpServer(config)` *(legacy)* | v5 handler-bag entry. Mid-migration / escape-hatch only; reach via `@adcp/sdk/server/legacy/v5` |
- | `serve(() => createAdcpServerFromPlatform(platform, opts))` | Start HTTP server on `:3001/mcp` |
- | `signals: { getSignals, activateSignal }` | Domain group — register handlers by name |
- | `ctx.store.put(collection, id, data)` | Persist state (activations, segment cache) across requests |
- | `ctx.store.get(collection, id)` | Retrieve persisted state |
- | `getSignalsResponse(data)` | Auto-applied response builder (don't call manually) |
- | `activateSignalResponse(data)` | Auto-applied response builder (don't call manually) |
- | `adcpError(code, { message })` | Structured error (`SIGNAL_NOT_FOUND`, `INVALID_DESTINATION`) |
- | `createIdempotencyStore({ backend, ttlSeconds })` | Required on every mutating tool — pass via `createAdcpServerFromPlatform(platform, { idempotency })` |
- | `memoryBackend()` / `pgBackend(pool)` | Idempotency backends (from `@adcp/sdk/server`) |
- | `type Signal = GetSignalsResponse['signals'][number]` | Type for a single signal object |
-
- Import: `import { createAdcpServerFromPlatform, serve, adcpError } from '@adcp/sdk/server';`
- Server-only: `import { createIdempotencyStore, memoryBackend } from '@adcp/sdk/server';`
- Types: `import type { GetSignalsResponse } from '@adcp/sdk';`
-
- ## Setup
-
- ```bash
- npm init -y
- npm install @adcp/sdk
- npm install -D typescript @types/node
- ```
-
- Minimal `tsconfig.json`:
-
- ```json
- {
- "compilerOptions": {
- "target": "ES2022",
- "module": "Node16",
- "moduleResolution": "Node16",
- "strict": true,
- "skipLibCheck": true,
- "outDir": "dist"
- }
- }
- ```
-
- `skipLibCheck: true` avoids false-positive errors from transitive `.d.ts` files (e.g., `@opentelemetry/api`).
-
- ## Implementation
-
- 1. Single `.ts` file — all tools in one file
- 2. Use `createAdcpServerFromPlatform` with `signals: SignalsPlatform` on a typed `DecisioningPlatform` class — `get_adcp_capabilities` is auto-generated
- 3. Handlers return raw data objects — response builders (`getSignalsResponse`, `activateSignalResponse`) are auto-applied
- 4. Use `ctx.store` for persisting signal activations across requests (InMemoryStateStore by default)
- 5. Set `sandbox: true` for mock/demo data
- 6. Context passthrough is handled by the framework — no need to manually echo `args.context`
-
- ```typescript
- import {
- createAdcpServerFromPlatform,
- serve,
- AdcpError,
- createIdempotencyStore,
- memoryBackend,
- type DecisioningPlatform,
- type SignalsPlatform,
- type AccountStore,
- } from '@adcp/sdk/server';
- import type { GetSignalsResponse } from '@adcp/sdk';
-
- // Type the catalog explicitly. Each entry MUST include `signal_agent_segment_id`
- // — it's the key buyers pass to `activate_signal` and the field the framework
- // uses to auto-store the signal for hydration on the activate call. If you
- // omit it, `tsc` flags every entry; the response validator rejects the
- // `get_signals` reply at runtime in dev/test (strict mode default).
- type Signal = GetSignalsResponse['signals'][number];
- const signals: Signal[] = [
- /* your signal objects — `signal_agent_segment_id` is required on each */
- ];
-
- // Publisher-internal storage for activations. Persist to your real DB in
- // production; this Map is for the worked example. Note: a module-level Map
- // is fine for single-tenant agents but leaks across tenants in a
- // multi-tenant deployment — partition by `account.id` if you front many.
- const activations = new Map<string, { destinations: unknown[]; activated_at: string }>();
-
- // Idempotency — required for v3 compliance. `activate_signal` is mutating;
- // `get_signals` is read-only and exempt from key validation.
- const idempotency = createIdempotencyStore({
- backend: memoryBackend(), // pgBackend(pool) for production
- ttlSeconds: 86400, // 24 hours (spec bounds: 1h–7d)
- });
-
- class MySignals implements DecisioningPlatform {
- capabilities = {
- specialisms: ['signal-marketplace'] as const,
- config: {},
- };
-
- // Single-tenant agent: AccountStore.resolve returns the same synthetic
- // account every request. `upsert` and `list` are optional — omit them on
- // stateless platforms (the framework returns `UNSUPPORTED_FEATURE` to
- // buyers calling `sync_accounts` / `list_accounts`).
- accounts: AccountStore = {
- resolve: async () => ({
- id: 'sg_acc_1',
- operator: 'me',
- ctx_metadata: {},
- }),
- };
-
- signals: SignalsPlatform = {
- getSignals: async params => {
- let results = signals;
- if (params.signal_spec) {
- const query = params.signal_spec.toLowerCase();
- results = results.filter(
- s => s.name.toLowerCase().includes(query) || s.description.toLowerCase().includes(query)
- );
- }
- if (params.signal_ids) {
- results = results.filter(s => params.signal_ids!.some(id => id.id === s.signal_id.id));
- }
- return { signals: results, sandbox: true };
- },
-
- activateSignal: async params => {
- const signal = signals.find(s => s.signal_agent_segment_id === params.signal_agent_segment_id);
- // Throw `AdcpError` for buyer-fixable rejection. The framework
- // catches the throw, projects to the spec's error envelope, and
- // returns it to the buyer. Returning `adcpError(...)` from the
- // success arm doesn't typecheck — `activateSignal` is declared
- // `Promise<ActivateSignalSuccess>`, error is the throw path.
- if (!signal) {
- throw new AdcpError('SIGNAL_NOT_FOUND', {
- recovery: 'correctable',
- message: `Unknown segment: ${params.signal_agent_segment_id}`,
- });
- }
+ A signals agent serves audience and contextual targeting segments to buyer agents. The fastest path to a passing agent is to **fork the worked adapter** and replace its `// SWAP:` markers with calls to your backend.
- // Persist activation. Single-tenant: module-level Map is fine.
- // Multi-tenant: partition by ctx.account.id (passed as 2nd arg).
- activations.set(params.signal_agent_segment_id, {
- destinations: params.destinations,
- activated_at: new Date().toISOString(),
- });
+ ## Pick your fork target
- // Platform (DSP) activation is ASYNC per spec — return `is_live: false`
- // with `estimated_activation_duration_minutes`. The buyer polls
- // (a subsequent `activate_signal` with the same destinations, or a
- // provider-specific status tool) until `is_live: true`.
- //
- // Agent (sales-agent) activation is SYNC — return `is_live: true` with
- // `activation_key.type: 'key_value'` and a `deployed_at` timestamp.
- //
- // Both shapes include `activation_key` so the buyer knows how to
- // reference the segment when building media buys through the DSP or SA.
- const deployments = params.destinations.map(dest => {
- if (dest.type === 'platform') {
- return {
- type: 'platform' as const,
- platform: dest.platform,
- is_live: false,
- estimated_activation_duration_minutes: 30,
- // Return activation_key even while is_live is false — the
- // buyer needs to know the planned segment_id now so it can
- // reference it in downstream media buys. `is_live` just
- // flags whether the DSP has confirmed provisioning;
- // `activation_key` is the agent's commitment.
- activation_key: {
- type: 'segment_id' as const,
- segment_id: `${dest.platform}_${signal.signal_id.id}`,
- },
- };
- }
- return {
- type: 'agent' as const,
- agent_url: dest.agent_url,
- is_live: true,
- activation_key: { type: 'key_value' as const, key: 'audience', value: signal.signal_id.id },
- deployed_at: new Date().toISOString(),
- };
- });
- return { deployments, sandbox: true };
- },
- };
- }
+ | Specialism | Archetype | Fork this | Mock upstream | Storyboard |
+ | --- | --- | --- | --- | --- |
+ | `signal-marketplace` | Multi-provider data marketplace (Oracle Data Cloud, LiveRamp, third-party data) | [`hello_signals_adapter_marketplace.ts`](../../examples/hello_signals_adapter_marketplace.ts) | `npx adcp mock-server signal-marketplace` | `signal_marketplace` |
+ | `signal-owned` | First-party / single-provider data (CDP, identity provider, contextual) | Fork the marketplace adapter; collapse the multi-provider seed | — | `signal_owned` |
- const platform = new MySignals();
+ Both specialisms share the same tool surface (`get_signals`, `activate_signal`, `list_accounts`); the difference is whether you serve segments from multiple `data_provider_domain` values or one. A `signal-owned` adapter is the marketplace adapter with the multi-provider directory simplified to a single seed.
- serve(() =>
- createAdcpServerFromPlatform(platform, {
- name: 'My Signals Agent',
- version: '1.0.0',
- idempotency,
- // Principal scoping for idempotency. MUST never return undefined.
- resolveSessionKey: () => 'default-principal',
- })
- );
- ```
+ For exact response shapes, error codes, and optional fields, `docs/llms.txt` is the canonical reference. The fork target stays in sync with the spec because PR #1394's three-gate contract fails CI when it drifts.
- ## Idempotency
+ ## When to use this skill
- AdCP v3 requires an `idempotency_key` on every mutating request — for signals agents that's `activate_signal` only (`get_signals` is read-only and exempt). Idempotency is already wired in the Implementation example above. The framework then handles:
+ - User wants to serve audience segments, identity data, or contextual targeting to buyers
+ - User mentions `get_signals`, `activate_signal`, or the AdCP signals protocol
+ - User describes themselves as a CDP, DMP, identity provider, or data marketplace
- - Missing/malformed key → `INVALID_REQUEST` (spec pattern `^[A-Za-z0-9_.:-]{16,255}$`)
- - JCS-canonicalized payload hashing with same-key-different-payload → `IDEMPOTENCY_CONFLICT` (no payload leaked in the error body)
- - Past-TTL replay → `IDEMPOTENCY_EXPIRED` (±60s clock-skew tolerance)
- - Cache hits replay the cached envelope with `replayed: true` injected
- - `adcp.idempotency.replay_ttl_seconds` auto-declared on `get_adcp_capabilities`
- - Only successful responses cache — a failed activation re-executes on retry
- - Atomic claim so concurrent retries with a fresh key don't all race to activate
+ **Not this skill:**
- Scoping is per-principal via `resolveSessionKey` (override with `resolveIdempotencyPrincipal` for custom scoping). `ttlSeconds` must be 3600–604800 — out of range throws at construction. If you register mutating handlers without wiring `idempotency`, the framework logs an error at server-creation time.
+ - Selling ad inventory → `skills/build-seller-agent/`
+ - Audience push (sync to a walled garden) → that's the `audience-sync` track in `skills/build-seller-agent/`
- **Critical: probe the pool at boot (pgBackend).** `pg.Pool` is lazy — `new Pool({ connectionString })` does not validate the URL. A bad `DATABASE_URL` lets the server start, advertise `IdempotencySupported`, and then silently fail every `activate_signal` call. Wire `readinessCheck` on `serve()` so the server never accepts traffic with a broken pool:
+ ## Cross-cutting rules
- ```ts
- const store = createIdempotencyStore({ backend: pgBackend(pool), ttlSeconds: 86400 });
- pool.on('error', err => console.error('pg pool error', err)); // prevent crash on idle-client errors
- serve(createAgent, {
- readinessCheck: () => store.probe(), // throws with a descriptive error if pool/table is broken
- });
- ```
+ Every signals agent hits the cross-cutting rules in [`../cross-cutting.md`](../cross-cutting.md). Two signals-specific notes on top of those:
- ## Protecting your agent
+ ### Async platform activation
- **An AdCP agent that accepts unauthenticated requests is non-compliant** (see `security_baseline` in the universal storyboard bundle). Ask the operator: "API key, OAuth, or both?" — then wire one of these into `serve()`.
+ Platform activations (`type: 'platform'`) take minutes-to-hours to propagate to the DSP. Return `is_live: false` with `estimated_activation_duration_minutes` on first call; the buyer polls `activate_signal` again until `is_live: true`. **Commit `activation_key` up front** so the buyer can trust it across the poll window. Agent activations (`type: 'agent'`) are instant — return `is_live: true` immediately.
- ```typescript
- import { serve } from '@adcp/sdk';
- import { verifyApiKey, verifyBearer, anyOf } from '@adcp/sdk/server';
+ `forceDeploymentStatus` in your `TestControllerStore` flips pending deployments to live for deterministic compliance tests.
- // API key — simplest, good for B2B integrations
- serve(createAgent, {
- authenticate: verifyApiKey({
- verify: async token => {
- const row = await db.api_keys.findUnique({ where: { token } });
- return row ? { principal: row.account_id } : null;
- },
- }),
- });
+ ### Provenance — `data_provider_domain` must resolve
- // OAuth — best when buyers authenticate as themselves
- const AGENT_URL = 'https://my-agent.example.com/mcp';
- serve(createAgent, {
- publicUrl: AGENT_URL, // canonical RFC 8707 audience — also served as `resource` in protected-resource metadata
- authenticate: verifyBearer({
- jwksUri: 'https://auth.example.com/.well-known/jwks.json',
- issuer: 'https://auth.example.com',
- audience: AGENT_URL, // MUST equal publicUrl
- }),
- protectedResource: { authorization_servers: ['https://auth.example.com'] },
- });
+ Buyers fetch `https://{domain}/adagents.json` out-of-band to verify the provider. Use real domains even in demos, not `example.com`. For marketplace adopters, seed ≥2 different `data_provider_domain` values so the multi-provider nature is visible to the storyboard.
- // Both
- serve(createAgent, {
- publicUrl: AGENT_URL,
- authenticate: anyOf(verifyApiKey({ verify: lookupKey }), verifyBearer({ jwksUri, issuer, audience: AGENT_URL })),
- protectedResource: { authorization_servers: [issuer] },
- });
- ```
+ ## Specialism deltas at a glance
- The framework produces RFC 6750-compliant `WWW-Authenticate: Bearer` 401s on failure, and serves `/.well-known/oauth-protected-resource<mountPath>` with `publicUrl` as the `resource` field so buyers get tokens bound to the right audience. The default JWT allowlist is asymmetric-only (RS*/ES*/PS\*/EdDSA) to prevent algorithm-confusion attacks.
+ **`signal-marketplace`** — multi-provider directory (`signals[].data_provider_domain` varies), platform-activation polling pattern, marketplace governance sub-scenario in the storyboard exercises consent flows.
- ## Validate Locally
+ **`signal-owned`** — single `data_provider_domain` across all signals. `value_type` drives targeting semantics: `binary` (in/out), `categorical` (with `allowed_values: [...]`), `numeric` (with `min`, `max`, optional `units`). `signal_type: 'custom'` is for first-party signals outside the `owned` user-identity model (e.g. contextual signals from page content) — use `owned` by default.
- **Full validation checklist:** [docs/guides/VALIDATE-YOUR-AGENT.md](../../docs/guides/VALIDATE-YOUR-AGENT.md). Signals-specific commands:
+ ## Validate locally
```bash
- # Boot
- npx tsx agent.ts &
-
- # Happy path — the specialism you're claiming
- npx @adcp/sdk@latest storyboard run http://localhost:3001/mcp signal_owned --auth $TOKEN # owned data
- npx @adcp/sdk@latest storyboard run http://localhost:3001/mcp signal_marketplace --auth $TOKEN # marketplace
-
- # Marketplace governance sub-scenario (if you claim signal_marketplace)
- npx @adcp/sdk@latest storyboard run http://localhost:3001/mcp signal_marketplace/governance_denied --auth $TOKEN
-
- # Cross-cutting obligations
- npx @adcp/sdk@latest storyboard run http://localhost:3001/mcp \
- --storyboards security_baseline,idempotency,schema_validation,error_compliance --auth $TOKEN
-
- # Rejection-surface fuzz
- npx @adcp/sdk@latest fuzz http://localhost:3001/mcp --tools get_signals --auth-token $TOKEN
- ```
-
- Common failure decoder:
-
- - `value_type` mismatch → `binary` vs. `continuous` — pick one; continuous signals return `value`, binary signals return membership
- - Missing `deployments` on signal → required even if empty `[]`
- - `activate_signal` returns sync success on a marketplace signal → marketplace activations are async; commit to an `activation_key` up-front and return `submitted`
- - Missing `coverage_percentage` or `pricing_options` → required on every signal
-
- **Keep iterating until all steps pass.** Can't bind ports? `npm run compliance:skill-matrix -- --filter signals` runs an isolated end-to-end test.
-
- ## Common Mistakes
-
- | Mistake | Fix |
- | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
- | Using `createTaskCapableServer` + `server.tool()` | Use `createAdcpServerFromPlatform` with `signals: SignalsPlatform` on a typed `DecisioningPlatform` class |
- | Calling `createAdcpServer` directly in new code | Reach for `createAdcpServerFromPlatform`; `createAdcpServer` lives at `@adcp/sdk/server/legacy/v5` for mid-migration / escape-hatch only |
- | Using module-level Maps for state | Use `ctx.store.put/get` — framework provides `InMemoryStateStore` by default |
- | Manually registering `get_adcp_capabilities` | Auto-generated by `createAdcpServerFromPlatform` from your typed `DecisioningPlatform` |
- | Calling response builders manually | Handlers return raw data — `getSignalsResponse`/`activateSignalResponse` are auto-applied |
- | Missing `signal_agent_segment_id` on signals | Buyers can't activate without it |
- | Wrong `signal_id` shape | Marketplace: `{ source: "catalog", data_provider_domain, id }`. Owned: `{ source: "agent", agent_url, id }` |
- | Missing `data_provider` field | Required on every signal — your company/brand name |
- | Empty `pricing_options` array | Must have at least one pricing option per signal |
- | `is_live: true` in get_signals deployments | Signals aren't live until `activate_signal` — use empty `deployments: []` |
- | Activation doesn't match destination type | If request has `type: "platform"`, deployment must be `type: "platform"` |
- | `sandbox: false` on mock data | Buyers may treat mock data as real |
-
- ## Specialism Details
-
- ### <a name="specialism-signal-marketplace"></a>signal-marketplace
-
- **Async platform activation.** Unlike agent activations (`type: 'agent'`), platform activations (`type: 'platform'`) are not instant — the segment takes minutes-to-hours to propagate to the DSP. Return `is_live: false` with an estimate, then the buyer polls `activate_signal` again until `is_live: true`.
-
- ```typescript
- activateSignal: async (params, ctx) => {
- const signal = signals.find((s) => s.signal_agent_segment_id === params.signal_agent_segment_id);
- if (!signal) return adcpError('SIGNAL_NOT_FOUND', { message: `Unknown segment` });
-
- const deployments = await Promise.all(params.destinations.map(async (dest) => {
- if (dest.type === 'platform') {
- // Async — check whether this destination has already been propagated
- const existing = await ctx.store.get('deployments', `${params.signal_agent_segment_id}:${dest.platform}`);
- if (existing?.is_live) return existing;
- if (!existing) {
- const pending = {
- type: 'platform' as const,
- platform: dest.platform,
- account: dest.account ?? null,
- is_live: false,
- estimated_activation_duration_minutes: 45,
- // Commit the activation_key up front so the buyer can trust it across the poll window:
- activation_key: {
- type: 'segment_id' as const,
- segment_id: `${dest.platform}_${signal.signal_id.id}`,
- },
- };
- await ctx.store.put('deployments', `${params.signal_agent_segment_id}:${dest.platform}`, pending);
- return pending;
- }
- return existing; // still propagating
- }
- // Agent activations are instant
- return {
- type: 'agent' as const,
- agent_url: dest.agent_url,
- is_live: true,
- deployed_at: new Date().toISOString(),
- activation_key: { type: 'key_value' as const, key: 'audience', value: signal.signal_id.id },
- };
- }));
- return { deployments, sandbox: true };
- },
- ```
-
- Use `forceDeploymentStatus` in your `TestControllerStore` (if you implement compliance_testing) to flip pending deployments to live for deterministic tests.
-
- **Provenance.** `data_provider_domain` must be resolvable — buyers fetch `https://{domain}/adagents.json` out-of-band to verify the provider. Use real domains even in demos, not `example.com`. For a marketplace demo, seed ≥2 different `data_provider_domain` values so the multi-provider nature is visible.
-
- ### <a name="specialism-signal-owned"></a>signal-owned
-
- **Value types** drive targeting semantics. The storyboard validates these fields:
-
- ```typescript
- // Binary — in/out of the segment
- { value_type: 'binary' as const }
-
- // Categorical — enumerated tier/level
- {
- value_type: 'categorical' as const,
- allowed_values: ['bronze', 'silver', 'gold', 'platinum'], // required for categorical
- }
+ # Run the fork-matrix gate
+ npm run compliance:fork-matrix -- --test-name-pattern="hello-signals-adapter-marketplace"
- // Numeric — continuous score or count
- {
- value_type: 'numeric' as const,
- min: 0,
- max: 100,
- units: 'purchase_frequency_last_90d', // optional but recommended
- }
+ # Or validate your forked agent directly against its storyboard
+ adcp storyboard run http://127.0.0.1:3001/mcp signal_marketplace \
+ --bearer "$ADCP_AUTH_TOKEN" --include-bundles --json
```
- `signal_type: 'custom'` is for first-party signals that don't fit the `owned` conceptual model (e.g. contextual signals derived from page content rather than user identity). Use `owned` by default; pick `custom` only when the user data model differs materially.
-
- Platform activations are async (same pattern as marketplace). Agent activations include `deployed_at`:
+ The fork-matrix gate is the three-gate contract from [`docs/guides/EXAMPLE-TEST-CONTRACT.md`](../../docs/guides/EXAMPLE-TEST-CONTRACT.md): tsc strict / storyboard zero-failures / upstream façade.
- ```typescript
- {
- type: 'agent' as const,
- agent_url: dest.agent_url,
- is_live: true,
- deployed_at: new Date().toISOString(),
- activation_key: { type: 'key_value' as const, key: 'audience', value: signal.signal_id.id },
- }
- ```
+ For deeper validation: [`docs/guides/VALIDATE-YOUR-AGENT.md`](../../docs/guides/VALIDATE-YOUR-AGENT.md).
- ## Reference
+ ## Migration notes
- - `examples/signals-agent.ts` — complete runnable example
- - `storyboards/signal_marketplace.yaml` — buyer call sequences for marketplace agent
- - `storyboards/signal_owned.yaml` — call sequences for owned data agent
- - `docs/guides/BUILD-AN-AGENT.md` — SDK patterns
- - `docs/TYPE-SUMMARY.md` — curated type signatures
- - `docs/llms.txt` — full protocol reference
+ - 6.6 → 6.7: [`docs/migration-6.6-to-6.7.md`](../../docs/migration-6.6-to-6.7.md)
+ - 4.x → 5.x: [`docs/migration-4.x-to-5.x.md`](../../docs/migration-4.x-to-5.x.md)