# Agent Readiness Scanner — spec v0.1 A visitor enters a domain. The server opens it in a fresh headless browser, runs fixed, repeatable checks and probes, and returns **"Your site is X% agent-friendly"** with a score per task, deductions for blocking agents, every check's measured value and the top three fixes. No LLM runs during a scan. The scanner is a robot that perceives and acts the way browsing agents do: it reads `innerText` and the accessibility tree, and it only clicks or types into elements it finds by **role and accessible name**. It never uses site-specific selectors. If that robot cannot search a site, an agent will probably struggle too. ## Why these checks The sources below are paths in the `common-thread-commerce` repository, where these experiments were run. Each check comes from friction we measured in this repo's agent runs: | Observation | Source | Check it motivates | |---|---|---| | Airbnb agents without an adapter still succeeded, but every `innerText` read on a listing waited ~6.2 s and each run made ~4 page loads | `airbnb/reports/LUNA_MEDIUM_5PAIRS_2026-10-14.md`, bare `commands.jsonl` | Settle time, URL state; *not* "JS framework = bad" | | With page tools (WebMCP), Airbnb took 48% less time and 38% fewer tokens; WooCommerce used 4.3× fewer tokens; store-choice workers needed 5–9 browser actions instead of 16–20 | Airbnb 5-pair, Woo real-catalog and store-choice reports | WebMCP tools are the main built-for-agents path | | The browser-use agent clicked an ambiguous "Add to cart" and finished 0/5 | `output/browser-use-luna-woocommerce-20261008/transcript.txt` | Unique accessible names | | Truncated accessibility snapshots cost 53–74 s per Glovo run | `glovo/benchmarks/v0.3/transcript-audit.md` | Snapshot size | | `querySelectorAll('button')` returned `[]` because controls sat in a shadow root; Airbnb adapter v0.1.0 sat outside `
` and no agent noticed it | Glovo audit, Airbnb pair report | Hidden surfaces | | 17/100 target sites returned 403/429/202 to plain HTTP; 35/100 show a consent manager | `experiments/agentface/data/test-sites.csv` | HTTP access, blocking pop-ups | | The Woo Store API was the backbone of `shopping.js` | `shopping.js` | Public API | ## Universal by design Nothing is inferred from the domain. A site type is detected from the page itself, and modules that don't apply are marked **N/A**, which removes their points from the total instead of counting them as failures. - **Core checks** (Access, Read, Act, Built for agents) run on every site. - **Commerce module** turns on when the homepage shows a shop: a known platform fingerprint (Shopify, WooCommerce, Magento, PrestaShop, BigCommerce, Salesforce Commerce), **or** at least two of: a cart/basket/bag link, ≥3 price-like strings, an add-to-cart button, Product/Offer structured data. Several independent signals are used so that a poorly marked-up shop is not excused by failing detection. - **Search probe** is N/A on a non-shop without a search box, but **fails** on a shop without one. - **A site that is down is not scored.** HTTP 500/502/504, Cloudflare 52x/530 or a refused/unresolvable connection ends the scan with "the site looks down" (503 is excluded because bot managers use it for challenges). - **Not every URL is an HTML page.** When the response is Markdown, plain text or JSON, HTML-only checks (`structured-data`, `landmarks`) are N/A. A page with no controls at all makes `semantic-controls` and `hidden-surfaces` N/A — there is nothing to operate, so nothing to fail. An agent endpoint (e.g. a remote MCP server that also answers GET with Markdown) is judged by the checks that apply. ## Scan procedure One scan = one fresh browser context (cookies, storage and cache isolated) in a shared headless Chromium. 1280×900, `en-US`, UTC, service workers blocked. Budget: ≤7 page loads (home, search, deep link, up to 3 product pages), 90 s hard timeout. 1. **HTTP fetches** (plain `fetch`, honest UA `AgentReadinessScanner/0.1`): start URL, `/robots.txt`, `/llms.txt`, sitemap, public API endpoints, and the discovery, sign-in and payment documents listed under optional integrations. DNS-AID and ARD records are queried over DNS-over-HTTPS (`cloudflare-dns.com`), because Node's resolver cannot read SVCB/HTTPS records. 2. **Page A — homepage.** Load, wait for text to settle and for at least 5 s after navigation (most pop-ups appear late), then measure everything static, take a screenshot and pick probe inputs (nav link, search term, product links). 3. **Clear pop-ups, then the navigate probe** on page A. Before every probe the scanner does what an agent would: click a consent "Accept"/"I agree" button (in the page or a consent iframe), then a "Close" button, then press Escape, up to 4 rounds. Whatever it had to dismiss is noted in the probe's detail; the cost is scored once, by `overlays`, not again by each probe. 4. **Page B — search probe.** Reload home, find the search box by role/name, type a term taken from the page, press Enter. 5. **Page C — deep link.** Open the search result URL in a new page. 6. **Pages D — product pages** (commerce only). Product links come from the home page and the search results. Up to 3 are tried, because the first is often sold out or "coming soon"; the best product-data and add-to-cart results are kept. The browser uses a standard Chrome UA with ` AgentReadinessScanner/0.1` appended (real agent browsers look like Chrome; we still identify ourselves). A block is reported as the finding: "your site blocks automated visitors, which is what agents are." ### Never Submit forms (contact, newsletter, login), start checkout, call MCP tools or send A2A messages (the MCP check runs only `initialize` and `tools/list`, and only against an endpoint the site itself advertises on the same site; A2A is read from the Agent Card by GET), register, sign in or pay (OAuth, Auth.md, Poppy and payment checks only GET public discovery documents; an HTTP 402 is observed, never answered), or follow links to other sites (a search form that submits to another host is stopped and scored as a hand-off). The add-to-cart click is the only write, into a guest cart that is thrown away with the context. ## Score: tasks, best path The score answers *how directly can an agent do the main tasks here?* Each task is scored by the best path an agent has. **The ordinary page earns at most 50** — an agent can get it done, slowly and fallibly, by reading HTML and clicking by name. **Something built for agents earns up to 100.** A site with zero agent features but a flawless human UI therefore lands near 50%, not near 100%. | Task | Weight | Ordinary-page path (≤ 0.5) | Built-for-agents path (≤ 1) | |---|---:|---|---| | Read | 20 | 0.5 × weighted mean of the Read checks | `markdown` ratio, or a WebMCP tool for reading/asking | | Find | 20 | 0.5 × weighted mean of the Act checks | WebMCP tool for search/browse/catalog | | Product details (shops) | 15 | max(0.5 × Act mean, 0.8 × `product-data`) — Product JSON-LD is machine-readable, so it can reach 0.8 | WebMCP tool for products/items/variants | | Take action (shown as "Add to cart" on shops) | 20 | Shops: 0.5 × `add-to-cart-probe`. Other sites: none — the scanner never submits forms, so the task is N/A unless tools exist | WebMCP tool for an action (book, reserve, order, request, subscribe, contact…) or, on shops, cart/basket/checkout | A WebMCP tool covers a task when a word in its name, title or description starts with one of that task's words (reading and actions: name and title only, since words like "page" or "order" appear in any description). Its path value is the tool's quality: 0.6, +0.2 if described (≥20 chars), +0.2 with an object input schema. On the site as it is, tools are never called, so coverage is what the site claims. Comparison scans (adapter or router) plan up to 12 eligible tools after the UI probes, then call each once in a fresh browser context, at most four concurrently. Tools must have `readOnlyHint: true`, explicit `openWorldHint: false`, a callable `execute`, and no `destructiveHint: true`. Missing open-world hints default to open-world and are skipped. Calls use the smallest input their schema allows (the probe term for required strings). During execution the context intercepts write requests, WebSockets and requests to other origins. Bootstrap sockets are allowed during registration, then closed before execution. Eligible site-owned tools are included. Hints are untrusted; same-origin GETs may have side effects. This is not a guarantee of harmless arbitrary page code. Discovery has a 25 s preparation budget. The subsequent execution batch has 30 s, including each tool page's setup; each actual invocation gets its own 8 s Node timer. The timer can close a context even when page JavaScript is in a busy loop. A tool that throws, reports an error, returns nothing or HTML, loses its execution context, or exceeds a deadline after its call started covers no task. Completed results are stored in Node and survive failures in other contexts. A failure before any call starts (including navigation or discovery errors) stays uncalled. Uncalled tools keep advertised coverage, with reasons in the comparison table: unsafe or missing hints, missing execute, unsupported input, the tool limit, or an exhausted batch. `score = Σ weight × task ÷ Σ weight of applicable tasks × 100 − deductions`, floored at 0. Tasks with no measurement are N/A (Product details on non-shops, Take action on non-shops without action tools; Find on a page with nothing to operate and no tool). **Countermeasures are deductions, not credit.** Not blocking agents earns nothing; blocking them costs points: `deduction = max × (1 − ratio)` — browser access failure up to −15, plain HTTP failure up to −7.5 when the browser succeeds, and blocking pop-ups up to −10. Browser and HTTP access penalties do not stack. A browser-level block also caps the total at 20%. When blocked, the page checks can't be measured; they count as 0 in their tasks (a blocked agent can't do them either), are marked "Not measured", are never deducted or offered as fixes, and the only fix offered is browser access. `robots.txt` directives do not affect the score. The file is fetched only to discover sitemap URLs. **Optional agent integrations** (`agent-protocols`, `public-api`, `agent-docs`, `agent-discovery`, `agent-auth`, `agent-payments`) are informational only. Their presence or absence never changes task scores, deductions, caps or suggested fixes. Observed services and documentation remain visible in a separate section labeled “not scored”. **Top fixes** = the three checks whose fix (ratio → 1; for WebMCP, tools covering every task) would raise the score the most, with the points gained and a one-line remedy. ## Checks Status: **pass** ≥ 0.9, **warn** ≥ 0.4, **fail** < 0.4, **n/a** when not applicable. "Weight" is the check's weight inside its task's mean; for Access it is the most it can deduct. ### Access — deductions | Check | Weight | Measures | Scoring | |---|---:|---|---| | `browser-access` | 15 | Automated browser status, navigation failures and challenge markers | Failure → −15 and total capped at 20%. This is an observed access failure, not proof of deliberate bot blocking | | `http-access` | 7.5 | Ordinary HTTP GET status, errors and challenge markers | Failure → −7.5 only when browser access succeeds; no additional deduction when browser access fails | | `user-agent-headers` | 0 | Diagnostic GETs with `Claude-User` and `ChatGPT-User` headers, with the scanner token appended | Informational only, regardless of response. These requests do not originate from the real vendor services and never affect scores, caps or suggested fixes | | `overlays` | 10 | "No blocking pop-ups": ≥5 s after load, a 9×9 hit-test grid finds fixed/sticky layers (site headers excluded) and classifies them as cookie consent (incl. known consent managers), region picker, newsletter pop-up, age gate or generic modal; scroll lock | None → 1. Small banner → 0.7. Covers ≥30% of the viewport or locks scrolling → 0 | ### Read (ordinary page) | Check | Weight | Measures | Scoring | |---|---:|---|---| | `ssr-text` | 7 | Share of distinct rendered (`innerText`) words that already appear in the served HTML | ≥0.7 → 1, ≤0.15 → 0, linear | | `settle-time` | 5 | Time from navigation until `innerText` stays within ±2% for 1 s | ≤2.5 s → 1, ≥10 s → 0 | | `snapshot-size` | 5 | Accessibility snapshot (`ariaSnapshot`) size in tokens (chars ÷ 4) | ≤8k → 1, ≥40k → 0, log scale | | `structured-data` | 5 | Valid JSON-LD (0.6, or 0.4 if any block is invalid), Open Graph title+description (0.2), meta description (0.2) | Sum | | `landmarks` | 3 | `