{
  "service": "402registry",
  "summary": "Pay-per-check discoverability audit: does an AI crawler actually see your site? Signed HTTP audit over the x402 protocol (USDC on Base) — no account, no card, no human.",
  "quickstart": [
    {
      "step": 1,
      "title": "Check if a site is already listed",
      "request": "GET https://402registry.com/api/check?url=https://example.com/",
      "note": "Free, cached, no payment. Unknown host is a normal 200 with known:false, not an error."
    },
    {
      "step": 2,
      "title": "Buy a check",
      "request": "GET https://402registry.com/buy/one?site=https://example.com/",
      "note": "Without a payment header this answers 402 with the x402 payment requirements ($0.01 for one check). Pay with an x402-capable client (e.g. @x402/fetch) and resend the same request with the payment header attached. One payment buys one check, run now; one site gets at most 3 checks per hour and 10 per day (refused before payment). Lost the response? Resend the identical request and header within 5 minutes; after that, sign a Recovery message with the paying wallet and POST https://402registry.com/recover (see \"recovery\" below)."
    },
    {
      "step": 3,
      "title": "Read the report",
      "request": "GET https://402registry.com/r/{id}",
      "note": "The `report` URL from the purchase response. HTML by default; add ?format=json or send Accept: application/json for the machine-readable form."
    },
    {
      "step": 4,
      "title": "Show the badge",
      "request": "GET https://402registry.com/badge/{host}.svg",
      "note": "Free, cached SVG. Embed it anywhere — it always renders, even for a host that was never checked. Use the host that answers (the receipt field `host`): an address that only redirects to your site is not listed under its own name."
    }
  ],
  "verdicts": {
    "listed": "The site passed: no critical findings. Earns a dated badge and a free registry listing — the listing is a bonus for passing, not what the payment buys. Opt out with ?catalog=no.",
    "refused": "The site failed a hard rule (e.g. an empty shell with no metadata at all). No registry listing, and nothing about the result is published: the report is private to you and the public badge and lookup report this host as never checked. You paid for the analysis and you get it in full.",
    "doubt": "The rules found no hard failure, but the model judged the content too thin to confirm a pass. This is a verdict like the others and it uses one credit. The site is not listed and nothing about it is published; add real content and buy again. If our model cannot answer at all, the purchase is refused before payment with 503 model_unavailable and nothing is charged."
  },
  "packs": [
    {
      "id": "one",
      "checks": 1,
      "priceUsdc": "0.01",
      "perCheckUsdc": "0.01",
      "label": "One check",
      "buy": "https://402registry.com/buy/one"
    }
  ],
  "packsNote": "One payment buys one check, run now. Scheduled re-checks are not sold: monitor= and notify= are refused before payment (monitoring_not_sold). Buy again whenever you want a fresh check.",
  "sinceLast": "Every paid and scheduled check says what changed since the previous check you may see (receipt: sinceLast, short; report: a \"Since the previous check\" section). The previous check is your own earlier check (same wallet), or — for a listed site — its public catalog record, with private findings left out. Findings and score are compared only under the same rules version; after a rules change only the verdict is compared. No earlier check you may see: sinceLast is null.",
  "recovery": {
    "endpoint": "POST https://402registry.com/recover",
    "when": "You paid for a check but lost the response, and the replay window is over. The report itself is never lost: it stays at /r/{id} for 400 days. What you can lose is the id; this route returns it. It needs a signature from the PAYING wallet, so the public payment header on chain is not enough.",
    "body": {
      "wallet": "0x… the paying wallet",
      "subject": "nonce:0x… | tx:0x…",
      "issued_at": "unix time in whole seconds, within 300 s of now",
      "signature": "0x… 65-byte ECDSA signature"
    },
    "subjects": [
      "nonce:<0x… 32-byte payment nonce>",
      "tx:<0x… 32-byte settlement transaction hash>"
    ],
    "signature": {
      "standard": "EIP-712 signTypedData (viem signTypedData, ethers signTypedData)",
      "domain": {
        "name": "402registry.com",
        "version": "1",
        "chainId": 8453
      },
      "primaryType": "Recovery",
      "types": {
        "Recovery": [
          {
            "name": "wallet",
            "type": "address"
          },
          {
            "name": "subject",
            "type": "string"
          },
          {
            "name": "issuedAt",
            "type": "uint256"
          }
        ]
      },
      "message": {
        "wallet": "the paying wallet address",
        "subject": "the same string as in the body",
        "issuedAt": "the same number as issued_at (uint256)"
      }
    },
    "success": "200 { ok, pack, checks, payment (settlement tx), settled_at, report (https://402registry.com/r/{id}), report_json (…?format=json) }. After 400 days the report is deleted: 200 with report: null and expired: true.",
    "errors": {
      "bad_request": "400 malformed body",
      "bad_subject": "400 subject is not nonce:<0x…> or tx:<0x…>",
      "stale_proof": "400 issued_at is more than 300 s from our clock; sign again",
      "signature_unsupported": "401 only a plain 65-byte ECDSA signature is accepted (contract wallets: ERC-1271 / ERC-6492 are recovered by hand, see manual)",
      "bad_signature": "401 the signature does not recover to wallet",
      "not_found": "404 no settled payment of this wallet matches the subject (the same answer for an unknown payment and for a payment of another wallet)",
      "not_in_recent": "404 tx: only searches your 50 most recent payments; use nonce: instead",
      "manual_recovery": "409 settled, but there is no report to hand back automatically; write to the contact with the payment hash",
      "rate_limited": "429 at most 10 attempts per hour per address (Retry-After)"
    },
    "manual": "Contract wallets and anything we broke: write to abuse@402registry.com quoting the settlement transaction hash. We fix it by hand, including a manual refund when the fault is ours. Refunds are never automatic.",
    "note": "Replaying the payment header after the window keeps returning 409 already_paid with the receipt only, on purpose: that header is public on chain. Recovery is a different route with a different proof."
  },
  "catalogSnapshot": {
    "id": "catalog",
    "priceUsdc": "0.005",
    "label": "Catalog snapshot",
    "what": "Every site in the catalog as one JSON file, rendered at purchase: per site the findings by name and severity, the stack, the title and what the crawler saw without JavaScript. No parameters. Resending the same payment (same wallet, while the header is valid) returns the current snapshot with originalGeneratedAt, and nothing is charged again; after the header expires it returns 409 already_paid with the receipt. Never sign again for the same purchase.",
    "free": "https://402registry.com/registry.json",
    "buy": "https://402registry.com/buy/catalog"
  },
  "routes": [
    {
      "method": "GET",
      "path": "/",
      "summary": "Landing page",
      "description": "Human-readable landing page: what the service does, current pricing, and machine endpoints.",
      "example": "https://402registry.com/"
    },
    {
      "method": "GET",
      "path": "/registry",
      "summary": "Public registry",
      "description": "Sites that came back \"listed\" from a check, newest first. The check is the paid product; the listing is a free bonus, earned by passing and never bought. Free, cached, no payment.",
      "example": "https://402registry.com/registry"
    },
    {
      "method": "GET",
      "path": "/registry/{category}",
      "summary": "Catalog by category",
      "description": "The listed sites in one category (tool, saas, docs, agency and so on), newest first, with a JSON-LD ItemList. A category gets its own page only once it has enough entries; below that it returns 404 rather than a thin page. Free, cached, no payment.",
      "example": "https://402registry.com/registry/tool"
    },
    {
      "method": "GET",
      "path": "/registry.json",
      "summary": "The whole catalog, machine-readable",
      "description": "Every listed site with its category, score, check date and badge URL, plus generatedAt. This is the endpoint an agent or an AI search model should read: no HTML to parse and nothing to render. Free, cached, no payment, CORS open.",
      "example": "https://402registry.com/registry.json"
    },
    {
      "method": "GET",
      "path": "/site/{host}",
      "summary": "Profile of one listed site",
      "description": "A page about one site in the catalog: dated verdict, score, category, one-sentence description and the findings a crawler runs into. Returns 404 for a host that is not listed, was never checked, or asked to be removed. The paid report adds the evidence behind each finding and how to fix it.",
      "example": "https://402registry.com/site/402cron.com"
    },
    {
      "method": "GET",
      "path": "/registry.jpg",
      "summary": "Catalog social preview image",
      "description": "1200x630 JPEG used as og:image when a link to /registry or to a category page is shared.",
      "example": "https://402registry.com/registry.jpg"
    },
    {
      "method": "GET",
      "path": "/site.jpg",
      "summary": "Profile social preview image",
      "description": "1200x630 JPEG used as og:image when a link to a profile page (/site/{host}) is shared.",
      "example": "https://402registry.com/site.jpg"
    },
    {
      "method": "GET",
      "path": "/blog",
      "summary": "Blog",
      "description": "Measurements of what AI crawlers actually see on websites: plain HTTP, no JavaScript, with a control group.",
      "example": "https://402registry.com/blog"
    },
    {
      "method": "GET",
      "path": "/blog.jpg",
      "summary": "Blog social preview image",
      "description": "1200x630 JPEG used as og:image when a link to the blog index is shared.",
      "example": "https://402registry.com/blog.jpg"
    },
    {
      "method": "GET",
      "path": "/blog/{slug}",
      "summary": "Blog article",
      "description": "One article. Append .jpg to the slug for its 1200x630 social preview image. Returns 404 for an unknown slug.",
      "example": "https://402registry.com/blog/ai-built-sites-empty-without-javascript"
    },
    {
      "method": "GET",
      "path": "/bot",
      "summary": "About our crawler",
      "description": "What the 402registry-check crawler is, why it visits a site, exactly what one check requests (including 4 requests made with the full user-agent strings of AI crawlers), and how to opt out.",
      "example": "https://402registry.com/bot"
    },
    {
      "method": "GET",
      "path": "/trust",
      "summary": "Trust and verification",
      "description": "How to verify us before paying: the live 402 terms (network, USDC contract, payTo, amount per product), the receiving wallet on the block explorer, what one check measures, what a passing check does not mean, and what we never ask for.",
      "example": "https://402registry.com/trust"
    },
    {
      "method": "GET",
      "path": "/privacy",
      "summary": "Privacy",
      "description": "What the service stores (payments, reports, short-lived rate-limit counters, daily usage counts), for how long, and which other parties see it. No cookies, no accounts.",
      "example": "https://402registry.com/privacy"
    },
    {
      "method": "GET",
      "path": "/terms",
      "summary": "Terms of service",
      "description": "What a check is, the per-check price paid over x402 in USDC on Base, when you are charged, what you may do, and the limit of liability.",
      "example": "https://402registry.com/terms"
    },
    {
      "method": "GET",
      "path": "/.well-known/security.txt",
      "summary": "Security contact (RFC 9116)",
      "description": "Where to report a security issue or abuse. Expires is computed at request time, so the file never goes stale.",
      "example": "https://402registry.com/.well-known/security.txt"
    },
    {
      "method": "POST",
      "path": "/submit",
      "summary": "Ask for a free check",
      "description": "Submit a hostname to be checked for free and, if it passes, listed in the catalog. Send JSON {\"host\":\"example.com\"} or a form field named host. No payment, no account, no API key. Being queued is not a promise of listing: only sites that pass the check are listed, and the queue is processed slowly. To be removed instead, disallow the 402registry-check user agent in robots.txt or write to the abuse address in the manifest.",
      "example": "https://402registry.com/submit"
    },
    {
      "method": "POST",
      "path": "/recover",
      "summary": "Recover the report address of a paid check",
      "description": "You paid but lost the response and the replay window is over? The paying wallet signs an EIP-712 Recovery message and gets the report URL back. Body {\"wallet\":\"0x…\",\"subject\":\"nonce:0x…\" or \"tx:0x…\",\"issued_at\":<unix seconds, within 300 s of now>,\"signature\":\"0x… 65 bytes\"}. Signed: domain {name:\"402registry.com\",version:\"1\",chainId:8453}, primaryType Recovery, types Recovery:[{wallet:address},{subject:string},{issuedAt:uint256}]. Nothing is charged and no payment header is involved. Errors: bad_request, bad_subject, stale_proof (400); signature_unsupported, bad_signature (401); not_found, not_in_recent (404; not_found is the same answer for an unknown payment and for a payment of another wallet); manual_recovery (409); rate_limited (429, ten attempts per hour per address). Contract wallets are recovered by hand: write to the abuse address in the manifest quoting the payment hash. The full field list is the `recovery` object in /docs.",
      "example": "https://402registry.com/recover"
    },
    {
      "method": "POST",
      "path": "/mcp",
      "summary": "MCP server (streamable HTTP)",
      "description": "Model Context Protocol over streamable HTTP, one JSON-RPC 2.0 object per POST, no session, no key, no OAuth. Read-only tools: a free registry lookup, every product with its exact x402 payment requirements, the machine guide and the selling status. Buying is not a tool: pay over plain HTTP at /buy/{pack} with an x402 client.",
      "example": "https://402registry.com/mcp"
    },
    {
      "method": "GET",
      "path": "/docs",
      "summary": "API guide (JSON)",
      "description": "This document, as machine-readable JSON: quickstart, verdict meanings, current pricing, and the same route list as /openapi.json.",
      "example": "https://402registry.com/docs"
    },
    {
      "method": "GET",
      "path": "/openapi.json",
      "summary": "OpenAPI specification",
      "description": "This OpenAPI 3.1 document.",
      "example": "https://402registry.com/openapi.json"
    },
    {
      "method": "GET",
      "path": "/sitemap.xml",
      "summary": "Sitemap",
      "description": "Real HTML pages only: the landing page, /registry, one profile page per listed site, the blog and its posts, and /bot.",
      "example": "https://402registry.com/sitemap.xml"
    },
    {
      "method": "GET",
      "path": "/robots.txt",
      "summary": "Crawler rules",
      "description": "Every crawler is allowed, AI crawlers included; the Sitemap line points to /sitemap.xml.",
      "example": "https://402registry.com/robots.txt"
    },
    {
      "method": "GET",
      "path": "/a85b7d2b441ca934ae0c6f2a9fc98435.txt",
      "summary": "IndexNow key",
      "description": "The IndexNow ownership key as plain text, so search engines accept the URLs we submit for this host.",
      "example": "https://402registry.com/a85b7d2b441ca934ae0c6f2a9fc98435.txt"
    },
    {
      "method": "GET",
      "path": "/og.png",
      "summary": "Brand card",
      "description": "1200x630 PNG brand card; the logo in the landing page JSON-LD.",
      "example": "https://402registry.com/og.png"
    },
    {
      "method": "GET",
      "path": "/og.jpg",
      "summary": "Social preview image",
      "description": "1200x630 JPEG used as og:image / twitter:image when a link to the landing page is shared.",
      "example": "https://402registry.com/og.jpg"
    },
    {
      "method": "GET",
      "path": "/georgi.webp",
      "summary": "Owner portrait",
      "description": "400x400 WebP portrait shown in the \"Who runs it\" block on the landing page.",
      "example": "https://402registry.com/georgi.webp"
    },
    {
      "method": "GET",
      "path": "/art/{name}.webp",
      "summary": "Page header artwork",
      "description": "1536x1024 WebP artwork shown in the header band of the human pages (home, blog, registry, site, trust, bot). No text in the image.",
      "example": "https://402registry.com/art/home.webp"
    },
    {
      "method": "GET",
      "path": "/favicon.svg",
      "summary": "Site icon (SVG)",
      "description": "The site icon as SVG, referenced by <link rel=\"icon\"> on every page.",
      "example": "https://402registry.com/favicon.svg"
    },
    {
      "method": "GET",
      "path": "/favicon.png",
      "summary": "Site icon (PNG)",
      "description": "The site icon as a 192x192 PNG, for clients that do not read SVG icons.",
      "example": "https://402registry.com/favicon.png"
    },
    {
      "method": "GET",
      "path": "/favicon.ico",
      "summary": "Site icon (root fallback)",
      "description": "The same 192x192 PNG, served at the root path that browsers and catalogs probe by convention.",
      "example": "https://402registry.com/favicon.ico"
    },
    {
      "method": "GET",
      "path": "/.well-known/402registry.json",
      "summary": "Machine manifest",
      "description": "Service identity, x402 payment protocol details, current packs, and whether the till is open right now.",
      "example": "https://402registry.com/.well-known/402registry.json"
    },
    {
      "method": "GET",
      "path": "/.well-known/x402",
      "summary": "x402 discovery document",
      "description": "The x402 discovery document read by x402 catalogs: {\"version\":1,\"resources\":[...]} listing the check and the catalog snapshot. Both are also in this OpenAPI document and in the manifest.",
      "example": "https://402registry.com/.well-known/x402"
    },
    {
      "method": "GET",
      "path": "/llms.txt",
      "summary": "llms.txt",
      "description": "A plain-text summary for language models: what the service does, current prices, and the machine addresses.",
      "example": "https://402registry.com/llms.txt"
    },
    {
      "method": "GET",
      "path": "/.well-known/ai-plugin.json",
      "summary": "AI plugin manifest",
      "description": "The OpenAI plugin manifest (v1): name, description, no auth, and a pointer to this OpenAPI document.",
      "example": "https://402registry.com/.well-known/ai-plugin.json"
    },
    {
      "method": "GET",
      "path": "/health",
      "summary": "Liveness probe",
      "description": "Cheap health check for uptime monitors: kill switch state and how long ago the maintenance sweep last ran.",
      "example": "https://402registry.com/health"
    },
    {
      "method": "GET",
      "path": "/buy/one",
      "summary": "Buy 1 check for $0.01 (x402)",
      "description": "One check: $0.01 for 1 check, $0.01 per check. Without a payment header this is a browse request: it answers 402 with the x402 payment requirements. With a valid `PAYMENT-SIGNATURE` (x402 v2) or `X-PAYMENT` (x402 v1) header it is a real purchase: the payment is verified, the site is fetched and checked, the payment is settled only after the check succeeds, and the response carries the verdict and a link to the report. One payment buys one check, run now. To protect the site being checked, one site gets at most 3 checks per hour and 10 per day; over that, the request is refused before any payment is taken (site_rate_limited).",
      "example": "https://402registry.com/buy/one"
    },
    {
      "method": "GET",
      "path": "/buy/catalog",
      "summary": "The whole catalog as one JSON file for $0.005 (x402)",
      "description": "Every site in the catalog, rendered at purchase, for $0.005. Per site: the free fields plus the findings by name and severity (never the evidence or the fix), the stack, the page title, the content hash, the date and rules version of the report, when a model last judged it, and what the crawler saw without JavaScript. The basic fields are free at /registry.json. No parameters: site, monitor, catalog and notify are refused before any payment is taken.",
      "example": "https://402registry.com/buy/catalog"
    },
    {
      "method": "GET",
      "path": "/r/{id}",
      "summary": "Check report",
      "description": "The report for a paid check: findings, each with evidence quoted verbatim, and a fix for the detected stack, plus \"Since the previous check\" (sinceLast: fixed, new and still-open findings against the previous check you may see). HTML by default (for the human behind the agent); `?format=json` or `Accept: application/json` returns the same data as JSON. Never indexed by search engines.",
      "example": "https://402registry.com/r/00000000000000000000000000000000"
    },
    {
      "method": "GET",
      "path": "/api/check",
      "summary": "Free badge lookup",
      "description": "Looks up a host in the registry by URL. Free, cached, no payment. Unknown host is a normal 200 with `known:false`, not an error.",
      "example": "https://402registry.com/api/check?url=https://example.com/"
    },
    {
      "method": "GET",
      "path": "/badge/{host}",
      "summary": "Status badge (SVG)",
      "description": "An embeddable status badge. The path segment is the host plus a literal \".svg\" suffix, e.g. \"/badge/example.com.svg\". Free, cached, no payment.",
      "example": "https://402registry.com/badge/example.com.svg"
    }
  ],
  "rulesVersion": "2026-09-13",
  "rules": [
    {
      "id": "js_only_shell",
      "group": "invisible",
      "severity": "critical",
      "refuses": false,
      "fix": "Serve the main copy in the HTML itself, not only after JavaScript runs. Prerender or server-render the landing route."
    },
    {
      "id": "parked_domain",
      "group": "invisible",
      "severity": "critical",
      "refuses": true,
      "fix": "Point the domain at a real website — replace the registrar parking page or the for-sale lander with your own pages — and resubmit. If the domain is genuinely for sale or unused, there is nothing to list yet."
    },
    {
      "id": "redirect_other_site",
      "group": "invisible",
      "severity": "critical",
      "refuses": true,
      "fix": "Submit the destination address — the one the redirect lands on — so the check and the listing describe the site that actually answers. If this address is meant to be the site, serve the pages here instead of redirecting."
    },
    {
      "id": "empty_shell",
      "group": "invisible",
      "severity": "critical",
      "refuses": true,
      "fix": "Add a real <title>, a <meta name=\"description\">, and text in the HTML. Any one of the three is enough for the entry; all three are what a crawler needs."
    },
    {
      "id": "soft_404",
      "group": "invisible",
      "severity": "high",
      "refuses": false,
      "fix": "Return HTTP 404 for paths that do not exist, instead of serving the app shell with 200."
    },
    {
      "id": "www_apex_dead",
      "group": "invisible",
      "severity": "high",
      "refuses": false,
      "fix": "Point both the apex and the www hostname at your site, and 301 one to the other. In Cloudflare: a DNS record for both, plus a Redirect Rule."
    },
    {
      "id": "www_apex_duplicate",
      "group": "invisible",
      "severity": "high",
      "refuses": false,
      "fix": "Pick one hostname and 301 the other to it, keeping the path. Then make sure <link rel=\"canonical\"> points at the one you picked."
    },
    {
      "id": "sitemap_missing",
      "group": "invisible",
      "severity": "high",
      "refuses": false,
      "fix": "Publish /sitemap.xml listing your real routes, and add a \"Sitemap:\" line to robots.txt."
    },
    {
      "id": "sitemap_is_html",
      "group": "invisible",
      "severity": "high",
      "refuses": false,
      "fix": "Serve sitemap.xml as a real static file, before the SPA catch-all route sees the request."
    },
    {
      "id": "sitemap_foreign_urls",
      "group": "invisible",
      "severity": "high",
      "refuses": false,
      "fix": "List only URLs on this hostname. If the site really lives on another domain, publish the sitemap there instead."
    },
    {
      "id": "noindex",
      "group": "invisible",
      "severity": "critical",
      "refuses": false,
      "fix": "Remove noindex from the robots meta tag or the X-Robots-Tag header. It is usually left over from a staging setup."
    },
    {
      "id": "nosnippet",
      "group": "invisible",
      "severity": "medium",
      "refuses": false,
      "fix": "Remove nosnippet (or max-snippet:0) from the robots meta tag or the X-Robots-Tag header, unless hiding the text is deliberate."
    },
    {
      "id": "robots_blocks_search",
      "group": "invisible",
      "severity": "critical",
      "refuses": true,
      "fix": "Remove the \"Disallow: /\" line for * / Googlebot / Bingbot in robots.txt. It is almost always left over from a staging deployment."
    },
    {
      "id": "robots_is_html",
      "group": "invisible",
      "severity": "high",
      "refuses": false,
      "fix": "Serve robots.txt as a static text file, before the SPA catch-all route."
    },
    {
      "id": "redirect_chain",
      "group": "invisible",
      "severity": "medium",
      "refuses": false,
      "fix": "Collapse the chain into a single 301 that lands on the final URL directly — http→https and www→apex in one step, not two."
    },
    {
      "id": "no_jsonld",
      "group": "machines",
      "severity": "medium",
      "refuses": false,
      "fix": "Add a <script type=\"application/ld+json\"> block describing the page (Organization, WebSite, Product or Article schema, whichever fits)."
    },
    {
      "id": "jsonld_invalid",
      "group": "machines",
      "severity": "high",
      "refuses": false,
      "fix": "Validate every JSON-LD block (a trailing comma or an unescaped quote is enough to break it) with a JSON parser before shipping — schema.org's validator or a plain JSON.parse() in a build step both work."
    },
    {
      "id": "no_og",
      "group": "machines",
      "severity": "medium",
      "refuses": false,
      "fix": "Add og:title, og:description and og:image meta tags — most frameworks generate these from the same title/description you already have."
    },
    {
      "id": "og_image_template",
      "group": "machines",
      "severity": "high",
      "refuses": false,
      "fix": "Set og:image to an image hosted on your own domain instead of the page builder's default."
    },
    {
      "id": "og_image_relative",
      "group": "machines",
      "severity": "low",
      "refuses": false,
      "fix": "Set og:image to the full https:// address of the image, e.g. https://yourdomain.com/og.png."
    },
    {
      "id": "default_title",
      "group": "machines",
      "severity": "high",
      "refuses": false,
      "fix": "Set a real <title> that names your site or business, not the generator's placeholder."
    },
    {
      "id": "default_description",
      "group": "machines",
      "severity": "high",
      "refuses": false,
      "fix": "Write a one-sentence <meta name=\"description\"> that says what the site actually offers."
    },
    {
      "id": "no_description",
      "group": "machines",
      "severity": "medium",
      "refuses": false,
      "fix": "Add a <meta name=\"description\"> with one sentence describing the page."
    },
    {
      "id": "no_max_image_preview",
      "group": "machines",
      "severity": "medium",
      "refuses": false,
      "fix": "Add max-image-preview:large to your robots meta tag, e.g. <meta name=\"robots\" content=\"max-image-preview:large\">, in the root layout so it applies to every page."
    },
    {
      "id": "no_llms_txt",
      "group": "machines",
      "severity": "low",
      "refuses": false,
      "fix": "Publish a plain-text /llms.txt with a short description of the site and links to your most important pages."
    },
    {
      "id": "llms_txt_is_html",
      "group": "machines",
      "severity": "medium",
      "refuses": false,
      "fix": "Serve llms.txt as a real static text file, before the SPA catch-all route sees the request."
    },
    {
      "id": "no_lang",
      "group": "machines",
      "severity": "low",
      "refuses": false,
      "fix": "Add lang=\"en\" (or your content's actual language) to the <html> tag."
    },
    {
      "id": "ai_bots_blocked_edge",
      "group": "machines",
      "severity": "high",
      "refuses": false,
      "fix": "Check your firewall or hosting provider's bot rules for an entry that matches OAI-SearchBot or PerplexityBot, and allow them. Test with each crawler's complete user-agent string, not the bare name: some firewalls recognise only the full form."
    },
    {
      "id": "ai_training_blocked_edge",
      "group": "machines",
      "severity": "info",
      "refuses": false,
      "fix": "This is informational. If you want AI models to learn about your service, allow GPTBot and ClaudeBot in your firewall or hosting bot rules. If you do not want your content used for training, no change is needed."
    },
    {
      "id": "robots_blocks_ai",
      "group": "machines",
      "severity": "info",
      "refuses": false,
      "fix": "This is informational, not necessarily a problem. If you want AI systems such as ChatGPT, Claude or Perplexity to reference your content, remove the Disallow rule for the relevant agents in robots.txt; if you deliberately do not want your content used this way, no change is needed."
    },
    {
      "id": "catalog_optout",
      "group": "machines",
      "severity": "info",
      "refuses": false,
      "fix": "This is a choice, not a defect. Remove the Disallow rule for 402registry-check in robots.txt if you do want the site listed in the free catalog at https://402registry.com/registry."
    },
    {
      "id": "robots_no_sitemap",
      "group": "machines",
      "severity": "low",
      "refuses": false,
      "fix": "Add \"Sitemap: https://yourdomain.com/sitemap.xml\" as its own line in robots.txt."
    },
    {
      "id": "canonical_missing",
      "group": "machines",
      "severity": "low",
      "refuses": false,
      "fix": "Add <link rel=\"canonical\" href=\"https://yourdomain.com/the-current-path\"> pointing at itself with the final URL, on every page."
    },
    {
      "id": "canonical_other_domain",
      "group": "machines",
      "severity": "high",
      "refuses": false,
      "fix": "Point the canonical link at this same domain, or if another domain really is the canonical one, submit that domain to the registry instead."
    },
    {
      "id": "canonical_http",
      "group": "machines",
      "severity": "medium",
      "refuses": false,
      "fix": "Change the canonical href to the https:// version of the URL."
    },
    {
      "id": "no_h1_in_html",
      "group": "machines",
      "severity": "low",
      "refuses": false,
      "fix": "Give the page a single <h1> that states what it is, matching or close to the <title>."
    },
    {
      "id": "link_farm",
      "group": "spam",
      "severity": "high",
      "refuses": true,
      "fix": "Remove or nofollow bulk outgoing links that exist only to pass link equity elsewhere, and keep navigation focused on your own pages."
    },
    {
      "id": "keyword_stuffing",
      "group": "spam",
      "severity": "medium",
      "refuses": false,
      "fix": "Rewrite the repeated phrase in natural variations instead of repeating the exact keyword — say it once clearly and let synonyms carry the rest of the page."
    },
    {
      "id": "forbidden_vocabulary",
      "group": "spam",
      "severity": "critical",
      "refuses": true,
      "fix": "Remove the gambling, pharmacy or adult content from the page. This registry does not list this category of site, independent of any other check result."
    },
    {
      "id": "duplicate_content",
      "group": "spam",
      "severity": "critical",
      "refuses": true,
      "fix": "If the listed address is your own mirror, preview or old domain, make it redirect (301) to the address you want listed or point its rel=\"canonical\" there, and resubmit; the registry keeps one entry per site and re-checks free listings every 30 days. If the listed address is not yours, write to the abuse address in our manifest."
    },
    {
      "id": "spam_vocabulary_mention",
      "group": "spam",
      "severity": "info",
      "refuses": false,
      "fix": "Nothing to fix if the mention is incidental. A site whose primary purpose is gambling, unlicensed pharmacy or adult content is not listed regardless of this check."
    },
    {
      "id": "outbound_heavy",
      "group": "spam",
      "severity": "low",
      "refuses": false,
      "fix": "Keep your own description next to each outgoing link, and mark paid or unvetted links rel=\"nofollow\" or rel=\"sponsored\"."
    },
    {
      "id": "unreachable",
      "group": "technical",
      "severity": "critical",
      "refuses": true,
      "fix": "Confirm the domain resolves and the origin server is actually up — the address currently fails for a plain, unauthenticated request."
    },
    {
      "id": "not_https",
      "group": "technical",
      "severity": "critical",
      "refuses": true,
      "fix": "Get a TLS certificate for the domain and redirect all http:// requests to the https:// version."
    },
    {
      "id": "http_not_redirected",
      "group": "technical",
      "severity": "medium",
      "refuses": false,
      "fix": "Add a permanent redirect (301) from the http:// version of every URL to its https:// equivalent."
    },
    {
      "id": "http_serves_content",
      "group": "technical",
      "severity": "high",
      "refuses": false,
      "fix": "Configure the server to redirect or refuse plain http:// requests instead of serving the page over them."
    },
    {
      "id": "no_hsts",
      "group": "technical",
      "severity": "low",
      "refuses": false,
      "fix": "Add a \"Strict-Transport-Security: max-age=31536000; includeSubDomains\" response header."
    },
    {
      "id": "page_weight",
      "group": "technical",
      "severity": "high",
      "refuses": false,
      "fix": "Reduce inlined scripts/styles and move large embedded data out of the HTML document itself."
    },
    {
      "id": "imgs_without_alt",
      "group": "technical",
      "severity": "low",
      "refuses": false,
      "fix": "Add a descriptive alt attribute to every <img> that carries meaning; use alt=\"\" only for purely decorative images."
    },
    {
      "id": "no_viewport",
      "group": "technical",
      "severity": "medium",
      "refuses": false,
      "fix": "Add <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"> to <head>."
    },
    {
      "id": "no_favicon",
      "group": "technical",
      "severity": "low",
      "refuses": false,
      "fix": "Add a favicon.ico at the site root and declare it with <link rel=\"icon\" href=\"/favicon.ico\">."
    },
    {
      "id": "slow_ttfb",
      "group": "technical",
      "severity": "medium",
      "refuses": false,
      "fix": "Investigate what the server does before sending the first byte — cold starts, uncached database queries, or a slow origin are the usual causes."
    },
    {
      "id": "sitemap_invalid",
      "group": "technical",
      "severity": "high",
      "refuses": false,
      "fix": "Make sure the sitemap generator actually enumerates your routes into <loc> entries — an empty <urlset> usually means the route list it reads from is empty at build time."
    },
    {
      "id": "sitemap_dead_sample",
      "group": "technical",
      "severity": "medium",
      "refuses": false,
      "fix": "Regenerate the sitemap from your live route list, and remove or fix entries that no longer resolve."
    },
    {
      "id": "leaked_secret",
      "group": "technical",
      "severity": "critical",
      "refuses": false,
      "fix": "Revoke the key now, issue a new one, and keep it server-side (an environment variable read only by server code). Publishable keys (pk_, anon) are fine in the browser; secret ones never are."
    },
    {
      "id": "mixed_content",
      "group": "technical",
      "severity": "high",
      "refuses": false,
      "fix": "Reference every resource over https:// (or a relative path) — the same address with an s almost always works."
    },
    {
      "id": "server_version_disclosed",
      "group": "technical",
      "severity": "low",
      "refuses": false,
      "fix": "Remove or genericise the header — Nginx: server_tokens off; Apache: ServerTokens Prod; PHP: expose_php = Off; Express: app.disable('x-powered-by')."
    },
    {
      "id": "security_headers_missing",
      "group": "technical",
      "severity": "low",
      "refuses": false,
      "fix": "Send Content-Security-Policy, X-Frame-Options: SAMEORIGIN (or CSP frame-ancestors) and X-Content-Type-Options: nosniff with every page."
    },
    {
      "id": "session_cookie_unprotected",
      "group": "technical",
      "severity": "medium",
      "refuses": false,
      "fix": "Set session cookies with HttpOnly; Secure; SameSite=Lax (or Strict)."
    },
    {
      "id": "no_charset",
      "group": "technical",
      "severity": "low",
      "refuses": false,
      "fix": "Add <meta charset=\"utf-8\"> as the first element inside <head>, or send Content-Type: text/html; charset=utf-8."
    }
  ],
  "rulesNote": "Every check runs all of these, in this order, and the report names each one that fired with the evidence we saw. Rules marked refuses:true are hard stops: the report is still delivered in full, but the site is not listed. This list is generated from the code that runs, so it cannot drift from what actually happens.",
  "links": {
    "registry": "https://402registry.com/registry",
    "openapi": "https://402registry.com/openapi.json",
    "manifest": "https://402registry.com/.well-known/402registry.json",
    "sitemap": "https://402registry.com/sitemap.xml",
    "mcp": "https://402registry.com/mcp",
    "a2a_card": "https://402registry.com/.well-known/agent-card.json"
  },
  "abuseContact": "abuse@402registry.com"
}