> ## 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.

# Get a demand exchange (proposal pass)

> The full story of one exchange for the calling Seller Account: condensed brief facts read from the persisted brief artifact, ordered passes with product allocation + pricing rows and expand-to-inspect snapshots from the persisted proposal artifacts, per-pass commercial outcome, and an exchange-level result (won if any pass won, else lost if the most recent decided pass lost, else pending). Grouped from persisted linkage only — one compose run and its proposal artifacts, never the content-addressed brief digest — so an exchange reads as a single composition pass until persisted cross-run lineage exists. A run with no captured artifact renders as unavailable, never reconstructed; a metric with no input is null, never 0. Scoped to the caller's Seller Account — a run outside it returns 404.



## OpenAPI

````yaml /v2/storefront-api-v2.yaml get /demand-inbox/{runId}/exchange
openapi: 3.0.0
info:
  title: Scope3 Storefront API
  version: 2.0.0
  description: >-
    REST API for partners to manage Seller Accounts, inventory sources, and
    billing.


    ## Authentication


    All endpoints require a Bearer token in the Authorization header:

    ```

    Authorization: Bearer your-api-key

    ```


    ## Base URL


    `https://api.interchange.io/api/v2/storefront`


    ## For AI Agents


    AI agents can use the MCP endpoint at `/mcp/v2/storefront` with three tools:

    - `initialize`: Start an MCP session

    - `api_call`: Make REST API calls

    - `ask_about_capability`: Learn about API features
servers:
  - url: https://api.interchange.io/api/v2/storefront
    description: Production server
security: []
tags:
  - name: Account
    description: Account management, service tokens, and preferences
  - name: Asks
    description: >-
      What you are waiting on Scope3 for — support, product, and supply asks in
      one list
  - name: Storefront
    description: Manage storefront and inventory sources
  - name: Storefront Agents
    description: List and manage registered sales, signals, and outcomes agents
  - name: Storefront Activity
    description: Audit log of configuration and inventory changes on the storefront
  - name: Storefront Billing
    description: Payout bank details and billing configuration for Seller Accounts
  - name: AI Usage
    description: Seller Account AI token usage visibility by model
  - name: MCP
    description: Model Context Protocol endpoints
paths:
  /demand-inbox/{runId}/exchange:
    get:
      tags:
        - Storefront
      summary: Get a demand exchange (proposal pass)
      description: >-
        The full story of one exchange for the calling Seller Account: condensed
        brief facts read from the persisted brief artifact, ordered passes with
        product allocation + pricing rows and expand-to-inspect snapshots from
        the persisted proposal artifacts, per-pass commercial outcome, and an
        exchange-level result (won if any pass won, else lost if the most recent
        decided pass lost, else pending). Grouped from persisted linkage only —
        one compose run and its proposal artifacts, never the content-addressed
        brief digest — so an exchange reads as a single composition pass until
        persisted cross-run lineage exists. A run with no captured artifact
        renders as unavailable, never reconstructed; a metric with no input is
        null, never 0. Scoped to the caller's Seller Account — a run outside it
        returns 404.
      operationId: getDemandExchange
      parameters:
        - in: path
          name: runId
          schema:
            type: integer
            minimum: 0
            exclusiveMinimum: true
            maximum: 9007199254740991
            description: >-
              Surrogate id of the chef compose run that anchors this exchange
              (the demand-inbox ledger row id).
            example: 42
          required: true
          description: >-
            Surrogate id of the chef compose run that anchors this exchange (the
            demand-inbox ledger row id).
      responses:
        '200':
          description: Get a demand exchange (proposal pass)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DemandExchange'
        '400':
          description: Bad request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: No demand-inbox exchange with that run id in the calling storefront.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearerAuth: []
components:
  schemas:
    DemandExchange:
      type: object
      properties:
        exchangeId:
          type: string
          description: The compose-run id anchoring this exchange.
        buyer:
          nullable: true
          type: string
        commercialIdentity:
          $ref: '#/components/schemas/DemandCommercialIdentity'
        provenance:
          nullable: true
          description: >-
            How this brief reached the ledger: 'live' = an AdCP get_products
            call from a buyer agent, 'uploaded' = a seller-uploaded RFP/brief
            confirmed in chat (AI-4728). Reflects this run's own intake origin
            (AI-5025), falling back to the linked brief artifact's source column
            only for historical runs recorded before that origin was captured.
            Null when neither is available. An uploaded exchange is never
            presented as live buyer demand.
          type: string
        runPurpose:
          nullable: true
          description: >-
            The compose run's own run_purpose. null means the run predates
            classification (an absent historical value, treated as real demand);
            a persisted value outside this build's known set surfaces as
            'unknown', which consumers must treat exactly like a synthetic
            purpose — never as a live buyer. Together with provenance this is
            how the pass classifies the exchange into a matrix-ruling cell: any
            purpose other than 'live'/'unclassified' (including 'unknown') is
            never real buyer demand, regardless of what provenance says.
          allOf:
            - $ref: '#/components/schemas/DemandExchangeRunPurpose'
        briefText:
          nullable: true
          type: string
        briefFacts:
          nullable: true
          allOf:
            - $ref: '#/components/schemas/DemandExchangeBriefFacts'
        briefArtifactId:
          nullable: true
          type: string
        briefArtifactAvailable:
          type: boolean
          description: >-
            False when no brief artifact was captured (historical run). Readers
            must say "brief unavailable" rather than reconstruct it.
        proposalArtifactId:
          nullable: true
          description: >-
            The most recent seller-response artifact id (historical
            storage-table name), from the full persisted pass list. Presence
            alone does not prove a Proposal; use responseKind on the matching
            pass.
          type: string
        passesTruncated:
          type: boolean
          description: >-
            True when the run had more persisted proposal artifacts than the
            pass cap; the widget discloses the cap rather than silently dropping
            older passes.
        result:
          type: string
          enum:
            - closed_won
            - closed_lost
            - pending
          description: >-
            Exchange-level commercial result. Derivation rule: won if ANY pass
            is won; else lost if the most recent decided pass is lost; else
            pending. With one run per exchange today this equals the run
            outcome.
        commercialLifecycle:
          $ref: '#/components/schemas/DemandCommercialLifecycle'
        wonValue:
          nullable: true
          description: >-
            Money won across the exchange, one entry per currency
            (both-or-nothing amount+currency per the AI-4726 money model; never
            summed cross-currency). Null when nothing won recorded a priced
            outcome.
          type: array
          items:
            type: object
            properties:
              amount:
                type: number
              currency:
                type: string
            required:
              - amount
              - currency
            additionalProperties: false
        statedBudget:
          nullable: true
          description: >-
            The buyer's stated budget range. Surfaced at the exchange level so a
            lost exchange still shows the demand it represented (the AI-4726
            lost-demand visibility).
          allOf:
            - $ref: '#/components/schemas/DemandExchangeBudgetRange'
        ledBy:
          nullable: true
          type: string
          enum:
            - agent
            - human
        grade:
          nullable: true
          type: string
          enum:
            - A
            - B
            - C
            - D
            - F
        feedback:
          nullable: true
          type: string
        receivedAt:
          type: string
        respondedAt:
          nullable: true
          type: string
        responseTimeMs:
          nullable: true
          type: number
          minimum: 0
        grouping:
          $ref: '#/components/schemas/DemandExchangeGrouping'
        passes:
          type: array
          items:
            $ref: '#/components/schemas/DemandExchangePass'
        adjustable:
          type: boolean
          description: >-
            Whether Adjust is available on this exchange right now, from the
            shared eligibility rule the compose mutation re-checks (AI-5571).
            False for anything but classified live demand with a retained brief,
            active operating instructions, and lifetime adjustments remaining.
        adjustabilityReason:
          nullable: true
          description: >-
            Null when adjustable is true. Otherwise the server's own named
            reason — never a bare not-found — so the pass renders Adjust
            disabled with the reason instead of a button that fails when
            pressed.
          type: string
          enum:
            - not_live_demand
            - brief_not_retained
            - no_operating_instructions
            - adjustment_limit_reached
            - open_revision_exists
        openRevision:
          nullable: true
          description: >-
            The exchange's open draft or awaiting-approval revision, surfaced
            separately from the pass list — a seller-composed revision is never
            mistaken for a captured pass until it is actually sent.
          allOf:
            - $ref: '#/components/schemas/DemandExchangeRevision'
      required:
        - exchangeId
        - buyer
        - commercialIdentity
        - provenance
        - runPurpose
        - briefText
        - briefFacts
        - briefArtifactId
        - briefArtifactAvailable
        - proposalArtifactId
        - passesTruncated
        - result
        - commercialLifecycle
        - wonValue
        - statedBudget
        - ledBy
        - grade
        - feedback
        - receivedAt
        - respondedAt
        - responseTimeMs
        - grouping
        - passes
        - adjustable
        - adjustabilityReason
        - openRevision
      additionalProperties: false
      description: >-
        The full story of one brief → proposal → result exchange: brief facts,
        ordered passes with product allocation + pricing rows, per-pass outcome,
        and an exchange-level result. Every number is traceable to a persisted
        artifact or outcome record; missing inputs render unavailable, never 0.
    ErrorResponse:
      type: object
      properties:
        data:
          type: string
          nullable: true
          enum:
            - null
        error:
          $ref: '#/components/schemas/ApiError'
      required:
        - data
        - error
      additionalProperties: false
      description: Standard error response
    DemandCommercialIdentity:
      type: object
      properties:
        advertiser:
          nullable: true
          description: Advertiser BrandRef domain. This is the primary commercial identity.
          type: object
          properties:
            value:
              type: string
            source:
              type: string
              enum:
                - request.brand
                - request.account.brand
                - request.account.operator
                - request.account.account_id
                - compose_run
          required:
            - value
            - source
          additionalProperties: false
        buyingOperator:
          nullable: true
          description: >-
            Organization operating the buy, independently of the advertiser
            brand.
          type: object
          properties:
            value:
              type: string
            source:
              type: string
              enum:
                - request.brand
                - request.account.brand
                - request.account.operator
                - request.account.account_id
                - compose_run
          required:
            - value
            - source
          additionalProperties: false
        account:
          nullable: true
          description: >-
            Buyer-declared AdCP account_id, when supplied. This is not a CRM id
            or a seller-native source mapping.
          type: object
          properties:
            value:
              type: string
            source:
              type: string
              enum:
                - request.brand
                - request.account.brand
                - request.account.operator
                - request.account.account_id
                - compose_run
          required:
            - value
            - source
          additionalProperties: false
      required:
        - advertiser
        - buyingOperator
        - account
      additionalProperties: false
      description: >-
        Advertiser = BrandRef × Operator (AI-5080), projected from the persisted
        brief request first and the compose-run identity snapshot only as a
        historical fallback. CRM evidence and seller-native mappings are
        deliberately not joined into this identity.
    DemandExchangeRunPurpose:
      type: string
      enum:
        - live
        - setup_test
        - simulation
        - probe
        - evaluation
        - unclassified
        - unknown
      description: >-
        The compose run's own trusted execution classification. 'live' (or the
        historical 'unclassified' sentinel) is real buyer demand; every other
        value — including 'unknown', which marks a classification this build
        does not recognize — is never treated as a live buyer.
    DemandExchangeBriefFacts:
      type: object
      properties:
        channels:
          nullable: true
          type: array
          items:
            type: string
        countries:
          nullable: true
          type: array
          items:
            type: string
        budgetRange:
          nullable: true
          allOf:
            - $ref: '#/components/schemas/DemandExchangeBudgetRange'
        promotedOffering:
          nullable: true
          type: string
        brandDomain:
          nullable: true
          type: string
      required:
        - channels
        - countries
        - budgetRange
        - promotedOffering
        - brandDomain
      additionalProperties: false
      description: >-
        Condensed brief facts read from the persisted brief artifact request
        (never reconstructed). Any field the buyer did not send is null.
    DemandCommercialLifecycle:
      type: string
      enum:
        - closed_won
        - closed_lost
        - awaiting_advertiser_decision
        - awaiting_seller_approval
        - forwarding_failed
        - no_offer_returned
        - response_not_recorded
        - response_unavailable
      description: >-
        The most specific commercial lifecycle supported by the persisted
        response and attributed outcome. This supplements result so an undecided
        exchange is never presented as an ambiguous Pending state.
    DemandExchangeBudgetRange:
      type: object
      properties:
        min:
          nullable: true
          type: number
        max:
          nullable: true
          type: number
        currency:
          type: string
      required:
        - min
        - max
        - currency
      additionalProperties: false
      description: >-
        The buyer's stated budget range, exactly as sent in the brief's
        filters.budget_range. Null when the buyer did not state one.
    DemandExchangeGrouping:
      type: object
      properties:
        basis:
          type: string
          enum:
            - compose_run
          description: >-
            The persisted linkage the exchange is grouped on: a single compose
            run and its proposal artifacts.
        passCount:
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
        multiPass:
          type: boolean
        note:
          type: string
          description: >-
            Human-readable disclosure of the grouping honesty (e.g. that refine
            passes are not yet rolled up because no persisted cross-run lineage
            exists).
      required:
        - basis
        - passCount
        - multiPass
        - note
      additionalProperties: false
    DemandExchangePass:
      type: object
      properties:
        passId:
          type: string
        sequence:
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
          description: 1-based order of this pass within the exchange.
        version:
          nullable: true
          description: Proposal artifact version within its supersedes chain.
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
        kind:
          type: string
          description: >-
            The run's buying mode ('brief' for a first response, 'refine' for a
            renegotiation pass).
        createdAt:
          type: string
        planName:
          nullable: true
          type: string
        planBriefAlignment:
          nullable: true
          type: string
        planCount:
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
          description: >-
            How many media plans the proposal carried. Allocation is read from
            one selected plan (the committed plan if present, else the first
            draft); when this exceeds 1, a product unallocated here may be
            allocated in another plan and the widget discloses that rather than
            conflating the two.
        captureState:
          type: string
          enum:
            - captured
            - truncated
            - unavailable
          description: >-
            How completely this pass was persisted: 'captured' = full response
            and snapshots stored; 'truncated' = a proposal artifact exists but
            its response or product snapshots exceeded the size limit, so some
            detail (e.g. per-product snapshots) is absent though product rows
            still render; 'unavailable' = no proposal artifact was captured for
            the run (historical / predates capture). Readers word the banner
            from this so a truncated pass is never described as predating
            capture.
        responseKind:
          $ref: '#/components/schemas/DemandResponseKind'
        artifactAvailable:
          type: boolean
          description: >-
            True when a proposal response was persisted and product rows can
            render (captureState captured or truncated). False when no response
            was captured (captureState unavailable) — products render as
            unavailable, never reconstructed.
        products:
          type: array
          items:
            $ref: '#/components/schemas/DemandExchangeProduct'
        productsTruncated:
          type: boolean
          description: >-
            True when the pass had more products than the response cap; the
            widget discloses the cap rather than silently dropping rows.
        outcome:
          $ref: '#/components/schemas/DemandExchangePassOutcome'
        origin:
          type: string
          enum:
            - agent
            - seller_adjusted
          description: >-
            'seller_adjusted' when this pass was captured from a cleared AI-5571
            revision send; 'agent' otherwise. States authorship per pass so a
            seller-adjusted revision can never read as the agent's first
            response.
        fedBy:
          default: []
          description: >-
            The seller's ingredients the engine resolved once for this whole
            pass — the Playbook version it composed under and the buyer's
            resolved terms. Separate from a product's own fedBy because these
            genuinely have no per-product attribution: they collapse into one
            prompt, and claiming otherwise would be an invention. Empty for a
            pass with no persisted provenance.
          type: array
          items:
            $ref: '#/components/schemas/DemandExchangeFedBy'
        pitch:
          default: null
          description: >-
            The composed argument for this pass (AI-5869), or null when none was
            composed — a pass that predates the pitch model, one whose composer
            failed or timed out, one for a seller not yet enrolled, one still
            composing, or one whose pitch lookup itself failed. Null renders
            exactly as the pass rendered before pitches existed: the plan, and
            nothing invented around it. `pitchStatus` says WHY it is null;
            `pitch` itself never changes shape.
          nullable: true
          allOf:
            - $ref: '#/components/schemas/DemandExchangePitch'
        pitchStatus:
          default: absent
          description: >-
            The pitch's lifecycle state, additive to `pitch` so a seller can
            tell the absences apart instead of reading one undifferentiated
            null: 'ready' if and only if `pitch` is non-null; 'pending' means
            the composer dispatched and has not resolved yet (durably recorded —
            a crash does not silently drop this to absent); 'absent' means it
            resolved with no narrative, or was never dispatched; 'unavailable'
            means the pitch lookup itself failed or timed out — a transient read
            fault, never a claim about whether a pitch exists. Render
            'unavailable' exactly like 'absent': silence, never an error banner
            — it is not seller-actionable. A pass captured before this field
            existed reads as 'absent' with `pitchAbsentReason: predates_pitch`,
            derived from the pass's own timestamp rather than persisted.
          type: string
          enum:
            - pending
            - ready
            - absent
            - unavailable
        pitchAbsentReason:
          default: null
          description: >-
            Bounded, seller-safe reason `pitchStatus` is 'absent' — never
            present for 'ready', 'pending', or 'unavailable', never raw error
            text, and always non-null when `pitchStatus` is 'absent'.
            'not_enrolled' = the storefront-pitch-composer flag was off for this
            seller (or, derived rather than persisted, no pitch row exists at
            all for a pass composed after the pitch model existed).
            'composing_failed' = a real model or verification fault.
            'no_groundable_input' = the pass composed no products at all, so
            there was nothing for the composer to try citing. 'nothing_grounded'
            = the composer answered but no claim survived citation grounding or
            the independent entailment check. 'superseded' = a newer composition
            generation overtook this one before it resolved. 'timeout' = the
            composer's deadline was exceeded, or the crash-recovery retry sweep
            gave up after its one permitted retry. 'predates_pitch' = derived,
            never persisted: this pass was composed before the pitch model
            existed at all, so it could never have had a row. 'expired' =
            adjudicated ON READ and persisted (AI-6397), not merely derived like
            'predates_pitch': an ordinary read found a `pending` row well past
            every write-side path's legitimate window (the dispatch retries, the
            background sweep) and wrote absent/expired for it directly, through
            the same terminal-resolution path every other absent reason uses.
            'unavailable' carries no reason code at all — see `pitchStatus`.
          nullable: true
          type: string
          enum:
            - not_enrolled
            - composing_failed
            - no_groundable_input
            - nothing_grounded
            - superseded
            - timeout
            - predates_pitch
            - expired
        pitchAbsentDetail:
          default: null
          description: >-
            Counts-only diagnosis (AI-7032) of WHY the composer's model produced
            no groundable pitch — never present for any `pitchAbsentReason`
            other than 'nothing_grounded', and even then may still be null for a
            row written before this diagnosis existed. Never claim text or brief
            text: `catalogue` states how many of each citable ingredient kind
            fed this pass, `drops` states how many claims the model asserted and
            how many each of the two independent checks (citation grounding,
            then the entailment gate) removed.
          nullable: true
          allOf:
            - $ref: '#/components/schemas/PitchAbsentDetail'
        droppedClaims:
          default: []
          description: >-
            Every sentence the truth gate rejected on this pass's compose
            (AI-7077 slice 1), each with its citations, the facts it was judged
            against, and the seller's ruling on it, if any
            (adjudicate_pitch_claim_drop, AI-7079 slice 2). Non-empty (or null)
            only on a `pitchStatus: 'ready'` pass with partial drops, or on a
            `pitchAbsentReason: 'nothing_grounded'` terminal absence; `[]`
            everywhere else, including a pass composed before this column
            existed. `null` is a DISTINCT fact from `[]` in those two states: it
            means the drop-record lookup itself failed (a transient read fault),
            never that the pass genuinely has none — the same null-means-'not
            read', not 'read as nothing' discipline `pitchAbsentDetail` already
            holds for its own diagnosis.
          nullable: true
          maxItems: 200
          type: array
          items:
            $ref: '#/components/schemas/DemandExchangeDroppedClaim'
        bidDecision:
          default: null
          description: >-
            The qualification judgment this pass composed under (AI-5870), or
            null when none was recorded — a pass that predates the decision
            stage, or one composed for a seller not enrolled in the composer.
            Independent of `pitch`: a decision exists even when the narrative
            stage then failed, because 'we passed on this brief' is a fact about
            the pass rather than about the composer's luck. Readers render a
            'pass' honestly — the decline, never an empty pitch.
          nullable: true
          allOf:
            - $ref: '#/components/schemas/DemandExchangeBidDecision'
        story:
          default: null
          description: >-
            The narrative case study matched to this pass's brief (AI-6736), or
            null when no live story matched (or this pass has no brief text to
            match against). Independent of `pitch`/`pitchStatus`: computed by
            deterministic keyword matching, not the pitch composer, so it can be
            present even while the pitch is pending or absent. Feeds the
            story-first card's Proof section; never rendered as a standalone
            claim about the pitch's own readiness.
          nullable: true
          allOf:
            - $ref: '#/components/schemas/DemandExchangeStory'
      required:
        - passId
        - sequence
        - version
        - kind
        - createdAt
        - planName
        - planBriefAlignment
        - planCount
        - captureState
        - responseKind
        - artifactAvailable
        - products
        - productsTruncated
        - outcome
        - origin
        - fedBy
        - pitch
        - pitchStatus
        - pitchAbsentReason
        - pitchAbsentDetail
        - droppedClaims
        - bidDecision
        - story
      additionalProperties: false
    DemandExchangeRevision:
      type: object
      properties:
        id:
          type: string
        exchangeId:
          type: string
        state:
          type: string
          enum:
            - draft
            - awaiting_approval
            - sent
            - discarded
        overrides:
          type: object
          properties:
            posture:
              type: object
              properties:
                selectedPosture:
                  type: string
                  enum:
                    - direct_fit
                    - hold_value
                    - value_preserving_compromise
                    - tradeoff_ladder
                    - price_first
                    - wholesale_mirror
              required:
                - selectedPosture
              additionalProperties: false
            pricing:
              type: object
              properties:
                priceAdjustmentBasisPoints:
                  type: integer
                  minimum: -10000
                  maximum: 100000
                floorCpm:
                  type: object
                  properties:
                    amount:
                      type: number
                      minimum: 0
                    currency:
                      type: string
                      minLength: 3
                      maxLength: 3
                  required:
                    - amount
                    - currency
                  additionalProperties: false
              additionalProperties: false
            rules:
              type: object
              properties:
                requiredBundleIds:
                  maxItems: 100
                  type: array
                  items:
                    type: string
                    minLength: 1
                excludedBundleIds:
                  maxItems: 100
                  type: array
                  items:
                    type: string
                    minLength: 1
                maxProducts:
                  type: integer
                  minimum: 0
                  exclusiveMinimum: true
                  maximum: 100
              additionalProperties: false
          additionalProperties: false
        composeResult:
          type: string
          enum:
            - ok
            - error
        composeError:
          nullable: true
          description: >-
            The merchandising engine's own error, kept mounted (with the
            seller's declared overrides) when composition failed, rather than
            the sheet being torn down.
          allOf:
            - $ref: '#/components/schemas/DemandExchangeRevisionComposeError'
        proposalPreview:
          nullable: true
          description: >-
            The draft response envelope, in the same shape the canonical
            proposal card already renders (redacted). Null when compose failed.
        proposalPreviewSnapshots:
          nullable: true
          description: >-
            The draft product snapshots keyed by product id, paired with
            proposalPreview so the canonical proposal card renders a draft with
            no builder-specific projection. Null when compose failed.
        settlementCurrency:
          nullable: true
          type: string
        buyerStatedCurrencies:
          nullable: true
          description: >-
            Read once from the persisted brief request at compose time. A draft
            never converts into these or issues a rate hold — that is the
            buyer-facing response path's job, not a discardable draft's.
          type: array
          items:
            type: string
        composedByUserId:
          nullable: true
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
        composedAt:
          type: string
        approval:
          $ref: '#/components/schemas/DemandExchangeRevisionApproval'
        send:
          $ref: '#/components/schemas/DemandExchangeRevisionSend'
        sentProposalArtifactId:
          nullable: true
          type: string
        delivery:
          nullable: true
          description: >-
            Truthful buyer-visibility (build-5 ruling): null until sent. A sent
            revision always reports not_delivered/no_delivery_lane today —
            investigation found no seller-to-buyer lane for a subsequent
            proposal and no honest staging lane either. The UI states the
            buyer's agent was not notified; it never claims a notification that
            did not happen.
          type: object
          properties:
            status:
              type: string
              enum:
                - not_delivered
            reason:
              type: string
              enum:
                - no_delivery_lane
          required:
            - status
            - reason
          additionalProperties: false
        canApprove:
          type: boolean
          description: >-
            False when the caller is this revision's own composer
            (maker-checker) or the revision is not awaiting approval.
        canAmend:
          type: boolean
          description: >-
            True for a draft (fresh or bounced back after rejection) composed by
            the caller; false once submitted, sent, or discarded, or when a
            different storefront user composed it.
        canSubmit:
          type: boolean
          description: >-
            True when the caller composed this draft, it hasn't been submitted,
            sent, or discarded, and its last compose succeeded. A draft whose
            compose failed can only be discarded or re-composed via amend.
        canDiscard:
          type: boolean
          description: >-
            True for a draft (fresh or bounced back after rejection) composed by
            the caller; false once submitted, sent, or discarded, or when a
            different storefront user composed it.
        submissionGeneration:
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
          description: >-
            Bumped on every submit, resubmit, and amend. A decision must be made
            against this exact value or it is refused as stale.
        amendCount:
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
          description: >-
            How many times this row has been amended in place. Counts toward the
            lifetime adjustment cap alongside the row itself.
      required:
        - id
        - exchangeId
        - state
        - overrides
        - composeResult
        - composeError
        - proposalPreview
        - proposalPreviewSnapshots
        - settlementCurrency
        - buyerStatedCurrencies
        - composedByUserId
        - composedAt
        - approval
        - send
        - sentProposalArtifactId
        - delivery
        - canApprove
        - canAmend
        - canSubmit
        - canDiscard
        - submissionGeneration
        - amendCount
      additionalProperties: false
      description: >-
        A seller-composed adjustment to a live demand exchange: silent while
        draft or awaiting approval, and a real pass on the exchange's compose
        run once sent.
    ApiError:
      type: object
      properties:
        code:
          type: string
          description: Machine-readable error code
        message:
          type: string
          description: Human-readable error message
        field:
          description: Field path associated with the error
          type: string
        details:
          description: Additional error context
          type: object
          additionalProperties: {}
      required:
        - code
        - message
      additionalProperties: false
      description: Structured error object
    DemandResponseKind:
      type: string
      enum:
        - product_offer
        - proposal
        - no_offer
        - unavailable
      description: >-
        What the persisted seller response actually proves: 'proposal' requires
        a non-empty proposal_id; products without that evidence are a
        product_offer, never a Proposal.
    DemandExchangeProduct:
      type: object
      properties:
        productId:
          type: string
        name:
          nullable: true
          type: string
        description:
          nullable: true
          type: string
        deliveryType:
          nullable: true
          type: string
        placements:
          type: array
          items:
            $ref: '#/components/schemas/Scope3MerchandisePlacementDelivery'
          description: >-
            One entry per backing avail, never merged — see
            `Scope3MerchandisePlacementDelivery`. Empty only when merchandise
            resolution has not run for this product (a pass composed before
            AI-6936 shipped).
        omittedPlacementCount:
          type: integer
          minimum: 0
          maximum: 9007199254740991
          description: >-
            How many of this product's backing avails could not be resolved at
            all (archived or deleted) and so are missing from `placements`.
            Nonzero means `placements` is a partial picture of the product's
            backing.
        audience:
          nullable: true
          description: >-
            The seller's own audience facts for this product, or null when they
            could not be read (a corpus-read failure, or this pass predates the
            merchandise block) — distinct from `{ facts: [] }`, a successful
            read that found nothing matching.
          allOf:
            - $ref: '#/components/schemas/Scope3MerchandiseAudience'
        pricing:
          nullable: true
          description: >-
            Buyer-facing price for the pricing option the plan priced this
            product against (matched by the allocation pricing_option_id); when
            the plan named none, the first fixed-price option is used, else the
            first floor-price option, else the first option. Within the chosen
            option a fixed price is shown, else the floor. Null when the
            persisted product carried no priced option.
          type: object
          properties:
            model:
              nullable: true
              type: string
            amount:
              nullable: true
              type: number
            currency:
              nullable: true
              type: string
            kind:
              nullable: true
              type: string
              enum:
                - fixed
                - floor
          required:
            - model
            - amount
            - currency
            - kind
          additionalProperties: false
        allocationPercent:
          nullable: true
          description: >-
            This product's share of the plan budget, exactly as the agent
            allocated it in the proposal (0–100). Null when the proposal carried
            no media plan for it.
          type: number
        allocationValue:
          nullable: true
          description: >-
            allocationPercent applied to the buyer's committed point budget
            (budget_range where min===max). Null unless the buyer stated a true
            point budget — never a guess against an open range or a single
            ceiling/floor.
          type: object
          properties:
            amount:
              type: number
            currency:
              type: string
          required:
            - amount
            - currency
          additionalProperties: false
        formats:
          type: array
          items:
            type: object
            properties:
              agentUrl:
                nullable: true
                type: string
              id:
                nullable: true
                type: string
            required:
              - agentUrl
              - id
            additionalProperties: false
        snapshot:
          nullable: true
          description: >-
            The persisted immutable product snapshot for expand-to-inspect. Null
            when the proposal artifact payload exceeded its size limit
            (complete=false).
        fedBy:
          default: []
          description: >-
            The seller's own ingredients the engine consulted for THIS product
            line — its selected inventory bundles and signals, and the rate-card
            pricing facts matched against them. Empty for a pass recorded before
            compose provenance existed, or one whose provenance write failed;
            absence is silence and nothing is reconstructed.
          type: array
          items:
            $ref: '#/components/schemas/DemandExchangeFedBy'
      required:
        - productId
        - name
        - description
        - deliveryType
        - placements
        - omittedPlacementCount
        - audience
        - pricing
        - allocationPercent
        - allocationValue
        - formats
        - snapshot
        - fedBy
      additionalProperties: false
    DemandExchangePassOutcome:
      type: object
      properties:
        result:
          type: string
          enum:
            - closed_won
            - closed_lost
            - pending
        wonValue:
          nullable: true
          type: object
          properties:
            amount:
              type: number
            currency:
              type: string
          required:
            - amount
            - currency
          additionalProperties: false
      required:
        - result
        - wonValue
      additionalProperties: false
      description: >-
        Commercial outcome attributed to this pass. Today one run backs an
        exchange, so every pass shares the run-level outcome.
    DemandExchangeFedBy:
      type: object
      properties:
        ingredientType:
          type: string
          enum:
            - inventory_bundle
            - signal
            - pricing_fact
            - operating_instructions
            - buyer_instructions
            - taught_guidance
            - marketing_claim
            - endorsed_pair
          description: >-
            Which of the seller's ingredient families this attribution names.
            'marketing_claim' is a claim extracted from the seller's own
            product-marketing corpus (their media kit, decks and one-sheets).
            'taught_guidance' is reserved for standing guidance a seller teaches
            the agent and is not written yet.
        ingredientId:
          type: string
          description: >-
            The real id on the ingredient's canonical owner as it stood at
            compose time — a wholesale bundle id, signal id, rate-card pricing
            fact id, playbook version, or buyer-terms row id. Never a
            synthesized key.
        label:
          nullable: true
          description: >-
            Operator-facing label captured at compose time, when the ingredient
            carries one. Null when the ingredient has no label of its own; the
            reader names the family instead of inventing copy. Renaming the
            ingredient later never rewrites this — chips name what fed the
            decision then.
          type: string
        version:
          nullable: true
          description: >-
            The version consulted, where the owner is versioned: the Playbook
            version, or the corpus revision a marketing claim was extracted
            from. Null for every other family.
          type: string
        granularity:
          type: string
          enum:
            - product_line
            - run
            - pitch_claim
          description: >-
            How honestly this attribution is scoped. 'product_line' = resolution
            genuinely named this ingredient for this product (its selected
            inventory bundles and signals, and the pricing facts matched against
            them). 'run' = the ingredient was resolved once for the whole pass
            and collapses into one prompt, so no per-product attribution exists
            (the Playbook, the buyer's resolved terms, and the seller's
            product-marketing corpus). 'pitch_claim' = the ingredient backs one
            sentence of the composed pitch; these appear inside the pitch's own
            claims rather than in a product's or pass's fedBy list. Never
            inferred — the writer records the level it actually had.
        claimRef:
          default: null
          description: >-
            The pitch sentence this attribution backs, for 'pitch_claim'
            entries; null for every other granularity. A stable server-minted
            handle, never model-authored, so a claim and its receipts always
            agree.
          nullable: true
          type: string
        ownerState:
          type: string
          enum:
            - available
            - ingredient_removed
            - no_owner_surface
            - owner_unavailable
          description: >-
            Whether the ingredient's owner can be opened now. 'available' = it
            still exists and has an owner Task. 'ingredient_removed' = a
            completed check proved the ingredient is gone, so the chip opens an
            honest missing state rather than a fabricated Task.
            'no_owner_surface' = it exists but no owner Task is reachable from
            this surface (per-buyer terms, or inventory reached through another
            seller's storefront). 'owner_unavailable' = the check that would
            have decided between those did not complete, so existence is unknown
            — never reported as deletion. Only 'available' carries a launch;
            readers must treat an unrecognized value as non-launchable.
        ownerToolName:
          nullable: true
          description: >-
            Canonical owner tool the chip launches through the shared MCP app
            contract. Non-null only when ownerState is 'available'.
          type: string
        ownerArguments:
          nullable: true
          description: >-
            Arguments that owner Task needs (e.g. the inventory source id).
            Non-null exactly when ownerToolName is.
          type: object
          additionalProperties:
            type: string
      required:
        - ingredientType
        - ingredientId
        - label
        - version
        - granularity
        - claimRef
        - ownerState
        - ownerToolName
        - ownerArguments
      additionalProperties: false
    DemandExchangePitch:
      type: object
      properties:
        composedAt:
          type: string
          minLength: 1
        composedVia:
          type: string
          enum:
            - inline
            - fast_follow
          description: >-
            Which path attached this pitch. 'fast_follow' = composed after the
            buyer's response shipped, which is every pitch today; 'inline' is
            reserved for when the buyer-wire projection makes an in-request
            compose worth its latency.
        claimsDropped:
          type: integer
          minimum: 0
          maximum: 9007199254740991
          description: >-
            How many sentences the grounding contract removed for lacking a real
            source reference. Surfaced because a persistently high count means
            the composer is reaching, not that the pitch is thin.
        whatWeHeard:
          type: array
          items:
            $ref: '#/components/schemas/DemandExchangePitchClaim'
          description: >-
            The brief mirrored back sharper, in the buyer's own terms. May be
            empty.
        thesis:
          type: array
          items:
            $ref: '#/components/schemas/DemandExchangePitchClaim'
          description: >-
            The single argument connecting this buyer's moment to this seller's
            supply. Non-empty for a 'pitch' or 'counter_pitch' disposition: a
            pitch whose thesis did not survive grounding is not persisted at
            all, because products with decoration is not a plainer pitch. Empty
            for a composed 'pass', which argues nothing and carries `decline`
            instead.
        roles:
          type: array
          items:
            $ref: '#/components/schemas/DemandExchangePitchRole'
          description: >-
            The plan presented as roles in a strategy rather than rows in a
            table, each with its fit argument. May be empty; the products still
            render.
        outcomeLinkage:
          type: array
          items:
            $ref: '#/components/schemas/DemandExchangePitchClaim'
          description: >-
            How success will be measured, from the products' real reporting
            capabilities — and what will not be. May be empty.
        valueCase:
          type: array
          items:
            $ref: '#/components/schemas/DemandExchangePitchClaim'
          description: >-
            The argument for the price: the guarantee, the packaging, how the
            price was constructed. May be empty.
        honestCounter:
          type: array
          items:
            $ref: '#/components/schemas/DemandExchangePitchClaim'
          description: >-
            What the brief asked for that this catalogue genuinely lacks, and
            what the seller would do instead. May be empty when there is no gap
            to name.
        invitation:
          type: array
          items:
            $ref: '#/components/schemas/DemandExchangePitchClaim'
          description: >-
            What happens next, stated as a verb and cited to a next-step
            affordance the platform can actually honour. May be empty when this
            pass offers none.
        decline:
          default: []
          description: >-
            The composed pass (AI-5870): a short, branded decline naming the
            mismatch and leaving the door open. Non-empty for a 'pass'
            disposition and EMPTY for every pitch and counter-pitch — a pass
            composes this INSTEAD of an argument, never alongside one, and the
            schema refuses a record carrying both. A counter-pitch's reframe
            rides its honest counter, not a decline. Sourced exactly like every
            other claim, so a decline cannot say anything a pitch could not.
          type: array
          items:
            $ref: '#/components/schemas/DemandExchangePitchClaim'
      required:
        - composedAt
        - composedVia
        - claimsDropped
        - whatWeHeard
        - thesis
        - roles
        - outcomeLinkage
        - valueCase
        - honestCounter
        - invitation
        - decline
      additionalProperties: false
      description: >-
        The composed argument for one pass: what we heard, the thesis, the plan
        as roles with per-product fit arguments, outcome linkage, the value
        case, the honest counter, and the invitation. Every sentence carried at
        least one real source reference at composition time or was dropped
        before persistence — nothing here is unsourced prose. Sections the
        seller's own ingredients could not support are empty rather than filled,
        so a pitch gets visibly plainer, never fabricated, as inputs thin out.
    PitchAbsentDetail:
      type: object
      properties:
        catalogue:
          $ref: '#/components/schemas/PitchAbsentCatalogue'
        drops:
          $ref: '#/components/schemas/PitchAbsentDrops'
      required:
        - catalogue
        - drops
      additionalProperties: false
      description: >-
        Counts-only diagnosis of a `nothing_grounded` pitch absence (AI-7032) —
        never claim text, never brief text. Present only alongside
        `pitchAbsentReason: 'nothing_grounded'`, and even then only for a row
        written after this diagnosis existed; absence of detail stays absent
        (null), never a fabricated all-zero object.
    DemandExchangeDroppedClaim:
      type: object
      properties:
        id:
          type: string
          description: >-
            This drop record's own id — the handle adjudicate_pitch_claim_drop
            targets (AI-7079 slice 2).
        claimText:
          type: string
          description: >-
            The composer's own sentence — never the buyer's raw brief text,
            though a composer sentence may itself paraphrase a buyer-stated
            value, the same way the rest of a pitch narrative can.
        section:
          type: string
          description: >-
            Where this sentence would have rendered (thesis, valueCase, ... or
            'roles' for a per-product fit argument).
        productId:
          nullable: true
          description: >-
            Set only when section is 'roles' — the product this fit argument was
            for.
          type: string
        citations:
          type: array
          items:
            $ref: '#/components/schemas/DemandExchangeDroppedClaimCitation'
          description: The sentence's own citations, as it carried them.
        resolvedFacts:
          type: array
          items:
            type: string
          description: >-
            The exact fact strings the truth gate judged this sentence against —
            seller-ingredient facts only, by construction of the gate that
            produced them. Empty for a citation_grounding-stage drop: nothing
            the sentence cited survived to be judged against anything.
        dropStage:
          type: string
          enum:
            - citation_grounding
            - numeric_containment
            - entailment_verdict
          description: >-
            Which of the truth gate's three checkable stages rejected this
            sentence. A sentence resting solely on the buyer's brief is never
            surfaced here at all — excluded before persistence, not merely
            hidden.
        adjudication:
          type: string
          enum:
            - open
            - fact_taught
            - paraphrase_attested
            - drop_confirmed
          description: >-
            The seller's ruling on this drop, or 'open' when none has been made
            yet. Write it with adjudicate_pitch_claim_drop (AI-7079 slice 2):
            'fact_taught' and 'paraphrase_attested' each link a corpus fact via
            id; 'drop_confirmed' needs none. Every ruling is one atomic database
            transaction, so this value is never anything other than 'open' or a
            fully-completed final ruling — there is no intermediate state to
            observe.
        adjudicatedAt:
          nullable: true
          description: >-
            When the ruling above was made, or null while adjudication is
            'open'.
          type: string
      required:
        - id
        - claimText
        - section
        - productId
        - citations
        - resolvedFacts
        - dropStage
        - adjudication
        - adjudicatedAt
      additionalProperties: false
      description: >-
        One sentence the truth gate rejected on this pass's compose, enriched
        with what it was judged against.
    DemandExchangeBidDecision:
      type: object
      properties:
        disposition:
          type: string
          enum:
            - pitch
            - counter_pitch
            - pass
          description: >-
            What the seller's doctrine decided to do with this brief. 'pitch' =
            the brief fits, argue it in full. 'counter_pitch' = the literal ask
            does not fit but a credible reframe exists, carried by the honest
            counter. 'pass' = decline, with a short branded decline in the
            pitch's `decline` section.
        decidedBy:
          type: string
          enum:
            - no_products
            - floor_posture
            - no_comparable_terms
            - no_doctrine_configured
            - category_fit_met
            - counter_appetite
            - category_fit_below_pass_floor
          description: >-
            Which rule reached the disposition. 'no_products' = nothing
            composed, so there was no plan to argue for. 'floor_posture' = the
            buyer stated a budget and no line could be priced against it.
            'no_comparable_terms' = the brief named no vertical or channel that
            could be compared, so fit was not measurable and the pass pitched.
            'no_doctrine_configured' = this storefront has never set a selling
            doctrine, so there was no gate to apply and the brief was answered
            exactly as it would have been before doctrine existed.
            'category_fit_met' / 'counter_appetite' /
            'category_fit_below_pass_floor' = measured fit against the
            doctrine's pitch bar and the pass floor its counter appetite
            implies.
        threshold:
          nullable: true
          description: >-
            The 0–1 fit share the measured fit was compared against, or null for
            the structural rules no threshold could have changed.
          type: number
          minimum: 0
          maximum: 1
        categoryFit:
          nullable: true
          description: >-
            The measured share, from 0 to 1, of the brief's comparable terms
            this catalogue serves. Null means nothing comparable was named —
            genuinely not measurable, NOT a fit of zero, and readers must never
            render the two the same way.
          type: number
          minimum: 0
          maximum: 1
        doctrineVersion:
          nullable: true
          description: >-
            The operating-instructions version whose doctrine decided this, or
            null when the platform default stood in. Same version a claim citing
            `operating_instructions` names, so the disposition receipt and a
            claim chip open the same Playbook.
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
        doctrineVariant:
          type: string
          enum:
            - house_default
            - premium_scarcity_house
            - volume_partner
            - consultative
            - custom
          description: >-
            The named doctrine the deciding thresholds correspond to. Derived
            from the thresholds themselves rather than the label the seller
            picked: a seller who adopts a named variant and then edits a number
            is running their own doctrine, which is what 'custom' says.
        fedBy:
          default: []
          description: >-
            The openable receipt behind this decision: the Playbook version
            whose doctrine decided, resolved to its owner Task exactly like a
            product row's chips and a pitch claim's chips. Same shape, same
            ledger, same code path — 'why did we pass on this brief?' opens the
            thing that decided it rather than describing it. Empty when the
            platform default decided (there is no seller-authored version to
            open), or when the decision's provenance write did not land.
          type: array
          items:
            $ref: '#/components/schemas/DemandExchangeFedBy'
      required:
        - disposition
        - decidedBy
        - threshold
        - categoryFit
        - doctrineVersion
        - doctrineVariant
        - fedBy
      additionalProperties: false
    DemandExchangeStory:
      type: object
      properties:
        storyId:
          type: string
          minLength: 1
        title:
          type: string
          minLength: 1
        advertiserName:
          nullable: true
          type: string
        vertical:
          nullable: true
          type: string
        challenge:
          type: string
        method:
          type: string
        results:
          type: array
          items:
            $ref: '#/components/schemas/DemandExchangeStoryResult'
          description: >-
            The story's own verbatim measured outcomes, in the seller's words.
            Never truncated or paraphrased.
        provenanceLabel:
          type: string
          minLength: 1
      required:
        - storyId
        - title
        - advertiserName
        - vertical
        - challenge
        - method
        - results
        - provenanceLabel
      additionalProperties: false
      description: >-
        The narrative case study (AI-6725) matched to this pass's brief, for the
        Proof section of the story-first proposal card (AI-6736). Deterministic
        keyword-tier matching, not a model call — present whenever a live story
        matched, regardless of the pitch composer's own lifecycle state.
    DemandExchangeRevisionComposeError:
      type: object
      properties:
        code:
          type: string
        message:
          type: string
      required:
        - code
        - message
      additionalProperties: false
    DemandExchangeRevisionApproval:
      type: object
      properties:
        gate:
          nullable: true
          description: >-
            Null before the revision is ever submitted. Cleared back to null
            (with decidedAt/decidedByUserId/notes) on every resubmission after a
            rejection, so a pending revision never reports a decision nobody has
            made against THIS submission.
          type: string
          enum:
            - pending
            - approved
            - rejected
        decidedByUserId:
          nullable: true
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
        decidedAt:
          nullable: true
          type: string
        notes:
          nullable: true
          type: string
      required:
        - gate
        - decidedByUserId
        - decidedAt
        - notes
      additionalProperties: false
    DemandExchangeRevisionSend:
      type: object
      properties:
        mode:
          nullable: true
          type: string
          enum:
            - manual
            - auto
        autoApproveReason:
          nullable: true
          type: string
          enum:
            - storefront_auto_approve
            - buyer_auto_approve
      required:
        - mode
        - autoApproveReason
      additionalProperties: false
      description: >-
        How a sent revision cleared. Null until sent. An auto send never carries
        a human decision record.
    Scope3MerchandisePlacementDelivery:
      type: object
      properties:
        placement:
          $ref: '#/components/schemas/Scope3MerchandisePlacement'
        delivery:
          nullable: true
          allOf:
            - $ref: '#/components/schemas/Scope3MerchandiseDelivery'
      required:
        - placement
        - delivery
      additionalProperties: false
      description: >-
        One avail's placement and its own delivery. `delivery` is null when the
        placement itself is unresolved (not backed by the static-avails-feed
        lane, or the avail could not be found).
    Scope3MerchandiseAudience:
      type: object
      properties:
        facts:
          maxItems: 5
          type: array
          items:
            $ref: '#/components/schemas/Scope3MerchandiseAudienceFact'
        truncated:
          type: boolean
          description: >-
            `true` when EITHER cap layer (not the seller's own data) is why
            `facts` is shorter than what actually matched for this product: the
            per-product cap (5, applied first) or the response-level budget (24
            total, applied across products in a large response) — a later
            product can run out of the response budget entirely (`facts: []`,
            `truncated: true`) or partially. Both layers set this SAME flag; a
            reader does not need to know which one cut. Never conflate this with
            a genuine `facts: []` from a seller who simply hasn't taught a
            matching fact yet (`truncated: false`): the two must render
            differently, since one is a deliberate cap and the other is real
            seller-data absence.
      required:
        - facts
        - truncated
      additionalProperties: false
      description: >-
        Audience facts from the seller's product-marketing corpus, matched to
        this product's channel(s) and capped at 5 per product (24 per response —
        later products in a large response may show fewer facts once that budget
        is spent, flagged via `truncated`). Empty with `truncated: false` means
        the seller has not taught any matching facts yet — honest absence, not a
        guess.
    DemandExchangePitchClaim:
      type: object
      properties:
        claimRef:
          type: string
          minLength: 1
          description: >-
            Stable server-minted handle for this sentence, matching the claimRef
            on its fedBy receipts. Never blank.
        text:
          type: string
          minLength: 1
          description: The sentence, as composed. Never blank.
        sources:
          minItems: 1
          type: array
          items:
            type: object
            properties:
              kind:
                type: string
                enum:
                  - brief
                  - inventory_bundle
                  - signal
                  - pricing_fact
                  - operating_instructions
                  - composed_product
                  - corpus
                  - affordance
              ref:
                type: string
                minLength: 1
            required:
              - kind
              - ref
            additionalProperties: false
          description: >-
            Every reference that entitled this sentence to be made — the
            complete citation list, including the exchange's own material (the
            brief's stated fields, this pass's composed products) and the
            platform's own next-step verbs ('affordance': 'refine',
            'create_media_buy'), none of which has a seller ingredient behind
            it. Never empty: a claim with no surviving source is dropped at
            composition time rather than rendered. 'corpus' is reserved for the
            seller's marketing corpus and is never present until that ingredient
            is wired.
        fedBy:
          default: []
          description: >-
            The openable receipts behind this sentence: the seller's own
            ingredients it cited, resolved to their owner Tasks exactly like a
            product row's chips. A subset of sources — brief and
            composed-product references are real citations but are not seller
            ingredients, so they carry no owner. Empty when the claim cited only
            those, or when the claim's provenance write failed.
          type: array
          items:
            $ref: '#/components/schemas/DemandExchangeFedBy'
      required:
        - claimRef
        - text
        - sources
        - fedBy
      additionalProperties: false
    DemandExchangePitchRole:
      type: object
      properties:
        productId:
          type: string
          minLength: 1
          description: >-
            A composed product id from this same pass. A role naming anything
            else is dropped at composition time. Never blank.
        role:
          type: string
          minLength: 1
          description: >-
            The part this product plays in the strategy, in the seller's words
            (e.g. the anchor, the amplifier, the experiment). Free text because
            the shape of a plan is the seller's judgment, not a platform enum.
        claims:
          minItems: 1
          type: array
          items:
            $ref: '#/components/schemas/DemandExchangePitchClaim'
          description: >-
            The fit argument: why this asset, for this outcome, for this buyer.
            Never empty — a role with no surviving claim is dropped, and the
            product still renders in the plan appendix from its own persisted
            row.
      required:
        - productId
        - role
        - claims
      additionalProperties: false
    PitchAbsentCatalogue:
      type: object
      properties:
        products:
          type: integer
          minimum: 0
          maximum: 9007199254740991
        inventoryBundles:
          type: integer
          minimum: 0
          maximum: 9007199254740991
        signals:
          type: integer
          minimum: 0
          maximum: 9007199254740991
        pricingFacts:
          type: integer
          minimum: 0
          maximum: 9007199254740991
        affordances:
          type: integer
          minimum: 0
          maximum: 9007199254740991
        briefRefs:
          type: integer
          minimum: 0
          maximum: 9007199254740991
        playbook:
          type: boolean
          description: >-
            Whether a Playbook (operating instructions) fed this pass — a single
            fact, not a count.
        corpus:
          type: boolean
          description: >-
            Whether the seller's marketing corpus fed this pass. Always false
            today: no production caller feeds the corpus into pitch composition
            yet (AI-5702 wires it into product composition only).
      required:
        - products
        - inventoryBundles
        - signals
        - pricingFacts
        - affordances
        - briefRefs
        - playbook
        - corpus
      additionalProperties: false
      description: >-
        How many of each citable ingredient kind the grounding index offered
        this pass — computed once, before either grounding decision, so it
        describes the catalogue the composer was actually handed rather than
        what survived enforcement.
    PitchAbsentDrops:
      type: object
      properties:
        modelClaims:
          type: integer
          minimum: 0
          maximum: 9007199254740991
          description: Every claim the model drafted, before either grounding decision.
        citationGroundingDropped:
          type: integer
          minimum: 0
          maximum: 9007199254740991
          description: >-
            Claims removed by citation grounding and disposition shaping — the
            first independent check.
        entailmentDropped:
          type: integer
          minimum: 0
          maximum: 9007199254740991
          description: >-
            Claims removed by the entailment gate — the second, independent
            check. Zero when the gate never ran (the pass never survived
            citation grounding to reach it).
        reasons:
          description: >-
            Closed, counts-only histogram of citation-grounding and entailment
            drop reasons. New diagnostics include it; its absence denotes a row
            written before this field existed.
          type: object
          properties:
            no_sources:
              type: integer
              minimum: 0
              maximum: 9007199254740991
            unknown_source_kind:
              type: integer
              minimum: 0
              maximum: 9007199254740991
            unsourced_reference:
              type: integer
              minimum: 0
              maximum: 9007199254740991
            corpus_unavailable:
              type: integer
              minimum: 0
              maximum: 9007199254740991
            affordance_unavailable:
              type: integer
              minimum: 0
              maximum: 9007199254740991
            decapitated_section:
              type: integer
              minimum: 0
              maximum: 9007199254740991
            section_not_permitted_for_disposition:
              type: integer
              minimum: 0
              maximum: 9007199254740991
            cross_product_reference:
              type: integer
              minimum: 0
              maximum: 9007199254740991
            ambiguous_owner:
              type: integer
              minimum: 0
              maximum: 9007199254740991
            unknown_product:
              type: integer
              minimum: 0
              maximum: 9007199254740991
            duplicate_role:
              type: integer
              minimum: 0
              maximum: 9007199254740991
            unlabelled_role:
              type: integer
              minimum: 0
              maximum: 9007199254740991
            empty_text:
              type: integer
              minimum: 0
              maximum: 9007199254740991
            not_entailed:
              type: integer
              minimum: 0
              maximum: 9007199254740991
            brief_only_outside_what_we_heard:
              type: integer
              minimum: 0
              maximum: 9007199254740991
            numeric_not_in_sources:
              type: integer
              minimum: 0
              maximum: 9007199254740991
          additionalProperties: false
      required:
        - modelClaims
        - citationGroundingDropped
        - entailmentDropped
      additionalProperties: false
      description: >-
        How many claims the model asserted, how many each independent grounding
        check removed, and (for new diagnostics) the bounded reason histogram.
    DemandExchangeDroppedClaimCitation:
      type: object
      properties:
        kind:
          type: string
        ref:
          type: string
      required:
        - kind
        - ref
      additionalProperties: false
      description: One citation a dropped claim carried, as the composer wrote it.
    DemandExchangeStoryResult:
      type: object
      properties:
        metric:
          type: string
          minLength: 1
        attribution:
          type: string
          minLength: 1
      required:
        - metric
        - attribution
      additionalProperties: false
    Scope3MerchandisePlacement:
      oneOf:
        - type: object
          properties:
            unresolved:
              type: boolean
              enum:
                - false
            properties:
              minItems: 1
              maxItems: 8
              type: array
              items:
                $ref: '#/components/schemas/Scope3MerchandisePlacementProperty'
              description: >-
                Every property this avail resolves to against the Property
                Roster, up to 8 — a selector like `by_tags` can legitimately
                match more than one live property, and all of them run this
                avail, so all are listed rather than picking one, up to this
                cap. `adUnit` below applies once to the avail group, not per
                property, to avoid double-counting a single avail's delivery
                across its properties. See `omittedPropertyCount` for how many
                more matched but were cut.
            adUnit:
              nullable: true
              type: string
            omittedPropertyCount:
              default: 0
              description: >-
                How many additional live properties this avail resolved to
                beyond the 8 listed in `properties` — 0 when nothing was cut. A
                `by_tags` selector can match hundreds of properties; this
                discloses the overflow instead of silently dropping it or
                letting one placement dominate the response budget. Defaults to
                0 for artifacts persisted before this field existed.
              type: integer
              minimum: 0
              maximum: 9007199254740991
          required:
            - unresolved
            - properties
            - adUnit
            - omittedPropertyCount
          additionalProperties: false
        - type: object
          properties:
            unresolved:
              type: boolean
              enum:
                - true
            reason:
              type: string
          required:
            - unresolved
            - reason
          additionalProperties: false
      description: >-
        Where this avail actually runs, resolved against the storefront's
        Property Roster. `unresolved: true` is an honest statement that the
        underlying property could not be matched — never a guessed name.
      type: object
    Scope3MerchandiseDelivery:
      type: object
      properties:
        kind:
          type: string
          enum:
            - full_slot
            - available_capacity
          description: >-
            'full_slot' — the product's ENTIRE backing is this one avail, so
            this number describes the whole slot. 'available_capacity' — every
            other case (a partial listing, or one avail among several backing
            this product). Neither is a guarantee: no hold exists on this avail
            until a real booking (create_media_buy) places one.
        netAvailableImpressions:
          type: integer
          minimum: 0
          maximum: 9007199254740991
          description: >-
            This avail's impressionsCapacity minus heldImpressions minus
            bookedImpressions, floored at 0 — a live feed snapshot, not a
            reservation. A genuinely sold-out avail reports a real 0 rather than
            being omitted.
        windowStart:
          type: string
          description: >-
            This avail's own window start — never unioned with another avail's
            window.
        windowEnd:
          type: string
          description: >-
            This avail's own window end — never unioned with another avail's
            window.
        provenance:
          type: string
          enum:
            - seller_available_capacity
          description: >-
            Always seller_available_capacity: derived from live
            inventory_source_avails rows, never a fabricated curve.
      required:
        - kind
        - netAvailableImpressions
        - windowStart
        - windowEnd
        - provenance
      additionalProperties: false
      description: >-
        How much delivery capacity the seller has made available for this
        avail's window — a snapshot of the feed, not a hold reserved for this
        proposal.
    Scope3MerchandiseAudienceFact:
      type: object
      properties:
        statement:
          type: string
          description: >-
            One thing the seller's own marketing material says about this
            audience.
        attribution:
          type: string
          description: >-
            How this fact is sourced: a verbatim document locator, or a label
            naming it the seller's own stated or attested account. Never blank.
        sourceRef:
          type: string
          description: The product-marketing corpus claim id this fact came from.
      required:
        - statement
        - attribution
        - sourceRef
      additionalProperties: false
    Scope3MerchandisePlacementProperty:
      type: object
      properties:
        publisherDomain:
          type: string
        propertyName:
          type: string
      required:
        - publisherDomain
        - propertyName
      additionalProperties: false
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: API key or access token

````