Replies: 4 comments 2 replies
|
Answering the question you carried over from #694, since you left it open here deliberately: a digest, and not as a belt-and-braces addition to
A digest over a canonical serialization of the decision-relevant fields removes the merchant from that step. The agent holds an offer, computes the digest itself, and compares it to the one inside the signed envelope. If they match, the artifact in hand is the artifact attested, and nobody has to be trusted for that comparison. If the merchant reissues, the digest changes and the old attestation stops matching, which is the behaviour you want rather than a failure mode. Keep both. Two notes on making a digest actually work, because this is where it usually goes wrong in practice. Canonicalization has to be specified rather than assumed. If two honest implementations serialize the same offer differently, they compute different digests, every attestation fails to match, and the mechanism degrades into noise that implementers learn to ignore. Whatever gets picked matters less than it being pinned in the text. And the digest should cover exactly the decision-relevant fields you already named ( For what it is worth, I think your source-state invariant is the more important of the two ideas here. Immutability makes an offer identifiable. Binding it to the state it was generated against is what stops three stale offers silently stacking onto a cart that has already moved, and that is the property that actually protects an autonomous buyer. The stacking behaviour you get for free from it is the strongest argument in the proposal. One thing I would push on. |
|
Thanks — this is very helpful, and I think we’re aligned on the model. I agree that
I’ll tighten the proposal so canonicalization is normative rather than implied, and so the digest coverage is explicit. My current intent is for it to cover the transaction-relevant fields such as I also agree that the source-state invariant is the key validity property. On the In other words, an offer is applicable only if:
So if the cart has changed, the offer is invalid even if I’ll make that explicit in the text and add the stale-but-unexpired case as an example/test case, since I agree that’s exactly where implementations could otherwise diverge. I’m planning to update the proposal with:
If that matches what you had in mind, I think the remaining design question is mostly the concrete canonicalization mechanism rather than the validity model itself. |
|
Following up on the last part of the proposal, here’s the smallest v0 shape I get after mapping this onto the current Cart model and incorporating the feedback above. There are two changes from the illustrative shape in the original post that I think are worth making immediately. First, I would drop the Offer-specific Second, I would keep the source-state identifier opaque to this extension. I found #113 while mapping this onto the current schemas; that looks directly adjacent to the source-state problem here. I don’t think Offer should independently invent a second Cart revision/version mechanism. It just needs a stable reference to the exact Cart state the proposal was generated against. So, conceptually, I’m thinking of a capability along these lines: with a Cart response surface roughly like: {
"offers": {
"available": [
{
"id": "offer_01",
"revision": "1",
"applies_to": {
"cart_id": "cart_123",
"state_ref": "..."
},
"expires_at": "2026-08-10T23:00:00Z",
"proposed_update": {
"line_items": [
"... same line-item concepts as an Update Cart request ..."
]
},
"impact": {
"total_delta": 2000
},
"recurrence": {
"...": "shared UCP recurring-commerce semantics"
},
"presentation": {
"title": "Add a travel charger",
"description": "Complete your setup with our compact 65W charger."
}
}
],
"applied": [
{
"id": "offer_00",
"revision": "2",
"affected_line_item_ids": ["li_27"]
}
]
}
}A few details in that sketch are intentional:
ValidityFollowing the point above about No condition overrides another. In particular: So applying Offer A to state ApplicationOne thing I don’t want to paper over in the schema: current The cleaner operation may be conceptually: The Business would validate the referenced Offer against the current Cart state, expiry, and current commercial validity, apply the stored proposal, and return the new authoritative Cart. I’d like guidance on whether there is an existing UCP extension/operation convention that should be reused for that before pinning the transport shape. Evidence bindingI also agree with @westonale-facet’s distinction above: I don’t think the Offer itself necessarily needs to carry its own digest. An agent can derive the digest and third-party evidence in Signals can bind to it. For a concrete starting point, I would define the digest subject as the canonical serialization of the decision/validity-relevant fields: and explicitly exclude: I included For canonicalization, RFC 8785/JCS + SHA-256 seems like the obvious starting point given the Signals discussion in #534/#535, but I’d rather align with whatever that work standardizes than create an Offer-specific convention. So the two things I’d most like feedback on before turning this into an actual JSON Schema/PR are:
|
|
This tightened up well, the conjunctive validity rule and the digest coverage are exactly right, and the stale-but-unexpired example is the one implementations would actually diverge on without it written down. Taking your two open questions in order. On line_items: yes, reuse Cart's semantics, don't invent a second mutation language. Same reasoning you gave for not inventing a second revision mechanism applies here too, every parallel vocabulary is something two implementations can drift on independently. On state_ref: keep it opaque and platform assigned for v0. I went looking for the general Cart/Checkout state identity mechanism you'd want to bind to, and it doesn't exist yet. #113 proposed exactly that, an ETag/If-Match revision field, and it was shelved as future roadmap and never shipped. So there's nothing live to point at today, and I don't think Offer should be the one to invent it either, for the same reason you don't want to invent a second mutation language. If a third extension shows up needing the same primitive, that's the signal to revive #113 as something shared rather than something each extension quietly reinvents on its own. On the transport shape for applying an offer: I checked how this is handled elsewhere in the spec before answering, since I didn't want to guess at precedent. Discounts are the closest existing case, and they go through the standard cart or checkout update request, |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Following up on #694, I wanted to narrow one of the questions there into something more concrete.
The specific gap I'm thinking about is cross-sell / upsell in an agent flow.
UCP already has structured cart state and buyer-initiated cart updates. What seems to be missing is the other side of that interaction:
Today, if a merchant suggests an additional product and the buyer agent adds it, the resulting cart tells us what is there now, but not necessarily why that change happened.
These can end up looking very similar:
For a normal human checkout, that distinction may not matter much.
For an agent that can spend on someone's behalf, I think it starts to matter.
The idea
My current mental model is that an Offer isn't another product type and isn't another pricing authority.
It's a merchant-proposed transition from one commerce state to another.
Something like:
The important part is that the proposed change is structured.
An agent shouldn't have to read:
and infer what transaction the merchant is asking it to make.
The Offer should say that explicitly.
Rough shape
The field names here are illustrative — I'd expect a real version to reuse existing UCP types and cart-update vocabulary.
{ "id": "offer_01", "revision": "1", "applies_to": { "cart_id": "cart_123", "state_ref": "..." }, "expires_at": "2026-08-10T23:00:00Z", "operations": [ { "type": "add", "item_id": "charger_65w", "quantity": 1 } ], "impact": { "total_delta": { "currency": "USD", "amount": "20.00" }, "recurrence": "one_time" }, "presentation": { "title": "Add a travel charger", "description": "Complete your setup with our compact 65W charger." } }recurrencethere is just illustrating the requirement, not proposing a new recurrence schema. I'd want the actual shape to reuse whatever semantics UCP defines for recurring commerce.A few design choices in that example matter more to me than the exact field names.
1. Make an Offer revision immutable, and bind it to the state it was evaluated against
There are really two things I think we need to be able to identify later:
and:
For the first, I'd treat
id + revisionas identifying an immutable Offer artifact.Once
offer_01, revision1has been issued, its decision-relevant meaning shouldn't change underneath the agent.If the merchant changes the proposed operation, price impact, recurrence, or other decision-relevant terms, that should be another revision.
Whether
id + revisionis enough for external evidence to bind to, or whether there should also be a digest over a canonical representation, feels like something worth aligning with the Signals work in #534/#535 rather than defining independently here.For the source state, if an Offer was generated for a particular cart state and that cart changes before the Offer is applied, I don't think the old Offer should silently apply to the new state.
So the second invariant is:
I don't think this proposal needs to invent a new Cart versioning mechanism just for that.
If Cart/Checkout exposes a revision/version, use that. If the appropriate convention is a stable content hash or another state identifier, use that instead.
The important thing is being able to say:
If that state is no longer current, application fails and the merchant can issue a new Offer.
This also gives useful stacking behavior for free
Say the merchant returns three cross-sell Offers, all based on the same cart state.
The agent applies Offer A.
That changes the cart.
Offers B and C are now based on stale state and can't simply be stacked onto the new cart. If the merchant still wants to propose them, it evaluates the new state and reissues them.
I like that property because:
This doesn't answer whether the buyer is authorized to apply $20, then another $20, then another $20. That still looks like an authorization-policy question.
But at the UCP layer, every new proposal is at least evaluated against the actual accumulated commerce state.
2. Keep transaction data separate from presentation text
Anything the buying system is expected to make a purchase decision on should be represented in structured fields.
Human-readable copy is still useful, but I'd treat it as presentation.
For example:
{ "operations": [ { "type": "add", "item_id": "charger_65w", "quantity": 1 } ] }is decision data.
{ "description": "Complete your setup!" }isn't.
This isn't meant to solve prompt injection generally.
It just means an agent doesn't need merchant-authored natural language to determine what commerce operation is actually being proposed.
3. Reuse Cart update semantics rather than inventing another mutation language
UCP already supports buyer-initiated Cart updates, so I think an Offer should reuse those concepts wherever possible.
In that sense, this is basically the merchant-proposed counterpart to an existing buyer-initiated Cart change.
I'd expect the actual operation vocabulary and field names to align with Cart rather than create a parallel mutation language.
Conceptually:
{ "operations": [ { "type": "add", "item_id": "charger_65w", "quantity": 1 }, { "type": "remove", "line_item_id": "li_27" }, { "type": "update_quantity", "line_item_id": "li_31", "quantity": 3 }, { "type": "replace", "line_item_id": "li_42", "item_id": "laptop_32gb" } ] }addnaturally references a catalog item.remove,replace, andupdate_quantityshould operate against a Cartline_item_id, since multiple lines can refer to the same catalog item.I'd also make quantity updates absolute:
rather than:
That makes retries much easier to reason about.
I'd prefer a small typed operation set over arbitrary JSON Patch unless there's a strong reason otherwise.
4. Let the agent preview impact, but keep the returned Cart authoritative
The agent needs enough information to decide whether an Offer is worth applying.
That probably includes a structured preview of relevant impact — price delta at minimum, and fulfillment or recurrence effects where applicable.
But I wouldn't make the Offer a second pricing authority.
The Offer says:
The business-returned Cart says:
That seems consistent with the pattern UCP already uses for discounts: input goes in, applied state comes back, and canonical totals remain on the returned commerce object.
5. Recurrence has to be explicit decision data
This is one place where the discussion in #694 changed my thinking.
I initially thought of recurrence as one useful field among several.
I now think it's more fundamental.
These are not equivalent Offers:
and:
even though both might change the immediate transaction by $20.
For an autonomous buyer, recurrence changes what is being authorized.
So I think an Offer that creates a recurring obligation has to expose that fact in its authoritative decision data. It can't be something the agent is expected to infer from description text.
I still wouldn't define an Offer-specific subscription model.
If UCP has or develops general recurring-commerce semantics, the Offer should reuse those.
The requirement here is simply:
6. Apply, not accept
I'd lean toward something like
apply_offerrather thanaccept_offer.That wording is intentional.
Applying an Offer means:
It does not necessarily mean:
The request could conceptually look like:
{ "offers": { "apply": [ { "id": "offer_01", "revision": "1" } ] } }and the business could return:
{ "offers": { "applied": [ { "id": "offer_01", "revision": "1", "affected_line_item_ids": ["li_27"] } ] } }I like this because it follows the existing UCP input/applied-output pattern rather than introducing a completely different interaction style.
The business validates that the Offer:
and then returns the updated Cart.
7. Keep lightweight provenance after application
I'd keep the applied Offer reference on the resulting commerce state, at least long enough to preserve where the mutation came from.
The reason isn't to create another ledger.
It's just to preserve the distinction between:
and:
That seems useful for debugging, auditability, downstream agent reasoning, and an authorization layer evaluating how the transaction arrived at its current state.
Claims and evidence
One thing I would explicitly not put inside this primitive is a new attestation mechanism.
The work in #534/#535 seems like the better place for that.
I like the separation @westonale-facet described in #694:
That keeps the Offer responsible for describing the proposal, without making it responsible for defining the trust model for every claim the merchant might attach to it.
It also makes Offer identity more important: evidence isn't very useful if it can't be tied back to the exact immutable artifact the agent evaluated.
Whether Signals should identify that artifact through
id + revision, a canonical digest, or both is something I'd prefer to align with the Signals design rather than solve separately here.Relationship to AP2
I think this boundary should stay pretty clean.
UCP answers:
AP2 can answer:
So I don't think an Offer needs its own authorization semantics.
For example, whether an agent may apply accessories below a certain amount — or whether multiple sequential Offers exceed a cumulative limit — feels like an AP2 policy question.
The UCP primitive just needs to make each proposal and resulting state explicit enough for that policy to be evaluated.
That separation is also why I prefer
applyoveraccept.Relationship to bundles
#448 looks like adjacent but different work.
A bundle describes a merchandising/product construct.
An Offer describes a merchant-proposed transition to a particular buyer's current commerce state.
A bundle can absolutely be the subject of an Offer.
For example:
So I'd expect the concepts to compose rather than compete.
Relationship to quotes / time-bound offers
There's also related discussion in #328 around time-bound offers/quotes, holds, and validity windows.
I see that as useful adjacent work rather than the same primitive.
A quote or hold can answer something like:
The gap I'm trying to address here is:
Those concepts can compose too. A state-bound Offer may expire or reference time-bound commercial terms.
I wouldn't try to redefine quote/hold semantics here.
Failure cases
A few failures seem useful to distinguish:
The behavior I care about is:
If the proposal materially changes, issue another revision and let the agent evaluate it again.
Where Offers surface
I'm deliberately leaving transport/discovery out of this first proposal.
Offers could eventually surface inside Cart/Checkout responses, through a capability endpoint, or somewhere else.
I'd rather take guidance from existing UCP capability conventions once there's agreement on whether the semantic primitive itself is useful.
Things I'd leave out initially
I wouldn't try to solve all of the questions from #694 here.
In particular:
For evidence, I'd compose with Signals.
For pricing, fulfillment, recurrence, and similar commerce concepts, I'd reuse existing or future general UCP primitives rather than define Offer-specific versions.
And cross-business planning / authorization feels like a separate problem above an individual merchant Cart.
Why introduce a primitive at all?
The invariants I care about are:
And after application:
If those invariants are useful at the protocol layer, then I think there's a case for a small primitive here.
If they aren't, this probably belongs higher in the implementation stack.
If the direction makes sense, I'm happy to put together a small namespaced schema next using the actual UCP Cart types and conventions.
Beyond #328, #448, and the Signals work in #534/#535, pointers to related prior discussions or primitives I've missed are very welcome.
All reactions