Your Shopify Price Data Is Lying to You
The same products.json URL returned 29.00 to curl and 2900.00 to node fetch, on the same machine in the same minute, both reproducible. What we measured, how to test your own store, and why our own dry run reported nothing wrong.
We publish questions with price anchors in them, of the shape "what are the best dog beds under $80". On 2 August 2026 we noticed we had published "what are the best dog treats under $1800", and "what are the best bags under $24725". The obvious explanation is that somebody divided when they should have multiplied. The actual explanation is stranger, and if you pull your own catalogue over HTTP it is worth ten minutes of your time.
The same URL returns a price in dollars to one HTTP client and the same price in cents to another. Same machine, same minute, same user agent. Both reproducible on repeat. Nothing in the response distinguishes them.
The reproduction
Two commands against a public Shopify products endpoint, run back to back on 2 August 2026:
$ curl -sL 'https://laylopets.com/products.json?limit=2'
-> "price":"29.00"
$ node -e 'fetch("https://laylopets.com/products.json?limit=2")'
-> "price":"2900.00"The product costs $29. One client is told 29.00 and the other is told 2900.00, and both are formatted identically as a two decimal string, so there is no unit marker to read. A parser cannot tell from the payload which one it received.
What we ruled out
The first instinct is that this is a per-store setting, and that the fix is a list of which stores report in minor units. We started writing exactly that list. It is wrong, and it is worth saying why, because a list like that would have quietly become a record of which HTTP client happened to ask on the day somebody checked.
- Not flakiness. Eight sequential node fetches returned 2900.00 every time. curl returned 29.00 every time.
- Not the HTTP version. curl returned 29.00 on both HTTP/1.1 and HTTP/2.
- Not the page size. `limit=5` and `limit=250` made no difference to either client.
- Not a presentment currency swap. `/meta.json` reported `currency: USD` to both clients.
- Not the user agent. Both requests carried the same one.
Why this reached production
Our fetch layer is node based. So production consistently saw the minor unit form, consistently multiplied the real price by a hundred, and published questions anchored to a number a hundred times too large. Anyone spot checking the store in a browser or with curl saw the correct price and would have concluded the data was fine.
That asymmetry is the interesting part. A pipeline reading over one client and a human checking over another will disagree forever, and neither will be able to see why.
Our own dry run reported nothing wrong
This nearly buried the finding, and it is the part worth stealing. We wrote a harness to measure how many stores were affected. It re-fetched each listing in order to probe it, got the other form back, computed a ratio of 100 between the probe and the stored value, decided that meant everything was in major units already, and reported that zero stores were affected.
A probe that re-fetches the thing it is checking is not measuring the thing it checked. It is measuring a second, different request.
The correction is one line of design: the probe now compares against the products already fetched in that pass, never a fresh listing, so the comparison is always like for like. If your data quality checks re-request the source, they are exposed to this class of error whether or not your particular source has this particular quirk.
How to check your own store
Run both clients against your own catalogue and compare a known price. If they disagree, everything downstream that reads over HTTP is suspect: feed exports, price monitoring, repricing rules, margin dashboards, and anything an external tool builds from your catalogue.
# Replace with your own domain and a product you know the price of.
curl -sL 'https://YOURSTORE.com/products.json?limit=2' | grep -o '"price":"[^"]*"' | head -3
node -e 'fetch("https://YOURSTORE.com/products.json?limit=2")
.then(r => r.json())
.then(d => console.log(d.products[0].variants[0].price))'There is a reliable second opinion available. The per product endpoint at `/products/<handle>.js` returns the price in a unit fixed by contract, so it can be used as a reference: fetch a variant both ways, match on variant id, and the ratio tells you which unit your listing gave you. A ratio near 1 means the listing was in minor units. Near 100 means it was in major units. Anything else means you should not act on it.
What we changed
Two independent layers, because the first one can fail and the second is what protects the reader.
- The collector settles the unit per run, using the `/products/<handle>.js` comparison above, matched by variant id, against the products already fetched in that pass. Ratio near 1 scales everything by one hundredth. Ratio near 100 leaves it alone. Anything else is recorded as indeterminate and nothing is touched.
- The generator refuses to publish a price anchor outside a plausible band for the store. This is the layer that matters, because layer 1 can fail: Cuts Clothing answers the per product endpoint with nothing at all, so there is no reference to compare against.
Verified by deliberately disabling layer 1 and checking that layer 2 held. Six catalogues captured in minor units, Aer, Lay Lo, Everlane, Found My Animal, Cuts Clothing and Bloom, all had their price question suppressed rather than published wrong. The other 43 price anchored questions published unchanged.
The general lesson is not about Shopify, and it is not about prices. It is that a payload can be correct, well formed, consistently formatted, and still mean something different depending on who asked for it. If a number crosses a network before you use it, the client is part of the measurement. Ours was, and we did not know.
This came out of building the Brand Index, where the questions we ask assistants are derived from real catalogues. If you want the same read on your own store, the free growth audit does it.
See what an assistant reads off your catalogue The free growth audit pulls your catalogue the way an automated client does and reports what it found, including whether your store can be read at all.
Frequently asked questions
Is this a Shopify bug?
Does this affect every Shopify store?
How do I know which unit my own data is in?
What went wrong on your side?
Written by Shubham Raghav, Founder & CEO, Cresva
Have a question? Email us