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.
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:
demos/payments/src/payment-policy.tscarries 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:budget: { windowMs, maxWindowAmount }onPaymentPolicy, using the same per-currency subunit shape as the existingmaxAutonomousAmount.authorizePaymentlayer that runs the existing per-transaction checks first and then the window check, leavingevaluatePaymentPolicyunchanged.Two details are what make it an enforcement mechanism rather than a display:
verifyPaymentRequestTokenbefore 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.Why this helps
Small first slice
demos/payments; no package, schema, or protocol change and no new dependency.Happy to adjust the shape — in particular whether a budget breach should return
denied(mirroring the per-transaction cap today) orapproval_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.