How payments work
The one thing to internalise before anything else: Aroha authorizes; it does not move money. A spending mandate is the cryptographic equivalent of “approved for up to $600 at this merchant” — signed, bounded, and auditable. Actual settlement happens on a real rail, plugged in behind the mandate. This page is the deep version of the handbook's boundaries section.
The two layers
Payment in Aroha is two independent layers that compose. Keeping them separate is the whole design — an authorization layer shouldn't move money, and a payment rail shouldn't reinvent delegated authority.
Aroha mandate chain — WHO delegated WHAT authority (Ed25519, narrows at every hop)
│ enforced cryptographically at attenuation time
▼
Settlement backend — the actual money movement (IArohaSettlement, opt-in)
│ Stripe · AP2 · escrow · quota · …
▼
Real rail / merchant — funds actually change hands (not Aroha)Where the spend limit is actually enforced
This is the most misread part, so it's worth stating exactly. A mandate's spendLimitUsd is enforced at attenuation time — when the mandate is issued or narrowed — cryptographically. Enforced today A child mandate that tries to exceed its parent simply cannot be signed (proven by the tests in the guarantee-evidence table).
Enforcement at payment time — refusing the actual charge if it would exceed the limit — is your application's responsibility, done by reading the verified constraints and (optionally) wiring a settlement backend:
import { verifyMandate } from "@aroha-sdk/credentials";
// At the vendor's payment gate, before charging:
const { valid, mandate } = await verifyMandate(token, issuerPublicKey);
if (!valid) throw new Error("unauthorized");
const limit = mandate.spendingConstraints?.spendLimitUsd ?? 0;
if (amountUsd > limit) {
throw new Error(`charge ${amountUsd} exceeds mandate limit ${limit}`);
}
// …now hand off to your settlement backend / railNullSettlement (the default) until you integrate a real backend, and enforce spend limits in your application logic by reading spendingConstraints from verifyMandate(). The built-in money-moving backends are alpha and not yet recommended for production financial flows.The settlement interface — hooked to the saga
Money movement integrates at the saga lifecycle — the reserve/commit/cancel pattern that gives multi-step agent transactions atomicity. The SagaEngine calls your IArohaSettlement backend at each phase:
onReserveauthorize / hold fundsA hold is placed — a Stripe manual-capture PaymentIntent, an AP2 Open Payment Mandate, an opened escrow, a decremented quota. Throwing here rejects the step and triggers compensation.
onCommitcapture — money movesThe hold is finalized. Stripe captures the PaymentIntent; AP2 issues a Closed Payment Mandate bound to the transaction. Throwing triggers LIFO compensation of prior steps.
onCancelrelease the holdDuring compensation — the Stripe hold is cancelled (no charge), the quota slot released. Best-effort; errors are logged, not fatal.
onSagaCompletefinal release / refundOnce, at the end, with the outcome (SUCCESS / COMPENSATED / FAILURE) — release or refund an escrow. Best-effort.
Settlement is always opt-in via SagaEngineOptions.settlement. The protocol — discovery, identity, signed messaging — never requires it. Sagas with no financial settlement still run (and can still record reputation via a separate injectable recorder).
The built-in adapters
NullSettlementEnforced todayThe default. Authorization only, no money movement — the correct choice until a real backend is integrated.
QuotaSettlementAlphaDecrements a usage/quota allowance per call. For metered, non-monetary settlement.
ApiKeySettlementAlphaGates settlement behind an API key check. Simple prepaid/allowance model.
StripeSettlementAlphaReal card/ACH via Stripe manual-capture PaymentIntents. Maps the saga to authorize → capture → release.
AP2SettlementAlphaGoogle Agent Payments Protocol. Maps the mandate chain to AP2 Open/Closed Payment Mandates (ES256 VDCs) for merchants and financial institutions.
EscrowSettlementAlphaOpens an escrow on reserve, releases or refunds on saga completion. For trust-minimized, on-chain-style flows.
PostgresSettlementAlphaA ledger-backed backend for self-managed accounting.
Real cards via Stripe
StripeSettlement uses Stripe's manual-capture PaymentIntents so the saga's reserve/commit maps cleanly onto authorize/capture — money is only ever captured on a committed saga, and a cancelled saga releases the hold with no charge. PCI scope stays with Stripe.
import { StripeSettlement } from "@aroha-sdk/settlement"; // npm i stripe (peer dep)
const settlement = new StripeSettlement({
secretKey: process.env.STRIPE_SECRET_KEY!,
resolveCharge: async (ctx) => {
if (ctx.capability === "book-flight") {
return { amountCents: 45000, currency: "usd", customerId: "cus_abc123" };
}
return null; // free capability — no charge
},
});
// reserve → PaymentIntent(capture_method: "manual") authorise only
// commit → intent.capture() money moves
// cancel → intent.cancel() hold releasedMerchant rails via AP2
AP2Settlement bridges to the Agent Payments Protocol (Google/Coinbase), the emerging standard for presenting agent authority to merchants and financial institutions. The two layers are complementary, not competing:
- •Aroha mandate chain — who delegated what authority, narrowing at every hop (Ed25519).
- •AP2 Payment Mandate — presenting that authority to merchants as signed Verifiable Digital Credentials (ES256).
The lifecycle maps: onReserve → an Open Payment Mandate (constraints, reusable pre-auth); onCommit → a Closed Payment Mandate bound to the specific transaction. So an Aroha delegation chain can carry authority across five agents and terminate in an AP2 mandate at the merchant boundary — end-to-end provenance from human intent to settlement rail.
import { AP2Settlement } from "@aroha-sdk/settlement";
const keyPair = await AP2Settlement.generateKeyPair();
const settlement = new AP2Settlement({
keyPair,
resolveCharge: async (ctx) => ctx.capability === "book-flight"
? { amountMinorUnits: 29900, currency: "USD",
payee: { id: "airline-1", name: "Demo Airline", website: "https://demo-airline.example" },
paymentInstrument: { id: "card-42", type: "card", description: "Card ••••4242" } }
: null,
processorEndpoint: process.env.AP2_PROCESSOR_URL, // optional
acknowledgeAlpha: true, // explicit opt-in — the backend is alpha
});Where KYC fits
Aroha does not perform KYC — it's an authority layer, not an identity provider. Needs a partner KYC of the human is done upstream, by whoever issues their identity or wallet (their bank, an identity provider, the personal-agent app that verified them at signup) — the same institutions that are already legally required to.
What Aroha contributes is making that binding composable: every mandate traces to the issuer's DID, and a vendor can require that issuer to be verified at a given tier (community → domain → org-signed), refusing mandates from unverified issuers. This is exactly how AP2 and Visa Intelligent Commerce assume the bank has already KYC'd the human, and the agent receives a tokenized credential bound to limits. Aroha carries proof of authorization; the rail carries proof of identity; a verifier can insist on both. See the financial-regulation guide.
What a real payment integration requires
To go from the mock sandbox to real money, honestly:
- A payment-rail partner (Stripe account, an AP2 processor, or a bank/PSP) — Aroha does not hold funds or a money-transmitter license.
- PCI scope handled by the rail (Stripe/AP2 keep card data out of your systems); never put card numbers through Aroha envelopes.
- A resolveCharge implementation mapping your capabilities to amounts and payees.
- Explicit acknowledgeAlpha opt-in — the money-moving backends require it, on purpose, so nobody ships alpha settlement unaware.
- Payment-time limit enforcement in your gate (read spendingConstraints from verifyMandate) — the cryptographic layer bounds issuance, your gate bounds the charge.
- KYC/AML handled by your rail or identity provider, with mandate issuer-tier requirements set accordingly.