Skip to content

x402: Sepolia E2E harness + client↔server contract test + LOW review nits - #48

Merged
douglasgan merged 2 commits into
mainfrom
x402-e2e-and-low-nits
Aug 26, 2026
Merged

douglasgan merged 2 commits into
mainfrom
x402-e2e-and-low-nits

Conversation

@douglasgan

Copy link
Copy Markdown
Contributor

Follow-up to #47. Delivers the live-testnet E2E verification the fail-closed payment gate needs before any mainnet flip, and cleans up the three LOW cosmetic items from the third adversarial review.

Why

Every payment check in the service is fail-closed, so the 92 unit tests prove the rejections work but not that a real payment can succeed. Only a live testnet run proves the EIP-712 USDC domain is configured right (the LOW-1 footgun: a X402_USDC_NAME/_VERSION mismatch silently rejects every payment). This PR makes that run one command, and de-risks its most fragile assumption in CI with zero infra.

E2E tooling

  • scripts/x402-client.mjs — minimal buyer-side EIP-3009 signer + X-PAYMENT header builder, matching the server's parser/verifier exactly. Refuses to sign for a from it doesn't hold the key to.
  • scripts/sepolia-e2e.mjs (npm run e2e:sepolia) — env-gated live harness against a running live-mode service + self-hosted facilitator on Ethereum Sepolia:
    • Proof 1 — a genuine payment SUCCEEDS end-to-end (catches the domain footgun)
    • Proof 2 — delivery gated on ≥ X402_CONFIRMATION_DEPTH
    • Proof 4 — forged-from rejected against a real facilitator
    • Missing config exits 2 with instructions; never invents keys or funds anything.
  • test/e2e-client-contract.test.ts — CI-side de-risking of proof 1 with no infra: a buyer-signed payment round-trips through the server's own parsePaymentAuthorization + verifyTransferAuthorization, and a wrong-domain signature is rejected. Catches client/server crypto drift before it becomes silent live-payment rejection.
  • E2E-SEPOLIA.md — full runbook incl. Proof 3 (cross-payment txHash replay + durable-ledger restart persistence, facilitator/restart-orchestrated) and the pre-mainnet sign-off checklist.

LOW nits (third adversarial review)

  • LOW-1env.ts prints a loud [x402] WARNING when a live non-base network runs the on-chain confirm with X402_USDC_NAME/_VERSION left at Base defaults.
  • LOW-2 — removed the dead confirmPaymentOnChain poll (no callers); fixed the stale checkOnce doc comment.
  • LOW-3 — simplified the eip3009 signature-length guard; documented that it rejects EIP-2098 compact sigs (known limitation, safe direction).

Verification

  • npx tsc --noEmit ✓ · npx vitest run92/92 (89 + 3 new contract tests) · npm audit --omit=dev → 0 vulns · biome check --changed clean

I could not run the live Sepolia E2E from here — it needs operator infra (a funded Sepolia payer key, a deployed facilitator, an RPC) and I won't handle private keys or fund wallets. This PR makes it runnable in one command; the contract test verifies the crypto contract in CI in the meantime.

🤖 Generated with Claude Code

douglasgan and others added 2 commits August 26, 2026 02:03
…nits

Follow-up to PR #47. Adds the live-testnet verification the fail-closed
payment gate needs (a green real payment is the only proof the EIP-712 USDC
domain is right), plus the three LOW cosmetic items from the third review.

E2E tooling (proofs from the round-3 review's "what Sepolia must prove"):
- scripts/x402-client.mjs: minimal buyer-side EIP-3009 signer + X-PAYMENT
  header builder, matching the server's parser/verifier exactly. Refuses to
  sign for a `from` it doesn't hold the key to.
- scripts/sepolia-e2e.mjs (`npm run e2e:sepolia`): env-gated live harness —
  proof 1 (genuine payment SUCCEEDS end-to-end, catches the LOW-1 domain
  footgun), proof 2 (delivery gated on >= confirmationDepth), proof 4
  (forged-`from` rejected against a real facilitator). Missing config exits 2
  with instructions; never invents keys or funds anything.
- test/e2e-client-contract.test.ts: CI-side de-risking of proof 1 with ZERO
  infra — a buyer-signed payment round-trips through the server's own
  parsePaymentAuthorization + verifyTransferAuthorization, and a wrong-domain
  signature is rejected (fail-closed). Catches client/server crypto drift
  before it becomes silent live-payment rejection.
- E2E-SEPOLIA.md: full runbook incl. proof 3 (cross-payment txHash replay +
  durable-ledger restart persistence, facilitator/restart-orchestrated) and
  the pre-mainnet sign-off checklist.

LOW nits from the third adversarial review:
- LOW-1: env.ts now prints a loud [x402] WARNING when a live non-base network
  runs the on-chain confirm with X402_USDC_NAME/_VERSION left at Base defaults
  — a domain mismatch silently rejects every payment (fail-closed footgun).
- LOW-2: removed the dead confirmPaymentOnChain poll (no callers); fixed the
  stale checkOnce doc comment.
- LOW-3: simplified the eip3009 signature-length guard and documented that it
  rejects EIP-2098 compact sigs (known limitation, safe direction).

92/92 tests pass (89 + 3 new contract tests); tsc + prod audit clean; biome
check clean. README "Remaining gate" points at the new tooling.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
gitleaks' generic-api-key rule fires on an assignment whose identifier
contains "key" plus a high-entropy value. The const held the well-known
public Anvil account-0 key (same one eip3009.test.ts uses as `PAYER`, which
passed because that identifier has no secret keyword). Renaming the const
clears the false positive without changing the (public, non-sensitive) test
key. Verified locally: gitleaks detect --no-git → "no leaks found".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@douglasgan
douglasgan merged commit ccc41ad into main Aug 26, 2026
4 checks passed
@douglasgan douglasgan added the rate-level-2-tier Moderate: multi-file mechanical / tooling + tests label Aug 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

rate-level-2-tier Moderate: multi-file mechanical / tooling + tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant