Skip to content

feat: Add basic schema for loyalty extension#251

Open
ziwuzhou-google wants to merge 4 commits intoUniversal-Commerce-Protocol:mainfrom
ziwuzhou-google:feat/loyalty
Open

feat: Add basic schema for loyalty extension#251
ziwuzhou-google wants to merge 4 commits intoUniversal-Commerce-Protocol:mainfrom
ziwuzhou-google:feat/loyalty

Conversation

@ziwuzhou-google
Copy link

@ziwuzhou-google ziwuzhou-google commented Mar 11, 2026

Summary

A new loyalty extension is introduced to facilitate high-fidelity loyalty experiences across various commerce journeys, ensuring that shoppers can for example sign-up for membership, access personalized benefits, redeem rewards, and manage memberships seamlessly across various existing and future new capabilities.

Motivation

Loyalty is a core concept in Commerce, serving as a primary driver for customer retention and long-term business growth. UCP in its current format can provide minimal and to some extent "hacky" support of loyalty in checkout for example. However, they are far from ideal and lack the generality. As Loyalty is a construct that spans across all verticals (retail, lodging, transportation, etc), the design is meant to encompass and capture the core common semantics of different capabilities and scenarios.

Design Details

Four core concepts are introduced in the schema and structured in a hierarchy:

  • Memberships: the overarching framework of a loyalty program, as well as the specific enrollment status and standing of a customer within it.
  • Tiers: progressive achievement levels within a membership that unlock increasing value based on activity or spend/fee payment.
  • Benefits: ongoing perks and privileges granted to a customer based on their current tier or membership status.
  • Rewards: the accumulated balances and/or stored value available for the customer to redeem on transactions
"memberships": [
  {
    "tiers": [
      {
        "benefits": [
          {
            ...
          }
        ]
      }
    ],
    "rewards": [
      {
        ...
      }
    ]
  }
]

With the help of these four building blocks, one can support use cases such as:

  • Checkout with loyalty benefits (e.g. discount) applied, where applies_on refers to an applied discount in the discount extension
"benefits": [
  {
    "id": "BEN_001",
    "title": "Member discount",
    "description": "Members get $5 off",
    "applies_on": ["$.discounts.applied[0]"]
  }
]
  • Request for loyalty points redemption with multiple types of rewards in one shot (real use case in Travel vertical)
"rewards": [
  {
    "currency": {
      "code": "FLYER_MILE",
    }, 
    "redemption": {
      "amount": 100
    }
  },
  {
    "currency": {
      "code": "PLUS_POINT",
    }, 
    "redemption": {
      "amount": 10
    }
  },
]
  • Customer sign-up multiple memberships (hypothetical case)
"loyalty": {
  "memberships": [
    {
      "id": "membership_1",
      "activated_tiers": ["tier_1"]
    },
    {
      "id": "membership_2",
      "activated_tiers": ["tier_2"]
    }
  ],
  "activated_memberships": ["membership_1", "membership_2"]
}

Type of change

Please delete options that are not relevant.

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing
    functionality to not work as expected, including removal of schema files
    or fields
    )
  • Documentation update

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published in downstream modules

@amithanda
Copy link
Contributor

Great start on this extension!
However, Is there an issue with how request payloads are mapped when a platform attempts to redeem rewards or activate tiers?
Currently, the schema explicitly sets "ucp_request": "omit" on critical identifier fields:

  • id in source/schemas/common/types/loyalty_membership.json
  • currency in source/schemas/common/types/membership_reward.json

The Problem:
If a customer has multiple different memberships (e.g., standard vs credit cardholder) or a single membership with multiple reward currencies (e.g., Points vs Miles), a platform needs a way to explicitly address exactly which membership or reward balance they are redeeming during a Checkout update or Checkout complete request.
Because id and currency are omit, the client is technically forbidden from sending them in the update payload. If a client sends this:

"loyalty": {
  "memberships": [
    {
      "activated_tiers": ["T_1"],
      "rewards": [ 
        { "redemption": { "amount": 100 } } 
      ]
    }
  ]
}

The business server has no stable identifier to know which membership ID T_1 belongs to, or which currency 100 refers to, except by relying on brittle array indexing which changes between requests.

@ziwuzhou-google
Copy link
Author

Great start on this extension! However, Is there an issue with how request payloads are mapped when a platform attempts to redeem rewards or activate tiers? Currently, the schema explicitly sets "ucp_request": "omit" on critical identifier fields:

  • id in source/schemas/common/types/loyalty_membership.json
  • currency in source/schemas/common/types/membership_reward.json

The Problem: If a customer has multiple different memberships (e.g., standard vs credit cardholder) or a single membership with multiple reward currencies (e.g., Points vs Miles), a platform needs a way to explicitly address exactly which membership or reward balance they are redeeming during a Checkout update or Checkout complete request. Because id and currency are omit, the client is technically forbidden from sending them in the update payload. If a client sends this:

"loyalty": {
  "memberships": [
    {
      "activated_tiers": ["T_1"],
      "rewards": [ 
        { "redemption": { "amount": 100 } } 
      ]
    }
  ]
}

The business server has no stable identifier to know which membership ID T_1 belongs to, or which currency 100 refers to, except by relying on brittle array indexing which changes between requests.

Both are correct. These identifiers should not have omit. See the updated example in #251 (comment) for how platform can request redemption with multiple types of award & sign-up for multiple memberships.

Copy link

@sinhanurag sinhanurag left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good start based on all our discussions! Left some comments. Let's iterate on it more and discuss further.

{
"id": "BEN_001",
"title": "Free shipping",
"description": "Complimentary standard shipping on all orders"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We will need some examples of cart, checkout, order objects (starting with checkout is fine) which have this extension as well as the discount object clearly displaying the benefit being applied and the discount object enumerating the benefit.

Like in this case "BEN_001" will apply to discounts.applied.allocations (path = fulfillment-options.total , amount = xx) etc.

Ref: https://ucp.dev/latest/specification/discount/#discounts-object

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added an example here for shipping but I don't think the applies_on needs to go all the way to allocations level as the shipping benefit applies on the entire order.


```json
{
"memberships": [

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we also add a request/response example of how an instrument based discount will look like?

I am thinking of cases when there is a co branded instrument used for purchase. At that point in time the agent might or might not be aware of the associated discount. The agent will send the chosen FOP or identifier as a signal. This signal can be part of the loyalty extension in the incoming checkout/cart request or passed in a different placeholder. The response should contain a loyalty extension with the applied discount based on the instrument.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am actually fine reconciling it with the PR raised here - #250

-cc @igrigorik

So the ingress signal for loyalty identification can come from context.eligibility but then the response can contain the loyalty extension decorating the objects.

@ziwuzhou-google ziwuzhou-google marked this pull request as ready for review March 12, 2026 02:24
@ziwuzhou-google ziwuzhou-google requested review from a team as code owners March 12, 2026 02:24
Copy link

@tpindel tpindel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work on this extension @ziwuzhou-google 👏 the hierarchical memberships → tiers → benefits → rewards model is well-designed, and placing it in common/ for cross-vertical use is the right call.

We've been building a reference implementation of incentives + loyalty on UCP (MCP transport, Shopify + Voucherify API/MCP, ~22 ucp_* tools covering the full shopping lifecycle). Based on real agent-customer interactions, there are a few capabilities we've found essential that the current schema doesn't cover yet. Happy to contribute PRs for any of these if there's interest.

1. Earning Forecast - "What will I earn from this purchase?"

This is the single most impactful missing piece for agent-driven commerce. In our implementation, showing "You'll earn 120 Loyalty Stars/Points with this order, bringing your balance to 4,620" before the customer commits is one of the strongest purchase motivators. It also helps agents handle price objections - points earning becomes additional value on top of any discount.

The current schema has balance (what you have) and redemption (what you're spending), but no way to show what the customer gains from the transaction.

Proposed addition - a new earning_forecast field on membership_reward, response-only:

"earning_forecast": {
    "type": "object",
    "description": "Preview of rewards to be earned from the current transaction.",
    "ucp_request": "omit",
    "properties": {
      "amount": {
        "type": "integer",
        "minimum": 0,
        "description": "Total points to be earned if the transaction completes."
      },
      "rules": {
        "type": "array",
        "items": {
          "type": "object",
          "required": ["id", "amount"],
          "properties": {
            "id": { "type": "string", "description": "Earning rule identifier." },
            "description": { "type": "string", "description": "Human-readable explanation (e.g. '2x points on footwear')." },
            "amount": { "type": "integer", "minimum": 0, "description": "Points earned from this rule." }
          }
        },
        "description": "Breakdown of earning rules contributing to the total."
      },
      "projected_balance": {
        "type": "integer",
        "minimum": 0,
        "description": "Projected available balance after earning and any redemption from this transaction."
      }
    }
}

This fits naturally into the existing membership_reward structure - each reward currency gets its own earning forecast. The per-rule breakdown gives agents transparency to explain why the customer is earning (e.g. "2x points on footwear" vs "base earning on all purchases"), which makes loyalty feel tangible rather than a black box.

2. Tier Progression - "How close am I to the next tier?"

The current schema shows which tiers exist and which are activated, but doesn't tell the agent how close the customer is to qualifying for a higher tier. In practice this drives natural upsell: "You're only 200 points from Platinum - this purchase would get you there!"

Proposed addition to membership_tier:

"progression": {
    "type": "object",
    "description": "Customer's progress toward qualifying for this tier.",
    "ucp_request": "omit",
    "properties": {
      "qualified": {
        "type": "boolean",
        "description": "Whether the customer currently qualifies for this tier."
      },
      "progress_amount": {
        "type": "integer", "minimum": 0,
        "description": "Current progress value toward qualification threshold."
      },
      "threshold_amount": {
        "type": "integer", "minimum": 0,
        "description": "Required value to qualify for this tier."
      },
      "remaining_amount": {
        "type": "integer", "minimum": 0,
        "description": "Gap between current progress and threshold."
      }
    }
}

This is also response-only. For tiers the customer already has, qualified: true. For higher tiers, the agent can use remaining_amount to suggest cart additions that would push the customer over the threshold.

3. Agent Journey Example - When to Present Loyalty Context

Regarding PR #250 (eligibility claims) - in our implementation the agent flow looks like:

  1. Identify - customer provides email or loyalty card → agent looks up member profile (memberships, tiers, rewards balance)
  2. Discover deals - agent checks what promotions + loyalty benefits apply to the current cart, including tier-specific benefits
  3. Present - agent shows price with discount + "you'll earn X points" + "you're Y away from next tier"
  4. Redeem (optional) - customer chooses to spend points → redemption.amount on the relevant reward, linked via applies_on to discount extension
  5. Complete - earning is confirmed in the response

A concrete request/response example in the spec doc showing this full flow - especially the interplay between loyalty benefits, discount applies_on references, beast deals, and earning in the response - would be very helpful for agent developers building on this extension.

Context
We have a working MCP-based UCP server implementing these patterns - earning forecast and the full incentives negotiation lifecycle (search best deal → validate → lock → redeem). Happy to share more details or contribute schema additions as a follow-up PR if any of these proposals are useful.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants