From 22cf57b9c807419a45ba420e0d3ec8fb2a6c6db1 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Apr 2026 11:33:42 +0000 Subject: [PATCH 1/3] fix(addie): surface did_you_mean in billing tools for non-canonical lookup_key aliases When the LLM passes an aliased lookup_key (e.g. explorer_annual instead of aao_membership_explorer_50), create_payment_link/send_invoice/confirm_send_invoice now include did_you_mean: canonicalKey in the JSON response so the model learns the canonical key for subsequent calls in the same conversation. Also tightens the three tool descriptions to explicitly forbid constructing the key from tier name and billing interval. Bumps CODE_VERSION to 2026.04.6. Closes #2550 https://claude.ai/code/cse_01HULrqPhn8kLM76nSmcpj4d --- .changeset/billing-did-you-mean-lookup-key.md | 6 +++ server/src/addie/config-version.ts | 2 +- server/src/addie/mcp/billing-tools.ts | 44 +++++++++++++++---- server/src/billing/stripe-client.ts | 31 +++++++------ 4 files changed, 58 insertions(+), 25 deletions(-) create mode 100644 .changeset/billing-did-you-mean-lookup-key.md diff --git a/.changeset/billing-did-you-mean-lookup-key.md b/.changeset/billing-did-you-mean-lookup-key.md new file mode 100644 index 0000000000..b6f0c94038 --- /dev/null +++ b/.changeset/billing-did-you-mean-lookup-key.md @@ -0,0 +1,6 @@ +--- +--- + +Surface `did_you_mean` in Addie billing tool responses when the LLM passes a non-canonical `lookup_key` alias (e.g. `explorer_annual` instead of `aao_membership_explorer_50`). Tighten tool descriptions on `create_payment_link`, `send_invoice`, and `confirm_send_invoice` to explicitly forbid constructing the key from tier name and billing interval. Closes #2550. + +Server-side Addie change only — no protocol schema changes. diff --git a/server/src/addie/config-version.ts b/server/src/addie/config-version.ts index 9d2418ce2a..1004846fa3 100644 --- a/server/src/addie/config-version.ts +++ b/server/src/addie/config-version.ts @@ -28,7 +28,7 @@ import { loadRules } from './rules/index.js'; * Format: YYYY.MM.N where N is incremented for multiple changes in a month * Example: 2025.01.1, 2025.01.2, 2025.02.1 */ -export const CODE_VERSION = '2026.04.5'; +export const CODE_VERSION = '2026.04.6'; // Types export interface ConfigVersion { diff --git a/server/src/addie/mcp/billing-tools.ts b/server/src/addie/mcp/billing-tools.ts index a5f70d1d66..b461db22f6 100644 --- a/server/src/addie/mcp/billing-tools.ts +++ b/server/src/addie/mcp/billing-tools.ts @@ -62,13 +62,15 @@ You should ask about their company type and approximate revenue to find the righ The link is issued to the signed-in member only — the customer email and identity are taken from the authenticated session, never from caller-supplied input. The member must be signed in at agenticadvertising.org and have a workspace; if not, refuse and direct them to sign up first. -This tool cannot generate payment links on behalf of other people or organizations.`, +This tool cannot generate payment links on behalf of other people or organizations. +IMPORTANT: Pass lookup_key verbatim from find_membership_products. Do NOT construct it from the tier +name and billing interval (e.g. do not pass "explorer_annual" — pass "aao_membership_explorer_50").`, input_schema: { type: 'object' as const, properties: { lookup_key: { type: 'string', - description: 'The product lookup key from find_membership_products', + description: 'The exact lookup_key value returned by find_membership_products — do not construct or guess this value', }, }, required: ['lookup_key'], @@ -79,13 +81,15 @@ This tool cannot generate payment links on behalf of other people or organizatio description: `Preview an invoice for the authenticated member's own organization so they can confirm the amount and billing email before it is sent. The contact email and company are taken from the signed-in session, never from caller-supplied input. After calling this and the member confirms, -call confirm_send_invoice to send.`, +call confirm_send_invoice to send. +IMPORTANT: Pass lookup_key verbatim from find_membership_products. Do NOT construct it from the tier +name and billing interval (e.g. do not pass "explorer_annual" — pass "aao_membership_explorer_50").`, input_schema: { type: 'object' as const, properties: { lookup_key: { type: 'string', - description: 'The product lookup key from find_membership_products', + description: 'The exact lookup_key value returned by find_membership_products — do not construct or guess this value', }, coupon_id: { type: 'string', @@ -105,13 +109,15 @@ call confirm_send_invoice to send.`, description: `Send an invoice for the authenticated member's own organization after they have confirmed the details shown by send_invoice. The contact email, company, and billing address come from the signed-in session — they cannot be overridden. The org must already have a billing address -on file (set via the dashboard or invite-acceptance flow).`, +on file (set via the dashboard or invite-acceptance flow). +IMPORTANT: Pass lookup_key verbatim from find_membership_products. Do NOT construct it from the tier +name and billing interval (e.g. do not pass "explorer_annual" — pass "aao_membership_explorer_50").`, input_schema: { type: 'object' as const, properties: { lookup_key: { type: 'string', - description: 'The product lookup key from find_membership_products', + description: 'The exact lookup_key value returned by find_membership_products — do not construct or guess this value', }, coupon_id: { type: 'string', @@ -261,13 +267,14 @@ export function createBillingToolHandlers(memberContext?: MemberContext | null): logger.info({ lookupKey, orgId, workosUserId }, 'Addie: Creating payment link for signed-in member'); try { - const priceId = await getPriceByLookupKey(lookupKey); - if (!priceId) { + const priceResult = await getPriceByLookupKey(lookupKey); + if (!priceResult) { return JSON.stringify({ success: false, error: `No product matches lookup_key "${lookupKey}". Call find_membership_products first, then pass the exact lookup_key from the result.`, }); } + const { priceId, canonicalKey } = priceResult; const org = await orgDb.getOrganization(orgId); @@ -305,6 +312,7 @@ export function createBillingToolHandlers(memberContext?: MemberContext | null): success: true, payment_url: session.url, message: 'Payment link created. Share this URL with the signed-in member to complete checkout.', + ...(canonicalKey !== lookupKey && { did_you_mean: canonicalKey }), }); } catch (error) { logger.error({ error }, 'Addie: Error creating payment link'); @@ -357,6 +365,15 @@ export function createBillingToolHandlers(memberContext?: MemberContext | null): logger.info({ lookupKey, orgId, hasCoupon: !!effectiveCouponId }, 'Addie: Previewing invoice for signed-in member'); try { + const priceResult = await getPriceByLookupKey(lookupKey); + if (!priceResult) { + return JSON.stringify({ + success: false, + error: `No product matches lookup_key "${lookupKey}". Call find_membership_products first, then pass the exact lookup_key from the result.`, + }); + } + const { canonicalKey } = priceResult; + const preview = await validateInvoiceDetails({ lookupKey, contactEmail: memberEmail, @@ -385,6 +402,7 @@ export function createBillingToolHandlers(memberContext?: MemberContext | null): discount_description: orgDiscount, discount_warning: preview.discountWarning, payment_terms: paymentTerms ?? 30, + ...(canonicalKey && canonicalKey !== lookupKey && { did_you_mean: canonicalKey }), }); } catch (error) { logger.error({ error }, 'Addie: Error previewing invoice'); @@ -455,6 +473,15 @@ export function createBillingToolHandlers(memberContext?: MemberContext | null): ); try { + const priceResult = await getPriceByLookupKey(lookupKey); + if (!priceResult) { + return JSON.stringify({ + success: false, + error: `No product matches lookup_key "${lookupKey}". Call find_membership_products first, then pass the exact lookup_key from the result.`, + }); + } + const { canonicalKey } = priceResult; + const result = await createAndSendInvoice({ lookupKey, companyName: org.name, @@ -480,6 +507,7 @@ export function createBillingToolHandlers(memberContext?: MemberContext | null): discount_applied: result.discountApplied, discount_description: orgDiscount, discount_warning: result.discountWarning, + ...(canonicalKey && canonicalKey !== lookupKey && { did_you_mean: canonicalKey }), }); } catch (error) { logger.error({ error }, 'Addie: Error sending invoice'); diff --git a/server/src/billing/stripe-client.ts b/server/src/billing/stripe-client.ts index 19eb39ea65..18cf0598ae 100644 --- a/server/src/billing/stripe-client.ts +++ b/server/src/billing/stripe-client.ts @@ -278,13 +278,6 @@ export async function getInvoiceableProducts(): Promise { * full list of valid keys. Silently picking "first match wins" would risk * charging the wrong price. * - * TODO(#2550): This resolver is a band-aid. The root fix is in the Addie - * tool layer: tighten `find_membership_products` / `create_payment_link` - * tool descriptions so the LLM passes lookup_key verbatim, and surface a - * `did_you_mean` field in the tool response so the model learns the - * canonical key. As soon as the catalog gains a monthly Explorer SKU, - * `explorer_annual` will collide here and stop resolving — track interval - * preservation in #2550 too. */ export function resolveLookupKeyAlias(input: string, products: BillingProduct[]): BillingProduct | undefined { const trimmed = input.trim().toLowerCase(); @@ -308,9 +301,13 @@ export function resolveLookupKeyAlias(input: string, products: BillingProduct[]) } /** - * Get a specific price by lookup key + * Get a specific price by lookup key. + * Returns both the Stripe price ID and the canonical lookup key — the canonical + * key differs from the input only when the input was an alias (e.g. "explorer_annual" + * resolves to "aao_membership_explorer_50"). Callers can surface the canonical key to + * the LLM so subsequent calls use it verbatim. */ -export async function getPriceByLookupKey(lookupKey: string): Promise { +export async function getPriceByLookupKey(lookupKey: string): Promise<{ priceId: string; canonicalKey: string } | null> { if (!stripe) { logger.warn({ lookupKey }, 'getPriceByLookupKey: Stripe not initialized'); return null; @@ -320,7 +317,7 @@ export async function getPriceByLookupKey(lookupKey: string): Promise p.lookup_key === lookupKey); if (cachedProduct) { logger.info({ lookupKey, priceId: cachedProduct.price_id }, 'getPriceByLookupKey: Found price in cache'); - return cachedProduct.price_id; + return { priceId: cachedProduct.price_id, canonicalKey: lookupKey }; } // Direct Stripe lookup as fallback when cache doesn't have the key @@ -333,7 +330,7 @@ export async function getPriceByLookupKey(lookupKey: string): Promise 0) { logger.info({ lookupKey, priceId: prices.data[0].id }, 'getPriceByLookupKey: Found price via direct Stripe lookup'); - return prices.data[0].id; + return { priceId: prices.data[0].id, canonicalKey: lookupKey }; } } catch (error) { logger.error({ err: error, lookupKey }, 'getPriceByLookupKey: Error in direct Stripe lookup'); @@ -345,7 +342,7 @@ export async function getPriceByLookupKey(lookupKey: string): Promise p.lookup_key).filter(Boolean); @@ -986,14 +983,15 @@ export async function createAndSendInvoice( } // Get price ID from lookup key - const priceId = await getPriceByLookupKey(data.lookupKey); + const priceResult = await getPriceByLookupKey(data.lookupKey); - if (!priceId) { + if (!priceResult) { logger.error({ lookupKey: data.lookupKey, }, 'No price found for lookup key'); return null; } + const priceId = priceResult.priceId; let subscriptionId: string | undefined; try { @@ -1310,11 +1308,12 @@ export async function validateInvoiceDetails(data: { return null; } - const priceId = await getPriceByLookupKey(data.lookupKey); - if (!priceId) { + const priceResult = await getPriceByLookupKey(data.lookupKey); + if (!priceResult) { logger.error({ lookupKey: data.lookupKey }, 'validateInvoiceDetails: No price found'); return null; } + const priceId = priceResult.priceId; try { const price = await stripe.prices.retrieve(priceId, { expand: ['product'] }); From c6c457dcfa177c22024d67bbfffea828bba1afcf Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Apr 2026 11:42:57 +0000 Subject: [PATCH 2/3] docs: add Media Buy Lifecycle Flow page with state diagram and PG deal variation Closes #2570. https://claude.ai/code/session_01HULrqPhn8kLM76nSmcpj4d --- .changeset/docs-media-buy-lifecycle-page.md | 6 + docs.json | 2 + .../sales-agent-creative-capabilities.mdx | 1 + docs/media-buy/media-buys/lifecycle.mdx | 170 ++++++++++++++++++ docs/quickstart.mdx | 1 + 5 files changed, 180 insertions(+) create mode 100644 .changeset/docs-media-buy-lifecycle-page.md create mode 100644 docs/media-buy/media-buys/lifecycle.mdx diff --git a/.changeset/docs-media-buy-lifecycle-page.md b/.changeset/docs-media-buy-lifecycle-page.md new file mode 100644 index 0000000000..7fa8a57f2f --- /dev/null +++ b/.changeset/docs-media-buy-lifecycle-page.md @@ -0,0 +1,6 @@ +--- +--- + +Add new canonical Media Buy Lifecycle Flow page (`docs/media-buy/media-buys/lifecycle.mdx`) covering the sequenced task flow, full state machine diagram, guaranteed/PG deal IO acceptance path, and creative sync timing. Adds nav entries in both navigation tabs, a link in the quickstart What's next section, and a link from `sales-agent-creative-capabilities.mdx`. Closes #2570. + +Docs-only change — no protocol schema changes. diff --git a/docs.json b/docs.json index 736a13c432..1b180193c4 100644 --- a/docs.json +++ b/docs.json @@ -190,6 +190,7 @@ "docs/media-buy/product-discovery/collections-and-installments", "docs/media-buy/product-discovery/refinement", "docs/media-buy/media-buys/index", + "docs/media-buy/media-buys/lifecycle", "docs/media-buy/media-buys/optimization-reporting", "docs/media-buy/media-buys/policy-compliance", "docs/media-buy/creatives/index", @@ -696,6 +697,7 @@ "docs/media-buy/product-discovery/collections-and-installments", "docs/media-buy/product-discovery/refinement", "docs/media-buy/media-buys/index", + "docs/media-buy/media-buys/lifecycle", "docs/media-buy/media-buys/optimization-reporting", "docs/media-buy/media-buys/policy-compliance", "docs/media-buy/creatives/index", diff --git a/docs/creative/sales-agent-creative-capabilities.mdx b/docs/creative/sales-agent-creative-capabilities.mdx index f0ce9e3e20..cd1a33f7b0 100644 --- a/docs/creative/sales-agent-creative-capabilities.mdx +++ b/docs/creative/sales-agent-creative-capabilities.mdx @@ -311,3 +311,4 @@ if (managesCreatives) { - [Generative Creative](/docs/creative/generative-creative) — Using `build_creative` for AI-powered generation - [get_creative_delivery](/docs/creative/task-reference/get_creative_delivery) — Variant-level delivery reporting - [get_media_buy_delivery](/docs/media-buy/task-reference/get_media_buy_delivery) — Media buy delivery reporting +- [Media Buy Lifecycle](/docs/media-buy/media-buys/lifecycle) — State machine, sequenced flow, guaranteed deal IO path, creative sync timing diff --git a/docs/media-buy/media-buys/lifecycle.mdx b/docs/media-buy/media-buys/lifecycle.mdx new file mode 100644 index 0000000000..cedaef7298 --- /dev/null +++ b/docs/media-buy/media-buys/lifecycle.mdx @@ -0,0 +1,170 @@ +--- +title: Media Buy Lifecycle Flow +description: "Step-by-step sequence from product discovery through delivery, including the guaranteed-deal IO acceptance path and creative sync timing." +"og:title": "AdCP — Media Buy Lifecycle Flow" +--- + +This page is the canonical sequence reference for media buy lifecycle. For conceptual background on the full lifecycle — campaign structure, package model, property targeting, and async operations — see [Media Buy Lifecycle](/docs/media-buy/media-buys/). + +## Standard flow + +Every media buy follows four steps: + +```mermaid +flowchart TD + A[get_products] --> B[create_media_buy] + B --> C{initial state} + C -->|creatives missing| D[pending_creatives] + C -->|creatives present,\nflight not yet started| E[pending_start] + C -->|creatives present,\nflight started| F[active] + D --> G[sync_creatives] + G --> E + E --> F + F --> H{delivery} + H -->|budget exhausted /\ngoal met / flight ended| I[completed] + H -->|buyer or seller action| J[paused] + J --> F +``` + +1. **`get_products`** — discover available inventory matching your brief. +2. **`create_media_buy`** — submit packages; the seller validates and confirms. +3. **`sync_creatives`** — assign creative assets to packages that need them. +4. **Delivery** — the buy enters `active`, accrues impressions, and eventually reaches a terminal state. + +## State machine + +### Media buy states + +| State | Meaning | Terminal? | +|-------|---------|-----------| +| `pending_creatives` | Approved; no creatives assigned yet | No | +| `pending_start` | Creatives assigned; waiting for flight date | No | +| `active` | Delivering impressions | No | +| `paused` | Temporarily halted | No | +| `completed` | Flight ended, goal met, or budget exhausted | Yes | +| `rejected` | Seller declined the buy | Yes | +| `canceled` | Buyer or seller terminated before completion | Yes | + + +`pending_manual` and `pending_permission` are **task-level** statuses — they describe whether the *operation* (e.g., `create_media_buy`) is queued for human review, not the media buy's own state. The media buy enters `pending_creatives`, `pending_start`, or `active` once the operation completes. See [Asynchronous Operations](/docs/media-buy/media-buys/#asynchronous-operations-and-human-in-the-loop). + + +### Transitions + +```mermaid +stateDiagram-v2 + [*] --> pending_creatives : create_media_buy\n(no creatives) + [*] --> pending_start : create_media_buy\n(creatives present,\nflight future) + [*] --> active : create_media_buy\n(creatives present,\nflight started) + + pending_creatives --> pending_start : sync_creatives + pending_start --> active : flight date reached + + active --> paused : update_media_buy\n(paused: true) + paused --> active : update_media_buy\n(paused: false) + + active --> completed : flight ended /\ngoal met / budget exhausted + paused --> completed : flight ended /\ngoal met / budget exhausted + + pending_creatives --> rejected : seller declines + pending_start --> rejected : seller declines + + pending_creatives --> canceled : update_media_buy\n(canceled: true) + pending_start --> canceled : update_media_buy\n(canceled: true) + active --> canceled : update_media_buy\n(canceled: true) + paused --> canceled : update_media_buy\n(canceled: true) + + completed --> [*] + rejected --> [*] + canceled --> [*] +``` + +### Discovering valid actions at runtime + +Rather than hardcoding the state machine, read `valid_actions` from `get_media_buys`. The seller returns exactly what the buyer can do in the current state: + +```json +{ + "media_buy_id": "mb_12345", + "status": "active", + "revision": 3, + "valid_actions": ["pause", "cancel", "update_budget", "update_dates", "update_packages", "add_packages", "sync_creatives"] +} +``` + +Always pass `revision` in `update_media_buy` calls. The seller rejects with `CONFLICT` if the revision has changed since your last read. + +## Guaranteed / PG deal variation + +Products with `delivery_type: "guaranteed"` require contractual commitment before delivery begins. The flow diverges after `create_media_buy`: + +```mermaid +flowchart TD + A[get_products\ndelivery_type: guaranteed] --> B[create_media_buy\nwith accountability_terms] + B --> C{task status} + C -->|IO signing needed| D[submitted\ntask_id returned] + C -->|IO pre-signed| E[pending_creatives\nor pending_start] + D --> F[IO signed\nout-of-band] + F --> E + E --> G[sync_creatives\nif needed] + G --> H[active — guaranteed delivery] + H --> I{performance} + I -->|standards met| J[completed] + I -->|under-delivery| K[makegood / remediation] + K --> J +``` + +### What makes a guaranteed buy different + +**`accountability_terms` are required** on each package with a guaranteed product. Three fields are required: + +- `performance_standards` — viewability, IVT, completion rate, and other thresholds with measurement vendor +- `measurement_terms` — who counts the billing metric, acceptable variance, and makegood remedies +- `cancellation_policy` — notice period and cancellation fee for early termination + +Omitting any of these on a guaranteed package causes the seller to return `TERMS_REJECTED`. + +**IO signing** — `create_media_buy` for a guaranteed product may return task status `submitted` with a `task_id` rather than completing synchronously. This means the seller's system is awaiting insertion order (IO) acceptance. Poll with `tasks/get` or configure a webhook. Once the IO is signed, the completion artifact carries the `media_buy_id` and the media buy enters `pending_creatives` or `pending_start`. + +**Makegoods** — if the seller under-delivers against agreed `performance_standards`, they propose a remedy from the `makegood_policy`: `additional_delivery`, `credit`, or `invoice_adjustment`. The buyer accepts or disputes. + + +A seller who accepts without under-delivering earns a favorable accountability signal. See [Accountability](/docs/media-buy/advanced-topics/accountability) for the full negotiation flow including how buyers can propose non-default terms at `create_media_buy` time. + + +## Creative sync timing + +### When creatives are required + +`create_media_buy` accepts inline `creative_assignments` or `creatives` per package. If you supply them at creation time and the flight date has passed, the buy enters `active` directly. If the flight date is in the future, it enters `pending_start`. + +If no creatives are assigned at creation, the buy enters `pending_creatives`. Delivery cannot begin until `sync_creatives` is called to assign at least one creative per package. + +### The `creative_deadline` + +`create_media_buy` returns a `creative_deadline` timestamp on the media buy response. Individual packages may carry their own `creative_deadline`. **Package-level deadlines take precedence over the media buy deadline.** This matters for mixed-channel orders — a print package may have a material deadline days before the digital packages in the same buy. + +After the deadline, `sync_creatives` calls for that package return `CREATIVE_REJECTED`. Creative changes are blocked; delivery continues with whatever creatives are currently assigned (or the package remains in `pending_creatives` if none were ever assigned). + +``` +Deadline hierarchy: + package.creative_deadline (if present — wins) + ↓ else + media_buy.creative_deadline +``` + +### Effect on creatives when a buy ends + +When a media buy reaches `rejected`, `canceled`, or `completed`, creative assignments are released. The creatives themselves are not deleted — they remain in the library with their existing review status and are available for assignment to other media buys. + + +Creative library state and creative assignment state are tracked independently. A creative that was assigned to a canceled buy still has whatever review status it earned and can be immediately assigned to a new buy. See [creative state and assignment state](/docs/creative/creative-libraries#creative-state-and-assignment-state-are-separate). + + +## See also + +- [Media Buy Lifecycle](/docs/media-buy/media-buys/) — full lifecycle reference: campaign structure, package model, async operations +- [`create_media_buy`](/docs/media-buy/task-reference/create_media_buy) — task reference with request parameters, response shapes, and examples +- [`sync_creatives`](/docs/creative/task-reference/sync_creatives) — assign and update creative assets on active packages +- [Accountability](/docs/media-buy/advanced-topics/accountability) — performance standards, measurement terms, makegood resolution +- [Optimization & Reporting](/docs/media-buy/media-buys/optimization-reporting) — delivery monitoring, dimensional reporting, campaign updates diff --git a/docs/quickstart.mdx b/docs/quickstart.mdx index dbabb9d4de..dfdd3f0f3a 100644 --- a/docs/quickstart.mdx +++ b/docs/quickstart.mdx @@ -299,6 +299,7 @@ console.log(result.data.products); - **[Compliance Catalog](/docs/building/compliance-catalog)** — the domains and specialisms an agent can claim, and the storyboards that verify each claim - **[MCP integration guide](/docs/building/integration/mcp-guide)** — transport, sessions, auth details - **[A2A integration guide](/docs/building/integration/a2a-guide)** — streaming, artifacts, push notifications +- **[Media Buy Lifecycle](/docs/media-buy/media-buys/lifecycle)** — state machine, sequenced flow, guaranteed deal IO path, creative sync timing - **[Task reference](/docs/media-buy/task-reference)** — all available tasks with testable examples - **[Error handling](/docs/building/implementation/transport-errors)** — error codes, recovery strategies - **[Authentication](/docs/building/integration/authentication)** — production credential setup From 86aa6bf5f4649dfdd2dd92aea4d3fe9fe9d9172e Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Apr 2026 11:45:33 +0000 Subject: [PATCH 3/3] =?UTF-8?q?docs(academy):=20add=20Track=20B=20publishe?= =?UTF-8?q?r/SSP=20on-ramp=20=E2=80=94=20B0=20module=20and=20landing=20sec?= =?UTF-8?q?tion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #2572. https://claude.ai/code/session_01HULrqPhn8kLM76nSmcpj4d --- .../academy-track-b-publisher-onramp.md | 6 ++++ docs/intro.mdx | 3 ++ docs/learning/overview.mdx | 12 ++++++++ docs/learning/tracks/publisher.mdx | 29 +++++++++++++++++++ 4 files changed, 50 insertions(+) create mode 100644 .changeset/academy-track-b-publisher-onramp.md diff --git a/.changeset/academy-track-b-publisher-onramp.md b/.changeset/academy-track-b-publisher-onramp.md new file mode 100644 index 0000000000..692d2eb5c5 --- /dev/null +++ b/.changeset/academy-track-b-publisher-onramp.md @@ -0,0 +1,6 @@ +--- +--- + +Add B0 pre-module ("Is Track B right for you?") to the publisher/SSP track with sell-side value prop and role → module mapping table. Add "For Publishers & SSPs" landing section to the Academy overview page. Cross-link from the intro homepage's "Where do you want to start?" section. Closes #2572. + +Docs/academy-only change — no protocol schema changes. diff --git a/docs/intro.mdx b/docs/intro.mdx index b45b163053..040d8018db 100644 --- a/docs/intro.mdx +++ b/docs/intro.mdx @@ -607,6 +607,9 @@ Governance isn't a gate that slows things down. It's the safety net that lets yo For platforms, publishers, and developers implementing the protocol + + Track B: expose your inventory to AI buyer agents. Start with B0 to see if it's the right fit. + ## Get started diff --git a/docs/learning/overview.mdx b/docs/learning/overview.mdx index 1208721165..57e1ba7f1c 100644 --- a/docs/learning/overview.mdx +++ b/docs/learning/overview.mdx @@ -84,6 +84,18 @@ Everyone starts here. All three modules are free — no membership required. | [A2](/docs/learning/foundations/a2-protocol-architecture) | Your first media buy | 20 min | Yes | | [A3](/docs/learning/foundations/a3-ecosystem-governance) | The AdCP landscape | 15 min | Yes | +### For publishers & SSPs + +AdCP gives publishers a single endpoint that exposes your product catalog, handles media buy creation, and delivers reporting to AI buyer agents — 24/7, without per-platform API integrations or inbound sales calls. Reach buyers who operate autonomously at programmatic scale while keeping direct deal quality and margins. + +If you work on the sell side, Track B is your path. A quick role map: ad ops and yield teams start with B1 (catalog design) and B3 (delivery reporting and signals); revenue and sales teams focus on B1 and B2 (creative specs); engineers should do the full B1–B4 track, which ends with building and validating a real sales agent. + +[Publisher / seller track](/docs/learning/tracks/publisher) — or start now: + + + "I'd like to start certification module B0." + + ### Role tracks (choose your path) After Basics, choose the track that matches your role. Each track is four modules culminating in a build project. diff --git a/docs/learning/tracks/publisher.mdx b/docs/learning/tracks/publisher.mdx index ef768ec0a4..4f61a3c34f 100644 --- a/docs/learning/tracks/publisher.mdx +++ b/docs/learning/tracks/publisher.mdx @@ -17,6 +17,35 @@ Completing this track (plus A1–A3) earns the **AdCP practitioner** credential. --- +## B0: Is Track B right for you? + +**~10 min** | No prerequisite — can be done before A1 + +AdCP gives publishers a single endpoint that AI buyer agents can use to discover your inventory, negotiate and create media buys, and pull delivery data — without a sales call, a custom API integration, or giving up your direct deal terms. One implementation; always-on access for buyers that operate autonomously. + +### Who Track B is for + +| Your role | Where to start | Modules that matter most | +|-----------|---------------|--------------------------| +| Ad operations / yield manager | B1, then B3 | B1 (catalog design), B3 (delivery reporting, signals) | +| Sales / revenue | B1, then B2 | B1 (what buyers see), B2 (creative specs) | +| Engineer / developer | B1 through B4 | Full track — ends with building and validating a real sales agent | +| Executive / strategist | B0 orientation, then A1–A3 | Track A for protocol and ecosystem context | + +If you're not sure whether you need the full track: engineers and ad tech builders should do B1–B4. Anyone else can take B0 with Addie now, then decide. + +### What you'll build toward + +- B4 is a build project: you create a working sales agent that handles real buyer queries — product discovery, media buy creation, creative specs, and delivery reporting +- The agent is validated against AdCP storyboards, not just reviewed manually +- Completing B1–B4 (plus A1–A3) earns the **AdCP practitioner** credential + + + "I'd like to start certification module B0." + + +--- + ## B1: Designing your product catalog **~20 min** | Prerequisite: A3