---
name: aisa-cdn-verify
description: >
  Act as the CDN/edge gateway (Alibaba ESA role) and exercise the AISA access-decision
  endpoint POST /api/v1/access/verify. Use for gateway-side integration testing: verifying
  free/paid/blocked rule decisions, price modes, the 12 documented return values, charge
  correctness, and crawler identification. This is the GATEWAY perspective — for crawling
  paid content as a bot, use the aisa-bot-access skill instead.
version: 1.0.0
license: MIT
metadata: {"openclaw":{"emoji":"🛡️","homepage":"https://esa.aisa.one/cdn/docs.html","primaryEnv":"ESA_GATEWAY_TOKEN","requires":{"anyBins":["curl","python3"]}}}
---

# AISA CDN Verify — test the access-decision node as the gateway

You are the **edge gateway** in the AISA pay-per-crawl architecture. In production this role is
played by Alibaba ESA: it intercepts a crawler's request to a publisher site, calls AISA's verify
endpoint **before going back to origin**, and serves or blocks based on the answer. This skill lets
you play that gateway to test the verify node's decisions and billing.

Do not confuse the two perspectives:
- **Gateway (this skill)**: calls `/api/v1/access/verify` server-to-server, holds the `ESA_GATEWAY_TOKEN`.
- **Bot (aisa-bot-access skill)**: never calls verify; just sends `X-AISA-Crawler-Token` to content sites.

## Environment

| Variable | Required | Description |
|---|---|---|
| `ESA_GATEWAY_TOKEN` | yes (when enforcement on) | Server-to-server token proving you are the gateway. Issued out-of-band by AISA, per environment. |
| `API_BASE` | no | Default `https://esa.aisapay.org` (new-dev direct entry, no CDN challenge — preferred for programmatic calls; same backend as `esa.aisa.one`). Production: `https://cdn.aisa.one`. |

## The call

```bash
curl -s -X POST "$API_BASE/api/v1/access/verify" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ESA_GATEWAY_TOKEN" \
  -H "X-AISA-Crawler-Token: $CRAWLER_TOKEN" \
  -H "X-ESA-Original-Crawler-Agent: Mozilla/5.0 (compatible; GPTBot/1.0)" \
  -d '{
    "publisherDomain": "<the Host the crawler requested>",
    "resourceUrl": "/research/some-report.data.json",
    "crawlerAutoPrice": true,
    "crawlerAgent": "Mozilla/5.0 (compatible; GPTBot/1.0)"
  }'
```

Semantics to preserve when simulating ESA:
- `publisherDomain` = the request **Host** (subdomains attribute to their parent publisher).
- Forward the crawler's `X-AISA-Crawler-Token` **verbatim**; omit the header entirely if the crawler sent none.
- Always send `X-ESA-Original-Crawler-Agent` with the original crawler UA (gateways rewrite User-Agent).
- `Authorization: Bearer` is ONLY for the gateway token — never put a crawler token there (that yields 401, not 402).
- Strip nothing from the path; verify itself strips `?query`/`#fragment` before rule matching.

## Reading the decision

Minimal integration: `accessGranted: true` → serve content (fetch origin); `false` → return
`statusCode` + the JSON body to the crawler **unmodified** (the body's `guidance` block is how new
crawlers learn to register and pay — do not swallow it).

Rich fields: `error`, `price`, `currency`, `charged`, `chargeAmount`, `crawler{balance,shortfall}`,
`rule{urlPattern,accessMode,price}`, `crawlerIdentification{recognized,identifier,verified,signed}`.

## Price modes (body, mutually exclusive)

| Mode | Field | Behavior |
|---|---|---|
| Auto pay (recommended) | `"crawlerAutoPrice": true` | Charge the publisher's price if balance covers it |
| Price discovery | *(none)* | 402 `price_required` + `price` quote, no charge |
| Exact | `"crawlerExactPrice": "USD 0.10"` | Charge only on exact match, else 402 `price_mismatch` |
| Budget cap | `"crawlerMaxPrice": "USD 1.00"` | Charge if price ≤ cap, else 402 `price_too_high` |

## The 12 return values (full coverage checklist)

| # | status / error | trigger |
|---|---|---|
| 1 | 200 (allow rule) | URL matching an allow rule, no token needed |
| 2 | 200 (no rule → default allow) | unmatched URL |
| 3 | 200 charged | pay rule + valid token + autoPrice + balance |
| 4 | 402 `invalid_token` | pay rule, no/unknown token |
| 5 | 402 `insufficient_balance` | pay rule, valid token, low balance |
| 6 | 402 `price_required` | pay rule, no price intent |
| 7 | 402 `price_mismatch` | exact price ≠ rule price |
| 8 | 402 `price_too_high` | max budget < rule price |
| 9 | 400 `invalid_price_format` | e.g. `"crawlerExactPrice": "notaprice"` |
| 10 | 403 `access_blocked` | block rule (token/balance irrelevant) |
| 11 | 401 `gateway_unauthorized` | wrong/missing gateway Bearer token |
| 12 | 500 `internal_error` | internal fault (documented, not black-box triggerable) |

Reference matrix with assertions: `$API_BASE/cdn/cdntest.html`. Ready-made full-chain script:
`cdn-agent/esa-pay-test.sh` (`--existing` non-destructive / `--auto-config` self-configuring).
Debug what the gateway actually sent vs got: `cdn-agent/esa-audit.sh [n] [grep]`.

## Test fixtures (new-dev / esa.aisa.one)

- Crawler accounts: `<type>@test.com` password `Test8888` (amazonbot/chatgpt/perplexitybot/...);
  top up via `POST /api/agent/balance/add {"crawlerId":<id>,"amount":100}` (test env only, no auth).
- Publishers: `test@test.com` (prefix rules: /premium/ pay $0.10, /free/ allow, /blocked/ block) and
  the AgentFlare regex publishers (`lr-pay-crawler*.alicdn-test.com`, 8 rules: 6 pay + guides allow + members block).

## Reporting (mandatory)

For every verify call, report: request essentials (domain, URL, price mode, token present?),
then `statusCode` / `error` / `accessGranted`, the charge (`chargeAmount` and the crawler's balance
delta when you can check it), and `crawlerIdentification`. For test batches, end with a summary
table: calls, grants, denials by error code, total charged.
