Skip to content

Take x402 2.23.0, keep limits the only spend ceiling, cut 0.4.12 - #92

Merged
vvillait88 merged 1 commit into
mainfrom
x402-2.23-spend-controls
Aug 21, 2026
Merged

Take x402 2.23.0, keep limits the only spend ceiling, cut 0.4.12#92
vvillait88 merged 1 commit into
mainfrom
x402-2.23-spend-controls

Conversation

@vvillait88

Copy link
Copy Markdown
Contributor

Summary

@x402/core, @x402/evm and @x402/fetch move 2.21.0 to 2.23.0 as a set, plus
@solana/kit 7.1.1 and vitest / @vitest/coverage-v8 4.1.11.

2.23.0 needed handling rather than a straight bump. It turns client-side spend
controls on by default:

  • a fresh x402Client carries spendControls = {}, not false
  • applySpendControls only early-returns on === false
  • an omitted maxAmountPerPayment falls back to DEFAULT_MAX_AMOUNT_PER_PAYMENT, which is "$1"
  • assets are additionally restricted to a default allowlist

Confirmed at runtime, not read off the source:

DEFAULT_MAX_AMOUNT_PER_PAYMENT = "$1"
fresh client spendControls     = {}      -> enforced by default: true
after setSpendControls(false)  = false   -> disabled: true

pay already owns that decision through limits (per-call, daily, per-merchant),
enforced in the onBeforePaymentCreation hook a few lines below the change. Two
ceilings means the lower one wins, and this one loses quietly: it filters the
payment requirements out instead of raising pay's structured limit verdict, so an
over-$1 payment would surface to the user as "no acceptable payment requirements"
rather than a message naming the cap they actually set.

So the client disables x402's controls and limits stays the single authority.
Behavior is unchanged from 0.4.11, which is why this is a patch release.

Worked with: Varun.

Type of change

  • Bug fix (no breaking change)
  • New feature (no breaking change)
  • Breaking change (existing callers must update)
  • Docs, tests, or internal maintenance only

Public API

None. No CLI flag, command, output shape or exit code changes. The only source
edit is one setSpendControls(false) call plus its comment.

The x402 set was scoped before taking it, since it sits on a wire contract:

  • @x402/core exported surface 2.21.0 to 2.23.0: zero removals, 8 additions
  • @x402/evm (the signing path): zero removals, 9 additions
  • @x402/fetch type surface: byte-identical
  • protocol-governed field counts (maxAmountRequired, x402Version, decimals)
    increase only
  • live 402 challenges from two storefronts still carry extra keys
    ["name", "version"] and no paymentFlow

Test plan

New file tests/x402-spend-controls.test.ts, three cases:

  1. a positive control asserting upstream still enables spend controls by
    default, which is what keeps case 3 meaningful. If upstream ever ships them
    off by default, this fails and the disable becomes redundant rather than
    load-bearing.
  2. setSpendControls(false) actually turns them off
  3. pay's source calls it, so the guard cannot be dropped silently

Proved the guard fires: deleted the setSpendControls(false) line, watched
case 3 go red, then restored from a copy taken beforehand and re-ran green.

Full gate set on this branch:

  • bun run typecheck clean
  • bun run lint clean (eslint, max-warnings 0)
  • bun run knip clean
  • bun run test: 57 files passed, 1 skipped; 573 tests passed, 2 skipped
  • bun run build: ESM build success, dist/index.js 251.54 KB
  • osv-scanner over bun.lock: no issues found across 723 packages

Checklist

  • Tests cover the new behavior, and the suite passes locally
  • Lint, format, and type checks pass
  • Docs and README examples updated if the public surface changed
    (no public surface change; the CLI is identical to 0.4.11)
  • No secrets, credentials, or personal data in the diff or the tests

Deliberately not done

  • The @agent-score/sdk floor stays ^2.7.5 rather than moving to the freshly
    published ^2.7.6. Nothing here needs it and the existing range accepts it.
  • mppx 0.8.18 and viem 2.55.19 are NOT taken. They are the coupled settle set
    with a standing live all-rails settle obligation, which cannot be discharged
    from this branch, so they move as their own deliberate change.
  • incur 0.5.1 is not taken: it is gated on re-cutting patches/incur@<version>.patch
    and running the serveCli protocol against the compiled binary.
  • No tag pushed. The publish workflow fires on v* and an npm version is
    permanent, so the tag is a deliberate step after this merges.

@x402/core, @x402/evm and @x402/fetch move 2.21.0 to 2.23.0 as a set, plus
@solana/kit 7.1.1 and vitest 4.1.11.

2.23.0 turns client-side spend controls on by default and that needed handling
rather than a straight bump. A fresh x402Client carries spendControls = {} rather
than false, applySpendControls only early-returns on === false, and an omitted
maxAmountPerPayment falls back to DEFAULT_MAX_AMOUNT_PER_PAYMENT ("$1"). It also
restricts assets to a default allowlist.

pay already owns that decision through `limits` (per-call, daily, per-merchant),
enforced in the onBeforePaymentCreation hook. Two ceilings means the lower one
wins, and this one loses quietly: it filters the payment requirements out instead
of raising pay's structured limit verdict, so an over-$1 payment would surface as
"no acceptable payment requirements" rather than a message naming the cap the
user actually set. So the client disables x402's controls and `limits` stays the
single authority. Behavior is unchanged from 0.4.11.

The new test pins both halves. Its first case is a positive control asserting the
upstream default is still enforcing, which is what keeps the second case
meaningful: if upstream ever ships controls off by default, that case fails and
the disable is redundant rather than load-bearing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@vvillait88
vvillait88 merged commit bcc1aae into main Aug 21, 2026
6 checks passed
@vvillait88
vvillait88 deleted the x402-2.23-spend-controls branch August 21, 2026 08:51
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