Skip to main content
A modular inventory source is an operator-managed source composed from smaller modules: an inventory feed, an optional booking ledger, optional execution modules, and optional human work queues. The storefront still presents one buyer-facing sales-agent surface. The modules describe how that source gets inventory, prevents overbooking, books upstream supply, traffics campaigns, syncs creatives, and imports reporting. Use a modular source when inventory and execution do not come from one fully compliant upstream sales agent. For example, a publisher may send a periodic avails spreadsheet, confirm bookings manually, traffic a campaign in an addressable TV system, and upload the final delivery report after the campaign ends.
Modular source operations are storefront-operator endpoints. Buyer agents do not call them directly. Buyers still transact through the storefront’s AdCP media-buy workflow.
Custom modular sources are included with Premium and Enterprise through the Merchandising profile. Basic does not include custom composition. Standard managed integrations are included with every seller plan and do not require separate access. The stable internal feature key is modular-sources; customer-facing surfaces display Custom modular sources. Existing buyer fulfillment and source runtime operations continue after a downgrade to Basic. The restriction applies to new custom composition, module attachment, and demo resets that replace the source composition.
Storefront MCP clients open the same three portable surfaces with prepare_modular_source, get_modular_inventory_source_readiness, and prepare_modular_avails_commit. The Page and Tasks use named api_call operations for their data and confirmed writes. The operator-facing operation set also includes list_modular_inventory_source_products, get_modular_inventory_capabilities, and search_modular_source_inventory_selectors. Work-item detail, capacity and booking changes, module configuration, and credential entry remain in confirmation-gated trusted Tasks or the operator UI. On the unified /mcp/v3 storefront surface, use open_page instead of calling those compatibility tools directly: These v3 page names launch the same portable MCP Apps used by other supported hosts; they are not Murph-only shortcuts. Feed files stay in the host-to-Page upload flow, so the model does not receive the feed bytes. Creating and committing remain explicit, confirmation-gated writes. All examples use the storefront base URL:

When to use a modular source

Set up your storefront first, then decide which source type each inventory system should become: A modular source is the right pilot shape when the seller can provide a feed of wholesale inventory but cannot yet expose the whole lifecycle through one upstream API.

Open the fictional publisher test pack

Copy complete CTV and display feeds, storefront materials, operating evidence, and a machine-graded buyer-brief corpus. Every property, format, price, and date is synthetic.
A manual or back-office source is modular because no single system can answer the complete AdCP workflow. A spreadsheet may describe what can be sold, an OMS may record the booking, an ad server may execute it, and a person may move the creative or final report between them. Modeling those responsibilities as modules makes every supported and human-owned step explicit without claiming automation that is not connected. Replacing one module with an integration is different from replacing the whole modular source with an external AdCP sales agent. A whole-source replacement changes the publisher authorization target: before the seller depends on the replacement inventory, each publisher authorizes the external agent’s URL in its own adagents.json. The modular source does not retroactively become an Interchange authorization case. See Changing from a modular source to an external agent.

Modules

Each module owns one part of the lifecycle. A source can start with manual modules and later replace a step with an automated integration. Every modular setup must account for these lifecycle responsibilities, even when the current answer is a named human rather than an integration: Read a stage across four separate axes: execution mode (automated, human, or hybrid), configuration (whether its module and credentials are set up), runtime input (whether this transaction has supplied what the stage needs), and declared capability (whether the source supports the operation at all). Human ownership does not by itself mean setup is missing. A configured manual stage can be ready for transactions while a particular media buy still creates seller-owned work. Conversely, an unsupported capability is not a task and requires no action until the source composition changes. Open manual booking, creative-sync, and final-reporting work appears both in the source workspace and in Pending Operations. Both surfaces use the same buyer-customer, media-buy, source, and work-item identifiers; opening a Pending Operations row focuses that exact work item. A blocked item remains seller-owned but says No action now until its prerequisite clears. The CitrusAd pilot is a standard composition, so sellers connect it as an ad server rather than assembling modules. It uses a retailer-namespace catalog module and a campaign-execution module behind one inventory source. It is a standard managed integration included with every seller plan; its dedicated ad-server route presents a simplified recipe instead of the advanced modular builder. The storefront remains in CHEF routing mode; buyers see the storefront, not a CitrusAd adapter or the private module graph. The first recipe supports sponsored-product campaigns. Their creative is rendered from retailer catalog data, so creative sync completes without an asset upload. CitrusAd also offers static banners and Banner X, but those are not exposed by this recipe until separate banner campaign and creative APIs are implemented and verified. Connect the standard recipe through POST /inventory-sources/ad-servers/citrusad, then save the storefront-scoped credential through POST /inventory-sources/{sourceId}/ad-servers/citrusad/credentials/{credentialKind}. These ad-server endpoints do not expose the private module graph.

Module credentials

Credentials belong to one module instance on one storefront inventory source. They are not process environment variables and are not shared across storefronts. Raw auth values are written to the configured secret manager; the API response contains status, scopes, expiry, and other non-secret metadata only. Credential mutations require a storefront admin role or the dedicated StorefrontCredentialsAdmin permission. Create a credential with POST, rotate it with PUT, and revoke it with DELETE at:
credentialKind is API_KEY, BEARER, or OAUTH. For example:
Credential-shaped fields are rejected from module config and credential metadata. Put all provider secrets under auth. Storefront-level approval policies still apply outside the modular source. For example, advertiser approval and creative review can happen before a modular source asks a human to map the approved creative into an upstream ad server.

Seller setup checklist

Send the publisher onboarding starter kit before setup. It is the reusable evidence checklist for identity, properties, inventory, pricing, avails, booking, creative, trafficking, reporting, and recurring ownership. It also includes static-avails-feed:v1 compatibility CSV and JSON templates and explains which evidence is needed for each readiness milestone. Ask the seller for what they already have and request only the gaps. Media kits, co-viewing studies, market-size notes, and screenshots remain useful context, but they are not avails feeds unless they contain row-level sellable capacity for a specific inventory scope and time window.

The three setup surfaces in chat

Setting up and running a modular source is three surfaces, each with one job. Ask Murph for any of them by name, or move between them from the workspace. Two things follow from that split:
  • Creating a source does not ask how you book, traffic, clear creative, or report. Those are derived per lifecycle stage from the modules attached to the source, and shown in the workspace — see Check readiness. Every modular source starts with an avails-feed module and Interchange’s booking ledger.
  • Nothing is offered to buyers until avails rows are committed. A source with no committed rows is registered but has no sellable capacity.

Preview buyer discovery from one source (read-only)

When Get products is Ready in the Modular inventory source workspace, select Open read-only discovery preview. In the Read-only source discovery preview Task, enter a representative buyer brief and select Run read-only preview. The Task shows only products that source would return through your storefront. The preview rechecks that the source is active and Get products is still Ready when you run it. If either check fails, the Task stops and asks you to finish the source setup. It never includes products from another inventory source. The Task records the preview decision for later review. It does not create a media buy, change live products or source setup, or turn on transacting. Brief tests started outside a source workspace remain storefront-wide.

Optional ad-server automation

Attaching ad-server automation to a modular source automates a lifecycle stage for that same inventory and operating boundary. It does not create another source. Add a parallel source only when it represents distinct inventory, authorization, or operational ownership—not merely because trafficking moves from a human workflow to an integration.

Avails feed requirements

The first successful upload should be boring: one row per sellable line, with stable ids and clear capacity semantics. Required columns:
The current static-avails-feed:v1 parser is a production compatibility path, not the target Inventory Feed schema. Its exact collectionId, collectionName, and collectionDescription names are legacy grouping/container fields. Their values do not establish AdCP Collection identity. Do not model a monthly pool, placement, channel, or portfolio as an AdCP Collection to make a row fit.
Optional columns:
The current modular avails-feed contract is impression-based: it requires impressionsCapacity and can carry an optional CPM. It does not yet represent click/engagement pricing, a whole-flight flat rate, or time-based sponsorship pricing. Do not convert those commercial models into invented impressions or CPMs. Keep the original rate and capacity semantics in the source material and ask Scope3 to add the missing contract support before committing those rows as sellable avails.
If the seller only has gross capacity, include one of avails, impressions, monthlyAvails, monthlyVideoAvails, or monthlyHomeScreenAvails, plus bookedImpressions, upstreamBookedImpressions, booked, or reservedImpressions. Preview will calculate net capacity as:
If upstream booked capacity exceeds gross capacity, preview rejects the row instead of silently clamping it to zero. Upload CSV, XLS, or XLSX files. Send JSON as jsonText, or upload it as a Murph document and pass its documentFileId. Pasted csvText can parse tab-delimited text, but .tsv is not a supported file-upload format; convert it to CSV first. Impression-count fields can use plain numbers or common suffixes such as 150m, 25 million, or 1.5B. Money fields such as CPM must use an ordinary monetary number; a magnitude suffix such as 5m is rejected.
Copy the static-avails-feed:v1 compatibility CSV or JSON template from the inventory source input guide.

Murph walkthrough

Murph should not force a seller into a blank template. A good setup conversation looks like this:
  1. Ask the seller to upload what they have: deck, media kit, rate card, publisher list, avails export, booking export, creative specs, reporting export, or screenshots.
  2. Classify the material. Keep market context as context; do not commit it as avails.
  3. Ask for missing row-level avails if the seller only supplied market scale.
  4. Create the modular source only after the operator confirms the source name and lifecycle plan.
  5. Preview the feed. Show normalized rows and whether capacity is net or gross-minus-booked.
  6. Commit only after explicit confirmation.
  7. Run a small test reservation and validate the handoff requirements. Only run finalization when the source has a real execution module or a configured demo execution module.
  8. Route unsupported integrations or missing systems into the normal Murph escalation flow so the need is tracked centrally.
For example, a note that says “150 million monthly video avails and 1.5 billion home-screen avails” is not enough to launch. Murph should ask for actual rows with stable inventory-scope and availability ids, dates, net capacity or booked counts, pricing, and reporting/booking ownership. The legacy static-v1 grouping fields do not turn those pools into AdCP Collections.

Drafting rows from facts you confirm

Drafting is different from inventing, and the boundary is who states the sellable facts. When you have uploaded your own media kit or rate card, Murph may assemble the feed rows for you rather than asking you to produce a CSV: placement names, formats, and rate-card prices may be seeded from your own document, but net sellable capacity, the availability window, and the row grain must come from you, stated in the conversation — Murph never estimates them, and reach or scale claims in a document are never converted into capacity. Every draft is previewed back to you as normalized rows first, and committing the feed — putting rows on sale — always requires your explicit confirmation on a server-enforced control, whether the rows were drafted, pasted, or uploaded. If you can’t state capacity or windows yet, Murph asks for your actual avails export instead. (Conversational drafting is rolling out and may not yet be enabled for your account; the confirmation control on feed commits applies to every account.)

1. Check readiness

Start with the runtime projection:
The response includes lifecycleSummary, one row per stage: activeAvailCount tells you whether the source currently has usable avails. Rows whose endTime has passed do not count as active and are not projected as sellable products. openWorkItemCountsByKind tells you what human queue is blocking a module or stage, for example MAP_CREATIVE_REF or UPLOAD_FINAL_REPORT. The same response includes catalogMappingReadiness, a tenant-scoped read projection for this exact source. It has three stable sections: Set up inventory, Make it merchandisable, and Prove it. Each row reports its requirement, current status, evidence, freshness, source of truth, diagnostics, external owner when one exists, and one typed next action. status: null means the owning system could not be read, so the row is Unknown and never counts as ready. The top-level progress and nextAction are the same values Seller Setup displays; neither surface stores a separate checklist. Catalog readiness covers the feed and current module contract, Property Roster authorization and format applicability, components, products, source pricing, Playbook, AI Business Rules, property-to-decisioning mappings, and source-scoped brief proof. Booking, creative, trafficking, and reporting stay in the lifecycleSummary pipeline.

2. Preview, then commit avails

An inventory-feed module accepts normalized avails. The feed can come from a publisher upload, a pasted CSV/JSON payload, or a file that Murph has parsed into rows. Murph previews parsed rows first; commit the feed only after an operator confirms the normalized rows and confirms impressionsCapacity is net sellable capacity. A preview is row-tolerant: one invalid row does not hide valid rows. The response returns normalized accepted rows in avails, invalid source rows in rejectedRows, and non-blocking normalization facts in warnings, with matching counts. Each diagnostic has a stable code, source rowNumber, safe message, and field when the field is known. rowNumber is the one-based data-row ordinal after excluding the header, so CSV data row 1 is file line 2. For JSON, it is the one-based position in the avails array. Commit by sending only the accepted avails back with confirmed: true; rejected rows and warnings are never committed.
A commit is an upsert patch keyed by availId, not a full-feed replacement. Rows omitted from a later file remain unchanged, and a rejected update does not retire the last committed version of that row. Resolve every rejected row before committing a complete refresh. There is currently no omission-as-delete or per-row archive operation: archive the source or let a row expire, and contact Scope3 when a committed row must be retired early.
Current row diagnostic codes are REQUIRED_FIELD_MISSING, INVALID_DATE_TIME, INVALID_NUMBER, INVALID_CURRENCY, INVALID_CANONICAL_FORMATS, INVALID_PUBLISHER_PROPERTIES, GROSS_CAPACITY_NORMALIZED, and GROSS_CAPACITY_ASSUMED_NET, plus the CATALOG_* Property Roster resolution codes published by the static-avails-feed:v1 profile. The two gross-capacity codes are warnings: preview either subtracted upstream-booked impressions from gross capacity, or found no booked input and treated the gross-capacity field as net pending operator confirmation. Warnings explain normalization only; they never make an otherwise ineligible row committable. Formats and Properties are attached to the static-v1 compatibility grouping rather than one availability window. Supply formatOptions and publisherProperties on each row, or send defaultFormatOptions and defaultPublisherProperties alongside a JSON preview request. Spreadsheet cells use JSON-encoded arrays. Every row with the same legacy collectionId field must repeat the same declarations. This attachment does not establish AdCP Collection identity. The service rejects missing, conflicting, malformed, or URL-bearing declarations and never infers a format from channel. Preview also resolves each declaration against the current tenant’s Property Roster, but catalog proof is an activation gate rather than an authoring gate. Every structurally valid row remains in the preview response; activationReady and activationBlockers distinguish authored rows from rows eligible to commit. An unauthorized or unavailable publisher, incomplete roster reconciliation, missing or ambiguous property identity, absent or mismatched canonical format, or format outside the selected publisher-property scope blocks commit. The row is not buyer-visible, reservable, or traffickable while blocked. Commit repeats the catalog read so a preview cannot bypass later roster drift. Accepted rows retain their source row number and resolved canonical property and format identities in server-owned source metadata. For a specifically approved pilot, Scope3 may grant an expiring exception for one exact availId so a paused Storefront can commit that row and run its seller-owned no-spend lifecycle test. Preview labels this as test-commit ready, not activation ready. The exception does not authorize the publisher or satisfy catalog readiness; ordinary buyer access and activation remain blocked until the publisher-origin catalog resolves.
Ingestion upserts by availId. If an avail already has held or booked impressions, the feed update will not reduce capacity below those commitments.

3. Inspect product projections

List the product-like projections generated from active avails:
Each product maps back to one normalized avail and returns the compatibility grouping’s canonical formatOptions and publisherProperties. A grouping missing valid declarations fails closed with the typed MODULAR_CATALOG_NOT_READY readiness code before the Merchandising Agent can use it. The legacy grouping does not become an AdCP Collection. Generic feed-only sources return cadent: null. Cadent fields appear only when the source has an active Cadent execution module, so uploaded publisher avails are not implicitly treated as Cadent-backed inventory.

4. Reserve capacity

If the source has an active booking-ledger module, hold capacity before finalizing:
You can pass either productId or availId. The reservation prevents this modular source from overbooking against capacity it controls. Upstream booked supply must be removed from impressionsCapacity before commit; upstreamBookedImpressions is explanatory metadata and does not increase sellable capacity.

5. Prepare supported execution handoffs

Execution is module-specific. In the current Cadent demo path, the handoff requires an active booking ledger and an active Cadent execution module. It keeps the reservation HELD, renders the Cadent campaign XML, and creates source-side work items for trafficking, creative mapping, and reporting. Do not treat the line as upstream-booked until the trafficking step succeeds; completing the TRAFFIC_TO_CADENT work item moves the held reservation to BOOKED.
For a source that books through a person rather than an execution API, create the source with execution.mode: "manual". Interchange attaches the provider-neutral manual execution module. Finalizing a held booking then opens three seller work items: EXECUTE_UPSTREAM_BOOKING, SYNC_UPSTREAM_CREATIVE, and UPLOAD_FINAL_REPORT. Completing them records the upstream campaign/order/placement ids, creative references, and final delivery evidence on the booking; the first completion moves capacity from HELD to BOOKED, and the final report moves it to DELIVERED.

6. Release capacity

Release a held reservation when a buyer cancels, a hold expires, or the execution handoff fails:
Downgrading to Basic does not cancel or delete modular-source lifecycle state. Existing HELD reservations continue to reduce available capacity until they expire or are released through this operation; BOOKED reservations remain booked. Open source-side work items and pending reports keep their current status and history. The downgrade prevents new composition and hides pilot surfaces, but it does not represent upstream cancellation, completed human work, or completed reporting.

7. Work the human queue

Some stages are intentionally human-in-the-loop. Common examples: These work items belong to the inventory source, not the buyer-facing storefront task. The storefront can still expose buyer task status or webhooks while source operators complete the underlying queue. RESOLVE_SOURCE_HEALTH tracks one continuous outage, not one item per failed check: it opens when the source first goes unhealthy and closes automatically once the source recovers. Repeated checks update the source’s health diagnostics while the same item remains open instead of raising duplicates. If Scope3 later determines the outage is on our side rather than yours, the item is withdrawn rather than left open against you.

Demo profiles and legacy source recipes

The canonical product demo is Demo Storefront. Its versioned, code-owned profiles compose existing modular contracts and are clearly labeled synthetic. They do not call or claim to emulate a vendor API. POST /inventory-sources/modular/cadent-demo creates or resets a demo-only source with static avails, a booking ledger, and Cadent execution. Use it to test the end-to-end lifecycle, not as a publisher avails ingestion path. Demo source IDs must be talpa-cadent-demo or start with demo-. That Cadent-shaped recipe and the FreeWheel sandbox route remain useful integration-development precedents. They are not the backing data plane for Demo Storefront and do not turn a vendor sandbox into the canonical demo.

Prepare inventory source inputs

Evidence checklist, templates, recovery steps, and readiness milestones

Get modular readiness

Inspect lifecycle status, module contracts, setup gaps, and work-item counts

Update module config

Write non-secret module setup fields