{
  "markdown": "# @adcp/sdk\n\n[![npm version](https://badge.fury.io/js/@adcp%2Fsdk.svg)](https://badge.fury.io/js/@adcp%2Fsdk)\n[![npm downloads](https://img.shields.io/npm/dm/@adcp/sdk.svg)](https://www.npmjs.com/package/@adcp/sdk)\n[![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0)\n[![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg)](https://www.typescriptlang.org/)\n[![API Documentation](https://img.shields.io/badge/API-Documentation-blue.svg)](https://adcontextprotocol.github.io/adcp-client/api/)\n[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/adcontextprotocol/adcp-client/ci.yml?branch=main)](https://github.com/adcontextprotocol/adcp-client/actions)\n\nOfficial TypeScript/JavaScript client for the **Ad Context Protocol (AdCP)**. Build distributed advertising operations that work synchronously OR asynchronously with the same code.\n\n## For AI Agents\n\nStart with [`docs/llms.txt`](./docs/llms.txt) — the full protocol spec in one file (tools, types, error codes, examples). Building a server? See [`docs/guides/BUILD-AN-AGENT.md`](./docs/guides/BUILD-AN-AGENT.md). **Calling** an AdCP agent as a buyer? Load [`skills/call-adcp-agent/SKILL.md`](./skills/call-adcp-agent/SKILL.md) — wire contract, async flow, and error-recovery priors that aren't in the type signatures. Setting up request signing? See [`docs/guides/SIGNING-GUIDE.md`](./docs/guides/SIGNING-GUIDE.md). For type signatures, use [`docs/TYPE-SUMMARY.md`](./docs/TYPE-SUMMARY.md). Skip `src/lib/types/*.generated.ts` — they're machine-generated and will burn context.\n\nThese docs are also available in `node_modules/@adcp/sdk/docs/` after install.\n\n## The Core Concept\n\nAdCP operations are **distributed and asynchronous by default**. An agent might:\n\n- Complete your request **immediately** (synchronous)\n- Need time to process and **send results via webhook** (asynchronous)\n- Ask for **clarifications** before proceeding\n- Send periodic **status updates** as work progresses\n\n**Your code stays the same.** You write handlers once, and they work for both sync completions and webhook deliveries.\n\n## Installation\n\n```bash\nnpm install @adcp/sdk@adcp-3.0   # 7.x, AdCP 3.0\nnpm install @adcp/sdk             # 13.x, maintained AdCP 3.1 stable line\nnpm install '@adcp/sdk@^14.0.0-0' # newest 14.x prerelease, AdCP 3.2 beta\n```\n\nTrying the v14 prerelease? Read the [14.0.0 prerelease notes](./docs/releases/14.0.0-beta.0.md), then use the [13-to-14](./docs/migration-13-to-14.md) or [12-to-14](./docs/migration-12-to-14.md) migration guide. The npm `latest` tag remains on v13 for the maintained AdCP 3.1 stable line. SDK 14 requires Node.js `^20.19.0 || >=22.12.0`. Older paths: [12-to-13](./docs/migration-12-to-13.md), **[MIGRATION-v8.md](./MIGRATION-v8.md)**, and [8.0-to-8.1](./docs/migration-8.0-to-8.1.md).\n\n### Narrow type imports (`@adcp/sdk/types/<tool>`)\n\nAdopters who only need a single AdCP tool's types can import a per-tool slice instead of the full surface. Each slice is a self-contained `.d.ts` covering the tool's `Request` / `Response` / `Success` / `Error` / `Submitted` types and every type they reference:\n\n```ts\nimport type { SyncAccountsRequest } from '@adcp/sdk/types/sync-accounts';\n```\n\nWhy bother: the full `@adcp/sdk` type surface is ~45,000 lines and crashes `tsc` at Node's default 4 GB heap under `strict + skipLibCheck:false`. A single per-tool slice peaks at ~50 MB. That's the difference between adopters chasing the cryptic `FATAL: mark-compact` Node flag and just having their build pass.\n\nSlices use kebab-case filenames matching the schema cache (`sync_accounts` → `@adcp/sdk/types/sync-accounts`). Requires `moduleResolution: \"node16\"` / `\"nodenext\"` / `\"bundler\"` on the adopter side. A machine-readable index of available slices ships at `@adcp/sdk/types/per-tool-index.json`.\n\n### TypeScript footprint in monorepos\n\nLarge workspaces should keep the generated schema surface out of the default type-check path unless they actually need runtime Zod validators. The root `@adcp/sdk` export and `@adcp/sdk/types` do not re-export generated Zod schemas; import those schemas from `@adcp/sdk/schemas` so ordinary SDK imports avoid pulling the full generated schema declaration set into `tsc`.\n\n| Need                                          | Recommended import                                                                      |\n| --------------------------------------------- | --------------------------------------------------------------------------------------- |\n| Client, server, signing, and response helpers | `@adcp/sdk`, `@adcp/sdk/client`, `@adcp/sdk/server`, or another focused runtime subpath |\n| One tool's request/response types             | `@adcp/sdk/types/<tool>` such as `@adcp/sdk/types/sync-accounts`                        |\n| Runtime Zod schemas and tool schema maps      | `@adcp/sdk/schemas`                                                                     |\n| Broad generated protocol type barrel          | `@adcp/sdk/types`                                                                       |\n\nFor application monorepos, keep `skipLibCheck: true` unless you are intentionally auditing SDK declarations. If a package only needs request/response types for a few tools, prefer the per-tool slices over importing generated types through the root package or the broad `@adcp/sdk/types` barrel.\n\n## Quick Start: AdCP 3.2\n\nSDK 14 makes the compact 3.2 lifecycle primary:\n\n```text\nlist_products → buy_products → control_media_buy\n             ↘ request_proposals → refine_proposals → accept_proposal\n```\n\nStart with the persona guide that matches your job:\n\n- [Build a seller](./docs/guides/SELLER-QUICKSTART-3.2.md)\n- [Call a seller](./docs/guides/BUYER-QUICKSTART-3.2.md)\n- [Upgrade from SDK 13](./docs/migration-13-to-14.md)\n- [Run in production](./docs/guides/PRODUCTION-DURABILITY.md)\n\n```typescript\nimport { ADCPMultiAgentClient } from '@adcp/sdk';\n\nconst client = ADCPMultiAgentClient.simple('https://seller.example/mcp/', {\n  authToken: process.env.ADCP_TOKEN,\n});\nconst seller = client.agent('default-agent');\n\nconst listed = await seller.listProducts({\n  account: { account_id: 'account-42' },\n  brand: { domain: 'advertiser.example' },\n});\n\nif (listed.status === 'completed') {\n  console.log(listed.data.products, listed.data.feed_version);\n}\n```\n\nThe established `get_products` / `create_media_buy` / `update_media_buy`\nsurface remains supported for AdCP 3.0/3.1 compatibility. Its detailed client\npatterns follow below.\n\n## Established lifecycle and distributed operations\n\n```typescript\nimport { ADCPMultiAgentClient } from '@adcp/sdk';\n\n// Configure agents and handlers\nconst client = new ADCPMultiAgentClient(\n  [\n    {\n      id: 'agent_x',\n      agent_uri: 'https://agent-x.com',\n      protocol: 'a2a',\n    },\n    {\n      id: 'agent_y',\n      agent_uri: 'https://agent-y.com/mcp/',\n      protocol: 'mcp',\n    },\n  ],\n  {\n    // Webhook URL template (macros: {agent_id}, {task_type}, {operation_id})\n    webhookUrlTemplate: 'https://myapp.com/webhook/{task_type}/{agent_id}/{operation_id}',\n\n    // Activity callback - fires for ALL events (requests, responses, status changes, webhooks)\n    onActivity: activity => {\n      console.log(`[${activity.type}] ${activity.task_type} - ${activity.operation_id}`);\n      // Log to monitoring, update UI, etc.\n    },\n\n    // Status change handlers - called for ALL status changes (completed, failed, input-required, working, etc)\n    handlers: {\n      onGetProductsStatusChange: (response, metadata) => {\n        // Called for sync completion, async webhook, AND status changes\n        console.log(`[${metadata.status}] Got products for ${metadata.operation_id}`);\n\n        if (metadata.status === 'completed') {\n          db.saveProducts(metadata.operation_id, response.products);\n        } else if (metadata.status === 'failed') {\n          db.markFailed(metadata.operation_id, metadata.message);\n        } else if (metadata.status === 'input-required') {\n          // Handle clarification needed\n          console.log('Needs input:', metadata.message);\n        }\n      },\n    },\n  }\n);\n\n// Execute operation - library handles operation IDs, webhook URLs, context management\nconst agent = client.agent('agent_x');\nconst result = await agent.getProducts({ brief: 'Coffee brands' });\n\n// onActivity fired: protocol_request\n// onActivity fired: protocol_response\n\n// Check result\nif (result.status === 'completed') {\n  // Agent completed synchronously!\n  console.log('✅ Sync completion:', result.data.products.length, 'products');\n  // Products expose canonical format_options[]; legacy named-format refs do not\n  // cross the primary AgentClient boundary.\n  console.log(result.data.products[0]?.format_options[0]?.format_kind);\n  // onGetProductsStatusChange handler ALREADY fired with status='completed' ✓\n}\n\nif (result.status === 'submitted') {\n  // Agent will send webhook when complete\n  console.log('⏳ Async - webhook registered at:', result.submitted?.webhookUrl);\n  // onGetProductsStatusChange handler will fire when webhook arrives ✓\n}\n```\n\n### Handling Clarifications (input-required)\n\nWhen an agent needs more information, you can continue the conversation:\n\n```typescript\nconst result = await agent.getProducts({ brief: 'Coffee brands' });\n\nif (result.status === 'input-required') {\n  console.log('❓ Agent needs clarification:', result.metadata.inputRequest?.question);\n  // onActivity fired: status_change (input-required)\n\n  // Continue the conversation with the same agent\n  const refined = await agent.continueConversation('Only premium brands above $50');\n  // onActivity fired: protocol_request\n  // onActivity fired: protocol_response\n\n  if (refined.status === 'completed') {\n    console.log('✅ Got refined results:', refined.data.products.length);\n    // onGetProductsStatusChange handler fired ✓\n  }\n}\n```\n\n## Webhook Pattern\n\nAll webhooks (task completions AND notifications) use one endpoint with flexible URL templates.\n\n### Configure Your Webhook URL Structure\n\n```typescript\nconst client = new ADCPMultiAgentClient(agents, {\n  // Path-based (default pattern)\n  webhookUrlTemplate: 'https://myapp.com/webhook/{task_type}/{agent_id}/{operation_id}',\n\n  // OR query string\n  webhookUrlTemplate: 'https://myapp.com/webhook?agent={agent_id}&op={operation_id}&type={task_type}',\n\n  // OR custom path\n  webhookUrlTemplate: 'https://myapp.com/api/v1/adcp/{agent_id}?operation={operation_id}',\n\n  // OR namespace to avoid conflicts\n  webhookUrlTemplate: 'https://myapp.com/adcp-webhooks/{agent_id}/{task_type}/{operation_id}',\n});\n```\n\n### Single Webhook Endpoint\n\n```typescript\n// Handles ALL webhooks (task completions and notifications)\napp.post('/webhook/:task_type/:agent_id/:operation_id', async (req, res) => {\n  const { task_type, agent_id, operation_id } = req.params;\n\n  // Route to agent client - handlers fire automatically\n  const agent = client.agent(agent_id);\n  await agent.handleWebhook(\n    req.body,\n    task_type,\n    operation_id,\n    req.headers['x-adcp-signature'],\n    req.headers['x-adcp-timestamp']\n  );\n\n  res.json({ received: true });\n});\n```\n\n### URL Generation is Automatic\n\n```typescript\nconst operationId = createOperationId();\nconst webhookUrl = agent.getWebhookUrl('sync_creatives', operationId);\n// Returns: https://myapp.com/webhook/sync_creatives/agent_x/op_123\n// (or whatever your template generates)\n```\n\n## Activity Events\n\nGet observability into everything happening:\n\n```typescript\nconst client = new ADCPMultiAgentClient(agents, {\n  onActivity: activity => {\n    console.log({\n      type: activity.type, // 'protocol_request', 'webhook_received', etc.\n      operation_id: activity.operation_id,\n      agent_id: activity.agent_id,\n      status: activity.status,\n    });\n\n    // Stream to UI, save to database, send to monitoring\n    eventStream.send(activity);\n  },\n});\n```\n\nActivity types:\n\n- `protocol_request` - Request sent to agent\n- `protocol_response` - Response received from agent\n- `status_change` - Task status changed\n- `webhook_received` - Webhook received from agent\n\n## Notifications (Agent-Initiated)\n\n**Mental Model**: Notifications are operations that get set up when you create a media buy. The agent sends periodic updates (like delivery reports) to the webhook URL you configured during media buy creation.\n\n```typescript\n// When creating a media buy, agent registers for delivery notifications\nconst result = await agent.createMediaBuy({\n  campaign_id: 'camp_123',\n  budget: { amount: 10000, currency: 'USD' },\n  // Agent internally sets up recurring delivery_report notifications\n});\n\n// Later, agent sends notifications to your webhook\nconst client = new ADCPMultiAgentClient(agents, {\n  handlers: {\n    onMediaBuyDeliveryNotification: (notification, metadata) => {\n      console.log(`Report #${metadata.sequence_number}: ${metadata.notification_type}`);\n\n      // notification_type indicates progress:\n      // 'scheduled' → Progress update (like status: 'working')\n      // 'final' → Operation complete (like status: 'completed')\n      // 'delayed' → Still waiting (extended timeline)\n\n      db.saveDeliveryUpdate(metadata.operation_id, notification);\n\n      if (metadata.notification_type === 'final') {\n        db.markOperationComplete(metadata.operation_id);\n      }\n    },\n  },\n});\n```\n\nNotifications use the **same webhook URL pattern** as regular operations:\n\n```\nPOST https://myapp.com/webhook/media_buy_delivery/agent_x/delivery_report_agent_x_2025-10\n```\n\nThe `operation_id` is lazily generated from agent + month: `delivery_report_{agent_id}_{YYYY-MM}`\n\nAll intermediate reports for the same agent + month → same `operation_id`\n\n## Type Safety\n\nFull TypeScript support with IntelliSense:\n\n```typescript\n// All responses are fully typed\nconst result = await agent.getProducts(params);\n// result: TaskResult<GetProductsResponse>\n\nif (result.success) {\n  result.data.products.forEach(p => {\n    console.log(p.name, p.price); // Full autocomplete!\n  });\n}\n\n// Handlers receive typed responses\nhandlers: {\n  onCreateMediaBuyStatusChange: (response, metadata) => {\n    // response: CreateMediaBuyResponse | CreateMediaBuyAsyncWorking | ...\n    // metadata: WebhookMetadata\n    if (metadata.status === 'completed') {\n      const buyId = (response as CreateMediaBuyResponse).media_buy_id; // Typed!\n    }\n  };\n}\n```\n\n### Platform Implementors\n\nBuilding a server that receives AdCP tool calls? **v6 (recommended for new agents):** declare a typed `DecisioningPlatform` per-specialism and let the framework wire idempotency, signing, capability projection, async tasks, status normalization, and lifecycle state.\n\n```typescript\nimport { serve } from '@adcp/sdk';\nimport {\n  createAdcpServerFromPlatform,\n  createIdempotencyStore,\n  definePlatform,\n  defineSalesCorePlatform,\n  memoryBackend,\n  refAccountId,\n} from '@adcp/sdk/server';\n\n// Single-process example. Use pgBackend(pool) or redisBackend(client) for\n// durable, replica-safe production replay.\nconst idempotency = createIdempotencyStore({ backend: memoryBackend(), ttlSeconds: 86400 });\n\nconst platform = definePlatform({\n  capabilities: {\n    specialisms: ['sales-non-guaranteed'] as const,\n    channels: ['display'] as const,\n    pricingModels: ['cpm'] as const,\n  },\n  accounts: {\n    resolve: async (ref, ctx) => {\n      const id = refAccountId(ref);\n      if (!id) return null; // → ACCOUNT_NOT_FOUND\n      return db.findAccount(id, ctx);\n    },\n  },\n  sales: defineSalesCorePlatform({\n    getProducts: async (req, ctx) => ({ products: catalog.search(req) }), // req typed ✓\n    createMediaBuy: async (req, ctx) => ({\n      media_buy_id: 'mb_1',\n      status: 'pending_creatives',\n      confirmed_at: new Date().toISOString(),\n      packages: [],\n    }),\n    updateMediaBuy: async (id, patch, ctx) => ({ media_buy_id: id, status: 'active' }),\n    getMediaBuyDelivery: async (req, ctx) => ({\n      currency: 'USD',\n      reporting_period: { start: '2026-05-01T00:00:00Z', end: '2026-05-31T23:59:59Z' },\n      media_buy_deliveries: [],\n    }),\n    getMediaBuys: async (req, ctx) => ({ media_buys: [] }),\n  }),\n});\n\nserve(() => createAdcpServerFromPlatform(platform, { name: 'My Publisher', version: '1.0.0', idempotency }));\n```\n\n`RequiredPlatformsFor<S>` enforces specialism claims at compile time — claim `'sales-non-guaranteed'` and the typechecker requires `SalesCorePlatform & SalesIngestionPlatform` on `sales`. `creative-template` and `creative-generative` claims both map to `CreativeBuilderPlatform`; `creative-ad-server` is its own archetype with `listCreatives` + `getCreativeDelivery`.\n\n**6.7 helpers worth knowing about:**\n\n- `definePlatform` / `defineSalesCorePlatform` / `defineSalesIngestionPlatform` / sibling `define<X>Platform` factories — drop `req: unknown` casts on inline platform objects.\n- `composeMethod(inner, { before, after })` — typed before/after wrappers around any platform method (caching, enrichment under `ext.*`, typed-error guards). Pre-built `accounts.resolve` guards: `requireAccountMatch`, `requireAdvertiserMatch`, `requireOrgScope`.\n- Typed errors: `AuthMissingError`, `AuthInvalidError`, `PermissionDeniedError`, `RateLimitedError`, `ServiceUnavailableError`, `GovernanceDeniedError`, `IdempotencyConflictError`, plus the not-found family. `AuthRequiredError` remains as a deprecated `AUTH_REQUIRED` compatibility wrapper. Throw these instead of `new AdcpError(code, ...)`.\n- `BuyerAgentRegistry` — durable buyer-agent identity surface threaded through `ctx.agent` to every `AccountStore` method. See [`docs/migration-buyer-agent-registry.md`](docs/migration-buyer-agent-registry.md).\n- Three reference `AccountStore` shapes: `InMemoryImplicitAccountStore` (Shape A — buyer-driven `sync_accounts`), `createOAuthPassthroughResolver` (Shape B — vendor OAuth + `/me/adaccounts`), `createRosterAccountStore` (Shape C — publisher-curated roster).\n- Multi-tenant: `createTenantRegistry({...})` for host-routed (one server per tenant) or `createTenantStore({...})` for account-routed (one server, per-entry tenant gate built in, fail-closed when auth principal can't be resolved).\n- `createMediaBuyStore` — opt-in `targeting_overlay` echo on `get_media_buys` for sellers claiming `property-lists` / `collection-lists`.\n- `MEDIA_BUY_TRANSITIONS` / `assertMediaBuyTransition` (and the creative pair) — canonical lifecycle graphs.\n\nWorked reference adapters live in `examples/hello_*` — pick the one whose specialism matches yours and fork.\n\n**v5 lower-level API** (still fully supported as the substrate the v6 path calls into):\n\n```typescript\nimport type { CreateMediaBuyRequest, CreateMediaBuyResponse } from '@adcp/sdk';\nimport { CreateMediaBuyRequestSchema } from '@adcp/sdk/schemas';\n\nfunction handleCreateMediaBuy(rawParams: unknown): CreateMediaBuyResponse {\n  const request: CreateMediaBuyRequest = CreateMediaBuyRequestSchema.parse(rawParams);\n  // request.buyer_ref, request.account, request.brand — all typed\n}\n```\n\nMigration path from 6.6 → 6.7: see [`docs/migration-6.6-to-6.7.md`](docs/migration-6.6-to-6.7.md) (fifteen recipes, two breaking — `'implicit'`-resolution platforms now actually enforce the inline-`account_id` refusal the docstring has long claimed (pre-6.7 it was silent-pass, so audit your callers); `SalesPlatform` split into `SalesCorePlatform & SalesIngestionPlatform`). 5.x → 6.x: [`docs/migration-5.x-to-6.x.md`](docs/migration-5.x-to-6.x.md). Note: `PackageRequest` (creation-shaped, required fields) differs from `Package` (response-shaped). See the [type catalog](docs/ZOD-SCHEMAS.md#type-catalog) for all request types and their required fields.\n\n## Multi-Agent Operations\n\nExecute across multiple agents simultaneously:\n\n```typescript\nconst client = new ADCPMultiAgentClient([agentX, agentY, agentZ]);\n\n// Parallel execution across all agents\nconst results = await client.allAgents().getProducts({ brief: 'Coffee brands' });\n// results: TaskResult<GetProductsResponse>[]\n\nconst agentIds = client.getAgentIds();\nresults.forEach((result, i) => {\n  console.log(`${agentIds[i]}: ${result.status}`);\n\n  if (result.status === 'completed') {\n    console.log(`  Sync: ${result.data?.products?.length} products`);\n  } else if (result.status === 'submitted') {\n    console.log(`  Async: webhook to ${result.submitted?.webhookUrl}`);\n  }\n});\n```\n\n## Idempotency\n\nEvery mutating tool call (`createMediaBuy`, `syncCreatives`, `activateSignal`, etc.) auto-generates an `idempotency_key` (UUID v4) when the caller omits one. Internal retries reuse the key so a re-sent request returns the cached response rather than double-booking. See `docs/llms.txt` for the full protocol story.\n\n```typescript\nconst result = await client.createMediaBuy({ account, brand, start_time, end_time, packages });\n\n// Key used on the wire (auto-generated or caller-supplied). Log alongside your own IDs.\nresult.metadata.idempotency_key;\n\n// true when the response was a cached replay. Side-effecting callers MUST gate\n// notifications, memory writes, downstream calls on this flag.\nresult.metadata.replayed;\n```\n\n**Typed errors on replay conflicts** — check `result.errorInstance` with `instanceof` instead of switching on error codes:\n\n```typescript\nimport { IdempotencyConflictError, IdempotencyExpiredError } from '@adcp/sdk';\n\nif (result.errorInstance instanceof IdempotencyConflictError) {\n  // Reconcile by natural key before deciding whether this is a new intent.\n  // Do not blindly mint a fresh key: the prior operation may have succeeded.\n}\nif (result.errorInstance instanceof IdempotencyExpiredError) {\n  // Key past the seller's replay window. Look up by natural key before retrying.\n}\n```\n\n**BYOK** (persist keys across process restarts so crash-recovery can resend the exact key):\n\n```typescript\nimport { useIdempotencyKey } from '@adcp/sdk';\n\n// Validates against the spec pattern `^[A-Za-z0-9_.:-]{16,255}$` before the round-trip.\nconst key = await db.getOrCreateIdempotencyKey(campaign.id);\nawait client.createMediaBuy({ ...params, ...useIdempotencyKey(key) });\n\n// Check the seller's replay window so you know when to fall back to natural-key lookup.\n// Throws on v3 sellers that omit the declaration — the SDK does NOT default to 24h.\nconst ttlSeconds = await client.getIdempotencyReplayTtlSeconds();\n```\n\nIdempotency keys are retry-pattern oracles within their TTL, so the SDK truncates them to the first 8 characters in debug logs by default. Set `ADCP_LOG_IDEMPOTENCY_KEYS=1` to opt into full logging for local debugging.\n\n**Crash recovery**: if your process dies mid-retry and you need to decide whether to re-send — look up the persisted key by natural key, check `result.metadata.replayed`, and handle `IdempotencyConflictError` / `IdempotencyExpiredError`. Worked recipe in [`docs/guides/idempotency-crash-recovery.md`](./docs/guides/idempotency-crash-recovery.md).\n\n## Security\n\n### Webhook Signature Verification\n\n```typescript\nconst client = new ADCPMultiAgentClient(agents, {\n  webhookSecret: process.env.WEBHOOK_SECRET,\n});\n\n// Signatures verified automatically on handleWebhook()\n// Returns 401 if signature invalid\n```\n\n### Request Signing (RFC 9421)\n\nAdCP 3.0 supports [HTTP Message Signatures (RFC 9421)](https://www.rfc-editor.org/rfc/rfc9421) for cryptographic request authentication. A buyer signs outbound requests so the seller can verify who sent them and that nothing was tampered with. A seller signs outbound webhooks so the buyer can verify authenticity. Optional in 3.0, mandatory in 3.1+ for mutating operations.\n\n**Generate a signing key:**\n\n```bash\nadcp signing generate-key --alg ed25519 --kid my-agent-2026 \\\n  --private-out ./private.jwk --public-out ./public-jwks.json\n# Publish public-jwks.json at your /.well-known/jwks.json endpoint.\n# Point to it from your /.well-known/brand.json agents[].jwks_uri.\n```\n\n**Sign outbound requests (buyer):**\n\n```typescript\nimport { createSigningFetch } from '@adcp/sdk/signing';\n\nconst signingFetch = createSigningFetch(fetch, {\n  keyid: 'my-agent-2026',\n  alg: 'ed25519',\n  privateKey: privateJwk, // JWK with `d` field\n});\n\nawait signingFetch('https://seller.example.com/mcp', {\n  method: 'POST',\n  headers: { 'Content-Type': 'application/json' },\n  body: JSON.stringify(payload),\n});\n// Signature, Signature-Input, and Content-Digest headers added automatically.\n```\n\n**Verify inbound signatures (seller):**\n\n```typescript\nimport { createExpressVerifier, StaticJwksResolver, InMemoryReplayStore } from '@adcp/sdk/signing';\n\n// Raw-body capture MUST be mounted ahead of the verifier — express.json()\n// would otherwise consume the stream and the verifier would have no bytes to\n// hash. `rawBodyVerify` comes from `createExpressAdapter()`; the inline form is\n// equivalent.\napp.use(express.json({ verify: adapter.rawBodyVerify }));\n// or: app.use(express.json({ verify: (req, _res, buf) => { req.rawBody = buf.toString('utf8'); } }));\n\napp.post(\n  '/mcp',\n  createExpressVerifier({\n    capability: {\n      supported: true,\n      covers_content_digest: 'required',\n      required_for: ['create_media_buy'],\n    },\n    jwks: new StaticJwksResolver(buyerPublicKeys),\n    replayStore: new InMemoryReplayStore(),\n    resolveOperation: req => req.body?.method ?? 'unknown',\n  }),\n  handler\n);\n// On verify: req.verifiedSigner = { keyid, agent_url?, verified_at }.\n// On reject: 401 with WWW-Authenticate: Signature error=\"<code>\".\n```\n\nFull guide covering key generation, JWKS publication, brand.json setup, webhook signing, capability declaration, key rotation, and conformance testing: **[docs/guides/SIGNING-GUIDE.md](./docs/guides/SIGNING-GUIDE.md)**.\n\n### Authentication\n\n```typescript\nconst agents = [\n  {\n    id: 'agent_x',\n    name: 'Agent X',\n    agent_uri: 'https://agent-x.com',\n    protocol: 'a2a',\n    auth_token: process.env.AGENT_X_TOKEN, // ✅ Secure - load from env\n  },\n];\n```\n\n## Environment Configuration\n\n```bash\n# .env\nWEBHOOK_URL_TEMPLATE=\"https://myapp.com/webhook/{task_type}/{agent_id}/{operation_id}\"\nWEBHOOK_SECRET=\"your-webhook-secret\"\n\nADCP_AGENTS_CONFIG='[\n  {\n    \"id\": \"agent_x\",\n    \"name\": \"Agent X\",\n    \"agent_uri\": \"https://agent-x.com\",\n    \"protocol\": \"a2a\",\n    \"auth_token\": \"actual-token-here\"\n  }\n]'\n```\n\n```typescript\n// Auto-discover from environment\nconst client = ADCPMultiAgentClient.fromEnv();\n```\n\n## Available Tools\n\nPrimary AdCP tools have typed request and response maps. Less-common or\nextension tools use `executeCustomTask<T>()` explicitly.\n\n**Media Buy Lifecycle:**\n\nThe primary client surface speaks canonical creatives: products use\n`format_options[]`, packages select `format_option_refs[]`, and creatives use\n`format_kind` plus an optional `format_option_ref`. The SDK performs negotiated\nlegacy-wire translation internally. Migration tooling can opt into the clearly\nnamed `createMediaBuyLegacy()`, `updateMediaBuyLegacy()`,\n`syncCreativesLegacy()`, `listCreativesLegacy()`, `buildCreativeLegacy()`, and\n`previewCreativeLegacy()` escape hatches. `executeCustomTask()` cannot invoke\ntyped primary tasks or legacy creative tools; other less-common standard and\nextension tools intentionally use that explicit raw-task route.\n\n- `getProducts()` - Discover advertising products\n- `listCreativeFormatsLegacy()` - Inspect the legacy named-format catalog (migration tooling only)\n- `createMediaBuy()` - Create new media buy\n- `updateMediaBuy()` - Update existing media buy\n- `syncCreatives()` - Upload/sync creative assets\n- `listCreatives()` - List creative assets\n- `buildCreativeLegacy()` - Build through the legacy named-format protocol (migration tooling only)\n- `previewCreative()` - Preview by advertised capability, inline manifest, or creative-library ID\n- `previewCreativeLegacy()` - Preview through the legacy named-format protocol (migration tooling only)\n- `getMediaBuyDelivery()` - Get delivery performance\n\n**Audience & Targeting:**\n\n- `getSignals()` - Get audience signals\n- `activateSignal()` - Activate audience signals\n- `providePerformanceFeedback()` - Send performance feedback\n\n**Protocol:**\n\n- `getAdcpCapabilities()` - Get agent capabilities (v3)\n\n## Property Discovery (AdCP v2.2.0)\n\nBuild agent registries by discovering properties agents can sell. Works with AdCP v2.2.0's publisher-domain model.\n\n### How It Works\n\n1. **Agents return publisher domains**: Call `listAuthorizedProperties()` → get `publisher_domains[]`\n2. **Fetch property definitions**: Get `https://{domain}/.well-known/adagents.json` from each domain\n3. **Index properties**: Build fast lookups for \"who can sell X?\" and \"what can agent Y sell?\"\n\n### Three Key Queries\n\n```typescript\nimport { PropertyCrawler, getPropertyIndex } from '@adcp/sdk';\n\n// First, crawl agents to discover properties\nconst crawler = new PropertyCrawler();\nawait crawler.crawlAgents([\n  { agent_url: 'https://agent-x.com', protocol: 'a2a' },\n  { agent_url: 'https://agent-y.com/mcp/', protocol: 'mcp' },\n]);\n\nconst index = getPropertyIndex();\n\n// Query 1: Who can sell this property?\nconst matches = index.findAgentsForProperty('domain', 'cnn.com');\n// Returns: [{ property, agent_url, publisher_domain }]\n\n// Query 2: What can this agent sell?\nconst auth = index.getAgentAuthorizations('https://agent-x.com');\n// Returns: { agent_url, publisher_domains: [...], properties: [...] }\n\n// Query 3: Find by tags\nconst premiumProperties = index.findAgentsByPropertyTags(['premium', 'ctv']);\n```\n\n### Full Example\n\n```typescript\nimport { PropertyCrawler, getPropertyIndex } from '@adcp/sdk';\n\nconst crawler = new PropertyCrawler();\n\n// Crawl agents - gets publisher_domains from each, then fetches adagents.json\nconst result = await crawler.crawlAgents([\n  { agent_url: 'https://sales.cnn.com' },\n  { agent_url: 'https://sales.espn.com' },\n]);\n\nconsole.log(`✅ ${result.successfulAgents} agents`);\nconsole.log(`📡 ${result.totalPublisherDomains} publisher domains`);\nconsole.log(`📦 ${result.totalProperties} properties indexed`);\n\n// Now query\nconst index = getPropertyIndex();\nconst whoCanSell = index.findAgentsForProperty('ios_bundle', 'com.cnn.app');\n\nfor (const match of whoCanSell) {\n  console.log(`${match.agent_url} can sell ${match.property.name}`);\n}\n```\n\n### Property Types\n\nSupports 18 identifier types: `domain`, `subdomain`, `ios_bundle`, `android_package`, `apple_app_store_id`, `google_play_id`, `roku_channel_id`, `podcast_rss_feed`, and more.\n\n### Use Case\n\nBuild a registry service that:\n\n- Periodically crawls agents with `PropertyCrawler`\n- Persists discovered properties to a database\n- Exposes fast query APIs using the in-memory index patterns\n- Provides web UI for browsing properties and agents\n\nLibrary provides discovery logic - you add persistence layer.\n\n### Brand relationship verification\n\nUse `RegistryClient.lookupBrand()` to resolve a domain and verify its relationship to a house. The public v3 registry does not expose an ordered-chain endpoint; v3 hierarchy is one level deep.\n\n```ts\nimport { RegistryClient } from '@adcp/sdk';\n\nconst registry = new RegistryClient();\nconst brand = await registry.lookupBrand('leaf.example', { fresh: true });\n\nconst verifiedHouse =\n  brand && !brand.live_brand_json && (brand.relationship_trust === 'mutual' || brand.relationship_trust === 'inline')\n    ? brand.house_domain\n    : undefined;\n```\n\nOnly `relationship_trust: \"mutual\"` and `\"inline\"` are reciprocated. For `mutual`, `relationship_verified_at` says when both sides were last observed agreeing. `claimed_house_domain` is a unilateral leaf claim and must not be used for authorization. `ResolvedBrand.parent_brand` is a registry reference that may be a portfolio-internal id, not a portable traversal API.\n\n`source` answers a different question: where the selected identity record came from. Provenance is not relationship authorization, and callers must not infer a relationship from `source`. Treat an absent `relationship_trust` as unknown, not `standalone`. Pass `{ fresh: true }` when a live origin check is required; if `live_brand_json` is present, that check failed and the response came from stored evidence, so a strict live-evidence policy must reject it. Policies that permit stored evidence should apply their own age ceiling to `relationship_verified_at` and `relationship_declared_at`. If `promoted_from_schema` is present, inspect every `migration_warnings` entry. Until the registry guarantees a warning for every discarded legacy field, absence of a warning is not evidence that a legacy relationship was promoted.\n\n### Community Mirror `adagents.json` Catalogs\n\nUse `buildCommunityMirrorAdagents()` when publishing catalog-only AAO/community mirrors for platforms that have not adopted AdCP or published seller-authorized files yet. The helper emits `authorized_agents: []` and refuses caller-supplied authorization entries, so format and placement metadata cannot be mistaken for a seller authorization claim.\n\n```ts\nimport { RegistryClient, buildCommunityMirrorAdagents } from '@adcp/sdk';\n\nconst catalog = buildCommunityMirrorAdagents({\n  catalog_etag: 'meta-creative-formats-2026-05',\n  formats: [\n    {\n      format_option_id: 'meta-feed-image',\n      format_kind: 'image',\n      params: {\n        width: 1080,\n        height: 1080,\n      },\n      v1_format_ref: [\n        {\n          agent_url: 'https://creative.adcontextprotocol.org/translated/meta',\n          id: 'feed_image',\n        },\n      ],\n    },\n  ],\n  placements: [\n    {\n      placement_id: 'feed',\n      name: 'Feed',\n      property_tags: ['feed'],\n      format_options: [{ format_option_id: 'meta-feed-image' }],\n    },\n  ],\n  placement_tags: {\n    feed: { name: 'Feed', description: 'Main feed placement' },\n  },\n});\n\nawait new RegistryClient().createAdagents(catalog);\n```\n\n`RegistryClient.createAdagents()` and `createCommunityMirrorAdagents()` are intended for build-time generation and cache fills. Public `/.well-known/adagents.json` routes should serve generated JSON from static storage or an application cache rather than calling the registry on every request.\n\nTo persist an AAO/community mirror in the registry, use the keyed upsert path:\n\n```typescript\nawait new RegistryClient({ apiKey: process.env.ADCP_REGISTRY_API_KEY }).upsertCommunityMirrorAdagents('meta', {\n  catalog_etag: 'meta-creative-formats-2026-05',\n  formats: catalog.formats,\n});\n\nawait new RegistryClient({ apiKey: process.env.ADCP_REGISTRY_API_KEY }).upsertCommunityMirrorAdagents({\n  platform: 'meta',\n  catalog_etag: 'meta-creative-formats-2026-05',\n  formats: catalog.formats,\n});\n```\n\n`upsertCommunityMirrorAdagents()` writes to the hosted mirror lifecycle endpoint, while `createCommunityMirrorAdagents()` remains a side-effect-free generator helper.\n\n## Database Schema\n\nSimple unified event log for all operations:\n\n```sql\nCREATE TABLE webhook_events (\n  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n  operation_id TEXT NOT NULL,        -- Groups related events\n  agent_id TEXT NOT NULL,\n  task_type TEXT NOT NULL,           -- 'sync_creatives', 'media_buy_delivery', etc.\n  status TEXT,                       -- For tasks: 'submitted', 'working', 'completed'\n  notification_type TEXT,            -- For notifications: 'scheduled', 'final', 'delayed'\n  sequence_number INTEGER,           -- For notifications: report sequence\n  payload JSONB NOT NULL,\n  timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW()\n);\n\nCREATE INDEX idx_events_operation ON webhook_events(operation_id);\nCREATE INDEX idx_events_agent ON webhook_events(agent_id);\nCREATE INDEX idx_events_timestamp ON webhook_events(timestamp DESC);\n\n-- Query all events for an operation\nSELECT * FROM webhook_events\nWHERE operation_id = 'op_123'\nORDER BY timestamp;\n\n-- Get all delivery reports for agent + month\nSELECT * FROM webhook_events\nWHERE operation_id = 'delivery_report_agent_x_2025-10'\nORDER BY sequence_number;\n```\n\n## CLI Tool\n\nFor development and testing, use the included CLI tool to interact with AdCP agents.\n\n### Quick Start with Aliases\n\nSave agents for quick access:\n\n```bash\n# Save an agent with an alias\nnpx @adcp/sdk@adcp-3.0 --save-auth test https://test-agent.adcontextprotocol.org\n\n# Use the alias\nnpx @adcp/sdk@adcp-3.0 test get_products '{\"brief\":\"Coffee brands\"}'\n\n# List saved agents\nnpx @adcp/sdk@adcp-3.0 --list-agents\n```\n\n### Direct URL Usage\n\nAuto-detect protocol and call directly:\n\n```bash\n# Protocol auto-detection (default)\nnpx @adcp/sdk@adcp-3.0 https://test-agent.adcontextprotocol.org get_products '{\"brief\":\"Coffee\"}'\n\n# Force specific protocol with --protocol flag\nnpx @adcp/sdk@adcp-3.0 https://agent.example.com get_products '{\"brief\":\"Coffee\"}' --protocol mcp\nnpx @adcp/sdk@adcp-3.0 https://agent.example.com list_authorized_properties --protocol a2a\n\n# List available tools\nnpx @adcp/sdk@adcp-3.0 https://agent.example.com\n\n# Use a file for payload\nnpx @adcp/sdk@adcp-3.0 https://agent.example.com create_media_buy @payload.json\n\n# JSON output for scripting\nnpx @adcp/sdk@adcp-3.0 https://agent.example.com get_products '{\"brief\":\"...\"}' --json | jq '.products'\n```\n\n### Authentication\n\nThree ways to provide auth tokens (priority order):\n\n```bash\n# 1. Explicit flag (highest priority)\nnpx @adcp/sdk@adcp-3.0 test get_products '{\"brief\":\"...\"}' --auth your-token\n\n# 2. Saved in agent config (recommended)\nnpx @adcp/sdk@adcp-3.0 --save-auth prod https://prod-agent.com\n# Will prompt for auth token securely\n\n# 3. Environment variable (fallback)\nexport ADCP_AUTH_TOKEN=your-token\nnpx @adcp/sdk@adcp-3.0 test get_products '{\"brief\":\"...\"}'\n```\n\n### Agent Management\n\n```bash\n# Save agent with auth\nnpx @adcp/sdk@adcp-3.0 --save-auth prod https://prod-agent.com mcp\n\n# List all saved agents\nnpx @adcp/sdk@adcp-3.0 --list-agents\n\n# Remove an agent\nnpx @adcp/sdk@adcp-3.0 --remove-agent test\n\n# Show config file location\nnpx @adcp/sdk@adcp-3.0 --show-config\n```\n\n### Testing & Compliance\n\n```bash\n# Run test scenarios against an agent\nnpx @adcp/sdk@adcp-3.0 test test-mcp full_sales_flow\nnpx @adcp/sdk@adcp-3.0 test test-mcp --list-scenarios\n\n# Run compliance assessment\nnpx @adcp/sdk@adcp-3.0 comply test-mcp\nnpx @adcp/sdk@adcp-3.0 comply test-mcp --platform-type social_platform\nnpx @adcp/sdk@adcp-3.0 comply --list-platform-types\n```\n\n**Protocol Auto-Detection**: The CLI automatically detects whether an endpoint uses MCP or A2A by checking URL patterns and discovery endpoints. Override with `--protocol mcp` or `--protocol a2a` if needed.\n\n**Config File**: Agent configurations are saved to `~/.adcp/config.json` with secure file permissions (0600).\n\nSee [docs/CLI.md](docs/CLI.md) for complete CLI documentation including webhook support for async operations.\n\n### Claude Code Plugin\n\nInstall the AdCP CLI as a Claude Code plugin to use `/adcp-client:adcp` directly in your AI coding assistant:\n\n```bash\n# Add the marketplace (one time)\n/plugin marketplace add adcontextprotocol/adcp-client\n\n# Install the plugin\n/plugin install adcp-client@adcp\n```\n\nOr test locally during development:\n\n```bash\nclaude --plugin-dir ./path/to/adcp-client\n```\n\n## Testing\n\nTry the live testing UI at `http://localhost:8080` when running the server:\n\n```bash\nnpm start\n```\n\nFeatures:\n\n- Configure multiple agents (test agents + your own)\n- Execute ONE operation across all agents\n- See live activity stream (protocol requests, webhooks, handlers)\n- View sync vs async completions side-by-side\n- Test different scenarios (clarifications, errors, timeouts)\n\n## Examples\n\n### Basic Operation\n\n```typescript\nconst result = await agent.getProducts({ brief: 'Coffee brands' });\n```\n\n### With Clarification Handler\n\n```typescript\nconst result = await agent.createMediaBuy(\n  { buyer_ref: 'campaign-123', account_id: 'acct-456', packages: [...] },\n  (context) => {\n    // Agent needs more info\n    if (context.inputRequest.field === 'budget') {\n      return 50000; // Provide programmatically\n    }\n    return context.deferToHuman(); // Or defer to human\n  }\n);\n```\n\n### With Webhook for Long-Running Operations\n\n```typescript\nconst operationId = createOperationId();\n\nconst result = await agent.syncCreatives(\n  { creatives: largeCreativeList },\n  null, // No clarification handler = webhook mode\n  {\n    contextId: operationId,\n    webhookUrl: agent.getWebhookUrl('sync_creatives', operationId),\n  }\n);\n\n// Result will be 'submitted', webhook arrives later\n// Handler fires when webhook received\n```\n\n## Building an Agent (Server)\n\nThe fastest way to build an AdCP agent is to point your coding tool (Claude Code, Codex, Cursor, etc.) at the right skill file:\n\n```\n# Seller agent (publisher, SSP, retail media)\n\"Read skills/build-seller-agent/SKILL.md and build me a [your platform description]\"\n\n# Signals agent (CDP, data provider)\n\"Read skills/build-signals-agent/SKILL.md and build me a [your data platform description]\"\n```\n\nThe skill guides domain decisions, scaffolds code, and tells you how to validate:\n\n```bash\nnpx tsx agent.ts\nnpx @adcp/sdk@adcp-3.0 storyboard run http://localhost:3001/mcp media_buy_seller --json\n```\n\nAvailable skills:\n\n| Skill                                                                                    | For                             | Storyboard                           |\n| ---------------------------------------------------------------------------------------- | ------------------------------- | ------------------------------------ |\n| [`skills/build-seller-agent/`](skills/build-seller-agent/SKILL.md)                       | Publishers, SSPs, retail media  | `media_buy_seller`                   |\n| [`skills/build-generative-seller-agent/`](skills/build-generative-seller-agent/SKILL.md) | AI ad networks, generative DSPs | `media_buy_generative_seller`        |\n| [`skills/build-signals-agent/`](skills/build-signals-agent/SKILL.md)                     | CDPs, data providers            | `signal_owned`, `signal_marketplace` |\n| [`skills/build-retail-media-agent/`](skills/build-retail-media-agent/SKILL.md)           | Retail media networks           | `media_buy_catalog_creative`         |\n| [`skills/build-creative-agent/`](skills/build-creative-agent/SKILL.md)                   | Ad servers, creative platforms  | `creative_lifecycle`                 |\n\nFor manual implementation, see the [Build an Agent guide](docs/guides/BUILD-AN-AGENT.md) and [`examples/signals-agent.ts`](examples/signals-agent.ts).\n\n## Contributing\n\nContributions welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.\n\n## License\n\nApache 2.0 License - see [LICENSE](LICENSE) file for details.\n\n## Support\n\n- **Documentation**: [docs.adcontextprotocol.org](https://docs.adcontextprotocol.org)\n- **Issues**: [GitHub Issues](https://github.com/adcontextprotocol/adcp-client/issues)\n- **Protocol Spec**: [AdCP Specification](https://github.com/adcontextprotocol/adcp)\n",
  "bytes": 42697,
  "sha": "188d43a6f7aac11691898585408a7d83c9fb69f166b485d2ce55a99d14266337",
  "repo_slug": "adcontextprotocol/adcp-client",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/plg_adcontextprotocol_adcp_client_adcp_clien_5a2d59fc/readme"
}