# Payment Proof Adapter: x402

> Part of [PEAC Adapters](./README.md) | Profile: `peac-x402-offer-receipt/0.2`

x402 payment flow verification and PEAC interaction record generation. This adapter transforms x402 signed offers and receipts into canonical PEAC records.

**Package:** `@peac/adapter-x402`
**Layer:** 4 (Adapters)
**Profile:** `peac-x402-offer-receipt/0.2`
**Spec:** [X402-PROFILE.md](../specs/X402-PROFILE.md)
**Status:** Implemented

## Overview

x402 is a payment protocol built on HTTP 402. It provides signed offers (payment terms) and signed receipts (settlement proofs). This adapter provides the verification, normalization, and evidence layer above x402.

| Layer                   | Owner | Responsibility                              |
| ----------------------- | ----- | ------------------------------------------- |
| Payment Handshake       | x402  | Signed offers, signed receipts, settlement  |
| Verification + Evidence | PEAC  | Term-matching, normalization, audit records |

## Mapping Table

x402 proofs are mapped to a PEAC interaction record (`X402PeacRecord`):

| x402 Source                   | PEAC Record Field         | Notes                                  |
| ----------------------------- | ------------------------- | -------------------------------------- |
| `offer.payload.resourceUrl`   | `evidence.resourceUrl`    | Resource binding (signed, required)    |
| `offer.payload.validUntil`    | `evidence.validUntil`     | Epoch seconds; absent if EIP-712 `0`   |
| `offer.payload.network`       | `evidence.network`        | CAIP-2 identifier (signed)             |
| `offer.payload.payTo`         | `evidence.payee`          | Recipient (signed, neutral name)       |
| `offer.payload.asset`         | `evidence.asset`          | Payment asset (signed)                 |
| `offer.payload.amount`        | `evidence.amount`         | Minor units (signed)                   |
| `offer.payload.version`       | `evidence.offerVersion`   | Schema version (`number`, signed)      |
| `receipt.payload.payer`       | `evidence.payer`          | Who paid (signed)                      |
| `receipt.payload.issuedAt`    | `evidence.issuedAt`       | When receipt was issued (signed)       |
| `receipt.payload.transaction` | `evidence.transaction`    | Optional; absent if EIP-712 `""`       |
| `receipt.payload.version`     | `evidence.receiptVersion` | Schema version (`number`, signed)      |
| `offer.acceptIndex`           | `hints.acceptIndex.value` | UNSIGNED, per-offer, `untrusted: true` |
| Full offer                    | `proofs.x402.offer`       | Exact raw artifact, never mutated      |
| Full receipt                  | `proofs.x402.receipt`     | Exact raw artifact, never mutated      |

## What this adapter enforces (and does not)

The x402 Offer/Receipt extension defines a payment handshake. PEAC adds an
evidence and verification layer above that handshake. The boundary is
intentional and load-bearing for audit reasoning.

| What this adapter validates                                                      | Where                                                     |
| -------------------------------------------------------------------------------- | --------------------------------------------------------- |
| Wire shape and required fields (discriminated union, required strings, bounds)   | [`verify.ts`](../../packages/adapters/x402/src/verify.ts) |
| Term-matching: `network`, `asset`, `payTo`, `amount`, `scheme` (string equality) | `matchAcceptTerms` at `verify.ts:750-776`                 |
| Offer-receipt consistency (`resourceUrl`, `network`, time window)                | `verifyConsistency` in `verify.ts`                        |
| Receipt recency and clock-skew tolerance                                         | `verifyReceipt` in `verify.ts`                            |
| Opt-in cryptographic verification (EIP-712 / JWS format)                         | Layer C callers; adapter exposes crypto hooks             |

| What this adapter does not validate                                                           | Where this lives                               |
| --------------------------------------------------------------------------------------------- | ---------------------------------------------- |
| On-chain settlement finality                                                                  | Facilitator and chain                          |
| Scheme-specific invariants (for example, `upto` single-use, time bounds, facilitator binding) | x402 scheme layer                              |
| Max-vs-actual delta audit (`upto` authorized maximum vs actual charged amount at settlement)  | x402 scheme layer and downstream audit tooling |
| Recipient binding as an on-chain check                                                        | Facilitator and chain                          |
| Payer identity verification beyond the signature on the receipt                               | Out of scope for the adapter                   |

The adapter is **scheme-agnostic at the verification layer**: `scheme` is
term-matched as a required string but never interpreted. The `scheme`
identifier is preserved verbatim in the raw artifact at
`proofs.x402.offer`, so downstream auditors can reason about phase semantics
(for example, `upto` authorized maximum vs actual charged) at review time.

See [`docs/specs/X402-PROFILE.md § 3.0`](../specs/X402-PROFILE.md) for the
normative statement and [`x402 Scheme Coverage in PEAC`](../compatibility/x402-scheme-coverage.md)
for the current compatibility matrix.

## Threat Model

### acceptIndex: unsigned envelope field

`acceptIndex` is outside the x402 signed payload. It can be modified in
transit without invalidating the signature. Verifiers MUST NOT use it as a
binding mechanism.

**Verification rule:**

1. If `acceptIndex` is present, bounds-check it, then term-match the
   entry at `accepts[acceptIndex]` against the signed payload fields.
   Reject on mismatch.
2. If absent, scan all `accepts[]` for a unique match. If ambiguous,
   reject.
3. Store `acceptIndex` in `hints.acceptIndex` with `untrusted: true`
   for audit purposes only.

### Expiry handling

`validUntil` is inside the signed payload (epoch seconds). The adapter
checks `validUntil > (now - clockSkew)` with a default tolerance of 60
seconds. Expired offers are rejected.

### Ambiguity rejection

When `acceptIndex` is absent and multiple accept entries match the offer
payload, the adapter rejects with `accept_ambiguous`. This forces callers
to provide a disambiguating index rather than silently picking one.

## Verification Flow

```
Offer arrives
  |
  +--> Structural validation (required fields, types)
  +--> Version check (supported versions)
  +--> Expiry check (validUntil vs now)
  +--> Signature format check (EIP-712 hex or JWS compact)
  +--> Term-matching:
       |
       +-- acceptIndex present?
       |     |
       |     +--> Bounds check
       |     +--> Term-match accepts[i] vs payload
       |     +--> Reject on mismatch
       |
       +-- acceptIndex absent?
             |
             +--> Scan all accepts
             +--> Require exactly 1 match
             +--> Reject if 0 or 2+ matches
```

## Error Codes

| Code                          | HTTP | Description                                      |
| ----------------------------- | ---- | ------------------------------------------------ |
| `offer_invalid_format`        | 400  | Offer structure is malformed                     |
| `offer_expired`               | 400  | `validUntil` is in the past                      |
| `offer_no_expiry`             | 400  | No expiry and `offerExpiryPolicy: 'require'`     |
| `offer_version_unsupported`   | 400  | Version not in supported list                    |
| `offer_signature_invalid`     | 401  | Signature format is structurally invalid         |
| `receipt_invalid_format`      | 400  | Receipt structure is malformed                   |
| `receipt_signature_invalid`   | 401  | Receipt signature format is structurally invalid |
| `receipt_version_unsupported` | 400  | Receipt version not in supported list            |
| `receipt_resource_mismatch`   | 400  | Receipt resourceUrl does not match offer         |
| `receipt_network_mismatch`    | 400  | Receipt network does not match offer             |
| `receipt_issuedAt_stale`      | 400  | Receipt issuedAt outside validity window         |
| `receipt_payer_invalid`       | 400  | Receipt payer format is invalid                  |
| `accept_index_out_of_range`   | 400  | acceptIndex exceeds accepts array bounds         |
| `accept_no_match`             | 400  | No accept entry matches signed payload           |
| `accept_ambiguous`            | 400  | Multiple entries match; index needed             |
| `accept_term_mismatch`        | 400  | acceptIndex entry does not match payload         |
| `accept_too_many_entries`     | 400  | Accepts array exceeds MAX_ACCEPT_ENTRIES         |
| `payload_missing_field`       | 400  | Required payload field is missing                |
| `payload_tampered`            | 401  | RESERVED: Cryptographic integrity check failed   |
| `amount_invalid`              | 400  | Amount is not a valid non-negative integer       |
| `network_invalid`             | 400  | Network does not match CAIP-2 format             |
| `jws_too_large`               | 400  | Compact JWS exceeds size limit (64 KiB)          |
| `jws_malformed`               | 400  | Compact JWS does not have exactly 3 segments     |
| `jws_padded_base64url`        | 400  | Compact JWS contains padding characters          |
| `jws_payload_not_object`      | 400  | JWS payload is not a JSON object                 |

See the [X402 Profile Spec](../specs/X402-PROFILE.md) for complete error taxonomy and verification algorithm.

## Cryptographic Verification

This adapter does NOT perform cryptographic signature verification
(EIP-712 ecrecover, JWS validation). That is the caller's responsibility
before passing artifacts to this adapter. The adapter focuses on
term-matching and record mapping: the verification layer that makes
unsigned `acceptIndex` irrelevant for security.

## Related Packages

| Package                | Layer | Purpose                                                                           |
| ---------------------- | ----- | --------------------------------------------------------------------------------- |
| `@peac/adapter-x402`   | 4     | **This package** - Offer/receipt verification, term-matching, PEAC record mapping |
| `@peac/rails-x402`     | 4     | Payment rail - x402 HTTP header extraction and payment normalization              |
| `@peac/adapter-x402-*` | 4     | Service-specific adapters (daydreams, fluora, pinata)                             |

The distinction:

- **Rail** (`@peac/rails-x402`): Extracts payment information from x402 HTTP headers (`Payment-Required`, `Payment-Response`) and normalizes to PEAC's `NormalizedPayment` format. Used for record issuance.
- **Adapter** (`@peac/adapter-x402`): Verifies signed x402 offer/receipt artifacts, performs term-matching, and maps to PEAC records with verification status. Used for proof verification and audit.
