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_aftertells 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_busywith aretry_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": truein 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
AXRAYare not fetched, and aCrawl-delayis 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.