> ## Documentation Index
> Fetch the complete documentation index at: https://docs.apostra.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Seller Workflows

> Use v3 to understand readiness, configure a Seller Account, operate work, and inspect delivery.

<Note>
  These workflows require a Seller Account. In v3, “Seller Account” is the
  operator workspace; a Storefront is the buyer-facing selling surface it
  operates.
</Note>

## Operate sponsored self-serve client campaigns

When a client operates a self-serve advertiser sponsored by your Seller
Account, select it in the shared Advertisers rail and open Campaigns. You can
edit the same campaign fields that the client can edit, including campaign
details, flight, budget, and lifecycle actions available in Campaigns.

The client remains the campaign owner. Each change is recorded in the
campaign's history and your Seller activity as `Edited by <your seller
organization> (operator)`, so both sides can see that your organization made
the edit. This authority applies only to that active sponsored client and its
linked Storefront; it does not apply to external, delegated, or System
advertisers. System advertisers remain read-only.

## Begin with readiness

Call `get_status` before changing configuration. Its blockers explain why work
is stopped before a buyer request reaches an inventory source and provide a
typed tool or Page for the next action.

Do not infer demand from source traffic alone. A Seller Account that is not
ready may correctly have no source calls because work stops earlier.

## Read and configure Seller identity

The Seller Account is a singleton, so read it without an ID:

```json theme={null}
{ "kind": "seller" }
```

The projection distinguishes:

* `configuredCapabilities` — the declaration you saved; and
* `capabilities` — what buyers can use after source topology and approval
  policy are applied.

These values may disagree. `capabilitiesLocked` and `capabilityDerivation`
explain why. Send capability patches rather than replacing the whole set:

```json theme={null}
{ "capabilities": { "offersCreativeReview": true } }
```

`setupIntent` describes how the Seller Account is intended to connect. It does
not apply a capability preset or change the capability flags. Declare each
supported capability explicitly through `capabilities`; then read the Seller
again to see the effective values derived from source topology and policy.

Changing `operatorDomain` may invalidate profile fields curated for the old
identity. If populated fields would be cleared, the tool refuses the write and
requires `confirmOperatorDomainProfileReset: true` after you review the impact.

## Admit an external buyer and inspect delivery

The buyer declares its Advertiser footprint before it connects. `brandCountries`
and `channels` are the countries and channels the Seller listing is qualified
against; they are not display-only labels. The buyer then uses
`save_connection` to connect that Advertiser to the selected Seller.

```json theme={null}
{
  "advertiserId": "ADVERTISER_ID",
  "brandCountries": ["US"],
  "channels": ["display"]
}
```

In the Seller Account, read the counterparty roster explicitly:

```json theme={null}
{ "kind": "account_relationship" }
```

Follow a returned relationship with `get` to obtain the current admission state
and, when it is awaiting the Seller, the exact intake ID and version:

```json theme={null}
{ "kind": "account_relationship", "id": "RELATIONSHIP_ID" }
```

Only a seller-controlled pending intake can be decided. Admit it in a separate
`save_seller` call, preserving the version from that read:

```json theme={null}
{
  "admission": {
    "intakeId": "INTAKE_ID",
    "expectedVersion": 3,
    "decision": "admit"
  }
}
```

Use `decision: "decline"` with a non-empty `message` to refuse an intake.
External-authority and delegated requests are reported as such and cannot be
decided by this Seller. A successful seller decision can still await
ready-to-invoice evidence before an active grant is available; re-read the
relationship rather than assuming that it is immediately eligible.

Open the existing relationship-scoped Media Buys Page to inspect the admitted
buyer's execution and delivery:

```json theme={null}
{
  "accountRelationshipId": "RELATIONSHIP_ID",
  "view": "delivery"
}
```

The relationship ID is checked against the current Seller Account before the
Page opens. A foreign or unknown relationship opens nothing.

## Add and diagnose inventory

Use `save_inventory_source` for typed source configuration. For an existing
modular source, pass `moduleConfig` with its seller-local `moduleInstanceId` to
update one module's non-secret configuration; save source fields in a separate
call so each write has an unambiguous outcome. Credentials and
OAuth grants stay in the relevant Page rather than model-visible arguments.

Read a source with connection and diagnostic evidence:

```json theme={null}
{
  "kind": "inventory_source",
  "id": "SOURCE_ID",
  "include": ["diagnostics"]
}
```

Connection and health answer different questions: usable credentials can exist
while every call fails. Diagnostics follow the source's actual connection. If
the source has no supported diagnostic marker, the include is reported as
unsupported instead of guessing another source or agent ID.

For a modular source, request its module roster and lifecycle coverage with:

```json theme={null}
{
  "kind": "inventory_source",
  "id": "SOURCE_ID",
  "include": ["modularReadiness"]
}
```

The response places a compact `moduleRoster` before the detailed
`modularReadiness` projection. `moduleRoster.modules` is the complete attached
module list and carries each module's instance ID, kind, name, contract ID, and
status; `supportedStages` lists the lifecycle stages covered by the attached
modules. A null contract ID means the contract could not be resolved — it does
not mean the module is absent. The detailed readiness projection can be large,
so treat an explicit truncation notice as evidence that only a prefix of that
detail is visible.

### Testing an ad-server connection

`get` contacts no ad server. Every connection fact it returns — the adapter
block, health, sync state — is state recorded earlier, and none of it is
evidence that the connection works right now.

For an ad-server-backed source the read makes that explicit in `adServerProbe`:
when the connection was last probed, what that probe recorded, and `null` when
nobody has ever probed it. A connection reporting `connected` with
`adServerProbe.lastTestedAt` of `null` has never been checked against the live
ad server.

Probing is a live call with a real cost, so it belongs to a person rather than
to an agent acting alone. Open `ad_server_diagnostics` and run **Test
connection** there:

```json theme={null}
{ "page": "ad_server_diagnostics", "esaId": "AD_SERVER_CONNECTION_ID" }
```

The Page reports the probe's own result, and the probe updates the recorded
state whether it passed, failed, or could not reach the ad server. On
FreeWheel the same probe is what observes an execution currency, so a source
that has never been probed cannot declare one.

An ad-server-backed source has two identifiers — its `sourceId` and its
ad-server connection id (`managedSa.connectionId`, used as `esaId` on Pages).
`get` accepts either and tells you which it resolved from, so holding one is
never a dead end.

Common Pages include:

* `connect_ad_server` for credentials and OAuth;
* `ad_server_source` for connection management;
* `ad_server_diagnostics` for sync history, refresh, and the connection test; and
* `source_diagnostics` for call-level investigation.

## Manage publisher coverage

Coverage separates declarations from evidence:

* declared domains and properties are operator claims;
* discovered properties remain evidence from publisher sources; and
* authorization is a verdict derived from the current roster and proof.

Read coverage with:

```json theme={null}
{ "kind": "coverage" }
```

Use `save_coverage` to add or remove one domain or property without rebuilding
the entire roster. Domain declarations in one call are applied before property
claims beneath them, so a new domain and property may be declared together.
Removing publisher-origin evidence is refused; retract only claims you own.

## Add and inspect seller material

Sellers can select **Add material** at the top of the Library page to upload a
supported file directly. The Library uses the `save_material` upload lifecycle
below and then reads the finished Material back to show its units, pricing
marks, and reusable toggles. The other paths remain available: drop material
into Murph chat, or upload it while closing a specific Library request.

Use `save_material` to register teaching material without requiring a first-party
Page. Supported source kinds are `url`, `site`, `upload`, `crawl_manifest`,
`inline`, and `history`. For an upload, provide `sha256` with the declared byte
count and media type. The first save returns an authorized, expiring `PUT` URL
and an immutable `assetRef`. Upload the exact bytes with the returned headers,
then call `replace_source` with that `assetRef`; it cannot be attached to a
different Material or a later refresh. Each refresh creates a new source
revision and retains the prior original and renditions.

The processing result has two complementary forms. Rendition blocks and visual
assets preserve extracted text, page or slide structure, images, crops,
captions, layout hints, and source locators for later human presentation. Typed
candidates project reusable facts such as selling points and case-study stories
for review. Neither extracted text nor an image becomes an approved proposal
claim merely because parsing succeeded.

```json theme={null}
{
  "action": "create",
  "clientRequestId": "material:media-kit:v1",
  "source": {
    "kind": "upload",
    "name": "2026 media kit",
    "fileName": "media-kit.pdf",
    "contentType": "application/pdf",
    "sizeBytes": 4280000,
    "sha256": "<64 lowercase hex characters>",
    "fileRole": "deck"
  },
  "metadata": {
    "visibility": "seller_private",
    "verticals": ["retail"],
    "markets": ["US"],
    "channels": ["display"]
  }
}
```

Read it back with:

```json theme={null}
{ "kind": "material", "id": "MATERIAL_ID" }
```

Uploaded decks and one-sheets expose unit summaries. A unit summary says
whether it is `reusable` and whether it `containsPricing`. Every unit starts
with `reusable: false`. Only a unit from an uploaded deck or one-sheet can be
made reusable; sites, URLs, crawl manifests, spreadsheets, and dictated
Material do not yield reusable units.

Use `save_material` with `action: "mark_reusable"` after reading the Material
and the unit you intend to mark. A unit that `containsPricing` cannot be marked
reusable until its price figure is replaced with a rate-card value. A reusable
unit keeps layout figures, while its prices resolve from the rate card.

### Review and confirm extracted candidates

Extraction returns review candidates; it never changes live selling state. Each
candidate names its source Material and revision, stable candidate ID, exact
evidence locator and digest, relevance and confidentiality scope, and canonical
owner. It contains either a schema-valid `proposedMutation` or an `unwritable`
reason.

Material-owned selling points and case-study stories are confirmed through the
`save_material` arguments returned on the candidate. Playbook instructions use
`save_playbook`; AI Business Rules use `save_business_rules`; wholesale products,
pricing, and formats use `save_wholesale_product`; signals use `save_signal`;
and Seller identity uses `save_seller`. Do not move a proposal to another tool
or remove its `materialCandidate` provenance.

An accepted Material-owned selling point or case-study story can ground later
RFP composition when its visibility and relevance match the RFP. Composition
reads only the candidate on the Material's current immutable source revision
and preserves the Material ID, revision, candidate ID, and evidence locator in
the proposal claim. Replacing the source, correcting or withdrawing the
candidate, or changing its scope invalidates Quick RFP reuse. Visual assets pass
a separate rights, confidentiality, expiry, media-type, and accessibility gate;
accepting a text candidate does not authorize its images.

Playbook and AI Business Rules content saves replace their complete active
documents. Extraction therefore returns these suggestions as `unwritable` when
it does not have the current complete destination document. Merge the evidence
with the current document first, then save the complete result through the
named canonical tool; do not confirm a one-line replacement.

For example, a Seller-description candidate can return:

```json theme={null}
{
  "candidateId": "mc_0123456789abcdef0123",
  "sourceMaterialId": "mat_0123456789ab",
  "sourceRevision": 1,
  "candidateType": "seller_identity",
  "proposedOwner": "seller",
  "destinationOwner": "seller",
  "status": "pending",
  "evidence": {
    "sourceRevision": 1,
    "locator": "inline:0-53"
  },
  "evidenceItems": [
    {
      "sourceMaterialId": "mat_0123456789ab",
      "sourceRevision": 1,
      "locator": "inline:0-53",
      "sourceDigest": "sha256:..."
    }
  ],
  "confidentiality": { "visibility": "seller_private" },
  "proposedMutation": {
    "tool": "save_seller",
    "arguments": {
      "description": "Independent publisher reaching local sports fans.",
      "materialCandidate": {
        "sourceMaterialId": "mat_0123456789ab",
        "sourceRevision": 1,
        "candidateId": "mc_0123456789abcdef0123"
      }
    }
  }
}
```

`destinationOwner` and the primary `evidence` locator remain available for
clients built against the first v3 Material response. New clients should use
`proposedOwner` and `evidenceItems` for the complete typed routing and evidence
envelope.

After a person or authorized agent explicitly confirms the proposal, call the
named tool with those exact arguments. A successful save adds
`materialReceipt` to the response. Reading the Material again projects the
bounded receipt with the destination tool and destination identity. Replaying
the same confirmation returns that receipt without writing twice. A conflicting
or concurrent decision returns an error and makes no additional write. If the
recorded destination save succeeds but receipt projection is interrupted,
retrying the same confirmation resumes the receipt projection without running
the destination save again. While a destination call is running, its dispatch
fence is heartbeated and cannot be reassigned. If the process ends without a
usable destination acknowledgment, the result becomes `indeterminate`: do not
repeat the save. Verify the canonical destination and reconcile that application.
Client cancellation after dispatch does not abandon the owner write; its
heartbeat is bounded and stops when the owner settles or the application is
marked indeterminate. An explicit candidate correction versions the exact new
typed proposal under the same source candidate identity, so confirming it does
not reuse the earlier proposal's application or receipt. A correction cannot
replace a reserved, dispatching, partially saved, indeterminate, or
receipt-pending proposal; first let it settle or explicitly reconcile it.

For multi-part Playbook and AI Business Rules proposals, every requested part must
be acknowledged before the candidate becomes accepted. If only some parts are
saved, `materialPartialReceipt` records the completed and pending parts with the
owner response and its `applicationId`. Do not replay the full proposal. Resume
only the pending parts through the same typed owner, then call `save_material`
with `action: "reconcile_candidate"`, that `applicationId`, and the verified
canonical record still in place. Reconciliation re-reads the named typed owner;
it does not accept a caller-supplied destination, dispatch, or repeat an owner save. Exact concurrent or replayed
reconciliation calls return the same receipt.
A late owner acknowledgment is stored as `destination_saved` before Material
projection. Projection failures retain bounded retry evidence and resume
without calling the owner again.

Use `save_material` with `action: "decide_candidate"` and
`decision.action: "reject"` or `"correct"` to record a review outcome without
applying a destination-owned proposal. A corrected typed proposal remains on
its original canonical tool and must be confirmed in a later call to that tool.

<Warning>
  Advertiser-confidential evidence requires an exact `advertiserRef`. Any
  advertiser-scoped candidate is not writable to a Seller-wide record when that
  record cannot preserve the advertiser scope; confirmation returns an error
  without writing that record.
</Warning>

Material states are `needs_upload`, `queued`, `processing`, `ready`,
`partial`, or `failed`. `partial` and `failed` include diagnostics and a safe
next step. Unsupported facets are reported as `unsupported`; bounded or lossy
facets are `degraded`. Neither state is presented as complete or ready.

The Product Marketing Page is a v3 Material client. It lists Material with
`search({ "kind": "material" })`, reads each visible record with
`get({ "kind": "material", "id": "MATERIAL_ID" })`, and records
Material-owned candidate decisions with `save_material`. Sellers can select a
supported file in the Page; it verifies the type, size, and SHA-256 digest
locally, reserves seller-private storage, uploads directly with the expiring
`PUT` capability, and finalizes the exact reservation with `replace_source`.
The signed URL and headers are used transiently by the widget and are never
rendered, written to model context, or retained as retry state. Supported Page
uploads are PDF, PowerPoint (`.pptx`), Excel (`.xlsx`), CSV, PNG, JPEG, GIF, and
WebP files up to 100 MB.
Each Material card shows the canonical processing state and whether original,
extraction-diagnostic, and visual-asset extensions are available. Visual
inspection loads only bounded descriptors (25 at a time); it never requests
the original source-access URL or retains preview capabilities.

Pending Material-owned candidates can be accepted, rejected, or corrected in
the Page. Corrections are limited to the candidate title and summary and are
fenced to the source revision shown. A destination-owned proposal is displayed
as a handoff to its named canonical workflow; Teach never converts it into a
generic Material write. The Page does not call a separate product-marketing
corpus endpoint or infer inventory gaps locally.
Candidate collections are bounded. When `candidates.nextCursor` is present,
pass it back as `options.pages.candidates.cursor`; the cursor is scoped to that
Material revision and cannot continue another record.

`get(material)` returns bounded rendition summaries by default. Request large
collections explicitly and page each collection independently:

```json theme={null}
{
  "kind": "material",
  "id": "MATERIAL_ID",
  "include": [
    "source_access",
    "rendition_blocks",
    "visual_assets",
    "extraction_diagnostics"
  ],
  "options": {
    "renditionRevision": 1,
    "pages": {
      "candidates": { "limit": 25 },
      "rendition_blocks": { "limit": 50 },
      "visual_assets": { "limit": 20 }
    }
  }
}
```

## Review live RFP demand

Use `search({ "kind": "rfp" })` for the seller's saved RFP ledger. The Demand
Inbox Page applies that same read model: it shows bounded pages of saved RFPs,
uses continuations from the search response, and treats missing metrics as
unavailable instead of recalculating them in the Page.

An imported historical brief appears with `origin: imported`. Read its RFP with
`get({ "kind": "rfp", "id": "RFP_ID" })` to see its pair state: whether a
response is attached, its commentary, and whether the pair is endorsed.

Open one response with `get({ "kind": "rfp_turn", "id": "RFP_TURN_ID" })`.
Proposal Pass reads the immutable turn, including its request, response state,
semantic response document, outcome, feedback, representations, receipts, and
diagnostics. Feedback, future revisions, and release actions go through
`save_rfp`; sent turns are not mutated in place.

Read aggregate seller analytics with `get_rfp_performance`. The tool owns
metric population, freshness, disclosure, dimensions, and pagination. Per-RFP
drill-in remains `get({ "kind": "rfp" })`, so the dashboard does not compute
aggregate rollups locally.

While the first rendition is still queued or processing, omit
`renditionRevision`: requested collections return bounded empty pages until an
active rendition exists. Selecting a rendition revision that does not exist is
still an error.

Blocks preserve page, slide, or sheet identity, reading order, tables, source
locators, geometry, and extraction provenance. Visual assets preserve their
digest, type, dimensions, crop and placement, caption/OCR/alt text,
confidentiality, and reuse rights. Original and preview binaries remain out of
band: `source_access` and asset access fields are tenant-authorized signed URLs
that expire in at most 15 minutes.

Extraction accepts at most 100 MB of source bytes, 5,000 archive entries,
10 MB per expanded archive entry, 50 MB across expanded archive entries, and
20,000 spreadsheet cells. Each rendition is also bounded to 500
page/slide/sheet units, 5,000 semantic blocks, and 500 visual assets.
Compressed spreadsheets and PPTX use the same archive expansion ceilings.
Reaching a bound produces an explicit failed or degraded
rendition while preserving the immutable original. An asset marked
`requires_approval` remains unusable until a durable approval receipt exists;
a caller-supplied approval assertion is never authorization.

Legacy Product Marketing projections retain their historical outer `ready`
state for compatibility. Their rendition remains `partial`, with unsupported
visual/table facets and a projection diagnostic. Requested rendition includes
return bounded empty or diagnostic pages instead of silently disappearing.

Upload reservations that are not finalized expire after 24 hours and are
cleaned in bounded worker batches. Finalized originals are immutable and are
retained with their Material revision; rendition refresh never overwrites or
aliases an older object. `save_material({ "action": "reprocess", ... })`
creates a new rendition revision over the same original. Candidate decisions
remain separate Material records and never update operational owners such as
products, rules, or inventory sources.

## Create and revise seller RFPs

Use `save_rfp` to create an RFP and automatically queue its first immutable
request-and-response turn. Poll that turn with `get({"kind":"rfp_turn"})` until
it reaches `ready`, `passed`, `needs_clarification`, or `failed`. A ready turn
contains a populated response composed from this Seller Account's active,
priced products and canonical creative formats; missing catalog facts produce
explicit clarification instead of an empty proposal.

See [Seller RFP lifecycle](/v2/setup/v3/rfp-lifecycle) for the action union,
idempotency contract, lifecycle states, response artifact, search filters, and
failure behavior.

To add the response you sent for a brief, use `save_rfp` with
`attach_response`. Use `endorse` or `unendorse` on the pair to control whether
it is offered as a good example on later compositions. Use the typed
`record_feedback` fields to save the pair's grade and feedback; those fields
are commentary on the pair rather than a rewrite of its response.

## Answer library requests

Use `search({ "kind": "library_request" })` to find gaps the agent recorded
while composing, then read one with
`get({ "kind": "library_request", "id": "LIBRARY_REQUEST_ID" })`. Open and
close requests with `save_library_request`. Close a request by uploading the
material that fills it or by telling Murph the material in conversation; the
closed request states which path supplied it.

## Operate work items and approvals

`search({ "kind": "work_item" })` is the unified queue for work waiting on the
Seller Account. The returned `workItemKind` determines its state vocabulary and
the fields required by `save_work_item`.

| Work-item kind           | Typical work                                                            |
| ------------------------ | ----------------------------------------------------------------------- |
| Creative review          | Decide whether a creative may run                                       |
| Media-buy approval       | Accept or reject a proposed buy                                         |
| Modular-source follow-up | Confirm availability, complete upstream booking, or resolve trafficking |

For a creative review, the returned work-item `id` is the exact immutable
`reviewRef`, not the reusable creative ID. Keep that `id` together with its
`contentDigest`, and pass both back when saving the decision:

```json theme={null}
{
  "kind": "creative_review",
  "id": "review:42",
  "status": "approved",
  "expectedContentDigest": "<contentDigest returned by search or get>"
}
```

If either value no longer matches the version read, `save_work_item` refuses
the decision. Search or get the work item again and review the replacement
content before retrying; a stale version is never reported as `unchanged`.

Saving the same completed result again returns `unchanged`. A conflicting
correction is refused rather than overwriting authoritative evidence.

AI evaluation, reassignment, and forwarding retry remain Page actions. They
cost money, move accountability, or repeat an external side effect, so they are
not fields on a declarative save.

## Read seller-side media buys

When an External buyer appears in Buyers, select it to see the Media Buys that
relationship placed with you in every supported host. The Page is scoped to
that seller-owned relationship. Its **Delivery on your inventory** view shows
reported impressions, seller-reported net spend, progress against the
seller-visible booked budget when one is available, and the latest reported
delivery day. A buyer awaiting admission cannot open Delivery yet, and a buy
without a reporting record is shown as unavailable rather than as zero
delivery. Use the row's **Account setup** action when you need to review the
relationship instead.

List every buy that landed in the Seller Account:

```json theme={null}
{ "kind": "media_buy" }
```

Filter by `sourceId`, seller status, buyer customer ID, or ISO 8601 flight-start
instants. Pass the opaque `nextCursor` back unchanged while `hasMore` is true.
Partial source coverage and stale status are reported explicitly.

Read one media buy to receive its Seller-scoped exchange timeline, approval,
source legs, identifiers, and newest failure. Recovery classes distinguish a
transient failure from a correctable or structural one. Add
`include: ["diagnostics"]` for the supporting observation trail.

## Query delivery and margin

`get_delivery` supports two reports:

* `delivery` — seller-reported delivery over an inclusive range of at most 90
  days; and
* `margin` — the cumulative seller spread ledger.

```json theme={null}
{
  "report": "delivery",
  "metrics": ["impressions", "clicks", "ctr"],
  "dimensions": ["date", "media_buy"],
  "range": {
    "startDate": "2026-08-01",
    "endDate": "2026-08-07"
  },
  "limit": 25
}
```

Delivery is seller-reported, not buyer-side measurement. Finality does not by
itself make a result eligible for billing. Missing source values remain
unavailable rather than being invented as zero. Reporting pages are live per
call; a cursor anchors continuation but does not create snapshot isolation.

## Query aggregate RFP performance

Use `get_rfp_performance` for grouped RFP quality, efficiency, and commercial
metrics. It is the shared reporting contract that Seller Analytics will adopt.
In this release, call it through `/mcp/v3`; the existing Seller Analytics page
is unchanged. Individual responses, evaluations, feedback, outcomes, and
receipts remain on `get({ "kind": "rfp" })`.

The default population is live production RFPs. Drafts and evaluations require
an explicit purpose filter, while commercial metrics remain live and released
only. See [RFP performance](/v2/setup/v3/rfp-performance) for metric formulas,
dimensions, privacy thresholds, and immutable cursor behavior.

## Test your own supply

Every Media Company can run the no-spend own-supply campaign workflow directly
from its Seller Account. In Apostra, select **Add advertiser** from
Advertisers or Campaigns. The shared Advertiser Setup Task creates through
`save_advertiser`, refreshes the Advertiser roster, and can switch straight into
the new Advertiser. Sandbox is available to every Media Company. The Task shows
Live only when the account is enrolled in `amc-campaign-management`.

Portable clients can use `/mcp/v3` for the same contract. Create a sandbox
Advertiser and Campaign, open the organization's own Seller Products, stage a
MediaBuy, add Creative, activate the sandbox Campaign, and inspect bounded
delivery. You do not need a related Buyer account, `switch_account`, or a
separately provisioned sandbox entitlement.

The server binds the sandbox Advertiser to the organization's exact Storefront.
The sandbox workflow cannot select another Seller's inventory, create a live
Advertiser, or spend money. See [Sandbox Mode](/v2/features/sandbox#media-company-v3-preview)
for the complete sequence and authority boundary.

Start with the public [Test a Sales Agent skill](https://api.interchange.io/skills/test-sales-agent/SKILL.md).
Version 1.5 uses the exact evidence names returned by ordinary V3 calls and
selects only from the same completed Product result
(`resultsComplete: true`). It keeps the selected Product ID, Seller, Source,
Sales Agent, Product query, revision, and pricing option together. If the
Product is absent or that provenance changes before `save_media_buy`, the
validation stops without staging instead of replaying a stale ID.

## Certify the headless demand loop

The versioned `v3-seller-demand-loop` scenario is the portable certification
for Materials → RFP → reporting. It provisions a disposable Seller Account,
uses only `/mcp/v3` tools, uploads representative source types, confirms typed
candidates, composes and revises immutable RFP turns, downloads every proposal
representation, exercises Quick RFP caching, reads Demand Inbox and Seller
Analytics contracts, then destroys the fixture. The trusted observer reads the
request audit ledger independently, rejects v2 MCP traffic, verifies bounded
collections, and publishes a credential-free JSON artifact.

Run the deterministic staging journey against the deployed canary revision:

```bash theme={null}
pnpm --dir apps/temporal tsx src/scripts/run-storefront-seller-journey.ts staging-demand-loop
```

The result contains the immutable evidence reference and digest. A failure to
provision credentials, reach the canary, render an artifact, or validate an
authorized exchange is reported as a failure; the harness never substitutes a
local or synthetic pass.

For an independent MCP-capable host, create an isolated Conductor workspace and
give the host the same machine-readable task. Replace the project id and base
commit with the deployed revision being certified:

```bash theme={null}
conductor --json workspace create \
  --project-id "$CONDUCTOR_PROJECT_ID" \
  --branch main \
  --agent claude \
  --name "Seller demand-loop external host" \
  --message-file .context/seller-demand-loop-external-host.md
```

The external-host prompt must use the supplied `/mcp/v3` credential, reject
any v2 fallback, record tool calls and artifact digests without tokens or
signed URLs, and clean up the disposable fixture. Its transcript is exploratory
product evidence; the deterministic artifact remains the certification result.
