Fictional example
A sample website and API audit
Northwind Fasteners Oy and all readings in this report are invented. The example shows what a client receives: the finding, its evidence, who makes the correction and how the result is checked.
Illustrative report date: 8 September 2026, which is the delivery date in the engagement record below. This page itself was last revised on 3 October 2026.
What to fix first
- Priority: Correct the product facts before improving scanner coverage.
- Evidence: Product pages show a real price and lead time. Structured data reports €0.00 and InStock. The API leaves the price empty and marks the product purchasable.
- Why it matters: An assistant may give a buyer the wrong price or availability.
- Owner: Implementation owner agreed with the client, and the source-data responsibilities remain explicit.
- Acceptance: Compare the corrected page, structured data and API against the same product record across the catalog.
- Scanner effect: This finding does not change the sample's scanner score.
The main findings
The site passes 2 of 21 scored checks and reads Level 1 of 5 on the sample's scanner run. It has robots.txt and a sitemap, but several discovery files and a Markdown version are missing.
The first priority is a problem the scanner does not score. All 138 product pages publish a structured price of 0.00, and all of them say InStock, which the API's stock state contradicts for part of the catalog. The API also withholds prices while marking products as purchasable. One fictional AI answer in the sample calls the catalog free.
There are nine findings. F2, F3, F5 and F6 address five scored checks. F7 enables DNSSEC and holds the sixth, dnsAid, until the company has an agent registry for the DNS record to point at. F1 and F4 correct information outside those checks. F8 records the company's decision not to offer agent checkout this year. F9 addresses an old address repeated in AI answers.
Estimated edge work is about eleven hours for F1 to F7, or eleven and a half hours including F9. Source corrections take about four hours for F1 to F7, or five hours including F9. The tables below keep the two kinds of correction apart and name who makes each. These are estimates for the sample, not a quote or a promised readiness level. The level moves with the check set the scanner runs on the day, and 22 checks were in the set on 2026-09-03.
Contents
- What to fix first
- The main findings
- The agreed work
- Decisions for the company
- Fixes, owners and estimated effort
- What each party delivers
- What was checked
- Technical scan results
- Findings from the manual review
- What the assistants answered
- Public security checks
- The findings in detail, F1 to F9
- After the report
- Scope limits
- Appendix A. From evidence to a checked fix
- Appendix B. Questions and test conditions
- About this sample
The agreed work
| Field | Value |
|---|---|
| Client | Northwind Fasteners Oy, industrial fastener wholesaler, invented |
| Surface audited | The public site northwind-fasteners.example, its 138 product pages and its REST API at /wp-json/ |
| What the company asked | That a buyer's assistant quotes the right price and the right lead time, that the catalog is reachable through agents, and no agent checkout this year |
| Kickoff, scope agreed in writing | 2026-08-25 |
| Scanner run | 2026-09-03, isitagentready.com, default profile, three serial runs, all three identical |
| Manual review | 2026-09-03 and 2026-09-04, whole catalog read through the API and the sitemap |
| AI visibility run | 2026-09-04, 15 questions to four assistants, one anonymous run per question, conditions in appendix B |
| Report delivered | 2026-09-08, fourteen days after the kickoff |
| Written follow-up round | Questions open until 2026-09-22, answered within five business days |
| Re-scan | One, included, on a day the company names: within 30 days of the report, by 2026-10-08, or within 30 days of the delivered corrections when the correction add-on is bought, the second route under After the report |
| Access used | Public surfaces only. No login, no credentials, no code repository |
Decisions for the company
The company needs to make five decisions. Each row shows which work depends on it, and none of them blocks the others.
| Decision | Options | Effect | Needed by |
|---|---|---|---|
| D1. Who implements the fix list | The company's team from this report, or turva.dev for the fixed price on the services page | Decides who makes the edge and the source corrections in the table below, and who owns the acceptance run in the section What each party delivers | Before the work starts |
| D2. The AI crawler preference for F5 | Allow named crawlers, disallow them, or allow search and ai-input and refuse ai-train | Decides the content of robots.txt. The report records no preference of its own | Before F5 is written |
| D3. The text of llms.txt for F3 | Approve the draft as written, or edit it | The file is the company's own description of itself and goes live only after the company has read it | Before F3 goes live |
| D4. The four discontinued products and the six price-on-request products, F1 | Keep them published with the right availability, or unpublish them | Decides whether 10 of 138 products stay in the catalog an agent reads | Before the F1 acceptance run |
| D5. Agent checkout, F8 | No agent checkout this year, as agreed at the kickoff, or a separate scoped engagement when that changes | The five commerce checks stay red on purpose until this decision changes. The report makes no recommendation on the strength of a scanner point | Whenever the company wants |
Fixes, owners and estimated effort
Fix the problems affecting buyers first. The table separates a correction made at the edge from a permanent correction at the source, because they are different pieces of work. Both columns are on this fix list. Decision D1 says who does them: the company's team from this report, or turva.dev for the fixed price on the services page, in which case the access each source row needs is arranged in writing first, as What each party delivers sets out. Two kinds of entry in the source column are not implementation work, and they are marked in the table: a decision (F3, F5, F8) belongs to the company, and a request to an outside directory (F9) belongs to the directory. The effort figures are estimates scoped to these findings, not a quote.
| Order | Finding | Checks moved | Correction at the edge | Correction at the source | Edge hours | Source hours |
|---|---|---|---|---|---|---|
| 1 | F1 product data on all three surfaces | None, manual | Correct the JSON-LD Product node on every product page from the fields the page renders, and run the whole-catalog acceptance script | Fix the plugin mapping and the API price visibility so the origin publishes the right data itself | 4 | 2,5 |
| 2 | F9 the old address | None, manual | Add the current address to the Organization node | Remove or update the 2022 price list PDF. Ask the directory to correct its listing, which is a third party's own action | 0,5 | 1 |
| 3 | F2 markdown next to HTML | markdownNegotiation | Edge worker with the acceptance list in F2 | None | 4 | 0 |
| 4 | F3 llms.txt, Link header, security.txt | linkHeaders | Serve llms.txt and /.well-known/security.txt, and add the describedby and alternate Link relations | Read and approve the llms.txt text, D3. A company decision, not implementation work | 1,5 | 0,5 |
| 5 | F4 sitemap content | None, manual | Serve a sitemap index over a product sitemap generated from the API and a content sitemap, the CMS sitemap without the two template pages | Four plugin settings so the origin sitemap is right on its own | 0,5 | 0,5 |
| 6 | F5 robots.txt | robotsTxtAiRules, contentSignals | Serve the file | Decide the preference, D2. A company decision, not implementation work | 0,5 | 0 |
| 7 | F6 api-catalog and real 404s | apiCatalog | Serve the linkset and the 404s | None | 0,5 | 0 |
| 8 | F7 DNSSEC, the DNS-AID record waits | None in this round. dnsAid waits for an agent registry | None. DNS is not served by the edge | Enable DNSSEC in the DNS zone and at the registrar. The _index._agents record waits until the company publishes an agent registry for it to point at | 0 | 0,5 |
| 9 | F8 commerce | None, on purpose | None | Decision D5 | 0 | 0 |
Edge total for F1 to F7 and F9: about eleven and a half hours. Source total: about five hours, of which the largest is the plugin work behind F1. Both totals are inside the fix list, and D1 decides who makes them.
What each party delivers
An edge correction and a source correction are different pieces of work, and both are on the fix list. The sections below explain what turva.dev delivers when the company buys the fixed price implementation, what the company has in place first, what depends on an outside party and when the delivery is accepted.
What turva.dev delivers
When the company chooses the fixed price route, turva.dev makes every correction in the table above, in both columns.
At the edge, in one worker in front of the origin:
- The corrected JSON-LD on every product page.
- The markdown twin of every page, with the acceptance list in F2.
- The llms.txt file and its Link relations.
- The security.txt file.
- The product sitemap, the content sitemap and the index.
- The robots.txt the company decided.
- The api-catalog linkset with real 404s under /.well-known/.
- The Organization address.
At the source, in the systems the company gives access to:
- The catalog plugin's structured data mapping and the API price visibility behind F1.
- The four sitemap settings behind F4.
- DNSSEC behind F7.
- The removal or replacement of the 2022 price list PDF behind F9.
For the acceptance, turva.dev delivers the whole-catalog acceptance script from F1 as a file the company keeps, and runs it once on the day the work is declared done.
What the company has in place first
The fixed price covers the whole list, and every row of it is reachable only through what the company arranges in writing before the work starts:
- An edge runtime in front of the origin and the access to deploy there.
- Administrator access to the catalog CMS and its plugin settings, which the F1 mapping, the F1 API price visibility and the F4 sitemap settings all need.
- DNS zone access and registrar access for F7, because enabling DNSSEC means signing the zone and publishing its DS record at the registrar, and the two can sit with different providers.
- Access to the media library or the server path for the F9 PDF.
- The three decisions D2, D3 and D4, because F5, F3 and the F1 acceptance run each wait on one of them.
If any of that access cannot be arranged, the add-on is not sold, as the services page states, and the report still carries the instruction for every row so that the company and its agency can do the work themselves.
The confirmed product record that F1 is checked against also comes from the company, whoever makes the corrections: its own list of products and variants with the price, currency, price basis and availability it stands behind.
Why the source rows are on the list
An edge correction serves the right data from the day the worker goes live, and it is a correction on the way through rather than a repair of the origin. If the origin's plugin still publishes price 0 on the day the edge worker is switched off or bypassed, the wrong data is back. The API is the clearest case. The edge cannot correct the API's empty price, because the origin is withholding data and there is nothing on the way through to correct it from, so that row is fixed in the origin's own setting or it is not fixed at all. This is why the fix list carries both columns, and why a green acceptance run on the edge column alone is not the plugin having been fixed.
What depends on an outside party
One item on the list is an outside party's own action. The business directory in F9 is asked to correct its listing, and it corrects it or it does not. turva.dev sends the request and records it with any answer it gets, and the report counts the request as delivered and the listing as the directory's state.
Three states, kept apart
The report is complete on the day it is delivered, 2026-09-08. The implementation is accepted on the day its acceptance tests pass, which is a later day and a separate test. A finding is closed when its own check reads clean at the retest, which for F1 means the three surfaces and the confirmed product record agree on every row. F9 is read in two parts, the same two parts its own section and the acceptance list keep apart. Its technical fix is closed when the Organization address check and the 2022 PDF check pass and the request to the directory is documented. Its AI observation is separate, and the retest records whether an answer still carries the old address and which source that answer used. The AI observation can stay open after the technical fix is closed. A wrong answer is not marked corrected, and it is not a condition of accepting the implementation either. The retest report says which of the three states each finding is in, so that an accepted implementation is never read as a closed finding.
When the delivery is accepted
On the day turva.dev declares the work done, the acceptance tests of F1 to F7 and F9 are run and their output is delivered with the retest report. The delivery is accepted when all five of the following hold on that day.
- The whole-catalog script from F1 reports zero rows where the visible page, the JSON-LD, the API and the confirmed product record disagree, counted against the published product list decision D4 settles. The ten exception products are exempt from the price rules F1 writes for them, not from the run. The price basis is compared on the page, in the JSON-LD and in the record, because the API has no field for it, and F1 records that as a remaining limit.
- The edge acceptance list in F2 is green in every line.
- markdownNegotiation, linkHeaders, robotsTxtAiRules, contentSignals and apiCatalog read PASS on a full default-profile scan, and the internet.nl website test shows DNSSEC as passing.
- The acceptance tests of F3, F4 and F6 pass as written, and the three tests F9 states for the site pass: the Organization node carries the current address on every page, the 2022 PDF returns 404 or the current list, and the request to the directory is recorded with the date it was sent, the recipient, the correction that was asked for and its state on the day of the check. An answer is recorded with the request when one has arrived, and a request with no answer is recorded as "No response received as of" the real date of that check. A documented request meets this delivery item whether or not the directory answers.
- The source corrections are read on the origin itself, apart from the edge: the origin's own JSON-LD on the product pages the F1 script reads, and the origin's own sitemap from F4. The method is agreed with the company in writing, and the edge routing is not switched off and no protection is bypassed for the test. A green edge reading does not accept a source correction. Where the agreed method cannot read the origin, the source rows are recorded as not verified and stay open.
What the acceptance does not cover
Four things stay outside it: the directory's own listing in F9, the five commerce checks in F8, which stay red by decision D5, the dnsAid check, which waits under F7 for an agent registry, and what any assistant answers on the retest day. The 15 questions are put again under the conditions in appendix B, and that reading is reported next to the fixes as an observation. An assistant can repeat an old fact from a copy nobody here controls, so the report does not make the delivery wait on it and does not promise it goes away.
What was checked
What was measured, in this order.
- The independent scanner. isitagentready.com was run three times in series against the origin with the default profile, and the three runs agreed. The denominator on the day was 22 checks, 21 scored and one informational, webBotAuth. Every check is recorded below individually rather than as one headline number.
- A manual review of what the scanner does not score. JSON-LD on the home page, one category page and every product page, 138 in all, read from the sitemap and the API rather than from a sample. Head metadata and Open Graph on seven pages. HTTP response headers on the home page and one product page. The /.well-known/ directory. robots.txt, sitemap.xml, ai.txt and llms.txt against the current norms. The first response of each page fetched as a bot, to see whether the content arrives or a script shell does.
- AI visibility today. A fixed set of 15 questions, 12 that describe the category without naming the company and 3 that name it, put once to four AI assistants in anonymous sessions on 2026-09-04. Recorded per answer: whether the company is named, whether the answer is correct where it names it, and which sources the assistant cites. The questions and the conditions are in appendix B.
- Published security scans. internet.nl website and mail tests and Hardenize, run on 2026-09-04, so the report rests on measurements the client can re-run without turva.dev.
What was not measured. No code was read, no credentials were used, no penetration test was run and no traffic analytics were seen. The REST API was read through its public routes only. Rankings on any AI platform were not measured and are not promised anywhere in this report. Where the report says an agent may do something, that is a possible effect and not a measured one, and the report keeps the two apart in every finding.
Technical scan results
These are the sample's readings from 2026-09-03. PASS and FAIL are the scanner's labels. INFO is not scored. Each row links to a fix or explains why no change is planned. The last column reads the check against what the company asked for, because a red check is not a defect when the company does not need what it measures.
| Category | Check | Reading | What the scanner saw | Fix | What it means for Northwind |
|---|---|---|---|---|---|
| Discoverability | robotsTxt | PASS | /robots.txt exists and parses. One wildcard group. | F5 changes its content, the check stays green | Green because the CMS ships the file, not because it says anything useful |
| Discoverability | sitemap | PASS | /sitemap.xml exists and parses, 14 URLs. | F4, a manual finding, the check stays green | Green with none of the 138 products in it. The check reads the file, not the catalog |
| Discoverability | linkHeaders | FAIL | No Link header on any response. | F3 | Cheap, and it is how a page points at its own markdown twin and at llms.txt |
| Discoverability | dnsAid | FAIL | No _index._agents record under the domain, DNSSEC not enabled. | F7 enables DNSSEC, the record waits | Needs the company's DNS and an agent registry for the record to point at. Northwind publishes none, so the check stays red after this round |
| Content | markdownNegotiation | FAIL | Accept: text/markdown returns text/html, 217 kB on the home page. | F2 | A buyer's assistant reads the catalog and the delivery terms, and today each page weighs between 188 kB and 340 kB of markup |
| Bot access control | robotsTxtAiRules | FAIL | No AI crawler named in robots.txt. | F5 | A stated preference the company has not stated. Decision D2 |
| Bot access control | contentSignals | FAIL | No Content-Signal line in robots.txt. | F5 | Same file, same decision |
| Bot access control | webBotAuth | INFO | No Web Bot Auth directory. Informational, not scored. | None | The company operates no bots that would sign requests |
| API, Auth, MCP & A2A Discovery | apiCatalog | FAIL | /.well-known/api-catalog returns the HTML 404 page. | F6 | The catalog API already exists. Naming it is the cheapest route to an agent reading prices as data |
| API, Auth, MCP & A2A Discovery | oauthDiscovery | FAIL | No /.well-known/oauth-authorization-server or openid-configuration. | Waits, see F6 | No agent needs to log in here. Stays red at no cost |
| API, Auth, MCP & A2A Discovery | oauthProtectedResource | FAIL | No /.well-known/oauth-protected-resource. | Waits, see F6 | Same |
| API, Auth, MCP & A2A Discovery | authMd | FAIL | No /auth.md and no agent_auth metadata. | Waits, see F6 | Same |
| API, Auth, MCP & A2A Discovery | mcpServerCard | FAIL | No /.well-known/mcp/server-card.json. | Waits, see F6 | There is no MCP server to point at, and a card that points at none is a false claim |
| API, Auth, MCP & A2A Discovery | a2aAgentCard | FAIL | No /.well-known/agent-card.json. | Waits, see F6 | Same |
| API, Auth, MCP & A2A Discovery | agentSkills | FAIL | No /.well-known/agent-skills/index.json. | Waits, see F6 | Same |
| API, Auth, MCP & A2A Discovery | webMcp | FAIL | No navigator.modelContext tools on the home page. | Waits, see F6 | Browser tools on a catalog page are worth a look after the data is right, not before |
| API, Auth, MCP & A2A Discovery | ard | FAIL | No capability manifest at the well-known path. | Waits, see F6 | Same as the server card |
| Commerce | x402 | FAIL | No 402 challenge on any route. | Stays red on purpose, F8 | No agent checkout this year, by the company's own decision |
| Commerce | mpp | FAIL | No payment discovery document. | Stays red on purpose, F8 | Same |
| Commerce | ucp | FAIL | No /.well-known/ucp. | Stays red on purpose, F8 | Same |
| Commerce | acp | FAIL | No ACP discovery document. | Stays red on purpose, F8 | Same |
| Commerce | ap2 | FAIL | No AP2 declaration. | Stays red on purpose, F8 | Same |
Category totals: Discoverability 2 of 4, Content 0 of 1, Bot access control 0 of 2, API, Auth, MCP & A2A Discovery 0 of 9, Commerce 0 of 5. Overall 2 of 21 scored checks, Level 1 of 5.
After F2, F3, F5 and F6: Discoverability 3 of 4, Content 1 of 1, Bot access control 2 of 2, API, Auth, MCP & A2A Discovery 1 of 9, Commerce 0 of 5. Overall 7 of 21, and the report does not translate that into a level, because the level on the retest day depends on the check set the scanner runs that day.
Findings from the manual review
The surfaces the scanner does not score, read by hand on 2026-09-03 and 2026-09-04.
| Surface | Reading | Finding |
|---|---|---|
| First response as a bot | Every page returns its full HTML on the first response. No script shell, no prerender. | No finding. The content is there, it is only expensive to read. |
| JSON-LD, product pages | Product node on all 138 pages: price 0.00, priceCurrency EUR, availability InStock. The 19 products with variants publish one node for the whole product and nothing per variant. The 12 products priced per box of 100 carry no unit. | F1 |
| JSON-LD, home and category pages | Organization node with name and logo, no address, no taxID, no sameAs. WebSite node present. No BreadcrumbList. | F9 adds the address. The rest is recorded, no fix in this round. Completeness, not correctness. |
| Head metadata and Open Graph | Title and description present on all seven pages read. og:image missing on the five product pages. | Recorded, no fix in this round. The missing image changes how a shared link looks, and every fact on the page is right. |
| HTTP response headers | Content-Type correct. No Link, no RateLimit-Policy, no security headers except HSTS. | F3 adds the Link relations. Security headers are in the internet.nl reading below. |
| /.well-known/ | Empty. Every path under it returns the HTML 404 page with status 200. | F3 and F6 populate it. The soft 404 is F6's first line. |
| robots.txt | The CMS default. One wildcard group, Disallow: /wp-admin/, no Sitemap line, no AI crawler, no Content-Signal. | F5 |
| sitemap.xml | 14 URLs. The front page, eleven content pages and two template pages titled Sample Page and Privacy Policy Draft. No product page. | F4 |
| ai.txt and llms.txt | Neither exists. | F3 publishes llms.txt. ai.txt is not published: the norm it followed has merged into robots.txt Content Signals. |
| REST API | /wp-json/wc/store/v1/products, the WooCommerce Store API v1, answers publicly, 100 products per page, two pages, X-WP-Total 138. As the Store API does by default, the listing leaves the variations out. Every row has prices.price as an empty string and is_purchasable true. The other stock fields show 90 rows in stock with no backorder, is_on_backorder true on 41 and is_in_stock false on 7. No field in the response carries a price basis. | F1 corrects the data, F6 declares the API. |
| OpenAPI description | None. The index at /wp-json/ lists its routes in WordPress's own JSON format, which is not an OpenAPI document, and the site publishes no OpenAPI file. | F6 declares the REST index. An OpenAPI description is added under F6 when the company asks for one. |
| Old documents still served | /wp-content/uploads/2022/hinnasto-2022.pdf is a 2022 price list with the company's previous Tampere address on its cover, linked from nowhere on the site and indexed by two search engines. | F9 |
What the assistants answered
The sample contains 60 invented answers: fifteen questions put to four assistants, one anonymous run per question on 2026-09-04. The question set is fixed so that the same run can be repeated after the fixes and the two runs compared. The four assistants are named in appendix B with the conditions of the run. A to D are used in the tables so that invented answers are not attributed to real products. A real report names the assistant on every row.
| Question group | Questions | Answers | Northwind named | Correct where named | What the assistants did instead |
|---|---|---|---|---|---|
| Category, no company name. "Where can a Finnish workshop buy DIN 933 bolts in bulk online" and eleven like it | 12 | 48 | 0 | Not applicable | Named three national distributors and two marketplaces. Two answers cited a distributor's price list page and quoted its markdown form as the source |
| Company by name. "What does Northwind Fasteners sell" and two like it | 3 | 12 | 12 | 9 | Two answers gave the Tampere address the company left in 2023. One answer said the catalog is free and cited a product page, the page whose structured data F1 corrects |
Every material error has a cited source on the site or off it, a follow-up in this report, an owner and a retest, so that the retest can say whether the error is gone and why.
| Error | Answers | Source cited | Follow-up | Owner | Retest |
|---|---|---|---|---|---|
| The catalog is free | 1 of 12, assistant B | The answer cited the product page URL. That page shows 0,42 EUR in its text and price 0.00 in its JSON-LD, read on 2026-09-03 in appendix A, chain 1. Nothing recorded with the answer shows which of the two it used, so tracing it to the JSON-LD is an interpretation | F1 | The implementer decision D1 names, at the edge and at the source. The company arranges the CMS and plugin access first | The three by-name questions, same conditions, recorded as an observation: whether any answer still states a price the page does not show, and which source it cites |
| The company is at the old Tampere address | 2 of 12, assistants A and C | A cited a business directory listing that still carries the old address. C cited the 2022 price list PDF at /wp-content/uploads/2022/hinnasto-2022.pdf. The site's own Organization node has no address at all, so neither assistant could have read the current one from structured data | F9 | The implementer decision D1 names, for the Organization node, the 2022 PDF and the request sent to the directory. The company arranges the media library access and confirms the address. The listing itself stays the directory's | The three by-name questions, same conditions, recorded as an observation: whether an answer still gives the old address and where it read it. An assistant can repeat it from a copy nobody here controls, so the reading is reported next to the fix and not as a failure of the site |
What is recorded and is not an error. Two category answers cited a distributor's price list page as markdown. That is an observation about the distributor's site and this report draws no conclusion from it about Northwind's. One category answer named no Finnish source at all. Nothing in this run shows why any assistant chose the sources it chose, and the report does not guess.
What this measures. Whether an assistant names the company when a buyer describes the need, and whether it gets the facts right when it does. It does not measure ranking, and nothing in this report predicts how the run reads after the fixes. The same 15 questions are run again at the retest under the same conditions, and the two tables are printed side by side. Each question is put once to each assistant, and an assistant can answer the same question differently on another day for reasons that have nothing to do with the site. A changed answer at the retest is therefore recorded as an observation next to the fix, and the report does not read it as proof that the fix caused it.
Public security checks
Run on 2026-09-04, recorded so that the client can re-run them without turva.dev.
| Scan | Reading | What it means for agents |
|---|---|---|
| internet.nl website test | 64 of 100. IPv6 absent, DNSSEC absent, HTTPS configuration passes, security headers partial. | F7 enables DNSSEC, which moves this reading. DNSSEC is also a prerequisite of the dnsAid check. |
| internet.nl mail test | 55 of 100. SPF present, DKIM present, DMARC policy none. | Not an agent surface. Recorded because a buyer checks it. |
| Hardenize | 17 of 24 categories passed. CAA, DNSSEC, HSTS preload, CSP, Referrer-Policy, security.txt and cookies did not. | security.txt is a five line file the same edge worker serves. It is listed under F3 as a same-day addition. |
The findings in detail
Nine findings. Each one shows the evidence, why it matters, the correction, the owner and the check used to confirm the result. Observed effects and possible effects are labelled separately. The findings are numbered by impact on a buyer first and on the score second. F9 is the exception: the AI run raised it after the technical findings were numbered, so it comes last here, and the fix table above puts it second in the order of work, right after F1.
F1. Product prices and availability disagree across the page, structured data and API
Category and scanner effect. Structured data. Manual review, not scored.
What was found. GET /products/din-933-m12x40-a2/ on 2026-09-03 returns a JSON-LD Product node with "price": "0.00", "priceCurrency": "EUR" and "availability": "https://schema.org/InStock". The visible page shows 0,42 EUR per piece, excl. VAT, and a lead time of six weeks. The same node shape appears on all 138 product pages, read by fetching both pages of /wp-json/wc/store/v1/products, following every product URL and reading the price field of each. The API returns prices.price as an empty string and is_purchasable true for every product. Its stock fields show 90 products in stock with no backorder, is_on_backorder true for 41 and is_in_stock false for 7. The 19 products that come in several thread lengths show a price per variant on the page and publish one Product node for all of them. The 12 products sold by the box of 100 show the box price on the page and publish no unit at all, in JSON-LD or in the API, so nothing on either surface says which quantity a price belongs to.
Why it matters. No scanner points, and the highest impact in this report. Observed in this run: one by-name answer on 2026-09-04 told a buyer the catalog is free and cited the product page. That page carries two prices, 0,42 EUR in its text and 0.00 in its JSON-LD, and the zero price is a technical finding of its own. Nothing recorded shows which part of the page the assistant used, so the link between the two is an interpretation. Possible and not observed: an agent that reads the API as data gets an empty price and a purchasable flag. A second effect belongs to later rather than now: a box price published without its unit may be read as the price of one piece, making the quoted per-piece price one hundred times the actual per-piece price. That is why price and price basis are two separate rules below. In this sample, correcting the product facts takes priority because one recorded answer already repeats a wrong price claim.
What to change. Publish the same price, currency and availability for every product and every variant in the catalog on the visible page, in the JSON-LD and in the API, and the same price basis on the visible page and in the JSON-LD. The Store API has no field for a price basis, so this fix cannot put one there, and the price basis rule below records that as a remaining limit. Four rules, and the acceptance test reads them back.
- Price and currency. The JSON-LD Offer price is the number the page shows, in EUR, VAT excluded as the page states, and the API's prices.price is the same number in minor units, with currency_code EUR and currency_minor_unit 2. Prices are not rounded on one surface and not on another.
- Price basis. A product sold by the box names the box of 100 next to its price on the visible page and carries a UnitPriceSpecification with referenceQuantity 100 in JSON-LD, so neither surface can be read as a per-piece price. The Store API has no field for a price basis, and this fix adds none. Its prices.price is the price of the box the product is sold in, and nothing in the API response says so. An agent that reads only the API therefore still cannot tell a box price from a piece price for these 12 products. That is a remaining limit of this fix, and the report does not count it as corrected. The confirmed product record lets the acceptance run check the basis on the page and in the JSON-LD, but the record is the company's own document and publishes nothing to an agent that reads the API.
- Availability from the stock state, not from the lead time text. In the Store API the stock state is two fields. BackOrder when is_on_backorder is true, OutOfStock when is_in_stock is false, InStock when is_in_stock is true and is_on_backorder is false. The schema notes that is_on_backorder also reads false when backorder notifications are turned off, so a product the confirmed record lists on backorder and the API shows only as in stock is a row to resolve, not a pass. PreOrder is not used: no product in this catalog has a release date, and a lead time on an existing product is a BackOrder with a deliveryLeadTime of six weeks, not a pre-order. The lead time is published in that field and in the page text, and it does not decide the availability value on its own.
- Variants. The 19 variable products publish one offer per variant, 61 in all, with the variant's own price and stock state, matching the variation list the page shows. The Store API leaves variations out of its default listing, and the parent's variations array carries only each variant's id and attributes, so the 61 variant rows are read from /wp-json/wc/store/v1/products?type=variation, where each variant has its own prices, is_in_stock and is_on_backorder.
Exceptions, listed so that the acceptance test does not read them as failures. Six products are priced on request: the page says so, and they carry no price in JSON-LD or in the API, only an availability from their stock state. After the fix the API marks them is_purchasable false. Four products are discontinued and still published: they carry availability Discontinued and no price, and after the fix the API marks them is_purchasable false as well. The Store API has no discontinued state, so the confirmed product record names the four. Neither group is then presented as purchasable on any of the three surfaces. Decision D4 says whether any of the ten stays published, and the acceptance run reads the published ones against these rules and the unpublished ones against the removal list. The exception is from the price rules, not from the run.
Who does it and estimated effort. Two corrections, and the report keeps them apart. At the source, the catalog plugin's structured data mapping and the setting that hides prices from the API for anonymous readers: about two and a half hours in the CMS and its plugin. At the edge, the JSON-LD is corrected on the way through, on every product page, from the price, unit, variant and stock elements the page itself renders, so the served node is right from the day the worker goes live: about three hours, plus one hour for the whole-catalog acceptance script the company keeps. Both are on the fix list, and D1 decides who makes them. The edge cannot correct the API, because the API's empty price is the origin withholding data and there is nothing on the way through to correct it from, so the API rows are fixed in the origin's own setting whoever holds it. The acceptance below says which rows are read on which surface.
How to check the fix. A script the company keeps, delivered with the report, and run against the live site. Every expected count comes from the product list decision D4 settles, not from a fixed number. The catalog holds 138 products on 2026-09-03, with 170 price rows, 109 simple products and 61 variants, and 180 availability rows, the same 170 rows plus the six price-on-request products and the four discontinued ones, which carry an availability and no price. Those are the counts if all ten exception products stay published, and each unpublished product takes its own rows out of them. The script prints the list it derived the counts from.
It then checks that the API total of the default listing, the variant count at type=variation, the sitemap product count and the number of product pages agree with that list, and follows every published product URL. It compares four columns row by row: the visible page, the JSON-LD, the API and the confirmed product record. The record is the company's own list of products and variants with the price, currency, price basis and availability it confirms in writing, and the other three are read against it, because three surfaces can agree with each other on the same wrong price. The API column is read on price in minor units, currency, availability and is_purchasable. The price basis is read on the page, in the JSON-LD and in the record. The API column carries no price basis because the API has no field for it, and an empty basis there is the remaining limit the price basis rule names, not a failed row. Nothing is skipped in silence: a product on neither the published list nor the agreed removal list fails the run.
The delivery is accepted when four things hold. Every price row and every availability row of the published catalog agrees on the three surfaces and with the confirmed product record on every value each surface carries, and the price basis of every row agrees on the page, in the JSON-LD and with the record. Each published price-on-request product carries no price, the availability its stock state gives and is_purchasable false. Each published discontinued product reads is_purchasable false on the API and Discontinued on the page and in the JSON-LD, with no price. Every product on the removal list is gone from the page, the sitemap and the API. Any row that still differs is listed with its product URL and the surface it differs on, not hidden inside a total. The finding closes at the retest, when the four columns agree on every value each of them carries. The retest report repeats the remaining limit, that the API publishes no price basis, so a closed F1 is not read as an API that states one. Appendix A, chain 1, shows one row of this test end to end.
Guide. JSON-LD and structured data for AI clients.
F2. Offer Markdown alongside HTML
Category and scanner effect. Content accessibility. Scored check markdownNegotiation.
What was found. GET / with Accept: text/markdown on 2026-09-03 returns Content-Type text/html and a 217 kB body. The same request to /products/ and to a product page returns HTML of 340 kB and 188 kB. No .md address exists for any page. The markdown form of the home page, produced from the same HTML on 2026-09-03, is 9 kB.
Why it matters. One scored check. Observed: the three page sizes above, and the 9 kB markdown form of the home page. Possible and not observed: an assistant with a fixed reading budget stops before the end of a 217 kB page. Nothing in the AI run of 2026-09-04 shows an assistant truncating a Northwind page, and the report does not claim it.
What to change. Put an edge worker in front of the origin that answers a text/markdown request with the markdown form of the page at the same address, and publishes each page at its .md address as well. The origin is not touched. The worker converts the rendered HTML on the way through and caches the result per URL. This is the worker every other edge finding in this report lives in, so its cost is paid once, and the acceptance list below is the acceptance list of the edge itself.
Who does it and estimated effort. turva.dev or the company's developer, at the edge. About four hours, of which the acceptance list is one.
How to check the fix. Every line is checked on the home page, on /products/ and on three product pages, one of them a variable product, and the list is delivered filled in.
- Content negotiation. A request with Accept: text/markdown returns Content-Type: text/markdown and a body that starts with the page title as a heading. A request with Accept: text/html, or with no Accept header, returns the origin's HTML page carrying the corrections this report agrees to and nothing else, which on a product page means the F1 JSON-LD and the F9 Organization address and every other byte as the origin served it. The markdown conversion changes no part of the HTML response. A request that lists both with a higher q on text/html gets HTML.
- Separate caches. Every response the worker negotiates or converts carries Vary: Accept, so no cache serves the markdown form to a browser or the HTML form to an agent that asked for markdown. The routes named in the Scope line below are outside this, because the worker adds nothing to them. Checked by requesting the two forms in both orders from a cold cache.
- Freshness. A price changed at the origin appears in the markdown form within ten minutes. The worker meets that by revalidating against the origin on every request, which is faster than the limit, or by caching the markdown for at most ten minutes. Checked with a controlled change: the price is changed at the origin at a recorded time, and both forms of the page are read at the recorded change time plus ten minutes, with the read time recorded. A fresh Markdown form at that read passes and a stale one fails. One comparison at a single moment does not measure a delay, so it does not settle this line.
- Canonicals. The .md twin carries Link: rel="canonical" pointing at the HTML page, and the HTML page carries rel="alternate" type="text/markdown" pointing at the twin, so the HTML page is the declared preferred URL and the twin its alternate.
- HTTP statuses. A page the origin serves with 404 is 404 in markdown. A 301 from the origin is passed through as a 301, not converted. An origin 500 is passed through as 500 and is never cached as markdown.
- Scope. /cart/, /checkout/, /my-account/, /wp-admin/, /wp-login.php, every /wp-json/ route and every response that sets a cookie or carries a session are passed through untouched and never converted or cached. Checked by requesting each with Accept: text/markdown and reading the response unchanged.
- Content. On every checked product page the markdown form carries the same price, price basis, availability and lead time as the HTML page and the confirmed product record from F1. markdown-parity-check, run with a selector for the product content, reports no missing or changed block in that content. A conversion that drops one of these facts fails this line even when the format is right.
- The scanner. markdownNegotiation reads PASS on the next full scan.
Guide. Serving Markdown to AI clients.
F3. Publish llms.txt and link to it in response headers
Category and scanner effect. Discoverability. Scored check linkHeaders.
What was found. GET /llms.txt on 2026-09-03 returns the HTML 404 page with status 200. No response on the site carries a Link header. GET /.well-known/security.txt returns the same soft 404.
Why it matters. One scored check. Observed: no file, no header. Possible and not observed: an assistant that looks for llms.txt gets a 200 with an HTML body and has to read the whole page to learn there is nothing there. The AI run of 2026-09-04 does not show whether any of the four assistants asked for the file, and the report does not claim that they do.
What to change. Write /llms.txt by hand: the company name, a four line summary, and the pages that matter grouped as products, delivery terms, technical documents and contact, each as an absolute link to the page's markdown form from F2. Announce it with Link: rel="describedby" pointing at llms.txt, which is one file for the whole site and the same target everywhere. The header goes on the responses the worker handles in this report's scope, which are the content pages F2 converts. The second relation is per page and not site wide: a response carries Link: rel="alternate" type="text/markdown" only when F2 publishes a markdown twin for that exact address, and the header points at that page's own .md address. The routes F2 passes through untouched, /cart/, /checkout/, /my-account/, /wp-admin/, /wp-login.php, every /wp-json/ route and every response that sets a cookie or carries a session, get no header added at all, neither describedby nor alternate, and any Link header the origin itself sends on them travels through as the origin sent it. Serve /.well-known/security.txt from the same worker with a contact address and an expiry date, which closes one Hardenize category the same day.
Who does it and estimated effort. turva.dev or the company's developer, at the edge. About 90 minutes. The text of llms.txt is decision D3: it goes live after the company has read it, because it is the company's own description of itself.
How to check the fix. linkHeaders reads PASS on the next full scan. The free validator at turva.dev/llms-txt-validator reads the file's structure as valid, which is a reading of the file's own shape and of the two home page links, and it fetches nothing that the file points at. Every link in llms.txt is then requested once by hand, and the result is recorded per link as the URL, the HTTP status and either the expected content or the redirect that was followed and where it ended. A link resolves when it answers 200 with the page it names, or redirects inside the same host to a page that does. GET /.well-known/security.txt returns text/plain with status 200 and an Expires date in the future. The two Link relations are then read apart on two kinds of address: an ordinary content page carries describedby and an alternate pointing at its own .md twin, and /cart/, /checkout/, /my-account/, /wp-admin/, /wp-login.php and a /wp-json/ route come back byte for byte as the origin served them, with no describedby and no alternate added.
Guides. llms.txt explained and Response headers for AI clients.
F4. Add the product catalog to the sitemap
Category and scanner effect. Discoverability. Manual review, not scored. The sitemap check passes and stays green.
What was found. GET /sitemap.xml on 2026-09-03 lists 14 URLs: the front page, eleven content pages and two CMS template pages titled Sample Page and Privacy Policy Draft. None of the 138 product URLs is in it. robots.txt carries no Sitemap line.
Why it matters. No scanner points. Observed: a sitemap with 14 pages and no product, and two template pages that two search engines index today. Possible and not observed: a reader that starts from the sitemap and reads only what it lists never reaches a product page. The report does not claim any assistant read the sitemap on 2026-09-04.
What to change. At the source: let the catalog plugin generate a product sitemap, reference it from a sitemap index, remove the two template pages from the site or from the sitemap, and add one Sitemap line to robots.txt. Four settings, no code. At the edge, when turva.dev implements the list, /sitemap.xml is answered with a sitemap index that references two files and nothing else. The first is /sitemap-products.xml, generated from the API. The second is /sitemap-pages.xml, the content sitemap: the CMS sitemap read from the origin and served without the two template pages, which the edge drops by the exact URLs the company confirms, so the old sitemap never reaches a reader with them in it. The Sitemap line in robots.txt names the index and comes with F5. This lets the acceptance test pass before the source settings are made. Two inputs come from the company first: the exact URLs of the two template pages and decision D4, which fixes the expected product count. The Sitemap line waits on the robots.txt file that F5 serves. The two template pages stay on the site until the source setting removes them, because the edge only takes them out of the sitemap. The origin's own sitemap is still read on its own, as What each party delivers sets out, and it passes only once the source settings are made.
Who does it and estimated effort. The four settings are about half an hour in the CMS. The edge sitemap is about half an hour. Both are on the fix list, and D1 decides who makes them.
How to check the fix. Every sitemap the index references is fetched and read, not only the new product sitemap. The product sitemap lists every published product URL, and its count agrees with the published product list the F1 script derives. The content sitemap lists the front page and the eleven content pages. Neither template page appears in any file the index references, and robots.txt names the index. The origin's own sitemap is read apart from the edge by the method agreed with the company, and the source row is accepted only when it also lists the products and leaves out the template pages. The scored check stays green either way, which is why this is a manual finding.
Guide. Sitemaps, robots.txt and agents.
F5. State the company's AI crawler preferences in robots.txt
Category and scanner effect. Bot access control. Scored checks robotsTxtAiRules and contentSignals.
What was found. /robots.txt on 2026-09-03 is the CMS default: User-agent: *, Disallow: /wp-admin/, Allow: /wp-admin/admin-ajax.php. No AI crawler is named and no Content-Signal line exists.
Why it matters. Two scored checks. Observed: the file names no AI crawler and carries no Content-Signal line. That does not leave AI crawlers without a rule. Under section 2.2.1 of RFC 9309, a crawler that finds no group matching its own name obeys the User-agent: * group, so today an AI crawler that follows the standard may fetch everything outside /wp-admin/ plus /wp-admin/admin-ajax.php, the same as any other crawler. What the file does not state is a choice the company made about AI crawlers in particular, or how it wants its content used once it has been fetched. Possible and not observed: a crawler that does not follow the standard ignores the file whatever it says. Two category answers on 2026-09-04 cited a distributor's price list page, and the report records that as an observation only: nothing in the run shows that robots.txt had anything to do with the choice.
What to change. Keep the User-agent: * group, because it is the rule for every crawler that has no group of its own, AI crawlers included. Add a named group for each AI crawler the company wants to allow or refuse by name under decision D2. A named group is how the company states a policy for one crawler that is more specific than the wildcard group, and a named AI crawler is what robotsTxtAiRules looks for. A crawler that finds its own group obeys that group and ignores the wildcard one, so the wildcard rules do not carry over to it. Each named group therefore repeats the path rules the company still wants for that crawler, here Disallow: /wp-admin/ and Allow: /wp-admin/admin-ajax.php, and a named group written without them leaves /wp-admin/ open to that crawler. Then add one Content-Signal line that states search yes, ai-input yes, ai-train no, or whichever preference the company holds under D2. That line says how the company wants its content used once it has been fetched. The path rules are a separate matter: they say only what a crawler that follows the standard may fetch. Neither one is access control. Both are preferences a crawler follows or ignores, and the file says so in a comment next to the Content-Signal line. The company decides the preference, the report only records what the file says today.
Who does it and estimated effort. The company decides, the edge worker serves the file. About half an hour once the preference is decided.
How to check the fix. robotsTxtAiRules and contentSignals read PASS on the next full scan. The file is then read by hand against decision D2. The wildcard group still carries Disallow: /wp-admin/ and Allow: /wp-admin/admin-ajax.php. The named groups are the crawlers the company chose, and each one carries the path rules the company kept for that crawler, so no named crawler reaches /wp-admin/ because a line was left out. The comment next to the Content-Signal line is in place.
Guide. Sitemaps, robots.txt and agents.
F6. Make the existing REST API discoverable
Category and scanner effect. API, Auth, MCP & A2A Discovery. Scored check apiCatalog.
What was found. The site runs a public read-only REST API at /wp-json/ with the product catalog behind it at /wp-json/wc/store/v1/products, and no discovery file names it. GET /.well-known/api-catalog on 2026-09-03 returns the HTML 404 page with status 200, as does every path under /.well-known/. The scanner reads apiCatalog as failing along with the eight other checks in the category. Appendix A, chain 3, carries the request and the response.
Why it matters. One scored check now, and the cheapest step in the category. Observed: the API answers and nothing announces it, and every well-known probe gets a 200 with an HTML body. Possible and not observed: an agent that finds the API reads the catalog as data instead of scraping HTML, which is only worth anything once F1 has made the data right.
What to change. Publish /.well-known/api-catalog, one JSON linkset that names the /wp-json/ base URL and its description, with the content type application/linkset+json. Return a real 404 with a plain text body for every path under /.well-known/ that the edge does not serve, which here is every path except this linkset and the security.txt that F3 publishes in the same directory. A path published on purpose keeps answering with its own content and its own content type. The other eight checks in the category wait, each for the reason its row in the check table above gives: no agent needs to log in here, there is no MCP server or agent for a card to describe, and browser tools are worth a look only after the data is right. A server card that points at no server is a false claim, and this report does not recommend one. If the company wants its API described in OpenAPI too, the implementer decision D1 names publishes an OpenAPI document for the Store API routes the catalog uses and adds it to the linkset as a second service-desc, and the retest reads it against live responses. It is not part of this finding's minimum, because apiCatalog passes on the REST index alone.
Who does it and estimated effort. turva.dev or the company's developer, at the edge. About half an hour.
How to check the fix. apiCatalog reads PASS on the next full scan. The category reads 1 of 9, and the retest report says which eight are left and why they wait. GET /.well-known/api-catalog returns 200 with application/linkset+json. GET /.well-known/security.txt returns 200 with text/plain. GET /.well-known/nothing returns 404 with a short plain text body. Every path the edge publishes under /.well-known/ is requested in the same run, so that the 404 rule is measured against the list of published paths and not against one invented address alone.
Guide. The /.well-known directory for agent discovery.
F7. Enable DNSSEC now and hold the DNS discovery record
Category and scanner effect. Discoverability. Scored check dnsAid, which stays red in this round.
What was found. No _index._agents record exists under northwind-fasteners.example, and DNSSEC is not enabled at the registrar, which internet.nl also reports.
Why it matters. One scored check. DNSSEC moves the internet.nl website reading and is the prerequisite of dnsAid, so it is worth enabling now. The record itself is a pointer, and Northwind publishes nothing for it to point at.
What the check reads, with versions. The record shape follows the IETF draft DNS for AI Discovery, draft-mozleywilliams-dnsop-dnsaid-02, which defines _index._agents under the domain as an SVCB record pointing at a registry of the organisation's agents, and leaves the registry's content, protocol and schema outside the draft. The scanner's dnsAid check, as it read on 2026-09-03, looks for that record and reports whether DNSSEC validated it. Both are moving: the draft has a revision number and an expiry date, and the scanner's check definition is not versioned by the scanner, so the report pins both to their state on 2026-09-03 and the retest reads them against their state on the retest day. If either has moved between the two dates, the retest report names both versions next to the two readings rather than treating the readings as comparable.
What to change. Enable DNSSEC now: sign the zone and publish its DS record at the registrar. Hold the _index._agents record. Northwind publishes no agent registry, and F6 records that it runs no MCP server or agent card for one to list, so a record today would point at nothing and read as a claim the site cannot back. That is the same reason F6 declares no server card. When the company publishes a registry, the record is added as a separate change that names the registry's URL, what it lists and who keeps it current, with the exact record text as of the draft revision on that day.
Who does it and estimated effort. DNSSEC is about half an hour in the DNS zone and at the registrar, plus the propagation wait, and it is on the fix list. When turva.dev implements the list, the zone access and the registrar access are prerequisites arranged in writing before the work starts. The discovery record is not on this round's list.
How to check the fix. The internet.nl website test shows DNSSEC as passing, and a validating resolver returns authenticated answers for the domain. dnsAid still reads FAIL, and the retest report says so, with the draft revision and the scanner check definition date the reading was taken against. Once a registry exists and the record is added, the check is a direct SVCB lookup of _index._agents under the domain, a fetch of the registry at the record's target and dnsAid reading PASS.
Guide. The /.well-known directory for agent discovery.
F8. Keep agent checkout undeclared until it is supported
Category and scanner effect. Commerce. Scored checks x402, mpp, ucp, acp and ap2.
What was found. The store checks out through a browser form with a card payment page. None of the five agent commerce surfaces the scanner reads is declared: an x402 payment challenge, an MPP payment discovery document, a UCP profile, an ACP discovery document or an AP2 declaration. All five commerce checks read FAIL.
Why it matters. Five scored checks, and none of them is worth flipping this year. A commerce declaration that points at a checkout an agent cannot complete is a false claim. The sample's declaration checks do not demonstrate that an agent can complete checkout.
What to change. No change. The company said at the kickoff that agents do not buy here this year, and that is decision D5. When it changes, agent checkout is a separate engagement scoped from the payment side, and the report makes no recommendation about which of the five surfaces to declare first, because the only reason to pick one on the evidence here would be that it scores, and that is not a reason.
Who does it and estimated effort. The company's management, as a decision. No hours in this round.
How to check the fix. The five checks stay red on the next scan, on purpose, and the retest report repeats this entry so that nobody reads the red as an oversight.
Guides. Agent commerce discovery and Agentic commerce readiness.
F9. Correct the published address
Category and scanner effect. Correctness of published facts. Manual review, not scored, raised by the AI run.
What was found. Two of the twelve by-name answers on 2026-09-04 gave the Tampere address the company left in 2023. Assistant A cited a business directory listing that still carries it. Assistant C cited /wp-content/uploads/2022/hinnasto-2022.pdf, a 2022 price list on the company's own site with the old address on its cover, linked from no page and indexed by two search engines. The site's Organization node carries no address, and the current address appears only as text on the contact page.
Why it matters. No scanner points. Observed: two wrong answers in twelve. Possible and not observed: a buyer's assistant sends a visitor or a delivery to the wrong address.
What to change. Three parts. Add a PostalAddress with the current address to the Organization node on every page, which the edge does from the text the company confirms. Remove the 2022 PDF or replace it with the current price list at the same URL, because the file is the company's own and its cover carries the old address. Ask the directory to correct its listing, and record the request with any answer it gets. The first two are corrections on the fix list. The third is a request to an outside party, and the listing itself stays that party's to change.
Who does it and estimated effort. About half an hour at the edge and about an hour at the source, the request to the directory included. Both are on the fix list. The directory's answer is not, and no one here promises it.
How to check the fix. Two things, kept apart. First, the site's own technical fix. It is closed when the Organization node carries the current address on every page, when GET /wp-content/uploads/2022/hinnasto-2022.pdf returns 404 or the current list, and when the request to the directory is recorded with the date it was sent, the recipient, the correction that was asked for and its state on the day of the check. An answer is recorded with the request when one has arrived, and a request with no answer is recorded as "No response received as of" the real date of that check. A documented request meets this item whether or not the directory answers, and a change in the listing itself is reported on its own. That closing does not wait on the AI reading. Second, the three by-name questions are put again at the retest and the answers are recorded as an observation, with the source each one cites. An answer can still carry the old address from the directory or from a copy nobody here controls, and the report says which, rather than counting it against the site.
Guide. JSON-LD and structured data for AI clients.
After the report
Three routes, and the report is written so that any of them works.
- The company's team does the work from this report. Every finding carries its change and its acceptance test, the section What each party delivers says what an accepted delivery means, and the guides linked above carry the patterns.
- turva.dev makes every correction on the fix list, in both columns, for the fixed price on the services page, bought with the audit, with its report, or after the report and before implementation starts. That price needs the access listed under What each party delivers, arranged in writing before the work starts. Without that access the add-on is not sold, and the report still carries every instruction.
- Nothing is done, and the report stands as a dated record of where the site was on 2026-09-03.
In every case the retest is the same, and it is included in the audit price: once within 30 days of this report, so by 2026-10-08, or within 30 days of the delivered corrections when turva.dev makes them under the second route, on the day the company names, the scanner is run again with the same profile, the whole-catalog script from F1 is run again, the 15 questions are put to the same four assistants under the same conditions, and the readings are printed next to the ones above with the versions from F7 stated. One round of written follow-up questions is open until 2026-09-22.
Scope limits
It is not a penetration test, a certification, an SEO audit or a promise of ranking on any AI platform. It reads what the site publishes and records what four assistants said on one day. Where it says an agent may do something, it says so as a possibility and not as a measurement. It does not read the organisation's readiness to adopt agents, which is a different question that often goes by the same name.
Appendix A. From evidence to a checked fix
These three examples connect a request and a response to a finding, a correction and a follow-up result. The follow-up readings are fictional too, and they are shown so that the shape of a closed finding is visible. In a real report those fields remain open until the retest is complete.
Chain 1, F1, one row of the whole-catalog test.
| Step | Content |
|---|---|
| Request | GET /products/din-933-m12x40-a2/ on the host northwind-fasteners.example with Accept: text/html, 2026-09-03 |
| Response excerpt | Visible: 0,42 EUR / kpl, ilman alv:tä, toimitusaika 6 viikkoa. JSON-LD: "@type": "Product", "offers": {"price": "0.00", "priceCurrency": "EUR", "availability": "https://schema.org/InStock"} |
| Second request | GET /wp-json/wc/store/v1/products?slug=din-933-m12x40-a2 on the same host |
| Response excerpt | "prices": {"price": "", "currency_code": "EUR", "currency_minor_unit": 2}, "is_purchasable": true, "is_in_stock": true, "is_on_backorder": true |
| Observation | Three surfaces, three answers: 0,42 EUR with a six week lead time, 0,00 EUR in stock, and no price at all but purchasable |
| Change | JSON-LD from the page's own price and stock elements at the edge. Plugin mapping and API price visibility at the source. Availability from is_on_backorder true, so BackOrder with deliveryLeadTime 6 weeks |
| Acceptance reading | JSON-LD "price": "0.42", "availability": "https://schema.org/BackOrder", "deliveryLeadTime" 6 weeks. API "price": "42", "is_on_backorder": true. The confirmed product record gives 0,42 EUR per piece, on backorder with a six week lead time, so the row is ALIGNED on all three surfaces and with the record, and the script's total line reads 170 of 170 price rows and 180 of 180 availability rows aligned, which are the counts of a catalog where decision D4 kept all ten exception products published. The API column reads right here because the source row of F1 was corrected as well, which is what the fix list covers. The origin's own JSON-LD for the page, read by the method agreed with the company, carries the same node, so the source correction is shown on the origin and not only at the edge |
The acceptance script from F1 is delivered as a file with the report. Its output for the same product at the retest:
| Surface | Price | Availability | Lead time | Result |
|---|---|---|---|---|
| Visible page | 0,42 EUR per piece | On backorder | 6 weeks | ALIGNED |
| JSON-LD | "0.42", EUR | BackOrder | 6 weeks | ALIGNED |
| Store API | "42" in minor units | is_on_backorder true | Not published | ALIGNED on the fields it carries |
| Confirmed product record | 0,42 EUR per piece | On backorder | 6 weeks | Reference |
Chain 2, F2, markdown negotiation on the home page.
| Step | Content |
|---|---|
| Request | GET / on the host northwind-fasteners.example with Accept: text/markdown, 2026-09-03 |
| Response excerpt | HTTP 200, Content-Type: text/html, Content-Length 217088, no Vary header, no Link header, body starts with the HTML doctype |
| Observation | The origin ignores the Accept header. 217 kB of markup for a page whose markdown form is 9 kB |
| Change | Edge worker converts the rendered HTML on the way through and serves it at the same address and at /index.md, with the acceptance list in F2 |
| Acceptance reading | HTTP 200, Content-Type: text/markdown, Vary: Accept, Link: rel="canonical" to the HTML page, body starts with the page title as a heading, 9 216 bytes. The same request to /cart/ passes through as HTML untouched. markdownNegotiation PASS on the retest scan |
Chain 3, F6, the soft 404 under /.well-known/.
| Step | Content |
|---|---|
| Request | GET /.well-known/api-catalog on the host northwind-fasteners.example with Accept: application/linkset+json, 2026-09-03 |
| Response excerpt | HTTP 200, Content-Type: text/html, body is the theme's page titled Sivua ei löytynyt |
| Observation | A probe for a manifest gets a 200 and an HTML page. Every path under /.well-known/ answers the same way, so nothing under it can be trusted by status alone |
| Change | Serve the linkset at /.well-known/api-catalog with application/linkset+json, and return a real 404 with a plain text body for every other path under /.well-known/ that the edge does not serve |
| Acceptance reading | /.well-known/api-catalog: HTTP 200, application/linkset+json, one link with rel service-desc to /wp-json/. /.well-known/nothing: HTTP 404, text/plain. apiCatalog PASS on the retest scan, category 1 of 9 |
Appendix B. Questions and test conditions
The four assistants are ChatGPT, Gemini, Perplexity and Google AI Mode, the same four as in the published measurement of fifty buyer questions. Claude is not measured because it requires a login. Each question was put once to each assistant on 2026-09-04 between 09:00 and 12:00 EEST, in a fresh conversation without memory, without an account where the platform allows it, with web browsing on, from a Finnish network location, in the language the question is written in. Where a platform did not answer, the row would read missing, and none did on the day. The retest repeats every condition and the report states any that could not be repeated.
Category questions, no company name, 12.
- Mistä suomalainen konepaja voi ostaa DIN 933 -pultteja tukkuerissä verkosta?
- Where can a Finnish workshop buy DIN 933 bolts in bulk online?
- Mikä suomalainen tukkuri myy A2-ruostumattomia ruuveja yrityksille?
- Which Finnish wholesaler sells A2 stainless fasteners to businesses?
- Mistä saa M12-kuusioruuveja sadan kappaleen laatikoissa Suomessa?
- Kuka toimittaa kiinnitystarvikkeita teollisuudelle Pirkanmaalla?
- Where do I buy DIN 912 socket head screws in Finland with business invoicing?
- Mikä on halvin paikka ostaa sinkittyjä muttereita tukkuhintaan Suomessa?
- Which Finnish fastener suppliers publish their prices online without a login?
- Mistä löydän teollisuuden kiinnikkeiden toimittajan, jolla on kuuden viikon toimitusajat isoille erille?
- Suomalainen ruuvitukku, joka toimittaa seuraavana päivänä, mitä vaihtoehtoja on?
- Which Finnish suppliers of industrial fasteners have an online catalog with stock information?
Questions by name, 3.
- Mitä Northwind Fasteners myy?
- What does Northwind Fasteners sell and where is the company located?
- Paljonko DIN 933 M12x40 A2 maksaa Northwind Fastenersilta?
The delivered report carries every one of the 60 answers as a row of this form. Two rows are shown here, the two errors named in the table of material errors:
| Question | Assistant | Time, EEST | Answer, excerpt | Northwind named | Correct where named | Source cited |
|---|---|---|---|---|---|---|
| What does Northwind Fasteners sell and where is the company located? | A | 2026-09-04 09:41 | "Northwind Fasteners is a Tampere wholesaler of industrial fasteners, located at [the address the company left in 2023]." | Yes | No, the address | A business directory listing |
| Paljonko DIN 933 M12x40 A2 maksaa Northwind Fastenersilta? | B | 2026-09-04 10:18 | "Tuote on Northwindin verkkokaupassa ilmainen, hinta 0,00 EUR." | Yes | No, the price | The product page /products/din-933-m12x40-a2/ |
About this sample
Every figure on this page is invented. The check names, the categories and the statuses are the scanner's real vocabulary as read on 2026-09-23, the draft name in F7 is the real IETF draft, and the four assistants in appendix B are the four the published measurement used, so that the sample shows how a real report reads. The site, the readings, the assistants' answers, the addresses and the security scores are fiction. A real report carries the raw scanner output, the request and response logs, the list of every page read for metadata and headers, the whole-catalog script and its output and the 60 AI answers with the assistant named on each. The sample covers one target. A real audit covers up to two named in the written scope.
The audit is described on the services page. To start one, email info@turva.dev with the site or API URL and what the audit should answer. The Shopify agent storefront check has its own sample report.