# aptu preflight rubric v1

The public grading rubric for `POST /v1/x402-preflight` ($0.05 USDC, Base + Solana). This page is the single published source for what the paid check grades and how the score is computed; the check table below is drift-guarded against the exported `CHECKS` constant in `workers/x402/src/x402-preflight.ts` by `src/lib/x402-preflight-drift.test.ts` (no codegen pipeline: CI fails if the doc and the implementation diverge).

## Scope

A seller-side, point-in-time protocol conformance check for one deployed x402 resource URL. It answers "can agents safely pay you"; the sibling `POST /v1/agent-readiness` answers "can agents find you". The paid call buys execution, evidence, and the remedy loop, not secrecy. Continuous monitoring, uptime history, on-chain settlement analytics, trust scoring, and paid-path delivery verification are out of scope. v1 asserts conformance of the payment challenge; paid-path error semantics (400-vs-402 mapping, `errorReason` taxonomy) are unchecked.

## Response shape

```json
{
  "url": "https://seller.example.com/v1/paid",
  "probedResource": "https://seller.example.com/v1/paid",
  "probedFacilitator": null,
  "rubricVersion": "aptu-preflight-1",
  "score": 96,
  "grade": "A",
  "summary": { "fails": 0, "warns": 1 },
  "findings": [
    {
      "check": "manifest_dual_timestamp",
      "pass": false,
      "severity": "warn",
      "detail": "Manifest carries 'updated' but not 'updated_at'.",
      "ms": 12,
      "hint": "Publish both 'updated' and 'updated_at' on the manifest.",
      "specRef": null
    }
  ]
}
```

`severity` is always present; `hint` and `specRef` are explicit `null` when not applicable, never absent fields. Agents branch on `severity`, not on absence. `specRef` is `null` for ecosystem-convention checks: the well-known manifest is NOT a V2-spec conformance surface (spec section 8 discovery is facilitator-side `GET /discovery/resources`), so manifest checks never cite the spec.

## Capture precedence

- The base64 `PAYMENT-REQUIRED` header is canonical (HTTP transport spec); the response body is fallback.
- Both present and divergent is itself a finding (`capture_divergence`, warn): the canonical header wins.
- Probe requests carry a cache-buster query parameter.
- `Age` / `CF-Cache-Status` hints on the captured 402 produce the `cache_freshness` warn: an edge-cached challenge is a false positive for the timeout and replay checks.
- Responses truncated at the 512 KB cap are reported as `response_truncated` (warn), never as malformed or invalid: large honest OpenAPI documents must not earn a bogus failure.

## Resource selection

Deterministic: an explicit `resource` URL in the request body wins; otherwise the first advertised `https://` resource in the manifest; otherwise the request `url` itself. The response echoes `probedResource` and `probedFacilitator` (null when the facilitator could not be probed) so findings are anchored.

## Check table

| ID | Severity | Weight | Spec ref | Description |
| --- | --- | --- | --- | --- |
| `challenge_present` | fail | 20 | x402-v2 5.1.2 | An unpaid request to the probed resource answers 402 with a PAYMENT-REQUIRED header. |
| `header_base64` | fail | 10 | transports/http | The PAYMENT-REQUIRED header decodes as base64/base64url JSON. |
| `schema_envelope` | fail | 20 | x402-v2 5.1.2 | The 402 envelope validates against @x402/core PaymentRequiredV2Schema (the validation real buyers run). |
| `x402_version` | fail | 10 | x402-v2 5.1.2 | x402Version is exactly 2. |
| `pay_to` | fail | 10 | x402-v2 5.1.2 | Every accepts[] entry carries a non-empty payTo address. |
| `max_timeout_sane` | fail | 5 | x402-v2 5.1.2 | maxTimeoutSeconds is a positive integer within a usable range (schema-clean 0/negative is practically unusable). |
| `amount_string` | fail | 5 | x402-v2 5.1.2 | Amounts are strings, not numbers. |
| `payment_flow` | fail | 10 | x402-v2 6.1 | accepts[].extra.paymentFlow is present when the resolved flow is not authorization. |
| `network_known` | fail | 10 | x402-v2 6.1 | accepts[].network resolves against the known-chain CAIP-2 registry (typo'd or unknown chain IDs fail). |
| `asset_unrecognized` | warn | 2 | null | accepts[].asset matches a known token contract for the declared network (advisory: registries drift). |
| `signable_scheme` | fail | 20 | x402-v2 6.1 | At least one accepts[] entry is signable by the pinned reference-client scheme set; zero signable entries is the silent-skip failure mode. |
| `facilitator_supported` | fail | 15 | x402-v2 7 | Every advertised accepts[] kind is supported by the declared facilitator GET /supported. |
| `facilitator_unverifiable` | warn | 3 | x402-v2 7 | The facilitator could be probed; undeclared or undiscoverable facilitators warn (grade A does not assert facilitator reachability). |
| `capture_divergence` | warn | 2 | transports/http | PAYMENT-REQUIRED header and body agree when both are present (header is canonical). |
| `response_truncated` | warn | 1 | null | The captured response was truncated at the 512KB cap (never misreported as malformed). |
| `cache_freshness` | warn | 1 | null | The captured 402 shows no edge-cache hints (Age / CF-Cache-Status); a cached challenge is a false positive for timeout and replay checks. |
| `manifest_present` | warn | 2 | null | /.well-known/x402 is present, valid JSON, and carries a version (ecosystem convention; spec 8 discovery is facilitator-side). |
| `manifest_dual_timestamp` | warn | 1 | null | The manifest carries both updated and updated_at timestamps. |
| `manifest_facilitator_shape` | warn | 1 | null | The manifest facilitator is an object {name, default}, not a bare string. |
| `manifest_challenge_match` | warn | 2 | null | Manifest top-level payTo/network/asset agree with the challenge accepts[] entries (case-insensitive; amounts never compared). |
| `manifest_stale` | warn | 1 | null | The manifest timestamp is fresh (older than 90 days warns). |
| `schemes_extra` | warn | 1 | null | Declared schemes beyond exact are intentional (informational). |
| `resource_url_match` | warn | 1 | null | The probed resource URL matches the request URL origin. |
| `openapi_payment_headers` | warn | 2 | null | The advertised OpenAPI document declares PAYMENT-REQUIRED on 402s and PAYMENT-RESPONSE on 200s. |
| `replay_shape` | warn | 1 | null | Two identical unpaid requests return the same challenge shape (dynamic nonces legitimately trigger this; best-effort, never a sole fail). |
| `sep_schema_drift` | warn | 1 | null | The manifest $schema URL resolves (a moved SEP warns, never fails). |

## Score and grades

Deterministic weighted score, no LLM in the scoring path: the score is `round(100 * earned / total)` where `earned` sums the weights of passing checks. Fails carry high weights (5-20), warns 1-3. Letter grades exist for human display only: `A` requires score >= 90 AND zero fails, `B` >= 75, `C` >= 50, else `D`. Grade A is attainable with advisory warnings present; every fail and warn carries a remedy specific enough to implement without reading the spec.

Grade-A-with-warns semantics: when the facilitator is undeclared or undiscoverable, `facilitator_unverifiable` warns and grade A does NOT assert facilitator reachability. Nothing the schema or the spec's normative sections require appears in the warn tier.

### Pinned reference-client set

The mechanism-reachability signable set is pinned inside `rubricVersion` `aptu-preflight-1`: schemes registered in `@x402/evm` 2.29.0 + `@x402/svm` 2.29.0 (`exact`). Fail-tier verdicts stay reproducible across SDK releases because the set is pinned, and this drift guard covers semantic drift of the rubric itself, not just the check table.

## Remediation loop

The intended usage is check, remediate, recheck (each recheck $0.05) until grade A, on the deployed production URL. Local development is covered by the free OSS CLIs and this public rubric.

## SSRF and probe hardening posture

Every probe response body is hard-capped at 512 KB (Content-Length pre-check plus a reader-loop byte counter in the shared `fetchText`). All outbound fetches (manifest, resource, facilitator `/supported`, SEP `$schema` resolution, OpenAPI) go through one hardened https-only fetch: https on every hop including redirects, at most 3 redirect hops, 15 s timeout per request, Web Bot Auth-signed per hop with fail-open signing. Cloudflare Workers egress does not route to private (RFC1918/loopback/link-local) ranges; the residual SSRF risk is redirects, which the per-hop https pin closes. The facilitator `kinds` array is capped at 50 entries before matching (defensive parsing of attacker-controllable input).

## When to use a CLI instead

The paid call buys hosted execution, evidence, and the remedy loop; for local iteration these free OSS tools are excellent and we link them deliberately:

- [x402-lint](https://github.com/holistis/x402-lint): CLI, free hosted MCP tool `check_x402_conformance`, and library; validates against `@x402/core`'s client-side `PaymentRequiredSchema`.
- [atsp-protocol/x402-conformance](https://github.com/atsp-protocol/x402-conformance): GitHub Action + PyPI + npm fault-injection conformance checks.
- [moped2110/x402-conformance](https://github.com/moped2110/x402-conformance): black-box CLI with check IDs, severity, and spec references, tracking upstream on a two-week cadence.
- [x402-seller-testkit](https://github.com/Bortlesboat/x402-seller-testkit): regression harness for seller deployments.

## Second-tier follow-ups (documented, not v1 blockers)

- Error-mapping probe: garbage `PAYMENT-SIGNATURE`, assert 400-vs-402 per the transport spec error table.
- Offer/receipt extension validation when emitted (EIP-712 domain / JWS shape).
- `errorReason` taxonomy discipline (declared codes from the spec section 9 registry).
- Staging-auth header support for authenticated targets.
- Settlement verification: gate on a settled receipt first, then run a dual-RPC on-chain proof; recover the payer identity from the Transfer event's `from` argument, never from `tx.from` (the facilitator submits settlement, so the transaction sender is not the buyer).

The empirical basis for the `manifest_challenge_match` cross-check is documented evidence of manifest/challenge settlement-identity divergence (Zenodo DOI [10.5281/zenodo.24024416](https://doi.org/10.5281/zenodo.24024416)) and the buyer-verification blog post describing the failure mode.