DEV Community

Cover image for UCP Variant Data: The #1 Reason Agent Checkouts Fail
Benji Fisher
Benji Fisher

Posted on • Originally published at ucpchecker.com

UCP Variant Data: The #1 Reason Agent Checkouts Fail

A user asks an AI shopping agent for "a medium grey t-shirt." The agent finds the product. It picks a variant. It adds it to the cart. The merchant rejects the cart. The agent retries with a different variant. The merchant rejects that one too. The session ends in cart_created without a checkout — the user's $40 purchase quietly disappears, and nobody on the merchant side ever sees the failure.

This pattern is the single largest source of agent checkout failures we see across the 4,500+ verified UCP stores in the directory. More than schema invalidity, more than tool errors, more than payment-handler problems. Variant mismatch — the agent and the merchant disagreeing on which SKU corresponds to "Medium" — is responsible for a meaningful fraction of the gap between "store has a UCP manifest" and "agent can actually buy from it."

The good news: it's almost entirely fixable on the merchant side, in your variant data structure, without changing any tooling. This post walks through the failure pattern, the five most common variant data anti-patterns we observe, and what clean variant data looks like in practice.

Anatomy of a variant mismatch

Here's the cleanest way to see the failure:

Two frontier agents — call them Agent A and Agent B — get the same prompt against the same store: "Add a medium grey t-shirt to my cart." Both agents call search_catalog, both get the same product back, both see three variants:

{
  "variants": [
    {"id": "var_5571", "options": [{"name": "Color", "label": "Grey"}, {"name": "Size", "label": "Small"}]},
    {"id": "var_5572", "options": [{"name": "Color", "label": "Grey"}, {"name": "Size", "label": "Medium"}]},
    {"id": "var_5573", "options": [{"name": "Color", "label": "Grey"}, {"name": "Size", "label": "Large"}]}
  ]
}
Enter fullscreen mode Exit fullscreen mode

Agent A picks var_5572. Agent B picks var_5572. Both add to cart. Both succeed. Clean data, predictable behaviour. Each variant declares its options as an array of {name, label} pairs — the spec's selected_option shape — so the agent matches "medium" against the Size axis unambiguously.

Now the broken version. Same prompt, same product, but the variant data looks like this:

{
  "variants": [
    {"id": "var_5571", "options": [{"name": "Color", "label": "Grey"}, {"name": "Size", "label": "S"}]},
    {"id": "var_5572", "options": [{"name": "Color", "label": "Grey"}, {"name": "Size", "label": "M"}]},
    {"id": "var_5573", "options": [{"name": "Color", "label": "Grey"}, {"name": "Size", "label": "L"}]},
    {"id": "var_5574", "options": [{"name": "Color", "label": "Grey"}, {"name": "Size", "label": "Medium / Regular Fit"}]},
    {"id": "var_5575", "options": [{"name": "Color", "label": "Grey"}, {"name": "Size", "label": "Medium / Slim Fit"}]}
  ]
}
Enter fullscreen mode Exit fullscreen mode

Agent A picks var_5572 (interpreting "M" as the canonical "Medium"). Agent B picks var_5574 (interpreting "Medium / Regular Fit" as the more explicit match). Neither is wrong. The user said "medium" and both interpretations are defensible. But because the variant data conflates two different axes — size and fit — into a single Size label, the two agents diverge, and the user's experience depends on which model they're using. The spec form makes the bug obvious: Fit should be its own selected_option, not crammed into the Size label.

Worse: many real implementations don't even include the option labels. They expose only opaque variant IDs:

{
  "variants": [
    {"id": "var_5571"},
    {"id": "var_5572"},
    {"id": "var_5573"}
  ]
}
Enter fullscreen mode Exit fullscreen mode

Now the agent has no way to know which variant corresponds to "Medium" at all. It guesses. Sometimes it guesses right. Often it doesn't. That's how checkout sessions end up in cart_created without ever reaching checkout_reached.

Why this is the #1 failure mode

Across the Playground session dataset, roughly 62% of sessions end without a completed checkout. The breakdown is informative:

Outcome Share
checkout_reached 38%
search_only (browsed, didn't add) 27%
failed (provider error, model refusal, max turns) 22%
cart_created (added, didn't proceed) 13%

The cart_created cohort — sessions where the agent successfully picked something but couldn't finish — is the variant-mismatch signal. The agent had enough information to add to cart but the cart contents weren't valid for checkout. That's the structural shape of "wrong variant picked."

Roughly half of the categorised failed sessions are also variant-shape problems — the agent picked a variant ID that the cart endpoint rejects, retried with another, hit max_turns_exceeded while flailing through the variant list. Add those in and variant-related failures account for somewhere around a fifth of all sessions, which is more than any other categorisable failure mode.

The thing that makes this pattern so consistent: clean variant data is not part of UCP Score or schema validation. A store can pass UCP Score at A grade and still emit variant data that breaks every agent in the field. The validator looks at whether the manifest parses; it doesn't look at whether the variants are agent-resolvable. That gap is exactly why this post exists.

A spec gap that compounds the problem

Even when a store is fully UCP-compliant, the protocol leaves room for ambiguity. The 2026-04-08 schema makes variant.options[] optional — including on products where product.options[] is non-empty and there are multiple variants. So a payload like {"options": [{"name": "Size", "values": [{"label": "Small"}, {"label": "Medium"}]}], "variants": [{"id": "var_a"}, {"id": "var_b"}]} is technically valid but agent-unresolvable: nothing links var_a to "Small" rather than "Medium." Two consumers looking at this payload can defensibly pick different variants for the same prompt.

A conditional MUST in the spec — "when product.options is non-empty and variants.length > 1, every variant MUST populate options[]" — would close this cleanly. Until that lands, agent-resolvability is on the merchant rather than the protocol.

The five variant anti-patterns

In rough order of frequency observed across the dataset:

1. Opaque variant IDs with no option metadata

The shape from the third example above — variants exposed only as var_5572, no options, no attributes, no human-readable axis. Agents have no way to map a user's "Medium" to a specific ID. They either guess or pick the first variant, both of which produce wrong outcomes routinely.

Fix: every variant must carry the axis values that distinguish it from siblings, in the spec's selected_option array form: "options": [{"name": "Size", "label": "Medium"}, {"name": "Color", "label": "Grey"}]. The name field tells the agent which axis the value belongs to; label is what gets matched against the user's request. Whatever the product's options page shows to a human shopper — size, colour, material, fit — the variant data should expose programmatically with one selected_option entry per axis.

The corollary: descriptive attributes that aren't selection axes belong in metadata, not product.options[]. A one-variant simple product with "Color: Gray" should expose Gray as metadata.attributes, not as a single-value product.option — otherwise consumer UIs render a one-button picker that looks selectable but isn't. The split: product.options[] is for axes the buyer chooses across; metadata is for descriptive properties of the (only) variant.

2. Conflated axes in a single string

The shape from the second example — "Medium / Regular Fit" as a single option value where size and fit are two separate user choices. Agents can parse this, but inconsistently across models, because the conflation is ambiguous. Different models split the string differently, and the variant they end up picking depends on which side of the slash they prioritise.

Fix: each variant attribute lives in its own field. Don't compose. If your product has size + fit as two axes, the variant data should look like:

{
  "id": "var_5574",
  "options": [
    {"name": "Size", "label": "Medium"},
    {"name": "Fit", "label": "Regular"}
  ]
}
Enter fullscreen mode Exit fullscreen mode

Two clean axes, two unambiguous values, no string parsing required. Agents pick consistently. The array-of-selected_option form is the shape UCP 2026-04-08 defines for variant.options — see selected_option.json.

3. Inconsistent labelling between sibling variants

Not all variants on the same product use the same option vocabulary. One says "M", another says "Medium", another says "med". We see this on stores that have grown organically — different teams added variants over different years, naming conventions drifted, the inconsistency is invisible to the merchandising team because the storefront UI hides it.

Fix: one canonical label per axis value, applied consistently across every variant on every product. If "Medium" is the canonical label, every Medium variant uses exactly "Medium". No "M", no "med", no "Medium " (trailing space). Agents reason by string match; consistency is what makes the match reliable.

4. Missing or inconsistent stock / availability flags

A variant exists in the catalogue but is sold out, and the variant data doesn't say so. The agent picks it, the cart accepts the add, the checkout endpoint rejects it. The agent doesn't know to retry with a different variant — it had no signal that the variant was unavailable.

Fix: every variant declares its availability object — {"available": true, "status": "in_stock"} is the spec shape, with well-known status values in_stock, backorder, preorder, out_of_stock, and discontinued. Agents skip unavailable variants if you tell them to, and status gives them enough signal to decide whether to wait, substitute, or surface an out-of-stock message to the user.

5. Declared axes that variants don't honor

product.options[] declares the selectable axes; variants[] is the universe of actual purchasable combinations. When the cardinality of declared axes doesn't match what variants actually carry — e.g., product.options declares Color × Size = 9 combinations but only 3 color-only variants exist — agents try to satisfy a Size selection that no variant honors. Strict consumers return null and refuse to add; lenient consumers guess and pick wrong.

Fix: keep product.options[] and variants[] in sync. Either every declared axis combination has a corresponding variant, or the axis shouldn't be in product.options[]. If sizes aren't actually configurable for this product, drop Size from the axes; don't leave it dangling.

What clean variant data looks like

Here's the shape that resolves cleanly across every frontier model we test:

{
  "id": "prod_42",
  "title": "Heavyweight Crew Tee",
  "description": "Heavyweight cotton crew-neck tee.",
  "price_range": {
    "min": {"amount": 4500, "currency": "USD"},
    "max": {"amount": 4500, "currency": "USD"}
  },
  "options": [
    {"name": "Color", "values": [{"label": "Charcoal"}]},
    {"name": "Size",  "values": [{"label": "Small"}, {"label": "Medium"}]},
    {"name": "Fit",   "values": [{"label": "Regular"}]}
  ],
  "variants": [
    {
      "id": "var_5571",
      "title": "Charcoal / Small / Regular",
      "description": "Heavyweight crew tee, charcoal, size small, regular fit.",
      "price": {"amount": 4500, "currency": "USD"},
      "availability": {"available": true, "status": "in_stock"},
      "options": [
        {"name": "Color", "label": "Charcoal"},
        {"name": "Size",  "label": "Small"},
        {"name": "Fit",   "label": "Regular"}
      ]
    },
    {
      "id": "var_5572",
      "title": "Charcoal / Medium / Regular",
      "description": "Heavyweight crew tee, charcoal, size medium, regular fit.",
      "price": {"amount": 4500, "currency": "USD"},
      "availability": {"available": true, "status": "in_stock"},
      "options": [
        {"name": "Color", "label": "Charcoal"},
        {"name": "Size",  "label": "Medium"},
        {"name": "Fit",   "label": "Regular"}
      ]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Four spec fields make this work:

  1. product.options at the product level — declares the axes (Color, Size, Fit) and their valid values as an array of product_option {name, values: [{label}]}. Agents know upfront how many dimensions a variant occupies and what values are valid on each axis.
  2. variant.options as an array of selected_option {name, label} — each axis has its own entry, no string parsing, no conflation. The name matches the product-level axis; the label matches the user's request.
  3. variant.availability with available and status — agents skip unavailable variants without trial-and-error, and status (in_stock, backorder, preorder, out_of_stock, discontinued) gives them enough signal to wait, substitute, or surface the right message.
  4. Required scaffoldingid, title, description, and price on every variant, and id, title, description, price_range, variants on the product. These aren't "nice to have"; they're the schema's required fields. Variants missing any of them won't validate.

Bonus stability: when present, option_value.id and selected_option.id give stable identifiers that survive label drift. If your platform supports it (most do — Shopify uses GIDs, WooCommerce uses pa_* taxonomy slugs), populate id alongside label and consumers can match on the stable key when labels change.

Stores running variant data in this shape resolve user prompts to specific variants reliably across every model we've benchmarked. The pattern isn't novel — it's the same shape Shopify uses internally, the same shape WooCommerce variations use when properly structured, the same shape every traditional e-commerce platform ends up at after enough years of evolution. UCP just exposes it programmatically to the agent layer.

How to validate your variant data

Three layers, in order:

1. Static audit. Run your store through UCPChecker. The validator surfaces variants with missing options data, conflated axes, inconsistent labels across sibling variants, and missing availability flags. None of this is part of strict UCP-spec conformance, but our methodology flags variant-quality issues as part of the Capability Coverage score because they materially affect whether agents can transact.

2. Live agent test. Run a multi-model agent session against your store via UCP Playground. The framework exercises the full search → variant-pick → cart → checkout flow against frontier agents across 15+ models. If your variant data is ambiguous, you'll see different models pick different variants for the same prompt — the exact pattern we walk through in the Playground 1,000-sessions analysis.

3. Continuous monitoring. Variant data changes over time as you add products and SKUs. Set up UCP Alerts so you get notified when a variant audit starts surfacing new issues — typically a sign that a recent merchandising change introduced inconsistent labelling at scale.

The order matters. Static audit catches the easy cases (missing fields, schema-shaped problems) cheaply. Live agent test catches the cases where the schema is fine but agents disagree (the conflated-axis cases, the inconsistent-label cases). Monitoring catches drift over time. Skipping any of the three leaves a class of variant problems undetected.

What to fix in your store, and how to verify it

If you're a merchant reading this and your store is running on Shopify, WooCommerce, BigCommerce, Magento, or PrestaShop, the variant data structure is mostly determined by your platform's defaults. The platform-specific fixes are documented in the platform guides — but the meta-pattern is the same:

  1. Audit at ucpchecker.com/check — get a list of variant-data issues
  2. Fix the most common one first (usually missing options metadata)
  3. Test at ucpplayground.com with two different models against the same product, asking for the same variant
  4. Verify that both models pick the same variant ID consistently
  5. Monitor weekly — variant drift is the most common reason a store's UCP Score regresses

Variant data is a back-office data-quality problem dressed up as an agentic commerce problem. The fix is mostly editorial — get your axis labels consistent, expose your option values structurally, mark sold-out variants as such. None of this is technically hard. It's the kind of work that adds up to "agents can buy from your store" rather than "agents try to buy from your store and quietly fail."

If you fix one thing on the agent-readiness side this quarter, fix variant data. The conversion lift is bigger than any other single change you can make.

One thing worth naming: consumer tools that silently paper over variant data problems (substring matching, positional guessing, falling back to variants[0]) make this worse, not better. They hide the failure mode from merchants who would otherwise see it and fix the data. Faithful rendering — null when the match is ambiguous, errors when the data is inconsistent — is what produces correct merchant behaviour. If your variant data only works in some agents, that's a signal the data is the problem, not the agent.

Try it

Top comments (0)