AXRAY
Sign inCreate account

Developers

CLI and JSON API

The same scoring engine in three places: this website, your terminal and your pipeline.

How do I run a scan from the terminal?

Run it with npx — no install, no account, no key. It is published on npm as axray-cli and scores against the same engine as this site.

npx axray-cli example.com
npx axray-cli example.com/pricing --verbose
npx axray-cli example.com --probe          # also fetch as real AI crawler user-agents
npx axray-cli example.com --crawl 50       # crawl the site
npx axray-cli example.com --json > ax.json

Can I scan a site before it is public?

Yes, on your own machine, with --allow-private. By default the scanner refuses private and loopback addresses — that is the server-side request forgery defence, and on this hosted service it can never be switched off. Locally the rule is backwards, because reaching your own dev server is not forgery, so the CLI can opt out per invocation:

npx axray-cli localhost:3000 --allow-private

It relaxes the address and port rules. It does not relax the scheme rule.

Can I see exactly what the model would read?

--context prints the page rebuilt as Markdown and nothing else, so it can be redirected into a file or piped straight into whatever is going to answer questions about the page. It is the same reconstruction the report shows, from the same parse the score came from.

npx axray-cli example.com --context > page.md

# what a model is given, and what that costs it, in one line
npx axray-cli example.com --json | jq '.agentView | {tokens, contentTokens, chars}'

Every result carries an agentView object: the Markdown, its length, an estimated token count for the whole page and for the page’s own content with the site chrome excluded. The token figures are estimates and the method and its error bars are published.

How do I fail a build when the score drops?

--min exits non-zero below the threshold, which is all a CI step needs.

# .github/workflows/ax.yml
- name: Agent Experience check
  run: npx axray-cli "${{ env.DEPLOY_URL }}" --min 70

An absolute threshold is the version most teams abandon, though: set it low and it never fires, set it high and it blocks work on the day it is added. The gate people keep is the one on the direction — save the result of a green build, and fail the next one only if the score fell.

npx axray-cli "$DEPLOY_URL" --json > ax.json          # on main, commit this
npx axray-cli "$DEPLOY_URL" --baseline ax.json       # on a pull request
npx axray-cli "$DEPLOY_URL" --baseline ax.json --max-drop 2

What does that look like in a GitHub workflow?

Two output formats exist for the two places a CI result is read. --format github emits workflow annotations, so each finding appears on the checks tab rather than inside a step somebody has to expand. --format markdown emits a report shaped for a pull request comment, and --summary <file> writes that Markdown wherever you want it — on GitHub, usually the job summary.

- name: Agent Experience
  run: |
    npx axray-cli "$DEPLOY_URL" \
      --format github \
      --baseline ax-baseline.json --max-drop 2 \
      --summary "$GITHUB_STEP_SUMMARY"
  env:
    DEPLOY_URL: https://staging.example.com

That is the whole integration, and it needs nothing installed and no action to trust. There is also a composite action in the AXRAY repository that wraps exactly these commands and adds a pull-request comment it keeps up to date rather than repeating; it is not published to the Marketplace yet, so the four lines above are the supported way to do this today.

The same gate through the API, if you would rather not install anything:

score=$(curl -s https://axray.online/api/v1/scan \
  -H "authorization: Bearer $AXRAY_API_KEY" \
  -H "content-type: application/json" \
  -d "$(jq -nc --arg u "$DEPLOY_URL" '{url:$u}')" | jq .score)
[ "$score" -ge 70 ] || exit 1

Is there a version of the report I can just work through?

Yes: /report/<id>/fix-pack.md, linked from every report that has anything to fix. It is the same findings as the report, with every snippet the scan produced, but grouped by the file you have to open rather than by the pillar they score in — robots.txt, the root files, the <head>, the structured data, the markup, the writing, the server. Inside each section they are ordered by how many points they recover.

The report is ordered for deciding whether to act; working down it means opening the same file four times. Nothing in the fix pack is invented — where a snippet contains a placeholder it is the scanner’s own placeholder, left visible. AXRAY will not write a description of your business for you, because that is the one thing on the page designed to be quoted verbatim by machines.

curl -sO https://axray.online/report/<id>/fix-pack.md

Can my assistant run the scan itself?

Yes. https://axray.online/mcp is a live Model Context Protocol server — JSON-RPC over a single HTTP POST, no session to keep — so you can add AXRAY to Claude, ChatGPT or your own agent and ask “is my pricing page readable by an AI agent?” wherever you are already working, and get the real scan instead of a guess.

{
  "mcpServers": {
    "axray": {
      "type": "http",
      "url": "https://axray.online/mcp"
    }
  }
}

Four tools. Only the first one costs anything:

  • scan_url — a real fetch and a full score, against your daily allowance.
  • explain_check — what one check in the rubric means and what it is worth.
  • list_crawlers — every AI crawler, its robots.txt token and what blocking it costs.
  • get_index — the public AX Index.

Anonymous callers get the free daily allowance, counted against a hashed IP address. Send Authorization: Bearer <your API key> to use your plan’s allowance instead. It is the same allowance, the same engine and the same rate limit as the JSON API — a second way to authenticate would be a second way to get authentication wrong.

Calling it by hand is two lines, which is worth knowing before you wire it into anything:

curl -s https://axray.online/mcp -H "content-type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
       "params":{"name":"scan_url","arguments":{"url":"example.com"}}}'

This exists partly because we were failing our own advice. The rubric gives six points for publishing an agent-callable manifest, and for months ours described tools that nothing could call. A manifest with no server behind it is exactly the defect this scanner looks for.

How do I get told when the score drops?

Put a page on a schedule from your dashboard and give it a threshold. AXRAY re-scans it daily or weekly and POSTs to your webhook when the verdict changes — when it falls below the threshold, and again when it comes back. A page that stays broken for a month sends one notification, not thirty. Scheduled checks start on Pro; the first check runs within a few minutes of adding one and sets the baseline without alerting.

POST https://hooks.example.com/axray
content-type: application/json
x-axray-event: ax.score.below
x-axray-timestamp: 1772000000000
x-axray-signature: sha256=<hex>

{
  "event": "ax.score.below",
  "monitorId": "K3f9x2Qa7B",
  "url": "https://example.com/pricing",
  "host": "example.com",
  "score": 61,
  "previousScore": 84,
  "threshold": 70,
  "grade": "C",
  "reportUrl": "https://axray.online/report/8pq463qhB4",
  "scannedAt": "2026-09-03T04:11:22.031Z",
  "topFindings": [
    { "id": "jsonld-types", "title": "Ships structured data", "impact": 6.2 }
  ]
}

The alert that is not about the score

A page can start telling agents something untrue without its score moving far enough to trip any threshold. A price changes in the template and not in the JSON-LD. A product sells out and the structured data still says InStock. Neither costs many points, and nobody finds out until a customer arrives expecting the other number — so it is a separate event, sent the moment a contradiction appears that was not there on the previous run.

x-axray-event: ax.contradiction.found

{
  "event": "ax.contradiction.found",
  "monitorId": "K3f9x2Qa7B",
  "url": "https://example.com/pricing",
  "host": "example.com",
  "score": 88,
  "grade": "A",
  "newContradictions": [
    {
      "id": "price-mismatch",
      "field": "price",
      "kind": "conflict",
      "structured": "29 EUR",
      "visible": "39.00"
    }
  ],
  "reportUrl": "https://axray.online/report/8pq463qhB4",
  "scannedAt": "2026-09-06T04:11:22.031Z"
}

Only contradictions that are new fire it, which is the same rule the score alert follows: a page that has disagreed with itself for a month sends one notification, not thirty. A run can send both events, because they answer different questions — is this page getting worse for agents, and is it now saying something untrue — and a receiver that wired up one of them should not silently lose the other.

Verifying the signature

Each monitor has its own secret, shown beside it on the dashboard, so one leaked secret cannot speak for the rest of the account. The signature is an HMAC-SHA256 over the timestamp, a colon and the raw body — the timestamp is inside the signed material rather than beside it, so a captured body cannot be replayed later with a fresh timestamp attached. Compare in constant time and reject anything older than a few minutes.

import { createHmac, timingSafeEqual } from 'node:crypto';

function verify(rawBody, headers, secret) {
  const ts = Number(headers['x-axray-timestamp']);
  if (!Number.isFinite(ts) || Math.abs(Date.now() - ts) > 5 * 60 * 1000) return false;
  const expected = createHmac('sha256', secret).update(ts + ':' + rawBody).digest('hex');
  const got = String(headers['x-axray-signature']).replace(/^sha256=/, '');
  const a = Buffer.from(expected, 'hex');
  const b = Buffer.from(got, 'hex');
  return a.length === b.length && timingSafeEqual(a, b);
}

Webhooks must be https, are not followed through redirects, and time out after eight seconds. Whatever happened to the last delivery is printed next to the monitor, including the status code if your endpoint refused it. A scheduled check is an ordinary scan of that one page: it obeys your robots.txt, the same per-host hourly limit as everything else here, and it stops entirely if you opt out.

What can I call from the JSON API?

Available on Team and Scale. Authenticate with a bearer token from your dashboard.

POST /api/v1/scan

curl -s https://axray.online/api/v1/scan \
  -H "authorization: Bearer $AXRAY_API_KEY" \
  -H "content-type: application/json" \
  -d '{"url":"example.com","probeAgents":false}'

Returns the complete ScanResult: score, grade, gates, pillars, every check and the ordered fix list.

GET /api/v1/report/:id

Fetch a previously produced report as JSON. Public reports need no key.

POST /api/v1/crawl

curl -s https://axray.online/api/v1/crawl \
  -H "authorization: Bearer $AXRAY_API_KEY" \
  -H "content-type: application/json" \
  -d '{"origin":"example.com","maxPages":50}'

Returns a crawl id immediately. Poll GET /api/v1/crawl/:id until status is done.

GET /api/v1/index

The public AX Index: best known score per host. No key required.

Errors and limits

  • 400 — the URL is malformed, private or unsupported. The message says which.
  • 401 — missing or unknown API key.
  • 402 — your plan does not include this endpoint.
  • 429 — quota exhausted. retry_after tells you when the window resets.

Machine-readable schema: /openapi.json.

How often will AXRAY fetch my site?

At most 30 times an hour, across every AXRAY user combined. Every scan is a request at somebody else's origin, so the limits below exist to protect the site being scanned rather than us:

  • Per target host. One host is fetched at most 30 times an hour across every AXRAY user combined, signed in or not. A crawl counts once per page. Over the line you get 429 target_busy with a retry_after.
  • Repeat requests are served from cache. The same URL asked for twice within 10 minutes is answered from the result we already have, with "cached": true in the response. It does not count against your quota and it does not touch the target.
  • We identify ourselves. Every request carries Mozilla/5.0 (compatible; AXRAY/1.0; +https://axray.online/bot) AgentExperienceScanner, and that URL explains what we are.
  • Crawls obey your robots.txt. Paths disallowed to AXRAY are not fetched, and a Crawl-delay is honoured.

How do I stop AXRAY scanning my site?

If you would rather AXRAY did not fetch your site at all, say so in the file you already use for this. Add to /robots.txt:

User-agent: AXRAY
Disallow: /

From the next scan onwards, any request for that host is refused with 403 robots_opt_out — for everybody, not only for you, and with no account or email to us needed. Narrower rules work too: Disallow: /admin under the same user-agent excludes just those paths. If you need something removed that has already been scanned, write to hello@axray.online.

A wildcard User-agent: * rule is deliberately not treated as an opt-out. It is how sites turn away crawlers that index and train on them, and if we obeyed it we could never produce the finding that matters most — that your robots.txt is turning away the answer engines. Naming AXRAY is unambiguous, and it is two lines.

Can I call it as a library?

Yes. The package exports the scanner itself, so you can score a URL inside your own code.

import { scan, describeSpec } from 'axray-cli';

const result = await scan('example.com');
console.log(result.score, result.priorities[0].title);

Zero runtime dependencies. Node 22.6 or newer. describeSpec() returns the whole rubric as data, which is the same call that generates the specification page.