From 47e59dbe31356875ee137ed261de5c531cc926b1 Mon Sep 17 00:00:00 2001 From: EfeDurmaz16 Date: Fri, 15 May 2026 21:49:54 +0300 Subject: [PATCH 1/5] demo(payments): enforce policy before signing --- demos/payments/README.md | 17 ++++++ demos/payments/src/payment-policy.test.ts | 66 ++++++++++++++++++++++ demos/payments/src/payment-policy.ts | 67 +++++++++++++++++++++++ demos/payments/src/payment-service.ts | 36 ++++++++++-- 4 files changed, 182 insertions(+), 4 deletions(-) create mode 100644 demos/payments/src/payment-policy.test.ts create mode 100644 demos/payments/src/payment-policy.ts diff --git a/demos/payments/README.md b/demos/payments/README.md index 56b3cfb..e3e165d 100644 --- a/demos/payments/README.md +++ b/demos/payments/README.md @@ -20,9 +20,26 @@ This interactive command-line demo showcases a common use case: the **Server-Ini - Handling currency conversions. - Integrating compliance checks (KYC/AML). - Facilitating complex payment routing. + - Enforcing local payment policy before returning an execution URL or signing a receipt-service payload. You can learn more about the full ACK-Pay protocol at [www.agentcommercekit.com](https://www.agentcommercekit.com). +## Policy-before-signing example + +The Stripe Payment Service path includes a tiny local policy guard in +`src/payment-policy.ts`. The guard runs after the Payment Request token and +payment option are verified, but before the demo returns a payment URL or signs +the payload that asks the Receipt Service to issue a receipt. + +This is an example pattern, not a normative ACK-Pay policy engine. Production +Payment Services should replace it with their own owner, risk, compliance, or +human-approval system. The important safety boundary is that policy enforcement +happens before execution or signing: + +- known low-value recipient: continue automatically +- unknown recipient: return `approval_required` +- amount above the autonomous spend limit: deny before payment execution + ## Demo Video

diff --git a/demos/payments/src/payment-policy.test.ts b/demos/payments/src/payment-policy.test.ts new file mode 100644 index 0000000..c61adce --- /dev/null +++ b/demos/payments/src/payment-policy.test.ts @@ -0,0 +1,66 @@ +import { describe, expect, it } from "vitest" + +import { evaluatePaymentPolicy } from "./payment-policy" + +const basePaymentOption = { + id: "base-usdc", + amount: 100, + decimals: 2, + currency: "USDC", + recipient: "did:example:merchant", +} + +describe("evaluatePaymentPolicy", () => { + it("approves below-threshold payments to allowed recipients", () => { + const decision = evaluatePaymentPolicy(basePaymentOption, { + allowedRecipients: [basePaymentOption.recipient], + maxAutonomousAmount: 1_000, + }) + + expect(decision).toEqual({ + status: "approved", + }) + }) + + it("approves payments back to the trusted request issuer", () => { + const decision = evaluatePaymentPolicy(basePaymentOption, { + allowedRecipients: [], + maxAutonomousAmount: 1_000, + trustedRequestIssuer: basePaymentOption.recipient, + }) + + expect(decision).toEqual({ + status: "approved", + }) + }) + + it("requires approval before execution for unknown recipients", () => { + const decision = evaluatePaymentPolicy(basePaymentOption, { + allowedRecipients: ["did:example:trusted-merchant"], + maxAutonomousAmount: 1_000, + }) + + expect(decision).toEqual({ + status: "approval_required", + reason: "Recipient is not on the autonomous payment allowlist", + }) + }) + + it("denies payments above the autonomous spend limit", () => { + const decision = evaluatePaymentPolicy( + { + ...basePaymentOption, + amount: 10_000, + }, + { + allowedRecipients: [basePaymentOption.recipient], + maxAutonomousAmount: 1_000, + }, + ) + + expect(decision).toEqual({ + status: "denied", + reason: "Payment amount exceeds the autonomous spend limit", + }) + }) +}) diff --git a/demos/payments/src/payment-policy.ts b/demos/payments/src/payment-policy.ts new file mode 100644 index 0000000..9c6b3ca --- /dev/null +++ b/demos/payments/src/payment-policy.ts @@ -0,0 +1,67 @@ +import type { PaymentOption } from "agentcommercekit" + +export type PaymentPolicyDecision = + | { + status: "approved" + } + | { + status: "approval_required" | "denied" + reason: string + } + +export interface PaymentPolicy { + allowedRecipients: readonly string[] + maxAutonomousAmount: number + trustedRequestIssuer?: string +} + +export const demoPaymentPolicy: PaymentPolicy = { + allowedRecipients: [], + maxAutonomousAmount: 1_000_000, +} + +export function evaluatePaymentPolicy( + paymentOption: PaymentOption, + policy: PaymentPolicy = demoPaymentPolicy, +): PaymentPolicyDecision { + const amount = Number(paymentOption.amount) + + if (!Number.isFinite(amount)) { + return { + status: "denied", + reason: "Payment amount must be a finite number", + } + } + + if (amount > policy.maxAutonomousAmount) { + return { + status: "denied", + reason: "Payment amount exceeds the autonomous spend limit", + } + } + + if ( + !policy.allowedRecipients.includes(paymentOption.recipient) && + paymentOption.recipient !== policy.trustedRequestIssuer + ) { + return { + status: "approval_required", + reason: "Recipient is not on the autonomous payment allowlist", + } + } + + return { + status: "approved", + } +} + +export function assertPaymentPolicyApproved( + paymentOption: PaymentOption, + policy: PaymentPolicy = demoPaymentPolicy, +) { + const decision = evaluatePaymentPolicy(paymentOption, policy) + + if (decision.status !== "approved") { + throw new Error(decision.reason) + } +} diff --git a/demos/payments/src/payment-service.ts b/demos/payments/src/payment-service.ts index 356ba5f..0b377e6 100644 --- a/demos/payments/src/payment-service.ts +++ b/demos/payments/src/payment-service.ts @@ -16,6 +16,7 @@ import { HTTPException } from "hono/http-exception" import * as v from "valibot" import { PAYMENT_SERVICE_URL } from "./constants" +import { evaluatePaymentPolicy } from "./payment-policy" import { getKeypairInfo } from "./utils/keypair-info" const app = new Hono() @@ -39,8 +40,13 @@ app.post("/", async (c): Promise> => { await c.req.json(), ) - // Verify the payment request token and payment option are valid - await validatePaymentOption(paymentOptionId, paymentRequestToken) + // Verify the payment request token and payment option are valid before + // returning an execution URL. + const { paymentOption, parsed } = await validatePaymentOption( + paymentOptionId, + paymentRequestToken, + ) + enforcePaymentPolicy(paymentOption, parsed.issuer) log(colors.dim(`${name} Generating Stripe payment URL ...`)) @@ -73,7 +79,7 @@ app.post( ) // Verify the payment request token and payment option are valid - const { paymentOption } = await validatePaymentOption( + const { paymentOption, parsed } = await validatePaymentOption( paymentOptionId, paymentRequestToken, ) @@ -81,6 +87,7 @@ app.post( if (!receiptServiceUrl) { throw new Error(errorMessage("Receipt service URL is required")) } + enforcePaymentPolicy(paymentOption, parsed.issuer) const payload = { paymentRequestToken, @@ -124,7 +131,7 @@ async function validatePaymentOption( const didResolver = getDidResolver() log(colors.dim(`${name} Verifying payment request token...`)) - const { paymentRequest } = await verifyPaymentRequestToken( + const { paymentRequest, parsed } = await verifyPaymentRequestToken( paymentRequestToken, { resolver: didResolver, @@ -146,6 +153,27 @@ async function validatePaymentOption( return { paymentRequest, paymentOption, + parsed, + } +} + +function enforcePaymentPolicy( + paymentOption: Awaited< + ReturnType + >["paymentOption"], + trustedRequestIssuer?: string, +) { + const decision = evaluatePaymentPolicy(paymentOption, { + allowedRecipients: [], + maxAutonomousAmount: 1_000_000, + trustedRequestIssuer, + }) + + if (decision.status !== "approved") { + log(errorMessage(`${name} ${decision.reason}`)) + throw new HTTPException(decision.status === "denied" ? 403 : 409, { + message: decision.reason, + }) } } From 02014d3bb436b682f7adcac8139fc421408f437d Mon Sep 17 00:00:00 2001 From: EfeDurmaz16 Date: Sat, 16 May 2026 07:56:44 +0300 Subject: [PATCH 2/5] fix(demo): use configured payment recipient allowlist Signed-off-by: EfeDurmaz16 --- demos/payments/README.md | 4 ++++ demos/payments/src/payment-policy.test.ts | 6 +++--- demos/payments/src/payment-policy.ts | 6 +----- demos/payments/src/payment-service.ts | 20 ++++++++++++-------- 4 files changed, 20 insertions(+), 16 deletions(-) diff --git a/demos/payments/README.md b/demos/payments/README.md index e3e165d..fe36d4e 100644 --- a/demos/payments/README.md +++ b/demos/payments/README.md @@ -40,6 +40,10 @@ happens before execution or signing: - unknown recipient: return `approval_required` - amount above the autonomous spend limit: deny before payment execution +The demo allowlist is based on the configured server identity, not the issuer +claimed by each incoming Payment Request token. A real Payment Service should +load this allowlist from operator-controlled configuration. + ## Demo Video

diff --git a/demos/payments/src/payment-policy.test.ts b/demos/payments/src/payment-policy.test.ts index c61adce..3590d5b 100644 --- a/demos/payments/src/payment-policy.test.ts +++ b/demos/payments/src/payment-policy.test.ts @@ -22,15 +22,15 @@ describe("evaluatePaymentPolicy", () => { }) }) - it("approves payments back to the trusted request issuer", () => { + it("does not approve self-asserted recipients without an allowlist", () => { const decision = evaluatePaymentPolicy(basePaymentOption, { allowedRecipients: [], maxAutonomousAmount: 1_000, - trustedRequestIssuer: basePaymentOption.recipient, }) expect(decision).toEqual({ - status: "approved", + status: "approval_required", + reason: "Recipient is not on the autonomous payment allowlist", }) }) diff --git a/demos/payments/src/payment-policy.ts b/demos/payments/src/payment-policy.ts index 9c6b3ca..4d15b4a 100644 --- a/demos/payments/src/payment-policy.ts +++ b/demos/payments/src/payment-policy.ts @@ -12,7 +12,6 @@ export type PaymentPolicyDecision = export interface PaymentPolicy { allowedRecipients: readonly string[] maxAutonomousAmount: number - trustedRequestIssuer?: string } export const demoPaymentPolicy: PaymentPolicy = { @@ -40,10 +39,7 @@ export function evaluatePaymentPolicy( } } - if ( - !policy.allowedRecipients.includes(paymentOption.recipient) && - paymentOption.recipient !== policy.trustedRequestIssuer - ) { + if (!policy.allowedRecipients.includes(paymentOption.recipient)) { return { status: "approval_required", reason: "Recipient is not on the autonomous payment allowlist", diff --git a/demos/payments/src/payment-service.ts b/demos/payments/src/payment-service.ts index 0b377e6..3baef9a 100644 --- a/demos/payments/src/payment-service.ts +++ b/demos/payments/src/payment-service.ts @@ -10,7 +10,7 @@ import { type Verifiable, } from "agentcommercekit" import { jwtStringSchema } from "agentcommercekit/schemas/valibot" -import { Hono, type Env, type TypedResponse } from "hono" +import { Hono, type Context, type Env, type TypedResponse } from "hono" import { env } from "hono/adapter" import { HTTPException } from "hono/http-exception" import * as v from "valibot" @@ -42,11 +42,11 @@ app.post("/", async (c): Promise> => { // Verify the payment request token and payment option are valid before // returning an execution URL. - const { paymentOption, parsed } = await validatePaymentOption( + const { paymentOption } = await validatePaymentOption( paymentOptionId, paymentRequestToken, ) - enforcePaymentPolicy(paymentOption, parsed.issuer) + enforcePaymentPolicy(paymentOption, await getTrustedRecipients(c)) log(colors.dim(`${name} Generating Stripe payment URL ...`)) @@ -79,7 +79,7 @@ app.post( ) // Verify the payment request token and payment option are valid - const { paymentOption, parsed } = await validatePaymentOption( + const { paymentOption } = await validatePaymentOption( paymentOptionId, paymentRequestToken, ) @@ -87,7 +87,7 @@ app.post( if (!receiptServiceUrl) { throw new Error(errorMessage("Receipt service URL is required")) } - enforcePaymentPolicy(paymentOption, parsed.issuer) + enforcePaymentPolicy(paymentOption, await getTrustedRecipients(c)) const payload = { paymentRequestToken, @@ -161,12 +161,11 @@ function enforcePaymentPolicy( paymentOption: Awaited< ReturnType >["paymentOption"], - trustedRequestIssuer?: string, + allowedRecipients: readonly string[], ) { const decision = evaluatePaymentPolicy(paymentOption, { - allowedRecipients: [], + allowedRecipients, maxAutonomousAmount: 1_000_000, - trustedRequestIssuer, }) if (decision.status !== "approved") { @@ -177,6 +176,11 @@ function enforcePaymentPolicy( } } +async function getTrustedRecipients(c: Context) { + const serverIdentity = await getKeypairInfo(env(c).SERVER_PRIVATE_KEY_HEX) + return [serverIdentity.did] +} + serve({ port: 4569, fetch: app.fetch, From 790ad42d7d8ecca95d662f3872fe2c4ff5c7d7b5 Mon Sep 17 00:00:00 2001 From: EfeDurmaz16 Date: Thu, 4 Jun 2026 21:53:52 +0300 Subject: [PATCH 3/5] fix(demo): use BigInt and per-currency limits in payment policy Follow the repo-wide BigInt money convention instead of Number() so the policy no longer loses precision or accepts fractional/non-positive/empty amounts the ACK-Pay schema's string branch permits. Replace the single flat maxAutonomousAmount with a per-currency map keyed on currency code, expressed in each currency's subunits, so the threshold is coherent across USD (2dp) and USDC (6dp); currencies with no configured limit are denied. Remove the unused assertPaymentPolicyApproved export. Signed-off-by: EfeDurmaz16 --- demos/payments/src/payment-policy.test.ts | 91 +++++++++++++++++++++-- demos/payments/src/payment-policy.ts | 57 +++++++++----- 2 files changed, 126 insertions(+), 22 deletions(-) diff --git a/demos/payments/src/payment-policy.test.ts b/demos/payments/src/payment-policy.test.ts index 3590d5b..635b212 100644 --- a/demos/payments/src/payment-policy.test.ts +++ b/demos/payments/src/payment-policy.test.ts @@ -5,7 +5,7 @@ import { evaluatePaymentPolicy } from "./payment-policy" const basePaymentOption = { id: "base-usdc", amount: 100, - decimals: 2, + decimals: 6, currency: "USDC", recipient: "did:example:merchant", } @@ -14,7 +14,7 @@ describe("evaluatePaymentPolicy", () => { it("approves below-threshold payments to allowed recipients", () => { const decision = evaluatePaymentPolicy(basePaymentOption, { allowedRecipients: [basePaymentOption.recipient], - maxAutonomousAmount: 1_000, + maxAutonomousAmount: { USDC: 1_000n }, }) expect(decision).toEqual({ @@ -22,10 +22,24 @@ describe("evaluatePaymentPolicy", () => { }) }) + it("approves string subunit amounts within the limit", () => { + const decision = evaluatePaymentPolicy( + { ...basePaymentOption, amount: "50000" }, + { + allowedRecipients: [basePaymentOption.recipient], + maxAutonomousAmount: { USDC: 5_000_000n }, + }, + ) + + expect(decision).toEqual({ + status: "approved", + }) + }) + it("does not approve self-asserted recipients without an allowlist", () => { const decision = evaluatePaymentPolicy(basePaymentOption, { allowedRecipients: [], - maxAutonomousAmount: 1_000, + maxAutonomousAmount: { USDC: 1_000n }, }) expect(decision).toEqual({ @@ -37,7 +51,7 @@ describe("evaluatePaymentPolicy", () => { it("requires approval before execution for unknown recipients", () => { const decision = evaluatePaymentPolicy(basePaymentOption, { allowedRecipients: ["did:example:trusted-merchant"], - maxAutonomousAmount: 1_000, + maxAutonomousAmount: { USDC: 1_000n }, }) expect(decision).toEqual({ @@ -54,7 +68,7 @@ describe("evaluatePaymentPolicy", () => { }, { allowedRecipients: [basePaymentOption.recipient], - maxAutonomousAmount: 1_000, + maxAutonomousAmount: { USDC: 1_000n }, }, ) @@ -63,4 +77,71 @@ describe("evaluatePaymentPolicy", () => { reason: "Payment amount exceeds the autonomous spend limit", }) }) + + it("applies the per-currency limit in the currency's own subunits", () => { + const policy = { + allowedRecipients: [basePaymentOption.recipient], + // 5.00 USD (2dp) and 5.000000 USDC (6dp) — same value, different subunits + maxAutonomousAmount: { USD: 500n, USDC: 5_000_000n }, + } + + // 4.00 USD is below the USD limit + expect( + evaluatePaymentPolicy( + { ...basePaymentOption, amount: 400, decimals: 2, currency: "USD" }, + policy, + ), + ).toEqual({ status: "approved" }) + + // The same 400 subunits in USDC (0.0004) is also below the USDC limit, + // confirming each currency is bounded by its own threshold + expect( + evaluatePaymentPolicy({ ...basePaymentOption, amount: 400 }, policy), + ).toEqual({ status: "approved" }) + }) + + it("denies currencies with no configured limit", () => { + const decision = evaluatePaymentPolicy( + { ...basePaymentOption, currency: "SOL", decimals: 9 }, + { + allowedRecipients: [basePaymentOption.recipient], + maxAutonomousAmount: { USDC: 1_000n }, + }, + ) + + expect(decision).toEqual({ + status: "denied", + reason: "No autonomous spend limit configured for currency SOL", + }) + }) + + it("denies non-positive amounts", () => { + const decision = evaluatePaymentPolicy( + { ...basePaymentOption, amount: 0 }, + { + allowedRecipients: [basePaymentOption.recipient], + maxAutonomousAmount: { USDC: 1_000n }, + }, + ) + + expect(decision).toEqual({ + status: "denied", + reason: "Payment amount must be greater than zero", + }) + }) + + it("denies fractional or malformed amounts the schema permits as strings", () => { + const decision = evaluatePaymentPolicy( + { ...basePaymentOption, amount: "1.5" }, + { + allowedRecipients: [basePaymentOption.recipient], + maxAutonomousAmount: { USDC: 1_000n }, + }, + ) + + expect(decision).toEqual({ + status: "denied", + reason: "Payment amount must be a positive integer in subunits", + }) + }) }) diff --git a/demos/payments/src/payment-policy.ts b/demos/payments/src/payment-policy.ts index 4d15b4a..61a6976 100644 --- a/demos/payments/src/payment-policy.ts +++ b/demos/payments/src/payment-policy.ts @@ -11,28 +11,62 @@ export type PaymentPolicyDecision = export interface PaymentPolicy { allowedRecipients: readonly string[] - maxAutonomousAmount: number + /** + * Per-transaction autonomous spend limits, expressed in each currency's + * smallest subunit (matching `PaymentOption.amount`) and keyed by currency + * code. Keeping the limit per-currency avoids comparing a single flat + * threshold across currencies with different decimals (e.g. USD at 2dp vs + * USDC at 6dp). A currency with no configured limit is denied. + * + * NOTE: this is a per-transaction cap only, not a cumulative or rate budget. + * See the demo README — a real spend control needs windowed/cumulative + * limits, since a per-transaction cap is trivially split-gameable. + */ + maxAutonomousAmount: Readonly> } export const demoPaymentPolicy: PaymentPolicy = { allowedRecipients: [], - maxAutonomousAmount: 1_000_000, + maxAutonomousAmount: { + // 5.00 USD (2 decimals) and 5.000000 USDC (6 decimals) + USD: 500n, + USDC: 5_000_000n, + }, } export function evaluatePaymentPolicy( paymentOption: PaymentOption, policy: PaymentPolicy = demoPaymentPolicy, ): PaymentPolicyDecision { - const amount = Number(paymentOption.amount) + let amount: bigint + try { + // Follows the repo-wide BigInt money convention (see receipt-service.ts, + // index.ts). `BigInt()` throws on fractional/malformed amounts the + // ACK-Pay schema's string branch otherwise permits. + amount = BigInt(paymentOption.amount) + } catch { + return { + status: "denied", + reason: "Payment amount must be a positive integer in subunits", + } + } - if (!Number.isFinite(amount)) { + if (amount <= 0n) { return { status: "denied", - reason: "Payment amount must be a finite number", + reason: "Payment amount must be greater than zero", } } - if (amount > policy.maxAutonomousAmount) { + const limit = policy.maxAutonomousAmount[paymentOption.currency] + if (limit === undefined) { + return { + status: "denied", + reason: `No autonomous spend limit configured for currency ${paymentOption.currency}`, + } + } + + if (amount > limit) { return { status: "denied", reason: "Payment amount exceeds the autonomous spend limit", @@ -50,14 +84,3 @@ export function evaluatePaymentPolicy( status: "approved", } } - -export function assertPaymentPolicyApproved( - paymentOption: PaymentOption, - policy: PaymentPolicy = demoPaymentPolicy, -) { - const decision = evaluatePaymentPolicy(paymentOption, policy) - - if (decision.status !== "approved") { - throw new Error(decision.reason) - } -} From 734f40a4c98586c02d998c089d195f4801dfbbb9 Mon Sep 17 00:00:00 2001 From: EfeDurmaz16 Date: Thu, 4 Jun 2026 21:54:02 +0300 Subject: [PATCH 4/5] refactor(demo): make demoPaymentPolicy the single policy source enforcePaymentPolicy hardcoded maxAutonomousAmount, bypassing the exported demoPaymentPolicy default. Spread demoPaymentPolicy and override only allowedRecipients so the exported config is the single source of truth. Also drop the now-dead parsed value from validatePaymentOption (no caller consumes it after the issuer-comparison iteration was removed). Signed-off-by: EfeDurmaz16 --- demos/payments/src/payment-service.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/demos/payments/src/payment-service.ts b/demos/payments/src/payment-service.ts index 3baef9a..10eb4a4 100644 --- a/demos/payments/src/payment-service.ts +++ b/demos/payments/src/payment-service.ts @@ -16,7 +16,7 @@ import { HTTPException } from "hono/http-exception" import * as v from "valibot" import { PAYMENT_SERVICE_URL } from "./constants" -import { evaluatePaymentPolicy } from "./payment-policy" +import { demoPaymentPolicy, evaluatePaymentPolicy } from "./payment-policy" import { getKeypairInfo } from "./utils/keypair-info" const app = new Hono() @@ -131,7 +131,7 @@ async function validatePaymentOption( const didResolver = getDidResolver() log(colors.dim(`${name} Verifying payment request token...`)) - const { paymentRequest, parsed } = await verifyPaymentRequestToken( + const { paymentRequest } = await verifyPaymentRequestToken( paymentRequestToken, { resolver: didResolver, @@ -153,7 +153,6 @@ async function validatePaymentOption( return { paymentRequest, paymentOption, - parsed, } } @@ -164,8 +163,8 @@ function enforcePaymentPolicy( allowedRecipients: readonly string[], ) { const decision = evaluatePaymentPolicy(paymentOption, { + ...demoPaymentPolicy, allowedRecipients, - maxAutonomousAmount: 1_000_000, }) if (decision.status !== "approved") { From 4cfc2e7cf991932df45cc6429be0eeb3c5a335a2 Mon Sep 17 00:00:00 2001 From: EfeDurmaz16 Date: Thu, 4 Jun 2026 21:54:08 +0300 Subject: [PATCH 5/5] docs(demo): clarify per-transaction cap is not a real spend control Reword the policy example so the amount check reads as an illustrative per-transaction cap rather than a true spend control, and call out that it is trivially split-gameable (cap x N) without a cumulative/rate-limited budget. Document the per-currency, subunit-denominated limit and that unconfigured currencies are denied. Signed-off-by: EfeDurmaz16 --- demos/payments/README.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/demos/payments/README.md b/demos/payments/README.md index fe36d4e..019fe26 100644 --- a/demos/payments/README.md +++ b/demos/payments/README.md @@ -38,7 +38,20 @@ happens before execution or signing: - known low-value recipient: continue automatically - unknown recipient: return `approval_required` -- amount above the autonomous spend limit: deny before payment execution +- amount above the illustrative per-transaction cap: deny before payment + execution + +The per-currency cap is expressed in each currency's smallest subunit, so a +single flat threshold is never compared across currencies with different +decimals (e.g. USD at 2dp vs USDC at 6dp). Currencies without a configured +limit are denied outright. + +> [!IMPORTANT] +> 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. The demo allowlist is based on the configured server identity, not the issuer claimed by each incoming Payment Request token. A real Payment Service should