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

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

The agreed work

FieldValue
ClientNorthwind Fasteners Oy, industrial fastener wholesaler, invented
Surface auditedThe public site northwind-fasteners.example, its 138 product pages and its REST API at /wp-json/
What the company askedThat 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 writing2026-08-25
Scanner run2026-09-03, isitagentready.com, default profile, three serial runs, all three identical
Manual review2026-09-03 and 2026-09-04, whole catalog read through the API and the sitemap
AI visibility run2026-09-04, 15 questions to four assistants, one anonymous run per question, conditions in appendix B
Report delivered2026-09-08, fourteen days after the kickoff
Written follow-up roundQuestions open until 2026-09-22, answered within five business days
Re-scanOne, 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 usedPublic 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.

DecisionOptionsEffectNeeded by
D1. Who implements the fix listThe company's team from this report, or turva.dev for the fixed price on the services pageDecides who makes the edge and the source corrections in the table below, and who owns the acceptance run in the section What each party deliversBefore the work starts
D2. The AI crawler preference for F5Allow named crawlers, disallow them, or allow search and ai-input and refuse ai-trainDecides the content of robots.txt. The report records no preference of its ownBefore F5 is written
D3. The text of llms.txt for F3Approve the draft as written, or edit itThe file is the company's own description of itself and goes live only after the company has read itBefore F3 goes live
D4. The four discontinued products and the six price-on-request products, F1Keep them published with the right availability, or unpublish themDecides whether 10 of 138 products stay in the catalog an agent readsBefore the F1 acceptance run
D5. Agent checkout, F8No agent checkout this year, as agreed at the kickoff, or a separate scoped engagement when that changesThe five commerce checks stay red on purpose until this decision changes. The report makes no recommendation on the strength of a scanner pointWhenever 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.

OrderFindingChecks movedCorrection at the edgeCorrection at the sourceEdge hoursSource hours
1F1 product data on all three surfacesNone, manualCorrect the JSON-LD Product node on every product page from the fields the page renders, and run the whole-catalog acceptance scriptFix the plugin mapping and the API price visibility so the origin publishes the right data itself42,5
2F9 the old addressNone, manualAdd the current address to the Organization nodeRemove or update the 2022 price list PDF. Ask the directory to correct its listing, which is a third party's own action0,51
3F2 markdown next to HTMLmarkdownNegotiationEdge worker with the acceptance list in F2None40
4F3 llms.txt, Link header, security.txtlinkHeadersServe llms.txt and /.well-known/security.txt, and add the describedby and alternate Link relationsRead and approve the llms.txt text, D3. A company decision, not implementation work1,50,5
5F4 sitemap contentNone, manualServe a sitemap index over a product sitemap generated from the API and a content sitemap, the CMS sitemap without the two template pagesFour plugin settings so the origin sitemap is right on its own0,50,5
6F5 robots.txtrobotsTxtAiRules, contentSignalsServe the fileDecide the preference, D2. A company decision, not implementation work0,50
7F6 api-catalog and real 404sapiCatalogServe the linkset and the 404sNone0,50
8F7 DNSSEC, the DNS-AID record waitsNone in this round. dnsAid waits for an agent registryNone. DNS is not served by the edgeEnable 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 at00,5
9F8 commerceNone, on purposeNoneDecision D500

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:

At the source, in the systems the company gives access to:

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:

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.

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.

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.

CategoryCheckReadingWhat the scanner sawFixWhat it means for Northwind
DiscoverabilityrobotsTxtPASS/robots.txt exists and parses. One wildcard group.F5 changes its content, the check stays greenGreen because the CMS ships the file, not because it says anything useful
DiscoverabilitysitemapPASS/sitemap.xml exists and parses, 14 URLs.F4, a manual finding, the check stays greenGreen with none of the 138 products in it. The check reads the file, not the catalog
DiscoverabilitylinkHeadersFAILNo Link header on any response.F3Cheap, and it is how a page points at its own markdown twin and at llms.txt
DiscoverabilitydnsAidFAILNo _index._agents record under the domain, DNSSEC not enabled.F7 enables DNSSEC, the record waitsNeeds 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
ContentmarkdownNegotiationFAILAccept: text/markdown returns text/html, 217 kB on the home page.F2A 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 controlrobotsTxtAiRulesFAILNo AI crawler named in robots.txt.F5A stated preference the company has not stated. Decision D2
Bot access controlcontentSignalsFAILNo Content-Signal line in robots.txt.F5Same file, same decision
Bot access controlwebBotAuthINFONo Web Bot Auth directory. Informational, not scored.NoneThe company operates no bots that would sign requests
API, Auth, MCP & A2A DiscoveryapiCatalogFAIL/.well-known/api-catalog returns the HTML 404 page.F6The catalog API already exists. Naming it is the cheapest route to an agent reading prices as data
API, Auth, MCP & A2A DiscoveryoauthDiscoveryFAILNo /.well-known/oauth-authorization-server or openid-configuration.Waits, see F6No agent needs to log in here. Stays red at no cost
API, Auth, MCP & A2A DiscoveryoauthProtectedResourceFAILNo /.well-known/oauth-protected-resource.Waits, see F6Same
API, Auth, MCP & A2A DiscoveryauthMdFAILNo /auth.md and no agent_auth metadata.Waits, see F6Same
API, Auth, MCP & A2A DiscoverymcpServerCardFAILNo /.well-known/mcp/server-card.json.Waits, see F6There is no MCP server to point at, and a card that points at none is a false claim
API, Auth, MCP & A2A Discoverya2aAgentCardFAILNo /.well-known/agent-card.json.Waits, see F6Same
API, Auth, MCP & A2A DiscoveryagentSkillsFAILNo /.well-known/agent-skills/index.json.Waits, see F6Same
API, Auth, MCP & A2A DiscoverywebMcpFAILNo navigator.modelContext tools on the home page.Waits, see F6Browser tools on a catalog page are worth a look after the data is right, not before
API, Auth, MCP & A2A DiscoveryardFAILNo capability manifest at the well-known path.Waits, see F6Same as the server card
Commercex402FAILNo 402 challenge on any route.Stays red on purpose, F8No agent checkout this year, by the company's own decision
CommercemppFAILNo payment discovery document.Stays red on purpose, F8Same
CommerceucpFAILNo /.well-known/ucp.Stays red on purpose, F8Same
CommerceacpFAILNo ACP discovery document.Stays red on purpose, F8Same
Commerceap2FAILNo AP2 declaration.Stays red on purpose, F8Same

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.

SurfaceReadingFinding
First response as a botEvery 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 pagesProduct 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 pagesOrganization 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 GraphTitle 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 headersContent-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.txtThe CMS default. One wildcard group, Disallow: /wp-admin/, no Sitemap line, no AI crawler, no Content-Signal.F5
sitemap.xml14 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.txtNeither 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 descriptionNone. 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 groupQuestionsAnswersNorthwind namedCorrect where namedWhat the assistants did instead
Category, no company name. "Where can a Finnish workshop buy DIN 933 bolts in bulk online" and eleven like it12480Not applicableNamed 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 it312129Two 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.

ErrorAnswersSource citedFollow-upOwnerRetest
The catalog is free1 of 12, assistant BThe 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 interpretationF1The implementer decision D1 names, at the edge and at the source. The company arranges the CMS and plugin access firstThe 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 address2 of 12, assistants A and CA 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 dataF9The 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'sThe 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.

ScanReadingWhat it means for agents
internet.nl website test64 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 test55 of 100. SPF present, DKIM present, DMARC policy none.Not an agent surface. Recorded because a buyer checks it.
Hardenize17 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.

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.

Guide. Serving Markdown to AI clients.

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.

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.

StepContent
RequestGET /products/din-933-m12x40-a2/ on the host northwind-fasteners.example with Accept: text/html, 2026-09-03
Response excerptVisible: 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 requestGET /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
ObservationThree surfaces, three answers: 0,42 EUR with a six week lead time, 0,00 EUR in stock, and no price at all but purchasable
ChangeJSON-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 readingJSON-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:

SurfacePriceAvailabilityLead timeResult
Visible page0,42 EUR per pieceOn backorder6 weeksALIGNED
JSON-LD"0.42", EURBackOrder6 weeksALIGNED
Store API"42" in minor unitsis_on_backorder trueNot publishedALIGNED on the fields it carries
Confirmed product record0,42 EUR per pieceOn backorder6 weeksReference

Chain 2, F2, markdown negotiation on the home page.

StepContent
RequestGET / on the host northwind-fasteners.example with Accept: text/markdown, 2026-09-03
Response excerptHTTP 200, Content-Type: text/html, Content-Length 217088, no Vary header, no Link header, body starts with the HTML doctype
ObservationThe origin ignores the Accept header. 217 kB of markup for a page whose markdown form is 9 kB
ChangeEdge 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 readingHTTP 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/.

StepContent
RequestGET /.well-known/api-catalog on the host northwind-fasteners.example with Accept: application/linkset+json, 2026-09-03
Response excerptHTTP 200, Content-Type: text/html, body is the theme's page titled Sivua ei löytynyt
ObservationA 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
ChangeServe 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.

Questions by name, 3.

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:

QuestionAssistantTime, EESTAnswer, excerptNorthwind namedCorrect where namedSource cited
What does Northwind Fasteners sell and where is the company located?A2026-09-04 09:41"Northwind Fasteners is a Tampere wholesaler of industrial fasteners, located at [the address the company left in 2023]."YesNo, the addressA business directory listing
Paljonko DIN 933 M12x40 A2 maksaa Northwind Fastenersilta?B2026-09-04 10:18"Tuote on Northwindin verkkokaupassa ilmainen, hinta 0,00 EUR."YesNo, the priceThe 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.