# Agent-Ready Storefront Check: five rules and a five-check script (free sample)

Version 1.0, 2026-10-29. By PaperChaseWebb. For anyone who sells from a website they build themselves and wants AI agent shoppers (ChatGPT, Claude, Perplexity and the agents that browse for people) to be able to find a product, read its price and reach the checkout. One Markdown file and one Python 3 script, standard library only. The script reads your built site folder. It does not fetch your live site or anyone else's. It was tested on its worked example (5 findings, then 0) and on a missing folder (exit 2). It was not run inside any agent harness or on a live store.

Most agent-readiness checkers take a URL, crawl the live page and hand back a score out of 100. That helps, but the fix lives in your repo, not on the page, and a score does not say which template to open. Some checkers are also stopped by the same bot rules they are meant to find, so the store looks fine to the checker and broken to the agent. This sample runs on the folder your build writes, before anything goes live, and each finding names the file it came from.

## Install in one line

Save this page as FREE-SAMPLE.md, then:

```
python3 -c "import re;t=open('FREE-SAMPLE.md').read();open('store_mini.py','w').write(re.search(r'~~~python\n(.*?)~~~',t,re.S).group(1))"
python3 store_mini.py path/to/your/built/site
```

## Five rules

1. **Let the agents in.** robots.txt should not disallow the agent user agents on product pages. Blocking training crawlers is your call; blocking the agent a customer sent to buy from you is usually an accident.
2. **List what a machine can read.** llms.txt at the site root should link every machine-readable file you publish (catalog, feeds, sitemap), as Markdown links. A file mentioned in a sentence is easy for a person to see and easy for an agent to miss.
3. **Say the price in a form a machine trusts.** Every product page carries Product JSON-LD with a name, a price and a currency, and the price matches the one on the page.
4. **A real link to pay.** A paid product page has an https link to the checkout. Not a placeholder, not #, not a mailto, not a div that only works with a click handler.
5. **No CAPTCHA in the way.** A product page that loads a CAPTCHA or challenge script stops most agents before checkout. Put challenges on login and forms that need them, not on the page someone is trying to buy from.

Agents check, explain and draft the edit. They do not change your checkout, your CDN or firewall rules, or deploy. The owner does those.

## The five checks

| Id | Check |
|---|---|
| M1 | robots.txt does not disallow GPTBot, OAI-SearchBot, ChatGPT-User, ClaudeBot, Claude-User or PerplexityBot from any product page (longest match wins, a tie goes to Allow) |
| M2 | llms.txt exists and links every .json and .xml file at the site root |
| M3 | every product page (any .html under an items/ or products/ folder) has Product JSON-LD with a name, a price and a priceCurrency |
| M4 | every paid product page has an https buy, checkout, pay, cart or order link that is not a placeholder |
| M5 | no product page loads a CAPTCHA or challenge script |

## The script

~~~python
#!/usr/bin/env python3
"""store_mini.py: five read-only checks an AI agent shopper trips on, run on your built site folder.
  python3 store_mini.py SITE_DIR          check the folder (exit 0 clean, 1 findings, 2 missing folder)
  python3 store_mini.py --example DIR     write a small before/ and after/ store into DIR to try it on
Product pages are the .html files under an items/ or products/ folder. Reads files only, no network."""
import json, os, re, sys

AGENTS = ("gptbot", "oai-searchbot", "chatgpt-user", "claudebot", "claude-user", "perplexitybot")
CAPTCHA = re.compile(r"recaptcha|hcaptcha|turnstile|challenges\.cloudflare\.com|captcha", re.I)


def pages(site):
    for root, _, files in os.walk(site):
        parts = os.path.relpath(root, site).replace(os.sep, "/").split("/")
        if "items" in parts or "products" in parts:
            for f in files:
                if f.endswith(".html"):
                    yield os.path.relpath(os.path.join(root, f), site).replace(os.sep, "/")


def url(rel):
    return "/" + (rel[: -len("index.html")] if rel.endswith("index.html") else rel)


def read(p):
    return open(p, encoding="utf-8", errors="replace").read()


def check(site):
    out, P = [], sorted(pages(site))
    # M1 robots.txt does not block the agents from product pages
    rp = os.path.join(site, "robots.txt")
    if os.path.exists(rp):
        groups, ua_run = [], False
        for line in read(rp).splitlines():
            k, _, v = line.split("#")[0].partition(":")
            k, v = k.strip().lower(), v.strip()
            if k == "user-agent":
                if not ua_run:
                    groups.append(([], []))
                groups[-1][0].append(v.lower()); ua_run = True
            elif k in ("allow", "disallow") and groups:
                groups[-1][1].append((k, v)); ua_run = False
        hits = []
        for a in AGENTS:
            use = [g for g in groups if a in g[0]] or [g for g in groups if "*" in g[0]]
            for p in P:
                rules = [(len(v), k == "allow", v) for g in use for k, v in g[1] if v and url(p).startswith(v.rstrip("*"))]
                if rules and not max(rules)[1]:
                    hits.append(f"{a} on {url(p)} (Disallow: {max(rules)[2]})")
        if hits:
            out.append(("M1", "robots.txt", "agents blocked: " + hits[0] + (f" and {len(hits) - 1} more" if len(hits) > 1 else "")))
    # M2 llms.txt links every machine-readable file at the root
    lp = os.path.join(site, "llms.txt")
    feeds = sorted(f for f in os.listdir(site) if f.endswith((".json", ".xml")))
    text = read(lp) if os.path.exists(lp) else ""
    links = re.findall(r"\]\(([^)\s]+)\)", text)
    miss = [f for f in feeds if not any(u.split("?")[0].endswith("/" + f) or u == f for u in links)]
    if not text:
        out.append(("M2", "llms.txt", "no llms.txt at the site root"))
    elif miss:
        out.append(("M2", "llms.txt", "not linked: " + ", ".join(miss)))
    for p in P:
        h = read(os.path.join(site, p))
        price = None
        # M3 Product JSON-LD with name, price and currency
        prod = None
        for blob in re.findall(r'<script[^>]+application/ld\+json[^>]*>(.*?)</script>', h, re.S | re.I):
            try:
                j = json.loads(blob)
            except ValueError:
                continue
            for o in (j.get("@graph", [j]) if isinstance(j, dict) else j):
                if isinstance(o, dict) and o.get("@type") == "Product":
                    prod = o
        off = (prod or {}).get("offers") or {}
        off = off[0] if isinstance(off, list) and off else off
        if not prod or not prod.get("name") or not isinstance(off, dict) or off.get("price") in (None, "") or not off.get("priceCurrency"):
            out.append(("M3", p, "no Product JSON-LD with name, price and priceCurrency"))
        else:
            try:
                price = float(off["price"])
            except ValueError:
                price = None
        # M4 paid pages have a real https buy link
        if price:
            buys = re.findall(r'<a\b[^>]*href="([^"]*)"[^>]*>([^<]*)</a>', h, re.I)
            good = [u for u, t in buys if re.search(r"buy|checkout|pay|cart|order", t, re.I) and u.startswith("https://") and "REPLACE" not in u]
            if not good:
                out.append(("M4", p, "no https buy link (placeholder, #, mailto or none)"))
        # M5 no CAPTCHA script
        if any(CAPTCHA.search(s) for s in re.findall(r'<script[^>]+src="([^"]+)"', h, re.I)):
            out.append(("M5", p, "a CAPTCHA or challenge script loads on the product page"))
    return out


def page(name, price, href, extra=""):
    ld = json.dumps({"@context": "https://schema.org", "@type": "Product", "name": name,
                     "offers": {"@type": "Offer", "price": str(price), "priceCurrency": "USD", "availability": "https://schema.org/InStock"}})
    return (f'<!doctype html><html><head><title>{name}</title><script type="application/ld+json">{ld}</script>{extra}</head>'
            f'<body><h1>{name}</h1><p>${price}</p><a href="{href}">Buy now</a></body></html>\n')


def example(d):
    if os.path.exists(d) and os.listdir(d):
        sys.exit(f"{d} is not empty; not touching it")
    for side in ("before", "after"):
        s = os.path.join(d, side)
        os.makedirs(os.path.join(s, "items", "kettle"))
        bad = side == "before"
        open(os.path.join(s, "robots.txt"), "w").write("User-agent: *\nDisallow: /checkout/\n" + ("User-agent: GPTBot\nDisallow: /items/\n" if bad else ""))
        open(os.path.join(s, "catalog.json"), "w").write('{"items": [{"name": "Copper kettle", "price": 48}]}\n')
        open(os.path.join(s, "llms.txt"), "w").write("# Kettle Corner\n\n> Kettles. Prices in USD.\n\n- [Copper kettle](/items/kettle/)\n" + ("" if bad else "- [Catalog](/catalog.json)\n"))
        open(os.path.join(s, "items", "kettle", "index.html"), "w").write(page("Copper kettle", 48, "#REPLACE-ME" if bad else "https://pay.example-checkout.com/c/kettle",
             '<script src="https://www.google.com/recaptcha/api.js"></script>' if bad else ""))
        os.makedirs(os.path.join(s, "items", "stand"))
        p = page("Kettle stand", 12, "https://pay.example-checkout.com/c/stand")
        open(os.path.join(s, "items", "stand", "index.html"), "w").write(p.replace("application/ld+json", "text/plain") if bad else p)
    print(f"wrote {d}/before (5 faults) and {d}/after (fixed)")


if __name__ == "__main__":
    a = sys.argv[1:]
    if len(a) == 2 and a[0] == "--example":
        example(a[1]); sys.exit(0)
    if len(a) != 1 or not os.path.isdir(a[0]):
        print(__doc__ if len(a) != 1 else f"no such folder: {a[0]}", file=sys.stderr); sys.exit(2)
    found = check(a[0])
    for c, f, m in found:
        print(f"{c} {f}: {m}")
    print(f"5 checks, {len(found)} findings")
    sys.exit(1 if found else 0)
~~~

## Worked example: Kettle Corner (made up)

`python3 store_mini.py --example demo` writes a two-product store twice: demo/before with five faults and demo/after with them fixed.

```
python3 store_mini.py demo/before
```

```
M1 robots.txt: agents blocked: gptbot on /items/kettle/ (Disallow: /items/) and 1 more
M2 llms.txt: not linked: catalog.json
M4 items/kettle/index.html: no https buy link (placeholder, #, mailto or none)
M5 items/kettle/index.html: a CAPTCHA or challenge script loads on the product page
M3 items/stand/index.html: no Product JSON-LD with name, price and priceCurrency
5 checks, 5 findings
```

The fixes: drop the GPTBot group that disallowed /items/, add `- [Catalog](/catalog.json)` to llms.txt, replace the placeholder buy link with the real checkout URL, remove the reCAPTCHA script from the product page, and change the stand's JSON-LD script type back to application/ld+json.

```
python3 store_mini.py demo/after
```

```
5 checks, 0 findings
```

## One prompt to try

Paste into your agent: "Build my site the usual way, then run python3 store_mini.py on the built output folder. For each finding, tell me which file in my repo produces that page or file, and the exact edit that would fix it. Do not fetch the live site. Do not edit checkout settings, CDN or firewall rules, and do not deploy. Show me the edits and wait for my yes before you change any file."

## The full pack

The paid Agent-Ready Storefront Check (pp-agent-ready-storefront-check, $19) grows this into fifteen read-only checks, and every finding names the built file and the source file to change: robots groups per agent, llms.txt structure and links, endpoints that exist and parse, sitemap coverage, JSON-LD against the visible price and your catalog, real checkout links, buy controls an agent can use, CAPTCHA on product and checkout pages, your exported bot rules read in order, noindex in pages and headers, policy pages linked from llms.txt, and a source file for every built file. It comes with a tool that turns findings into a fix list and a report grouped by the file to open, and edits that wait for the owner's yes, tied to a sha256 and typed at their own terminal. If you would rather have it done with you, book a call from the home page at paperchasewebb.ai.

Made-up example; the store, products and links are invented.

PaperChaseWebb 💸
