One call. One row per domain.
Send a list of domains, get back how ready each one is for search, how ready it is for answer engines, and the gap between them. No account, no key, no signup. If it speaks HTTP, it can use this.
https://artifact-machine-scan.vercel.app/api/batch
Try it right now
This works as-is. Paste it into a terminal.
curl -s -X POST https://artifact-machine-scan.vercel.app/api/batch \
-H 'Content-Type: application/json' \
-d '{"domains": ["basecamp.com", "linear.app"]}'
Which returns:
{
"ok": true,
"requested": 2,
"measured": 2,
"qualified": 1,
"rows": [
{
"domain": "basecamp.com",
"measured": true,
"reason": null,
"searchReadiness": 100,
"answerEngineReadiness": 23,
"aiCitability": 61,
"gap": 77,
"qualified": true,
"verdict": "qualified",
"summary": "Search readiness 100%, answer-engine readiness 23% — a 77-point gap on a site that has clearly invested in search."
}
]
}
What comes back
| Field | Type | What it is |
|---|---|---|
domain | string | The domain, normalised. |
measured | boolean | Check this first. False means we could not read the site, and every score is null. |
reason | string | Why we could not read it. Null when we could. |
searchReadiness | 0–100 | Meta completeness, heading structure, structured data. |
answerEngineReadiness | 0–100 | Whether the page states answers a machine can lift. |
aiCitability | 0–100 | Depth and structure of the content itself. |
gap | number | Search minus answer-engine. Can be negative. |
qualified | boolean | Search readiness ≥ 50 and a gap ≥ 50. |
verdict | string | One of qualified, modest-gap, low-investment, no-gap. |
summary | string | The finding in a sentence you could send to someone. |
Domains we could not read
About one site in four in a cold list cannot be read: dead domains,
timeouts, and bot protection. Those come back with
measured: false, a reason, and
every score null — never a zero.
That is deliberate. A zero is a measurement, and nothing was measured. In
a sheet of ten thousand rows a fabricated zero is worse than a missing
one, because it looks plausible and nobody audits it. Filter on
measured and you always know which rows are real.
Limits
- Domains per request
- 10. Each one is a real browser page load. Ask for more and the request is refused with the number — it is never silently truncated, because a shortened list looks exactly like a complete one.
- Rate
- Roughly 10 requests per minute per address, with a shared ceiling on top. A 429 tells you when to come back.
- Speed
- About two to four seconds per domain.
- Cost
- Free, and no key. If you need volume beyond this, get in touch rather than looping it.
- What it reads
- The homepage only, in a real browser, once per request.
Wiring it into the tools people actually use
All of these speak HTTP, so none of them needs anything installed or approved. Each takes about ten minutes.
Clay
Add an HTTP API enrichment to a table that already has a domain column.
Method POST
URL https://artifact-machine-scan.vercel.app/api/batch
Headers Content-Type: application/json
Body {"domains": ["{{Domain}}"]}
Map rows[0].gap and rows[0].qualified into
columns. One domain per row keeps you inside the per-request limit and
lets Clay handle its own concurrency.
n8n
An HTTP Request node, batching ten at a time.
Method: POST
URL: https://artifact-machine-scan.vercel.app/api/batch
Send Body: JSON
Body: {"domains": {{ $json.domains }}}
Put a Split In Batches node in front with a batch size
of 10, and an Item Lists node after it to split
rows back out into one item per domain.
Make or Zapier
A Webhooks → Make a request module (Make) or Webhooks by Zapier → POST (Zapier). Same URL, same JSON body. Both parse the response without extra configuration.
A spreadsheet
Google Apps Script, for when the list already lives in Sheets.
function gap(domain) {
const res = UrlFetchApp.fetch(
'https://artifact-machine-scan.vercel.app/api/batch',
{ method: 'post', contentType: 'application/json',
payload: JSON.stringify({ domains: [domain] }) });
const row = JSON.parse(res.getContentText()).rows[0];
return row.measured ? row.gap : row.reason;
}
Then =gap(A2). It returns the reason instead of a number
when a site could not be read, so a blank cell never gets mistaken for a
zero.
One domain at a time
There is a single-site endpoint too, with more detail in the response — the individual findings and the answer-engine breakdown.
https://artifact-machine-scan.vercel.app/api/scan
curl -s -X POST https://artifact-machine-scan.vercel.app/api/scan \
-H 'Content-Type: application/json' \
-d '{"url": "example.com"}'
Where the numbers come from
The same scanner and the same scoring produced the 57-company study, so a number here means what it means there. You can reproduce any row in it through this endpoint.
Worth knowing before you build on it: across 3,000 randomly sampled domains, the gap correlates 0.84 with search readiness alone. It is a genuinely different ranking — about half the top 200 differs — but it is not an independent signal, and anyone telling you otherwise is overselling it.