AgenticWebScore
Scan

Scan API

Check agent readiness in CI

Fail a CI job when your site's agent readiness score drops below a number you set. The step calls the public JSON API with curl and reads the scan record. It needs no account and no key.

What the step does

The step requests GET /api/scan?target=<domain> and reads score, grade, level, rubricVersion, fingerprint, and blocked from the scan record. Only score decides the outcome, and the log prints the rest. The step ends in 4 ways, and each has its own exit code.

The outcomes of the step
The outcomes of the step
Outcome Exit code When
Passed0The score is at or above MIN_SCORE.
Below the threshold1The score is under MIN_SCORE. The log prints the score, grade, level, and rubric version.
Not measured2score is null. The scanner could not read the site, and blocked says why.
No result3The API answered with a status other than 200, or did not answer. A missing or invalid setting also ends here.

A site that refuses automated clients, or serves an empty page that needs JavaScript, is not measured. Its record has score: null, grade: "NA", and a blocked object with the reason and the HTTP status.

The step exits 2 for it, not 0 or 1. A score of 0 is a measurement, and not measured is not one. A scan that reaches its 60 s deadline is also not measured, so a slow site can end the job with 2.

Set the threshold

MIN_SCORE is a number, and a decimal works. The workflow uses 60, the lowest score of grade A (solid).

Set it below the score your site has today, not at it. A check that reads the clock, or a response slower than 12 s, can move the score with no change to your site. Raise the number as you ship fixes.

A new rubric version can move a score as well. The log line prints the rubric version, so you can tell a rubric change from a change to your site.

GitHub Actions workflow

Save this as .github/workflows/agent-readiness.yml, then change SITE and MIN_SCORE. There is no GitHub Action to install: the workflow is the whole integration.

.github/workflows/agent-readiness.yml
name: Agent readiness
on:
  schedule:
    - cron: "0 6 * * 1" # Mondays, 06:00 UTC
  workflow_dispatch:
permissions: {}
jobs:
  agent-readiness:
    runs-on: ubuntu-latest
    steps:
      - name: Check the score
        env:
          SITE: example.com
          MIN_SCORE: 60
          # Uncomment to scan again instead of reading a stored record:
          # FRESH: 1
        run: |
          # Fail the job when the agent readiness score of SITE is below MIN_SCORE.
          # Exit 0: at or above. Exit 1: below. Exit 2: not measured. Exit 3: no result.
          # Needs curl and jq. Set FRESH=1 to scan again instead of reading a stored record.
          set -euo pipefail

          for name in SITE MIN_SCORE; do
            if [ -z "${!name:-}" ]; then
              echo "$name is not set." >&2
              exit 3
            fi
          done
          if [[ ! "$MIN_SCORE" =~ ^[0-9]+([.][0-9]+)?$ ]]; then
            echo "MIN_SCORE must be a number." >&2
            exit 3
          fi
          for tool in curl jq; do
            if ! command -v "$tool" > /dev/null; then
              echo "$tool is not installed." >&2
              exit 3
            fi
          done

          API_URL="${API_URL:-https://agenticwebscore.com/api/scan}"
          body=$(mktemp)
          headers=$(mktemp)
          trap 'rm -f "$body" "$headers"' EXIT
          query=(--data-urlencode "target=$SITE")
          if [ "${FRESH:-}" = 1 ]; then
            query+=(--data-urlencode "fresh=1")
          fi

          # A scan may run for 60 s, so curl waits for twice that.
          curl_options=(
            --silent --show-error --get
            --max-time 120
            --dump-header "$headers" --output "$body"
            --write-out "%{http_code}"
          )
          # Prints the HTTP status, or 000 when nothing answered.
          ask() {
            curl "${curl_options[@]}" "${query[@]}" "$API_URL" || true
          }

          status=$(ask)
          if [ "$status" = 429 ] || [ "$status" = 503 ]; then
            pause=$(grep -i "^retry-after:" "$headers" | cut -d: -f2 | tr -d "[:space:]" || true)
            if [[ "$pause" =~ ^[0-9]+$ ]]; then
              echo "HTTP $status. Waiting $pause s, as Retry-After says, then asking once more."
              sleep "$pause"
              status=$(ask)
            fi
          fi

          if [ "$status" != 200 ]; then
            what="answered HTTP $status"
            if [ "$status" = 000 ]; then what="did not answer"; fi
            message=$(jq -r '.message // empty' "$body" 2> /dev/null || true)
            echo "No result: the API $what. $message" >&2
            exit 3
          fi

          fields='[.score, .grade, .level, .rubricVersion, .fingerprint] | map(tostring) | join(" ")'
          read -r score grade level rubric fingerprint < <(jq -r "$fields" "$body" 2> /dev/null) || {
            echo "No result: the answer is not a scan record." >&2
            exit 3
          }
          report="${API_URL%/api/scan}/aws/$SITE"

          if [ "$score" = null ]; then
            reason=$(jq -r '.blocked.reason // "unknown"' "$body")
            detail=$(jq -r '.blocked.detail // "no detail given"' "$body")
            echo "Not measured ($reason): $detail"
            echo "A site that is not measured has no score, so it is neither above nor below $MIN_SCORE."
            echo "Report: $report"
            exit 2
          fi

          echo "Score $score, grade $grade, level L$level, rubric v$rubric, fingerprint $fingerprint."
          echo "Report: $report"
          if jq -e --arg min "$MIN_SCORE" '.score >= ($min | tonumber)' "$body" > /dev/null; then
            echo "At or above $MIN_SCORE."
          else
            echo "Below $MIN_SCORE." >&2
            exit 1
          fi

The job reads no repository and uses no token, so it has no permissions and no checkout step. jq is preinstalled on GitHub's ubuntu-latest runners, so the job installs nothing.

The scan measures the site as deployed, not the commit that started the run. To check a release, put the step in a job that lists your deploy job under needs, and set FRESH to 1.

When to scan again

A request for a domain returns the stored record when one is under 6 hours old. After a deploy, that record can describe the old version.

Set FRESH to 1 to scan again; the API reads it as fresh=1. A new scan asks your site for up to 136 URLs and counts against the limit of 10 scans per client per minute. A stored record costs neither.

Any other CI

The run: block above is a plain shell script. Save it as check-agent-readiness.sh and run it with bash in any CI that can run a shell. It needs curl and jq.

check-agent-readiness.sh
#!/usr/bin/env bash
# Fail the job when the agent readiness score of SITE is below MIN_SCORE.
# Exit 0: at or above. Exit 1: below. Exit 2: not measured. Exit 3: no result.
# Needs curl and jq. Set FRESH=1 to scan again instead of reading a stored record.
set -euo pipefail

for name in SITE MIN_SCORE; do
  if [ -z "${!name:-}" ]; then
    echo "$name is not set." >&2
    exit 3
  fi
done
if [[ ! "$MIN_SCORE" =~ ^[0-9]+([.][0-9]+)?$ ]]; then
  echo "MIN_SCORE must be a number." >&2
  exit 3
fi
for tool in curl jq; do
  if ! command -v "$tool" > /dev/null; then
    echo "$tool is not installed." >&2
    exit 3
  fi
done

API_URL="${API_URL:-https://agenticwebscore.com/api/scan}"
body=$(mktemp)
headers=$(mktemp)
trap 'rm -f "$body" "$headers"' EXIT
query=(--data-urlencode "target=$SITE")
if [ "${FRESH:-}" = 1 ]; then
  query+=(--data-urlencode "fresh=1")
fi

# A scan may run for 60 s, so curl waits for twice that.
curl_options=(
  --silent --show-error --get
  --max-time 120
  --dump-header "$headers" --output "$body"
  --write-out "%{http_code}"
)
# Prints the HTTP status, or 000 when nothing answered.
ask() {
  curl "${curl_options[@]}" "${query[@]}" "$API_URL" || true
}

status=$(ask)
if [ "$status" = 429 ] || [ "$status" = 503 ]; then
  pause=$(grep -i "^retry-after:" "$headers" | cut -d: -f2 | tr -d "[:space:]" || true)
  if [[ "$pause" =~ ^[0-9]+$ ]]; then
    echo "HTTP $status. Waiting $pause s, as Retry-After says, then asking once more."
    sleep "$pause"
    status=$(ask)
  fi
fi

if [ "$status" != 200 ]; then
  what="answered HTTP $status"
  if [ "$status" = 000 ]; then what="did not answer"; fi
  message=$(jq -r '.message // empty' "$body" 2> /dev/null || true)
  echo "No result: the API $what. $message" >&2
  exit 3
fi

fields='[.score, .grade, .level, .rubricVersion, .fingerprint] | map(tostring) | join(" ")'
read -r score grade level rubric fingerprint < <(jq -r "$fields" "$body" 2> /dev/null) || {
  echo "No result: the answer is not a scan record." >&2
  exit 3
}
report="${API_URL%/api/scan}/aws/$SITE"

if [ "$score" = null ]; then
  reason=$(jq -r '.blocked.reason // "unknown"' "$body")
  detail=$(jq -r '.blocked.detail // "no detail given"' "$body")
  echo "Not measured ($reason): $detail"
  echo "A site that is not measured has no score, so it is neither above nor below $MIN_SCORE."
  echo "Report: $report"
  exit 2
fi

echo "Score $score, grade $grade, level L$level, rubric v$rubric, fingerprint $fingerprint."
echo "Report: $report"
if jq -e --arg min "$MIN_SCORE" '.score >= ($min | tonumber)' "$body" > /dev/null; then
  echo "At or above $MIN_SCORE."
else
  echo "Below $MIN_SCORE." >&2
  exit 1
fi

Run it as SITE=example.com MIN_SCORE=60 bash check-agent-readiness.sh. The script reads these settings from the environment.

The settings of the script
The settings of the script
Variable Meaning Default
SITEThe domain to scan, as a host name such as example.com.Required.
MIN_SCOREThe lowest score that passes.Required.
FRESH1 scans again instead of reading a stored record.A stored record under 6 hours old is read.
API_URLThe scan endpoint.https://agenticwebscore.com/api/scan

What the API answers

The step reads the HTTP status first. This table lists each status it handles.

The statuses the step handles
The statuses the step handles
HTTP status Meaning The step
200A scan record, stored or new. score is null when the site is not measured.Compares score with MIN_SCORE, or exits 2.
400The target is not a usable domain, or the scanner refuses it, as it refuses a private address.Exits 3 and prints the message.
403The host is on the removal list, so the scanner does not scan it.Exits 3 and prints the message.
429Over the limit of 10 scans per client per minute, or 3 scans are already running. Retry-After gives the wait in seconds.Waits that long, asks once more, then exits 3.
502The scan failed inside the service.Exits 3 and prints the message.
503The service that holds the records did not answer. Retry-After gives the wait in seconds.Waits that long, asks once more, then exits 3.

The limit counts new scans per client address, so jobs that leave from the same address share one budget.

After the run

Each run prints a link to the report at /aws/<domain>. It lists every failed check with the change to make, and every request the scan made. The fingerprint in the log names the record, and /verify/<fingerprint> recomputes its score.

An agent can make the same call through the MCP server at /mcp: its scan_site tool returns this scan record.