---
title: "Make your pricing machine-readable for shopping agents"
slug: machine-readable-pricing-schema
published: 2026-09-22T21:45:03.467013+00:00
updated: 2026-09-22T21:45:03.467013+00:00
author: "Asif Rahman"
author_url: https://masifrahman.com
category: "AI Readiness"
tags: check:M4, platform:wordpress
description: "Shopping agents can't read prices from prose. Add schema.org Offer JSON-LD with price, currency, availability and billing period so they get it exactly right."
url: https://aiscan.site/blog/machine-readable-pricing-schema
---

## Quick summary

| If you want to… | Do this | Where | AIScan check |
|---|---|---|---|
| Let an agent answer "what does this cost" without reading prose | Add `Offer` or `AggregateOffer` JSON-LD to the pricing page | A `<script type="application/ld+json">` block | **M4** |
| Cover a single fixed price | `Offer` with `price`, `priceCurrency`, `availability` | Any platform | M4 |
| Cover a range of plans or variants | `AggregateOffer` with `lowPrice`/`highPrice`, or one `Offer` per plan | Any platform | M4 |
| Bill on a recurring cycle | `priceSpecification` → `UnitPriceSpecification` with `billingDuration` | Any platform | M4 |
| Skip hand-authoring entirely | Nothing. WooCommerce emits it in core | WordPress + WooCommerce | M4 |
| Confirm it worked | `npx aiscan-cli yoursite.com/pricing`, check M4 | Terminal or [aiscan.site](https://aiscan.site/) | M4 |

An agent that has to read "starting at $12/month, billed annually" out of a paragraph is guessing at three separate facts: the number, the currency, and the billing period. Structured pricing turns that guess into a field lookup.

## Why a shopping agent can't just read your price off the page

Think of a pricing page the way a human reads it: a headline number, a strikethrough for the discount, a footnote about annual billing, maybe an asterisk pointing at a currency note lower down. A person's eyes assemble that into one fact in about a second. A language model reading the same HTML has to do the same assembly from prose, and prose is where prices go wrong: a "starting at" that isn't the actual price, a currency symbol that's ambiguous between USD and CAD, a "monthly" that's actually billed yearly. None of that is a rendering failure. The page looks correct. It's a structured-data failure, because there's no field anywhere on the page that says, unambiguously, `price: 12.00, priceCurrency: USD, billingDuration: 1 month`.

Schema.org's `Offer` and `AggregateOffer` types exist to close exactly that gap. They put the price, the currency, the availability and the billing period into a machine-parseable block sitting next to the human-readable page, so an agent can read the field instead of parsing the sentence. AIScan's own check for this is **M4**, under the [commerce](https://aiscan.site/docs/checks/commerce) dimension, and its own fix guide states the reasoning in one line: a JSON-LD `Offer` block on a pricing page lets an agent answer "what does this cost" without reading prose. Fetched from AIScan's `get_check_guidance` tool on 22 September 2026, so this is the platform's own current wording, not a paraphrase.

## What the schema actually needs to say

Two schema.org types cover almost every pricing page. Reach for `Offer` when you're pricing one specific thing at one specific price. Reach for `AggregateOffer` when a single product or plan comes in a range, several tiers, several currencies, several variants, and you want one block that spans the range rather than one block per price.

| Field | What it carries | Example value |
|---|---|---|
| `price` | The number a human sees | `"12.00"` |
| `priceCurrency` | ISO 4217 currency code | `"USD"` |
| `priceSpecification` | Nested detail for recurring billing | see below |
| `billingDuration` | Length of one billing cycle | `1` with `unitCode: "MON"`, or the ISO 8601 form `"P1M"` |
| `availability` | Whether it can currently be bought | `"https://schema.org/InStock"` |

AIScan's own fix guide for M4 gives this as the base example, fetched from `get_check_guidance` on 22 September 2026:

```html
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Product",
  "name": "Pro",
  "offers": {
    "@type": "Offer",
    "price": "12.00",
    "priceCurrency": "USD",
    "priceSpecification": {
      "@type": "UnitPriceSpecification",
      "price": "12.00",
      "priceCurrency": "USD",
      "billingDuration": 1,
      "billingIncrement": 1,
      "unitCode": "MON"
    },
    "availability": "https://schema.org/InStock",
    "url": "https://example.com/pricing"
  }
}
</script>
```

That's a number-plus-unit-code form of `billingDuration`: 1 unit, where the unit is `MON` (UN/CEFACT code for month). Schema.org also accepts an ISO 8601 duration string for the same property, and it's worth knowing both forms exist because real sites use each. Squarespace's own pricing page, re-verified live on 22 September 2026 at `squarespace.com/pricing`, ships an `OfferCatalog` JSON-LD node where every plan carries monthly and annual `Offer` objects using `priceSpecification.billingDuration` written as `"P1M"` and `"P1Y"`, the ISO form, alongside an `eligibleRegion` field the AIScan example doesn't include. Both forms validate. Pick one and stay consistent across every `Offer` block on your own site, because a parser that assumes one form for a whole page will misread the other.

One rule matters more than either format choice: the structured price has to match the price a human sees on the same page. AIScan's own guidance is explicit about this, and it's the one line worth keeping in view while you write the block, because an agent quoting a stale or wrong structured price to a shopper is worse than shipping no schema at all.

## Path 1: any platform, hand-authored JSON-LD

This works on anything that lets you drop a `<script>` tag into a page: a WordPress Custom HTML block, a static site's template, a Framer Custom Code panel, a Webflow embed, or a raw HTML file.

1. **Decide `Offer` or `AggregateOffer`.** One fixed price for one thing gets `Offer`. Several plans or variants get either `AggregateOffer` with `lowPrice`/`highPrice`, or one `Offer` object per plan nested under an array, whichever reads more naturally for your catalogue.
2. **Fill in `price` and `priceCurrency`** using the exact number and ISO 4217 code shown on the page. Copy them, don't retype them, so a page edit can't quietly leave the schema stale.
3. **Add `priceSpecification` for recurring billing.** Nest a `UnitPriceSpecification` with `billingDuration`, `billingIncrement` and `unitCode` (or the ISO duration string), matching the AIScan example above.
4. **Set `availability`.** `https://schema.org/InStock` for anything currently purchasable, `https://schema.org/PreOrder` for something not yet on sale, `https://schema.org/Discontinued` for a retired plan.
5. **Wrap it in `<script type="application/ld+json">`** and paste that whole block into a Custom HTML block, a template partial, or a `<head>` include, wherever your platform lets you inject raw markup on the pricing page.
6. **Validate before you publish.** Paste the block into any JSON-LD linter, or run the verification steps below, before the page goes live. A malformed block fails silently: the page still renders, the schema just isn't read.

## Path 2: platform-native, no plugin needed

WooCommerce is the clean case, because it does this automatically. WooCommerce core's own structured-data class, `class-wc-structured-data.php`, confirmed live on `raw.githubusercontent.com/woocommerce/woocommerce` on 22 September 2026, builds a `Product` schema block with a nested `Offer` carrying `priceCurrency` and `availability` for every product, with no plugin installed and nothing to configure.

1. **Confirm it's already running.** View source on any WooCommerce product page and search for `application/ld+json`. If WooCommerce is active and untouched, the block is there.
2. **Check the values match your store settings.** `priceCurrency` follows **WooCommerce → Settings → General → Currency**, and `availability` follows the product's stock status field, so a currency change or a restock updates the schema automatically the next time the page renders.
3. **Watch for minification stripping the tag.** A caching or HTML-minification plugin configured aggressively can strip `<script>` tags it doesn't recognise as critical. If the schema disappears from a live page but is present with caching disabled, exclude JSON-LD scripts from your minifier's rules.
4. **Recurring billing on a WooCommerce Subscriptions product** needs the `priceSpecification`/`billingDuration` block added by hand, in a Custom HTML block or a small theme filter on the product template, following Path 1's steps 3 to 5. Core WooCommerce covers one-time pricing; the recurring-billing detail is not automatic.

Shopify does the equivalent through its own theme filter rather than a core class, and that mechanism is already documented in full, with the exact file and line number, in [The complete AI readiness setup for Shopify](https://aiscan.site/blog/ai-readiness-setup-shopify). If you're on Shopify, read that post's M4 section rather than re-deriving it here; the short version is that Dawn's `{{ product | structured_data }}` filter emits the same `price`/`priceCurrency`/`availability` fields for you.

If your WordPress site isn't running WooCommerce, don't assume your SEO plugin covers this. [ThinkRank](https://thinkrank.ai), the plugin we recommend first on WordPress for agent-facing SEO, handles `Organization`, `Person` and `WebSite` schema types out of the box; checked first-party against a live install's `get-schema-settings` on 22 September 2026, its enabled schema list does not include `Product` or `Offer`. A non-WooCommerce WordPress pricing page still needs Path 1's manual JSON-LD block, in a Custom HTML block or the page template, full stop.

## Two platforms worth knowing the limits of

Not every builder treats structured data the same way, and two are worth a callout rather than a full path.

| Platform | What it allows | What that means for a pricing page |
|---|---|---|
| Wix | JSON-LD only, up to 5 markups per page, each under 7,000 characters, static pages only | A large plan catalogue can't be split across dynamic pages the way it could on WooCommerce or Shopify; keep pricing on one static page and one compact block |
| Framer | Custom Code panel accepts a `{{Field}}` CMS variable piped through `json` for CMS-driven values, plus an `unsafeRaw` escape hatch Framer itself flags as risky | Works well for a CMS-backed plans collection; use `unsafeRaw` sparingly, and validate the rendered output rather than the template |

Both facts are still-live as recorded in the queue notes for this item; if either becomes the main path for a specific reader rather than background color, re-verify it against the platform's current documentation before publishing.

## How to verify it worked

Lead with AIScan, because it checks the same field the schema is meant to satisfy in one pass.

1. Run `npx aiscan-cli yoursite.com/pricing`, or paste the URL at [aiscan.site](https://aiscan.site/). No account needed.
2. Read check **M4** directly in the result. A pass means an `Offer` or `AggregateOffer` block was found with `price`, `priceCurrency` and `availability` all present.
3. If M4 still fails after you've added the block, re-fetch the live page yourself; a script tag that renders in your editor but not in the deployed HTML (a caching layer, a build step that strips inline scripts) is the single most common cause.

If you'd rather check by hand, two methods work without any tool:

- **Google's Rich Results Test.** Paste your pricing URL in and look for the `Offer` or `Product` type in the parsed results. It flags malformed JSON before search engines or agents ever see it.
- **`curl` plus a JSON-LD parse.** `curl -s https://yoursite.com/pricing | grep -o '<script type="application/ld+json">.*</script>'` pulls the raw block out of the page; paste the extracted JSON into any online JSON validator to confirm it parses, then check the `price`, `priceCurrency` and `availability` fields by eye against what the page actually shows.

## Where AIScan fits, and where it doesn't

A scan confirms the block exists and carries the required fields. It can't confirm the number inside it is current, because that's a business fact, not a markup fact: a JSON-LD `price` of `12.00` next to a page that now charges `15.00` still passes M4, because the check reads structure, not truth. Keeping the structured price and the displayed price in sync is on you, on whatever process already updates the visible price.

## Fix this, then check the rest

If your pricing page has no `Offer` or `AggregateOffer` block at all, Path 1's six steps are a same-day fix on any platform, and Path 2 means you may have nothing to do if you're already on WooCommerce. Run the scan afterward and read check M4 by name rather than trusting the page looks right. The rest of the commerce dimension, and every other check this series has covered, is indexed at [/docs/checks/commerce](https://aiscan.site/docs/checks/commerce) and [/guides](https://aiscan.site/guides).

