Skip to content

demo(payments): add a cumulative spend budget to the policy guard #138

Description

@mehmetkr-31

Context

#97 added a policy-before-signing guard to the payments demo, and closed with an explicit caveat in both the code and the README:

The amount check is an illustrative per-transaction cap, not a real spend control. A per-transaction limit is trivially defeated by splitting one payment into many smaller ones (cap × N). A production policy needs a cumulative and/or rate-limited budget (e.g. per-payer spend over a rolling window), not just a single-transaction threshold.

demos/payments/src/payment-policy.ts carries the same note. So the demo currently shows the right enforcement boundary (policy runs before execution or signing) with a check that does not actually bound spend.

Proposal

Close that gap inside demos/payments, without touching the protocol or the published packages:

  • A small in-memory ledger recording what a payer has put at risk inside a rolling window.
  • An optional budget: { windowMs, maxWindowAmount } on PaymentPolicy, using the same per-currency subunit shape as the existing maxAutonomousAmount.
  • An additive authorizePayment layer that runs the existing per-transaction checks first and then the window check, leaving evaluatePaymentPolicy unchanged.

Two details are what make it an enforcement mechanism rather than a display:

  • The check and the reservation have to be one synchronous step. The Payment Service handler awaits verifyPaymentRequestToken before policy runs, so a guard that reads the running total and then writes to it lets two concurrent payments both observe the pre-payment total and both pass.
  • Reservations have to be keyed by payment attempt. The Stripe path authorizes twice for a single payment — once for the payment URL, once on the callback — so a naive counter double-counts every payment. Keying on payment request id plus payment option id makes the second authorization re-check the window without consuming budget again, and gives a natural place to commit on receipt issuance or release when execution fails.

Why this helps

Small first slice

  • Everything under demos/payments; no package, schema, or protocol change and no new dependency.
  • Unit tests for the split attack, window expiry, per-subject and per-currency isolation, idempotent re-authorization, and commit/release.
  • README updated to describe the budget and to scope what is still demo-grade (in-memory storage, single-instance atomicity, denying rather than escalating to human approval).

Happy to adjust the shape — in particular whether a budget breach should return denied (mirroring the per-transaction cap today) or approval_required. I have this implemented and can open the PR if the direction looks right.

AI Usage Disclosure

This issue was written with AI assistance (Claude Code, Opus), used for repository and history navigation and for drafting. I reviewed and understand the content and take responsibility for it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions