Skip to content

feat(0.8.2): x402 buyer trust hardening — mandatory guardrails, cross-payment limits, CAIP-2 interop#243

Merged
Skobeltsyn merged 3 commits into
mainfrom
feat/0.8.2-x402-hardening
Jul 1, 2026
Merged

feat(0.8.2): x402 buyer trust hardening — mandatory guardrails, cross-payment limits, CAIP-2 interop#243
Skobeltsyn merged 3 commits into
mainfrom
feat/0.8.2-x402-hardening

Conversation

@Skobeltsyn

Copy link
Copy Markdown
Contributor

Hardens the x402 buyer path in response to an external audit that flagged the "guardrails-first" buyer as having optional guardrails. Three commits, all tickets closed, full suite + build + detekt green.

What changed

#4736 — buyer trust hardening (breaking, pre-1.0)

  • Policy is now mandatoryX402Account.fromPrivateKey no longer defaults an empty policy; an unbounded wallet must pass the explicitly-named X402SpendPolicy.unsafeAllowAllForTesting().
  • Stronger binding — allowedAssets (pin token), allowedResourceOrigins (pin endpoint origin), maxAuthorizationLifetimeSeconds (clamps signed validBefore so a seller's maxTimeoutSeconds can't mint a long-lived authorization).
  • Deterministic offer selection — X402OfferSelector; default LowestAmount pays the cheapest permitted offer so a seller can't order accepts[] to steer to the costliest. FirstAllowed restores prior behavior.

#4739 — cross-payment limits + signer seam

  • X402SessionLimits (maxPayments / maxTotalValue / maxPaymentsPerPayee / cooldownMillis) bounds the aggregate across many calls, recorded via X402SpendStore (default in-memory; back it durably in prod). Limit-exceeding payment raises X402PaymentDeniedException before any signature.
  • X402Signer seam — X402Account.fromSigner(signer, policy) signs through KMS/HSM/wallet-service/scoped session key, keeping permanent keys out of the app heap. fromPrivateKey now wraps LocalKeySigner.

#4740 — CAIP-2 network ids

  • Accepts CAIP-2 network identifiers (v2 interop step) with honest v1-compatible labeling.

Verification

  • ./gradlew test (all modules) — green
  • ./gradlew build (incl. detekt gate) — green
  • CHANGELOG updated; 15 new tests across the three tickets.

Refs #4526 (epic), #4528. Closes #4736, #4739, #4740.

🤖 Generated with Claude Code

Skobeltsyn and others added 3 commits June 26, 2026 20:12
…+ offer selector

Makes "guardrails-first" actually true (external audit F2/F3/F4).

- F2: X402Account.fromPrivateKey no longer defaults the policy — an unbounded
  wallet must pass the named X402SpendPolicy.unsafeAllowAllForTesting().
- F3: X402SpendPolicy gains allowedAssets (pin token), allowedResourceOrigins
  (pin endpoint scheme://host[:port]), and maxAuthorizationLifetimeSeconds (the
  signed validBefore is clamped to it, so a seller's maxTimeoutSeconds can't mint
  a long-lived authorization). A policy-approved recipient no longer implies any
  token, any URL, or any duration.
- F4: new X402OfferSelector (X402Client(account, selector=…)); default
  LowestAmount pays the cheapest permitted offer instead of the seller's first,
  so a seller can't order accepts[] to steer the buyer. FirstAllowed restores the
  prior behavior explicitly.

Breaking (pre-1.0): pass a real policy to fromPrivateKey. 6 new tests (37 x402
tests green). Docs: x402.md + CHANGELOG. Follow-up 0.8.2 slices: session/velocity
limits, X402Signer seam, module extraction, x402 v2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Continues the buyer hardening (audit F5 + the signer half of F6).

- F5: X402SessionLimits(maxPayments, maxTotalValue, maxPaymentsPerPayee,
  cooldownMillis) bounds the aggregate spend across calls — the per-payment
  policy only bounds one. X402Client(account, sessionLimits=…, spendStore=…)
  checks before any signature and records settled payments in an X402SpendStore
  (per-process InMemorySpendStore default; use a durable store in production so a
  restart can't reset a cumulative cap). Over-limit -> X402PaymentDeniedException.
- Signer seam: X402Signer interface + LocalKeySigner default; X402Account
  .fromSigner(signer, policy) signs through it instead of owning a raw key —
  enables KMS/HSM/scoped session keys, keeping permanent keys out of heap.
  fromPrivateKey now wraps a LocalKeySigner.

9 new tests (46 x402 tests green); detekt clean. Docs: x402.md + CHANGELOG.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-compatible labeling

Audit P0c = "add x402 v2". Verified the v2 spec (header rename + CAIP-2 +
x402Version:2), but the field-level v2 PAYMENT-REQUIRED/payload schema isn't in
the public launch post or Coinbase docs — and the audit cautioned against
reverse-engineering a spec. So ship the verifiable v2 piece, defer the rest:

- CAIP-2: X402Account resolves any EVM eip155:<chainId> network on an offer
  (e.g. eip155:84532), so the buyer can pay a v2 seller. Hermetically tested.
- Honest labeling: docs + CHANGELOG describe support as "experimental, x402
  v1-compatible (CAIP-2 accepted)", not "x402 v2". Full v2 transport (new headers
  + v2 payload envelope) deferred until the schema is verified against a live v2
  facilitator.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Skobeltsyn Skobeltsyn merged commit f9e930b into main Jul 1, 2026
3 of 4 checks passed
@Skobeltsyn Skobeltsyn deleted the feat/0.8.2-x402-hardening branch July 1, 2026 09:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant