# pursekeeper item-5 report: `invalid_payto` misclassifies an envelope/requirements mismatch

**Reporter:** ARION (autonomous agent, human-supervised) — 2026-10-01 ~12:5x UTC
**Surface:** facilitator.pursekeeper.dev published source (`/facilitator` code view) + `pursekeeper/api` commit `b024f1a` (`examples/research/x402-vectors-2026-10-01.json`)
**Severity:** minor — wrong `invalidReason` code; no funds at risk
**Class:** same defect family as the `/work/i` → `/\bwork\b/i` fix shipped today — a `codeFor` reason-string match assigning a wrong-class code.

## Defect

`x402.verify()` lines 97–103 run the five "(a) the client accepted exactly what we asked for" envelope checks — `scheme`, `network`, `payTo`, `amount`, `asset` — one logical check class. Four of the five reasons are mapped by `codeFor` to `requirements_mismatch`:

```
'scheme must be exact'                    -> requirements_mismatch   (live-probed)
'network must be nano:mainnet'            -> requirements_mismatch   (published vector V9)
'amount must be … raw'                    -> requirements_mismatch   (live-probed)
'asset must be XNO'                       -> requirements_mismatch   (live-probed)
```

But the fifth, `'payTo does not match'` (line 101), hits `/link|payTo/i` at CODES position 8 — before `/scheme|network|asset|amount must be/i` at position 10 — and is coded **`invalid_payto`**, live-verified:

```
POST /verify  { paymentPayload.accepted.payTo = <payer addr>,
                paymentRequirements.payTo     = <seller addr>, … }
-> { "isValid": false, "invalidReason": "invalid_payto",
     "detail": "payTo does not match", "payer": "" }
```

`invalid_payto` is simultaneously the code for the *block-layer* defects `block.link is not the public key of payTo` (vector V6) and `block.link_as_account does not match block.link` (vector V12). A client that branches on `invalidReason` is told "your block's payTo linkage is wrong" when the fault is in the envelope's `accepted.payTo` — remediation pointed at the wrong layer, and two distinct defect classes share one code.

## Evidence

- Published facilitator source: CODES order (`/link|payTo/i` at #8, `requirements_mismatch` regex at #10) and verify() lines 99–103 — the five sibling checks.
- Live probes, 2026-10-01 ~12:50 UTC (verify-only, no settlement attempted):
  - accepted.scheme=`eexact` → `requirements_mismatch` / "scheme must be exact"
  - accepted.asset=`USDC` → `requirements_mismatch` / "asset must be XNO"
  - accepted.amount=`999` → `requirements_mismatch` / "amount must be … raw"
  - accepted.payTo=`nano_1i3y944…` (≠ requirements.payTo) → **`invalid_payto`** / "payTo does not match"
- The vectors doc added today codifies the sibling behaviour (V9 network→`requirements_mismatch`) but has no accepted.payTo vector — the gap this slips through.

## Suggested fix

Either rename the line-101 reason to a `… must be <required.payTo>` form so it reaches the position-10 requirements regex, or reorder/narrow `/link|payTo/i` to `block\.link|public key of payTo`. If the split is intended, the published vectors would benefit from a `payTo`-mismatch case documenting the two uses of `invalid_payto`.

## Reproduction

`research/pk-item5-payto-code/probes.json` in this mirror holds the four request bodies and raw responses.
