How to write prices, currencies, stock and size or colour variants so an AI agent reads them right, with a price tester, the stock values and a variant markup builder.
How do you show prices, stock and variants so an AI agent can read them? Write the price as a number with an ISO currency code, the stock as a schema.org availability value, and give every size and colour its own offer with its own URL, price and stock, in the page's structured data and in its visible text. An agent that has to guess the currency from a "$", or the stock from a green dot, guesses. Sometimes it guesses wrong, and then it is the store that looks wrong.
This guide goes through prices, stock and variants in turn, each with a tool: a price tester, a guide to the stock values, and a builder for variant markup. Storewalk is ours: we build the scan, and its checks for price transparency, availability and variants are ours. Google and schema.org are quoted from their own pages, read on 3 November 2026. For the Product markup itself, start with product structured data for AI agents.
01How should a price be written so an agent can read it?
Write it on the page as text, where people read it, and not inside an image or only after a click. Write it again in the markup, for programs, as schema.org asks: the amount goes in price and the currency in priceCurrency, which schema.org says to use "instead of including ambiguous symbols such as '$' in the value". The decimal point is a full stop: "Use '.' (Unicode 'FULL STOP' (U+002E)) rather than ','". Google adds that merchant listings "require a price greater than zero", so a product priced at zero is not eligible.
The currency is the part a page most easily leaves open. A dollar sign is used by several currencies, and so is "kr". A program that reads "$49" knows the amount and has to guess the rest. Our price check counts a currency as explicit when the page names an ISO code such as EUR or USD, or uses a symbol that belongs to one currency only, such as € or £. Type a price the way your page shows it:
Does your price name its currency? How our price check reads it
What we found
The amount 49.90, in EUR.
PASSEVAL-003, with no priceCurrency in the markup: Prices are readable text with an explicit currency
Write it in the Offer as
"price": "49.90",
"priceCurrency": "EUR"
With priceCurrency in the markup, EVAL-003 takes the currency from there and the symbol on the page stops mattering. The token lists are our scanner’s own. Read in your browser; nothing is sent.
The simplest fix is in the markup. Once the offer says "priceCurrency": "SEK", our check takes the currency from there, and the "kr" on the page no longer leaves anything open.
02What about sale prices, price ranges and "price on request"?
A sale price needs two prices, and Google says how to mark the old one. The current price stays in price. The former price goes in a price specification whose priceType is https://schema.org/StrikethroughPrice, and Google says "Only use this property if you want Google to show sale pricing for your product." If the sale ends on a date, priceValidUntil says when, and Google warns that "Your listing may not display if the priceValidUntil property indicates a past date." Worth knowing if you are the kind of shop that forgets a sale was ever on.
A price range is a different problem. "From €89" only commits to a floor, and the thing in front of the agent may cost more. Our check marks a page whose only price is a "from" price as a warning. In the markup, a range usually arrives as an AggregateOffer with a lowPrice and a highPrice. Google accepts that for product snippets, but says "merchant listings require an Offer", and our product data check finds no price in it to read.
"Price on request" and "contact us for a price" are the end of the road for an agent. Our check fails a product page that defers its price and carries no price in the markup or on the page either.
03How do you tell an agent what is in stock?
With a word on the page and a value in the markup that agree. schema.org's availability takes one of a fixed list of values, and Google's merchant-listing page lists ten, from "The item is in stock." to "The item has been discontinued." Google accepts the short form (InStock) as well as the full URL (https://schema.org/InStock), and so does our availability check. Pick a value to see Google's words and how our check reads it:
Stock states, value by value: what Google says and how our check reads each
Google also accepts the short names, such as InStock, and so does our EVAL-004, which reads the short name and the full URL alike.
On the page, say it in words. "In stock", "Out of stock" and "Ships in 3 days" are readable; a coloured badge with no text is not, and our check's fix says as much. Then make the two agree. Our check compares them, and when the markup says in stock and the page says sold out, it warns, because an agent that finds both has no way to know which to believe.
Stock is per variant. A shirt that is sold out in L and in stock in S has two answers, and a single "In stock" line on the page gives one of them. Our check warns when a product has several variants and its stock is stated once, with no per-variant availability in the markup or on the options themselves.
04How do you mark up sizes and colours?
Google's answer is the ProductGroup: "use the ProductGroup class with associated properties variesBy, hasVariant, and productGroupID to group such variants together". The group carries what the variants share (the name, the brand, the parent SKU as productGroupID) and names what they differ by in variesBy, as full schema.org URLs such as https://schema.org/size. Each variant is a Product with its own SKU and its own Offer.
Google sets a few rules for the page as well. Each variant needs a unique ID, such as a SKU or GTIN. "The site must have the ability to preselect each variant directly with a distinct URL (using URL query parameters)", and preselecting "includes showing the right image, price, and availability, as well as allowing the user to add the variant to the cart." On a single page holding all the variants, "there must be only one distinct canonical URL for the overall ProductGroup". If each variant has its own page instead, each page needs "full and self-contained markup".
Google documents two ways to lay a single page out: the variants nested inside the group with hasVariant, or listed separately, each pointing back with isVariantOf. Build yours below, and change the price or stock of one size to see what changes:
A variant markup builder: sizes and colours in, ProductGroup out
“Each variant must have a unique ID in its corresponding structured data markup (for example, using the sku or gtin properties).”
“Each product group must have a unique ID in its corresponding structured data markup, specified with the inProductGroupWithID property in variant Product properties or the productGroupID property in the ProductGroup property.”
“Reference these variant-identifying properties through their full Schema.org URL (for example, https://schema.org/color ).”
“The site must have the ability to preselect each variant directly with a distinct URL (using URL query parameters)”
DISC-004 reads the ProductGroup as the product, with one offer per variant.
EVAL-003 and EVAL-004 read 3 offers from the markup, one for each variant.
EVAL-005 counts 3 declared variants and checks that your page’s size and colour controls can express that many.
Your variants cost different amounts. Each offer has its own url, which is how EVAL-003 and DISC-005 tell which price belongs to the page they are on.
Invented product on example.com. Up to 12 variants; the URLs use query parameters, as in Google’s examples. Built in your browser; nothing is sent.
The URL on each offer is what matters most to our checks when prices differ. With three sizes at three prices and no way to tell which offer belongs to the page, our price check warns that "Variants are priced differently and no offer says which one this page is for", and our price match check can't compare at all. An offer url that matches the page settles it.
05What do our checks read, and where do they stop?
Four of our checks read this part of the page. The price check wants a readable price and an explicit currency. The availability check wants stock stated, per variant, and in agreement between markup and page. The variant check wants size and colour exposed as named controls: a <select> with a label, labelled radio buttons, or swatches whose images or buttons carry the variant's name, so that an agent can enumerate the choices. The price match check compares the markup's price with the one a shopper sees.
The builder above shows how our checks read each layout as you switch between them:
In the nested layout, our checks read the ProductGroup as the product, with one offer per variant. That is the layout Google documents first and the one Shopify prints for products with variants, so it reads in full.
In the separate layout, our price and availability checks read the first variant's offer only, and take the other variants from your page. We know about that limit, and the builder shows it.
A pre-order page reads as consistent to our availability check. PreOrder and PreSale in the markup mean the item isn't in stock yet, which is what the word "pre-order" on the page says too, so the two agree.
06What do Shopify, WooCommerce, BigCommerce and Magento do here?
On Shopify, the structured_data filter that Dawn uses prints "a ProductGroup if they have one or more variants". Shopify's own advice for AI channels lists "Variants, including Option name", which is the same idea from the admin side: name your options. See the Shopify guide.
On WooCommerce, core prints a variable product whose variations cost different amounts as one AggregateOffer with a lowPrice and a highPrice, and no offer per variation. Equal prices get a single Offer. Either way there is one offer, so one stock state for every variation. The woocommerce_structured_data_product_offer filter is where a developer can change that; the WooCommerce guide has more.
On BigCommerce, the Cornerstone theme prints a price range as minPrice and maxPrice, which our product data check doesn't read as a price. See the BigCommerce guide. On Magento, stock Luma writes microdata with a price and currency but no availability; the Magento guide covers the fixes.
Whatever the platform, the test is the same. Open a product with variants, switch to a size that is out of stock, and check that the URL, the price, the stock line and the markup all changed together. Then run a scan, and see the agent readiness checklist for where this sits among the rest. Shipping costs and return windows are the other half of what an agent compares; shipping and return policies covers them.
07Frequently asked questions
Should the price in JSON-LD include the currency symbol?
No. schema.org asks for the amount in price and the currency in priceCurrency "instead of including ambiguous symbols such as '$' in the value", with a full stop as the decimal point: "price": "49.90", "priceCurrency": "EUR".
Is "$" a clear enough currency for an AI agent?
Not on its own. The dollar sign is used by several currencies, so our price check warns when a page's only currency is "$" or "kr". An ISO code in the markup's priceCurrency, or on the page, removes the doubt.
How do I mark a product as out of stock?
Set the offer's availability to https://schema.org/OutOfStock (or SoldOut, BackOrder or Discontinued, whichever is true) and say the same in words on the page. With variants, set it on the variant that is out, not on the whole product.
Does every variant need its own URL?
Google says the site "must have the ability to preselect each variant directly with a distinct URL", for example with query parameters. Each variant's offer can then carry that URL, which is also how our checks tell which variant's price a page shows.
What is the difference between hasVariant and isVariantOf?
They describe the same link from opposite ends. hasVariant lists the variants inside the ProductGroup; isVariantOf lets each variant Product point to its group. Google documents both layouts.
The sources below were last checked on the date shown.