Skip to content

[Proposal]: Add per-seller offers to catalog variants #810

Description

@proshoumma

Summary

Today UCP assumes the business serving a catalog is the only seller and Merchant of Record (MoR) for everything it lists: variant.id is the checkout item ID and variant.price is the price checkout honors. That breaks for an aggregator - a marketplace or shopping agent that lists the same product sold by several different merchants, each running its own checkout.

This proposal adds an optional offers[] array to the catalog variant. Each offer names the seller acting as MoR (seller.domain), that seller's own checkout item ID (item_id), and the price. A variant with no offers[] behaves exactly as today.

Motivation

The catalog schema ties three roles to one business - who serves the data, who is named in it, and who is the MoR. There is no way to say "sold by shop A at $10 and shop B at $12," and no machine-readable seller identity to route to. variant.seller is display-only.

Bulk product discovery is an active, chartered area of UCP, and this fills a gap that workstream explicitly leaves open:

  • Domain Working Group (DWG) Charter: Bulk Product Discovery #682 standardizes how a single business publishes its full catalog in bulk. It specifies business discovery for marketplaces as out-of-scope. So this proposal is complementary and reuses catalog/variant/GID model, along with the authority anchor. We'll bring it to the DWG for a scope check.
  • feat: define namespace authority binding + broaden RDNS grammar #530 already defines how a domain name proves it controls a namespace (provenance, not trust). offers[].seller.domain reuses it: the Platform confirms the seller controls the domain, then verifies its signing keys (RFC 9421 via /.well-known/ucp) before routing. Nothing new to invent.

Beneficiaries: aggregators/marketplaces, the merchants they list (keep their own MoR and checkout), and shopping agents (a verifiable routing target per offer).

Goals

  • Let one variant carry multiple offers, each attributed to a different seller/MoR.
  • Give each offer a machine-resolvable seller identity that verifies via existing authority binding.
  • Make checkout routing unambiguous when offers are present.
  • Keep the no-offers path fully unchanged: a variant without offers[] behaves identical as today, so existing single-merchant catalogs and examples are untouched.

Non-Goals

Detailed Design

New type shopping/types/offer.json — required: seller, item_id, price.

Field Req Meaning
seller.domain Seller's bare host (e.g. shop.example.com). The MoR, and the host whose checkout resolves item_id. The Platform resolves it to /.well-known/ucp and verifies keys via authority binding.
seller.name, seller.links Display name and policy links.
item_id Item ID for the seller's own checkout; used as item.id in place of variant.id.
price Authoritative price for this offer, on the variant's sale basis.
list_price, unit_price, availability, url, sku Optional display/fulfillment fields, reusing existing types.

variant.json — add optional offers[] (items: offer.json, minItems: 1); amend variant.id (checkout item ID only when offers is absent) and clarify variant.seller as display-only (assigns no MoR).

Behavior: When offers is present, the Platform MUST use the selected offers[].item_id, MUST NOT use variant.id, and MUST treat offers[].price as authoritative over variant.price. It resolves offers[].seller.domain and checks out at that seller. Checkout already accepts only an item.id the Business recognizes, so a Business that is not the MoR for an item already rejects it (e.g. item_unavailable).

Scope: offers[] sits on the shared variant type, so it appears wherever variants are returned.

Documentation: Update the catalog "Relationship to Checkout" rule; add an Offer concept, a schema section, and one worked two-merchant example (offers ride on the shared variant, so one example covers all responses). Reconcile the Business / MoR wording: the glossary currently calls a Business the MoR absolutely, while core-concepts already hedges ("typically… not limited to transactional use cases") - a pre-existing contradiction. The glossary definition becomes "the entity exposing UCP capabilities; in transactional contexts typically the MoR," and core-concepts gains a short paragraph: a catalog is single-seller by default and only names per-offer MoRs when offers[] is present. This aligns existing text; it introduces no new contract.

Risks and Mitigations

  • Security: A catalog could name a seller domain it doesn't control. The Platform doesn't trust seller.domain on sight - it confirms domain control and verifies the seller's signatures (RFC 9421 via /.well-known/ucp) before routing. This is UCP's existing identity check.
  • Performance: No-offers responses are byte-identical; aggregator responses grow only by the offers they carry. Seller-profile lookups are cacheable and already performed to verify any business.
  • Backwards compatibility: Additive and fail-safe, so classified non-breaking.
  • Complexity: One new type plus one optional array; no new actor, registry, capability, or endpoint.

Test Plan

  • Unit Tests: Validate the offer.json schema and the variant.offers[] composition: seller, item_id, and price required; seller.domain present; offers has minItems: 1; and the reused $ref types resolve.
  • Integration Tests: scripts/validate_examples.py validates the worked two-merchant example against the full schema graph across the catalog responses, and confirms the existing no-offers examples are unchanged.
  • End-to-End Tests. No runtime exists in this repo, so end-to-end automation is out of scope for this change.

Graduation Criteria

Working Draft → Candidate:

  • Schema merged and documented (with Working Draft disclaimer).
  • Unit and integration tests are passing.
  • Initial documentation is written.
  • TC majority vote to advance.

Candidate → Stable:

  • Adoption feedback has been collected and addressed.
  • Full documentation and migration guides are published.
  • TC majority vote to advance.

Implementation History

  • [2026-09-07]: Proposal submitted.
  • [YYYY-MM-DD]: TC approved "Provisional"; capability enters "Working Draft".
  • [YYYY-MM-DD]: TC approved advancement to "Candidate".
  • [YYYY-MM-DD]: TC approved "Implemented"; capability enters "Stable".

Code of Conduct

  • I agree to follow this project's Code of Conduct

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions