Skip to content

feat: eligibility claims & verification contract#250

Open
igrigorik wants to merge 7 commits intomainfrom
feat/context-eligibility
Open

feat: eligibility claims & verification contract#250
igrigorik wants to merge 7 commits intomainfrom
feat/context-eligibility

Conversation

@igrigorik
Copy link
Contributor

Context: #214 (comment). Supersedes #214, closes #137.

Introduce context.eligibility — buyer claims about eligible benefits (loyalty membership, payment instrument perks, etc.) that Businesses can act on across the shopping lifecycle.

Processing model:

  • Platform provides claims via context.eligibility on any request
  • Business MAY act on recognized claims (adjust pricing, product access, provisional discounts); MUST ignore unrecognized claims
  • At checkout completion, all claims that influenced the checkout MUST be resolved: verified against proof, or rescinded by Platform
  • Unresolved claims block completion (invalid_eligibility error)
  • Business MUST NOT mutate checkout on verification failure

Layering:

  • context.json: eligibility array with reverse-domain $ref validation
  • checkout.md: normative verification contract (core obligation)
  • discount.json: provisional + eligibility fields on applied_discount for structured attribution when extension is active
  • catalog/index.md: MAY adjust price/list_price for eligible claims
  • error_code.json: invalid_eligibility standard error

Eligibility lives on context (not PaymentInstrument) enabling full-funnel coverage from catalog through checkout. Verification is a core checkout concern; the discount extension adds attribution.


Checklist

  • New feature (non-breaking change which adds functionality)
  • 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

@igrigorik igrigorik requested a review from maximenajim March 10, 2026 23:25
@igrigorik igrigorik self-assigned this Mar 10, 2026
@igrigorik igrigorik requested review from a team as code owners March 10, 2026 23:25
@igrigorik igrigorik added the TC review Ready for TC review label Mar 10, 2026
Copy link

@ACSchil ACSchil left a comment

Choose a reason for hiding this comment

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

This LGTM. I like the direction we ended up taking; it works elegantly within the broader spec.

such as loyalty membership, payment instrument perks, and similar. These are
claims, not verified facts. Businesses **MAY** act on recognized claims during
the session (adjusting pricing, granting product access, applying provisional
discounts), but all claims that influenced the checkout **MUST** be resolved

Choose a reason for hiding this comment

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

The contract hinges on “all claims that influenced the checkout MUST be resolved”, but “influenced” (and even “recognized”) isn’t defined precisely enough to guarantee interoperable behavior.
In practice, platforms need a deterministic way to know which specific context.eligibility[] entries the Business actually used (pricing, access gating, etc.) so they can remediate without guesswork. Right now the only guidance is “SHOULD notify… when a claim is not applied,” which doesn’t cover the applied case.

Suggestion: define “influenced” as “reflected in the Business response” and require the Business to explicitly echo applied/active claims (e.g., a new response field like eligibility_applied: [...] / eligibility_pending_verification: [...], or a standardized messages entry per accepted claim). For discount-related effects, discounts.applied[].eligibility covers this, but there’s no equivalent for non-discount effects like product access or catalog price/list_price adjustments.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good observation. Tightened "influenced" to "accepted" in the latest revision — the partition is accepted vs. not-accepted, and Businesses SHOULD warn when a claim is not accepted.

The checkout state itself is the effect of accepted claims — pricing, availability, line items all reflect what the Business did with them. When a structured extension exists (like the discount extension's applied[].eligibility), it provides explicit attribution — I would keep this contract, instead of trying to model a generic eligibility_applied. For the general case, the Business MAY use messages with type: "info" to explain the effects of accepted claims — integrated into the spec text.

@douglasborthwick-crypto

The context.eligibility + provisional model here is clean. One gap worth flagging: the verification contract at completion says claims "MUST be resolved: verified against proof, or rescinded by Platform" — but the spec doesn't define what "proof" looks like for claims that aren't derivable from the payment credential.

For instrument-bound claims (RedCard BIN check), the Business can verify locally. For non-instrument claims — loyalty tiers, on-chain holdings, membership status — the Business needs something to verify against.

A minimal option: let context.eligibility entries optionally carry a companion attestation (sig + kid) that the Business can check against a published JWKS. That keeps verification deterministic without requiring the Business to call back to the Platform or the claim source. More detail in #214.

igrigorik added a commit that referenced this pull request Mar 12, 2026
  Terminology:
  - "claims that influenced the checkout" → "accepted claims" throughout,
    aligning with the accepted/not-accepted partition (maximenajim, ACSchil)
  - "not applied" → "not accepted" for consistency

  Verification semantics:
  - Clarify that verification failure MUST only affect the messages array,
    not checkout state (line items, totals, discounts, etc.) (maximenajim)
  - Add path field to eligibility_invalid example for machine-readable
    partial failure identification (ACSchil)

  Messages contract:
  - Add MAY use type: "info" to explain effects of accepted claims,
    complementing SHOULD warn on rejection (maximenajim)

  Catalog:
  - Add non-binding pricing contract to "Relationship to Checkout" section:
    catalog responses are not transactional commitments, checkout is
    authoritative, responses SHOULD NOT be reused across sessions
    without re-validation (maximenajim)
@igrigorik
Copy link
Contributor Author

@maximenajim ty, great feedback, ptal at updated draft.

@douglasborthwick-crypto we're intentionally leaving the verification mechanism open — "UCP does not prescribe how verification occurs" — because the proof model varies by claim type:

  • Payment instrument claims: the credential itself is the proof, no additional mechanism needed
  • Authenticated session claims (loyalty, membership): can be validated via provided identity / linking
  • Third-party attestation: this is where a signed proof would operate

A companion attestation mechanism (sig + kid against a published JWKS) is a great candidate for a capability extension that complements eligibility. Additive and non-breaking: platforms that can provide attestations send them alongside the claim; businesses that require them check, others ignore.

@igrigorik igrigorik requested a review from maximenajim March 12, 2026 16:42
@douglasborthwick-crypto

That's the right split — prescribing verification in core would overfit to one claim type. The capability extension model keeps eligibility clean.

Happy to draft that extension. The shape I have in mind:

{
  "context": {
    "eligibility": [{
      "type": "token_holder",
      "label": "USDC holder discount",
      "attestation": {
        "sig": "<base64 ECDSA signature>",
        "kid": "insumer-attest-v1",
        "jwks_uri": "https://insumermodel.com/.well-known/jwks.json",
        "condition_hash": "<SHA-256 of evaluated condition>",
        "attested_at": "2026-03-12T16:40:00Z",
        "expires_at": "2026-03-12T17:10:00Z"
      }
    }]
  }
}

The Business verifies the signature against the JWKS, checks condition_hash matches the claim, confirms expires_at hasn't passed. No callback to the Platform or the attestation provider needed — fully offline verification.

This is already the pattern in #203 (com.insumermodel.attestation) and the x402 cold-start spec. I'll put together a capability extension draft against the current eligibility schema once #250 lands.

Copy link
Contributor

@lemonmade lemonmade left a comment

Choose a reason for hiding this comment

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

I really like where you landed with this 👏

Comment on lines +69 to +72
"eligibility": {
"$ref": "../ucp.json#/$defs/reverse_domain_name",
"description": "The eligibility claim accepted by the Business for this discount. Corresponds to a value from context.eligibility. Omitted for code-based and non-eligibility automatic discounts."
},
Copy link
Contributor

Choose a reason for hiding this comment

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

Something doesn’t quite sit right with me about this. I think it’s that we are providing details about the source of one kind of automatic discount with a top-level field. If we wanted to indicate the source of other automatic discounts (from identity linking, based on the line items, etc), we would need additional fields. Did you consider any design for this that broadened this field a bit to leave space for more detail on other discount types?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good prompt. There is potential for "provenance proliferation" here, but so far I haven't spotted a generic schema. If you squint, codes and eligibility are both string[] — but they carry different lifecycle contracts that you'd want to capture and distinguish, so a generic source would still need type-specific semantics. All we'd have done is push the modeling complexity down one layer.

Open to suggestions.

@igrigorik
Copy link
Contributor Author

@douglasborthwick-crypto 👍 modulo one gotcha on example you shared...

context.eligibility is string[] with reverse-domain validated items. Your example changes items from strings to objects, which breaks the contract. The attestation should live separately from eligibility not inline, e.g...

  {
    "context": {
      "eligibility": ["com.insumermodel.token_holder"],
      "attestations": {
        "com.insumermodel.token_holder": {
          "sig": "<base64>",
           ...
        }
      }
    }
  }

^ either that or on cart/checkout object similar to discount extension.

igrigorik and others added 5 commits March 12, 2026 21:37
  Introduce `context.eligibility` — buyer claims about eligible benefits
  (loyalty membership, payment instrument perks, etc.) that Businesses
  can act on across the shopping lifecycle.

  Processing model:
  - Platform provides claims via context.eligibility on any request
  - Business MAY act on recognized claims (adjust pricing, product
    access, provisional discounts); MUST ignore unrecognized claims
  - At checkout completion, all claims that influenced the checkout
    MUST be resolved: verified against proof, or rescinded by Platform
  - Unresolved claims block completion (invalid_eligibility error)
  - Business MUST NOT mutate checkout on verification failure

  Layering:
  - context.json: eligibility array with reverse-domain $ref validation
  - checkout.md: normative verification contract (core obligation)
  - discount.json: provisional + eligibility fields on applied_discount
    for structured attribution when discount extension is active
  - catalog/index.md: MAY adjust price/list_price for eligible claims
  - error_code.json: invalid_eligibility standard error

  Key design decision: eligibility lives on context (not PaymentInstrument)
  enabling full-funnel coverage from catalog through checkout. Verification
  is a core checkout concern; the discount extension adds attribution.
Co-authored-by: Alex Schillinger <alexcschillinger@gmail.com>
Co-authored-by: Alex Schillinger <alexcschillinger@gmail.com>
  Terminology:
  - "claims that influenced the checkout" → "accepted claims" throughout,
    aligning with the accepted/not-accepted partition (maximenajim, ACSchil)
  - "not applied" → "not accepted" for consistency

  Verification semantics:
  - Clarify that verification failure MUST only affect the messages array,
    not checkout state (line items, totals, discounts, etc.) (maximenajim)
  - Add path field to eligibility_invalid example for machine-readable
    partial failure identification (ACSchil)

  Messages contract:
  - Add MAY use type: "info" to explain effects of accepted claims,
    complementing SHOULD warn on rejection (maximenajim)

  Catalog:
  - Add non-binding pricing contract to "Relationship to Checkout" section:
    catalog responses are not transactional commitments, checkout is
    authoritative, responses SHOULD NOT be reused across sessions
    without re-validation (maximenajim)
  Standardize codes for the three eligibility message types:
  eligibility_not_accepted (warning), eligibility_accepted (info),
  and eligibility_invalid (error at completion).
@igrigorik igrigorik force-pushed the feat/context-eligibility branch from c0fc5ee to f8e70b4 Compare March 13, 2026 04:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

TC review Ready for TC review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat]: Allow tender-based discounts via payment instrument qualifiers in Checkout Session Update

5 participants