Artifact Machine

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.

POST 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

One object per domain, in rows.
FieldTypeWhat it is
domainstringThe domain, normalised.
measuredbooleanCheck this first. False means we could not read the site, and every score is null.
reasonstringWhy we could not read it. Null when we could.
searchReadiness0–100Meta completeness, heading structure, structured data.
answerEngineReadiness0–100Whether the page states answers a machine can lift.
aiCitability0–100Depth and structure of the content itself.
gapnumberSearch minus answer-engine. Can be negative.
qualifiedbooleanSearch readiness ≥ 50 and a gap ≥ 50.
verdictstringOne of qualified, modest-gap, low-investment, no-gap.
summarystringThe 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.

POST 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.