Agentic Browser Checkout: PCI‑Safe 3DS2, Payment Request API, and Idempotent Purchases for Auto‑Agent AI Browsers
Auto‑agent AI browsers are crossing the threshold from novelty to utility. They read product pages, compare options, and execute buy flows without pixel‑perfect scripts. The next wave of commerce automation will be driven by agentic browsers that click, type, and decide on behalf of users and back‑office systems.
That creates new failure modes. An agent that replays a checkout button can accidentally buy 3 times. A headless browser that sees a 3DS2 challenge might choke and abandon a partially authorized transaction. An over‑eager log pipeline can leak PANs and push you into PCI DSS scope. To ship agent‑safe buy flows, we need a design that is both secure and predictable under retries.
This article lays out an opinionated blueprint:
- Keep PCI scope away from your origin using tokenization and Payment Request API
- Orchestrate EMV 3‑D Secure 2 (3DS2) for agents with decoupled and step‑up paths
- Use write‑ahead purchase intents with idempotency keys
- Reconcile webhooks and receipts into an append‑only ledger
- Provide auditable, exactly‑once semantics for refunds and retries
The patterns apply whether your agent is a human‑in‑the‑loop LLM pilot, a headless Playwright process, or a back‑office service steering a browser.
Threat model, constraints, and goals
Before proposing architecture, define the constraints.
- PCI exposure: If Primary Account Number (PAN) data touches your origin or logs, you expand your PCI DSS scope. Your goal is SAQ A or SAQ A‑EP at most, by isolating card entry into a PSP‑hosted context and never handling PAN or CVV directly.
- Strong customer authentication: EMV 3DS2 is mandated in many markets for SCA (for example PSD2 in the EEA/UK), and issuers decide when a challenge is required. Agents must survive friction: device fingerprinting, one‑time passwords, out‑of‑band approvals, or Secure Payment Confirmation (SPC).
- Idempotency across networks: The web is lossy. Agents will crash, time out, or retry. Your payment layer must make purchase commit idempotent and observable.
- Auditable operations: Exactly‑once delivery is a myth in distributed systems. You get at‑least‑once with deduplication, plus compensating actions. You need an auditable ledger that shows what happened and why.
- Privacy and bot controls: Agent user agents may trigger fraud rules. You need to prefer device‑bound authentication and issuer‑approved out‑of‑band (OOB) challenges when possible, and degrade gracefully when not.
Design goals:
- No PAN, CVV, or 3DS cryptograms should enter your origin frame or logs
- Payment commits are idempotent by order identity, not by accident
- 3DS2 challenges do not strand authorizations or confuse agents
- Refunds are exactly‑once from the perspective of your ledger and provider APIs
- Every hop emits structured, hash‑chained audit entries
Architecture at a glance
Textual diagram of the flow:
-
Agent opens merchant checkout page and binds to an agent‑safe UI surface
-
Merchant creates a write‑ahead Checkout Intent on the server with idempotency key and returns a client token
-
Front end uses tokenization: either PSP‑hosted fields or Payment Request API + PSP payment handler or SPC
-
On payment confirmation, merchant server requests authorization via PSP and orchestrates 3DS2 (either frictionless, challenge, or decoupled)
-
PSP sends webhooks for authorization, 3DS result, capture, settlement
-
Merchant ledger reconciles webhooks exactly‑once, emits a Receipt, and unlocks fulfillment
-
On failure or duplicate attempts, idempotency collapses them to a single PaymentAttempt, and compensating refunds are logged and audited
PCI scope isolation with tokenized cards and browser boundaries
The first rule: do not let PAN, CVV, or 3DS cryptograms enter your origin. There are two practical approaches:
- PSP‑hosted fields: Embed card number, expiry, and CVC iframes from your payment service provider. Your origin never sees raw card data. The PSP returns a token (such as a payment method id) usable for authorization.
- Payment Request API and Secure Payment Confirmation: Use Payment Request to collect shipping and contact details, and a PSP payment handler or SPC to obtain a cryptogram without exposing PAN to your DOM.
Security controls that reinforce isolation:
- Content Security Policy that only allows the PSP payment origin in frames and scripts. Example CSP directives: frame‑ancestors self; frame‑src PSP origins; connect‑src PSP API and your API; default‑src none.
- SameSite and Secure cookies for session state. Do not store anything sensitive in client storage.
- Trusted Types and strict CSP to reduce DOM data exfiltration risk.
- No keystroke logging or screenshotting while PSP iframes are focused.
A minimal example using PSP‑hosted fields and a write‑ahead intent.
Server creates a Checkout Intent and returns a client token the front end can use to mount fields:
ts// server/checkout.ts (Node + Express + TypeScript) import express from 'express' import crypto from 'crypto' const router = express.Router() // naive in‑memory store for illustration; use a database in production const intents = new Map<string, any>() // Helper to derive a stable idempotency key from a merchant order id function deriveIdempotencyKey(orderId: string): string { return crypto.createHash('sha256').update(orderId).digest('hex') } router.post('/checkout_intents', async (req, res) => { const { orderId, amount, currency, customerId, cartHash, metadata } = req.body if (!orderId || !amount || !currency) { return res.status(400).json({ error: 'missing_fields' }) } const idemKey = deriveIdempotencyKey(orderId) const existing = intents.get(idemKey) if (existing) { return res.json(existing) } // Create an intent record in your DB, status pending const intent = { intentId: 'ci_' + crypto.randomUUID(), orderId, idemKey, amount, currency, customerId: customerId ?? null, cartHash: cartHash ?? null, status: 'pending', createdAt: Date.now(), metadata: metadata ?? {}, // clientToken is a short‑lived token you mint for the PSP frontend SDK clientToken: 'tok_' + crypto.randomUUID(), } intents.set(idemKey, intent) return res.json(intent) }) export default router
Front end mounts PSP‑hosted fields with the client token returned by your server. When the agent clicks Pay, you ask the PSP to tokenize card data and return a payment method token, not PAN.
js// frontend/checkout.js import { mountHostedFields, tokenize } from 'psp-sdk' async function startCheckout(orderId) { const resp = await fetch('/api/checkout_intents', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ orderId, amount: 1299, currency: 'USD' }) }) const intent = await resp.json() const fields = mountHostedFields('#card-container', { clientToken: intent.clientToken, fields: { number: { selector: '#card-number' }, expiry: { selector: '#card-expiry' }, cvc: { selector: '#card-cvc' }, } }) document.querySelector('#pay').addEventListener('click', async () => { const result = await tokenize(fields) if (result.error) { showError(result.error) return } await confirmPayment(intent.intentId, result.paymentMethodId) }) }
Your server then uses the PSP server API to authorize using the token. You never see PAN or CVV.
This pattern keeps you within SAQ A or SAQ A‑EP and dramatically lowers the blast radius if your agent logs or DOM snapshots leak.
Payment Request API and Secure Payment Confirmation
Payment Request API consolidates shipping, contact, and payment UI into a browser‑native sheet. When available, this reduces DOM scraping, improves accessibility, and shortens the path for agent flows. Basic Card is deprecated in browsers, but two options matter for agentic checkout:
- Payment handlers from PSPs: Some PSPs provide a payment handler that integrates with Payment Request and returns a tokenized instrument.
- Secure Payment Confirmation (SPC): A W3C spec that allows the browser to authenticate a payment with FIDO credentials bound to the instrument and merchant. SPC works with EMV 3DS rails and can satisfy SCA with user presence or platform biometrics.
Agent guidance:
- Prefer SPC when available; it gives strong device‑bound proof without pages of HTML.
- If SPC is not available, fall back to hosted fields or PSP checkout, not to custom PAN forms.
Example flow with Payment Request collecting address plus SPC for SCA. The exact parameters depend on your PSP and issuer enrollment; this is indicative.
js// frontend/spc.js async function spcAvailable() { if (!window.PaymentRequest || !window.PaymentCredential) return false try { return await window.PaymentCredential.isConditionalMediationAvailable() } catch { return false } } async function payWithSPC(intent) { const method = { supportedMethods: 'secure-payment-confirmation', data: { rpId: 'shop.example', instrument: { displayName: 'Visa', icon: '/visa.png', }, challenge: base64urlRandom(32), payeeName: 'Example Shop', payeeOrigin: 'https://shop.example', // credentialIds issued during card enrollment credentialIds: intent.spcCredentialIds, timeout: 60000, } } const details = { total: { label: 'Total', amount: { currency: intent.currency, value: (intent.amount / 100).toFixed(2) } }, displayItems: intent.items, shippingOptions: intent.shippingOptions, } const request = new PaymentRequest([method], details, { requestShipping: true, requestPayerEmail: true }) request.addEventListener('shippingaddresschange', e => { e.updateWith(recalculateShipping(details, request.shippingAddress)) }) const response = await request.show() try { // response.details contains the SPC assertion const ok = await confirmSPCPayment(intent.intentId, response.details) if (ok) await response.complete('success') else await response.complete('fail') } catch (err) { await response.complete('fail') throw err } }
Key points:
- SPC can satisfy SCA as a 3DS2 alternative when supported by PSP and issuer. It is ideal for agent flows with a human in the loop who touches the sensor.
- For fully unattended agents, SPC still helps if a user pre‑approves and the issuer permits decoupled or risk‑based approvals.
If SPC or a PSP handler is not present, use hosted fields and a 3DS2 orchestration described next.
3DS2 orchestration that does not strand agents
EMV 3DS2 brings device fingerprinting and challenge flows. For agentic browsers, the challenge is threefold:
- The agent may not be able to render the issuer challenge window correctly
- Step‑up often needs human interaction or an OOB push through the issuer app
- Partial authorizations can get stuck if your server flow does not tie them to an intent and a later completion event
What to do:
- Use a server‑side 3DS server or your PSP to handle device data collection and challenge redirection. Do not hand‑roll ACS iframe lifecycles.
- Strongly prefer decoupled or OOB challenge where issuers allow it. This lets the issuer push a notification to a phone without relying on the agent UI.
- Attach every 3DS transaction to your Checkout Intent via a stable correlation id so later webhooks or poll responses reconcile to the same ledger entry.
Illustrative server orchestration with vendor SDK pseudocode:
ts// server/payments.ts import { startAuth, initiateThreeDS, completeThreeDS, capture } from 'psp-server-sdk' import { logAudit, withIdempotency } from './support' // Called after tokenization or SPC assertion arrives from the front end export async function authorizeWith3DS(intent, paymentMethodId, browserInfo) { return withIdempotency(intent.idemKey + ':auth', async () => { const auth = await startAuth({ amount: intent.amount, currency: intent.currency, paymentMethodId, merchantReference: intent.orderId, threeDS: { source: 'browser', browserInfo, // screen size, user agent, JavaScript enabled, accept headers, etc. requestorChallengeIndicator: 'no_preference', // or 'challenge_preferred' based on risk } }) logAudit('auth_started', { intentId: intent.intentId, pspAuthId: auth.id }) if (auth.status === 'authorised') { return auth } if (auth.status === 'three_d_required') { // ask PSP to perform device fingerprint collection and, if needed, issuer challenge const tds = await initiateThreeDS({ authId: auth.id, returnUrl: `https://shop.example/3ds/return?i=${intent.intentId}` }) logAudit('3ds_initiated', { authId: auth.id, tdsId: tds.id, flow: tds.flow }) return { status: 'three_d_pending', next: tds.nextAction } } throw new Error('auth_failed') }) } // Webhook callback from PSP after 3DS challenge or decoupled auth completes export async function onThreeDSResult(event) { if (event.type === 'three_d_completed') { const result = await completeThreeDS({ tdsId: event.data.tdsId }) logAudit('3ds_completed', { tdsId: event.data.tdsId, result: result.outcome }) // Update intent ledger state based on result } }
Agent compatibility tips:
- For iframe challenges, apply the PSP guidelines for sizing and origin isolation. Agents like Playwright can still handle the iframe switch reliably.
- Prefer decoupled authentication when available; many issuers allow a window of time where the ACS verifies OOB and calls back to your PSP.
- Ensure you set a long enough 3DS timeout and a retryable polling mechanism so an agent can wait or resume without re‑authorizing.
References:
- EMV 3‑D Secure 2.3.1 Core Specification (EMVCo)
- PCI DSS v4.0 (PCI Security Standards Council) guidance for iFrame‑based solutions and SAQ A
- W3C Secure Payment Confirmation
Write‑ahead intents: commit plans that survive retries
Idempotency is easier when you declare intent before you act. The Checkout Intent is a server resource that records:
- Business order identity: orderId and cartHash
- Amount and currency
- Optional customer identity
- Idempotency key derived from orderId
- Current payment state and last PSP references
- Short‑lived client token to mount PSP SDKs
Properties:
- A second request with the same idempotency key returns the same intent
- All downstream PSP calls include orderId or idempotency to allow deduplication
- Webhooks reconcile to this record by either intentId, orderId, or PSP reference
Design of the resource:
json{ 'intentId': 'ci_123', 'orderId': 'ord_2026_0000456', 'idemKey': 'b97d...sha256...', 'amount': 1299, 'currency': 'USD', 'customerId': 'cus_789', 'cartHash': 'sha256(items+prices+discounts)', 'status': 'pending | authorised | captured | cancelled | failed', 'psp': { 'authId': null, 'paymentMethodId': null, 'threeDSId': null }, 'clientToken': 'tok_...', 'createdAt': 1736966400000, 'metadata': { 'channel': 'agent' } }
The front end never creates charges directly. It only updates this intent with a payment method token or SPC assertion. The server is the sole actor that transitions the intent by calling the PSP. This eliminates many double‑charge races.
Idempotent purchases: from myth to engineering practice
In distributed systems, exactly‑once semantics are not attainable in the general case; you can achieve at‑least‑once with idempotent handlers and auditable compensations. The payment space is no different. The pattern:
- Make authorization idempotent by order identity: include orderId or a deterministic idempotency key in the PSP request. Many PSPs have explicit idempotency headers or fields.
- Separate authorization from capture. If your PSP supports separate capture, use a second idempotency scope keyed to the same orderId but with an operation discriminator (for example orderId + ':capture').
- Persist a payment ledger entry per attempt, with a unique PaymentAttempt id. Keep attempts linked to the intent via orderId.
Server example:
ts// server/idempotent.ts import { authorise, capture } from 'psp-server-sdk' import { saveAttempt, markIntentAuthorised, markCapture } from './store' import { withIdempotency, logAudit } from './support' export async function authoriseOnce(intent, paymentMethodId) { return withIdempotency(intent.idemKey + ':auth', async () => { const attemptId = 'pa_' + crypto.randomUUID() logAudit('auth_request', { intentId: intent.intentId, attemptId }) const res = await authorise({ amount: intent.amount, currency: intent.currency, paymentMethodId, merchantReference: intent.orderId, idempotencyKey: intent.idemKey // if PSP supports headers, pass there }) await saveAttempt({ attemptId, intentId: intent.intentId, pspRef: res.id, status: res.status }) if (res.status === 'authorised') { await markIntentAuthorised(intent.intentId, res.id) } return res }) } export async function captureOnce(intent) { return withIdempotency(intent.idemKey + ':capture', async () => { const res = await capture({ pspAuthId: intent.psp.authId, amount: intent.amount }) await markCapture(intent.intentId, res.id) logAudit('captured', { intentId: intent.intentId, pspCaptureId: res.id }) return res }) }
Notes:
- withIdempotency is a mutex plus write‑ahead record in your database keyed by operation id. On retry, it returns the stored result.
- If your PSP does not provide server‑side idempotency keys, you can still dedupe by checking if a previous authorization with the same orderId exists and returning that reference.
- Authorization timeouts and unknown statuses are common. Persist the pending attempt and reconcile via webhooks.
Payment Request plus intents: an agent‑friendly front end
Bring the pieces together on the front end:
- Use write‑ahead intent to pull a client token and the order identity
- Prefer Payment Request for shipping and contact info; use hosted fields or SPC for payment
- Do not expose any server API secrets to the agent context
Sketch:
jsasync function startAgentCheckout(order) { // 1) Create or resume an idempotent intent const res = await fetch('/api/checkout_intents', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(order) }) const intent = await res.json() // 2) Try SPC first if (await spcAvailable() && intent.spcCredentialIds && intent.spcCredentialIds.length) { return payWithSPC(intent) } // 3) Fall back to PSP‑hosted fields await mountHostedFields('#card', { clientToken: intent.clientToken }) }
This surface is simple for agents to control: a single Pay button that triggers SPC or tokenization and then calls your server to authoriseOnce.
Reconciliation: from PSP webhooks to a durable receipt
Webhooks are at‑least‑once. You must dedupe and reconcile them to the intent ledger. Steps:
- Verify signatures and record delivery attempts
- Idempotently apply state transitions to intents and attempts
- Emit a Receipt resource when an authorization is captured (or when funds are guaranteed by your provider)
Minimal webhook handler:
ts// server/webhooks.ts import { verifyWebhook } from 'psp-server-sdk' import { applyEvent, logAudit } from './store' export async function webhook(req, res) { const sigOk = verifyWebhook(req.headers, req.rawBody) if (!sigOk) return res.status(400).send('bad sig') const event = JSON.parse(req.rawBody) const outcome = await applyEvent(event) // idempotent upsert into ledger logAudit('webhook_applied', { type: event.type, id: event.id, outcome }) res.json({ ok: true }) }
Receipt semantics:
- A Receipt is a distinct, immutable resource that captures final payment outcome: amount, currency, authorization reference, capture reference, and a signed hash of the ledger entries that produced it.
- The Receipt id is derived from orderId plus capture id, so repeated builds are identical.
Example receipt:
json{ 'receiptId': 'rcpt_ord_2026_0000456', 'orderId': 'ord_2026_0000456', 'amount': 1299, 'currency': 'USD', 'authorisationRef': 'psp_auth_123', 'captureRef': 'psp_cap_456', 'createdAt': 1736966460000, 'ledgerHash': 'bafykbz...' }
The ledgerHash can be a hash chain over your audit log entries. This makes the receipt verifiable during audits.
Exactly‑once refunds and safe retries
Refunds are where systems get messy. Agents retry when they do not get a response, and users can click refund twice. Treat refunds as idempotent operations keyed by a refundId you assign.
- The refund idempotency key is refundId or orderId plus a refund index (for partial refunds)
- Store a Refund record with status pending; include the PSP refund reference when created
- Apply webhooks to move refunds to succeeded or failed
Pseudocode:
ts// server/refunds.ts import { createRefund } from 'psp-server-sdk' import { withIdempotency, logAudit, saveRefund } from './store' export async function refundOnce(orderId, amount) { const refundKey = orderId + ':refund:' + amount return withIdempotency(refundKey, async () => { const refundId = 'rf_' + crypto.randomUUID() await saveRefund({ refundId, orderId, amount, status: 'pending' }) const res = await createRefund({ merchantReference: refundId, amount }) logAudit('refund_requested', { orderId, refundId, pspRefundId: res.id }) // Some PSPs return immediate success; others are async return { refundId, status: res.status, pspRefundId: res.id } }) }
Behavioral guarantees:
- A second call with the same refundKey returns the same Refund record
- If network fails after PSP accepts the refund, a retry does not create a duplicate
- Your audit log contains a linearizable trace of refund operations and outcomes
When partial refunds are allowed, index them deterministically by the cumulative refunded amount or a client‑provided refund request id. For example, orderId + ':refundreq:' + refundRequestId.
Observability and audit: hash‑chained logs
To defend your operations and satisfy auditors, emit structured events for:
- Intent creation and state transitions
- Payment attempts and PSP references
- 3DS2 initiation and completion
- Capture, receipt issuance
- Refund requests and outcomes
- Webhook receipt and application
Hash‑chain each event with the previous event hash for the same orderId. At audit time you can prove no entries were deleted or reordered.
tsfunction hashChain(prevHash: string, event: object): string { const h = crypto.createHash('sha256') h.update(prevHash) h.update(JSON.stringify(event)) return h.digest('hex') }
Store both the event record and the cumulative hash in your database. Sign receipt payloads with a server key and keep the public key for verification.
Hardening agentic checkout surfaces
AI agents run in diverse environments. Assume the agent process is untrusted and may log or screen‑scrape. Harden your UI:
- Use a minimal checkout page dedicated to payment, with no unrelated PII
- Never render PAN in clear text; even masked values should come from PSP frames
- Disable autocomplete and avoid binding card inputs to your DOM
- Apply a narrow CSP and use Subresource Integrity for static assets
- Instruct agents via on‑page hints and rel=noopener links to avoid window handle leakage
- Prefer top‑level cross‑origin redirects for PSP flows instead of nested iframes when your bot detection or CSP policies require it
Operational controls:
- Ephemeral browser profiles for agents; clear storage after each checkout
- Network egress allowlist: PSP endpoints, your API, necessary static CDNs
- Redact secrets in screenshots and logs; do not allow fullscreen capture while PSP frames are focused
- Measure and cap retry storms with circuit breakers around PSP calls
Handling fraud and bot challenges without breaking automation
Fraud engines consider many signals. Agentic user agents can look suspicious. Two practices reduce friction:
- Use SPC or WebAuthn‑backed authentication for returning users. Device‑bound credentials are high signal.
- For mandatory SCA markets, prefer 3DS2 decoupled authentication or OOB challenges. Agents wait while the issuer app notifies the human.
When challenge presentation is unavoidable in‑browser:
- Render the 3DS challenge in a dedicated full‑viewport modal; avoid nested scroll containers
- Propagate and preserve the threeDSReference across page reloads using the intent id
- Expose a single Continue button that the agent can find deterministically, and instrument success and timeout events
Testing matrix: make failure the default
Test for the realities agents trigger:
- Network: drop connection after the PSP accepts an authorization but before your server responds
- Latency: delay 3DS2 result webhooks by minutes
- Idempotency: send the same Confirm call 5 times concurrently
- Crash: reload the checkout between tokenization and confirm
- Refund races: request a refund while capture webhook is in flight
- Browser quirks: run in headless Chrome and WebKit with different viewport sizes
- PR API: simulate absence of Payment Request and SPC, fallback to hosted fields
- 3DS: simulate frictionless, challenge, and decoupled outcomes using PSP test harnesses
Automate these with Playwright or WebDriver plus a mocked PSP sandbox where you control event timing.
Migration path for existing checkouts
- Phase 1: Introduce write‑ahead Checkout Intents for every order. Do not change UI yet. Begin signing webhooks and reconciling into a ledger and Receipt resource.
- Phase 2: Move card entry to PSP‑hosted fields. Eliminate PAN from your origin and logs. Add CSP restrictions.
- Phase 3: Integrate 3DS2 orchestration via your PSP. Handle decoupled and step‑up flows. Record all state in the intent.
- Phase 4: Add Payment Request for address and contact, and SPC for SCA where supported. Prefer SPC for agent flows.
- Phase 5: Implement idempotent refunds and a public, signed receipt. Add hash‑chained audit logs.
At each phase, keep the same intent idempotency boundary so your risk of double charge remains low.
Implementation checklist
- PCI scope isolation
- Card data only in PSP frames or Payment Request handlers
- Strict CSP and no PAN in logs or analytics
- Intent and idempotency
- Deterministic idempotency key per order
- Server‑side confirm only; front end never charges directly
- 3DS2 orchestration
- Decoupled or OOB preferred; handle challenge if required
- Map PSP refs to intents and attempts
- Reconciliation and receipts
- Verify webhook signatures; dedupe; ledger updates
- Immutable Receipt issued on capture
- Refunds
- Idempotent refund keys; reconcile webhook outcomes
- Audit trail with hash chaining
- Agent‑safe UI
- Minimal surface; deterministic buttons; robust iframe sizing
- SPC where possible; hosted fields fallback
Opinionated recommendations
- Always introduce a Checkout Intent before payment. It is the anchor for idempotency, observability, and 3DS.
- Prefer Secure Payment Confirmation for agent‑friendly SCA. It reduces DOM complexity and aligns with issuer expectations.
- If you must render 3DS challenges, do it via provider SDKs and attach everything to the intent id; never free‑hand ACS lifecycles.
- Treat refunds as first‑class, idempotent operations with their own keys and receipts.
- Build a ledger first. Make webhooks authoritative for state. Your synchronous API responses are hints; webhooks are truth.
References and standards
- PCI DSS v4.0: requirements for SAQ A and SAQ A‑EP and iFrame‑based card data entry
- EMV 3‑D Secure 2.3.1 Core Specification (EMVCo)
- W3C Payment Request API
- W3C Secure Payment Confirmation
- FIDO2 and WebAuthn for device‑bound authentication
- Provider docs: PSP idempotency and webhook signing guides (consult your PSP: Adyen, Braintree, Checkout, PayPal, Stripe, etc.)
- On exactly‑once semantics: engineering literature notes that exactly‑once is generally unattainable; design idempotent operations with compensations.
Appendix: data model sketch
Entities:
- CheckoutIntent
- intentId, orderId, idemKey, amount, currency, status, customerId, cartHash, metadata
- psp: authId, paymentMethodId, threeDSId
- PaymentAttempt
- attemptId, intentId, createdAt, status, pspRef, error
- Receipt
- receiptId, orderId, amount, currency, authorisationRef, captureRef, ledgerHash, createdAt
- Refund
- refundId, orderId, amount, status, pspRefundId, createdAt
- AuditEvent
- eventId, orderId, type, payload, prevHash, hash, createdAt
Minimal SQL to idempotently upsert events:
sqlcreate table audit_events ( event_id text primary key, order_id text not null, type text not null, payload jsonb not null, prev_hash text not null, hash text not null, created_at timestamptz not null default now() ); create unique index on audit_events (order_id, hash);
Closing thought
Agentic browsers are not a special case; they are the real‑world stress test of your payment architecture. If you keep cardholder data out of your origin, orchestrate 3DS2 deliberately, and drive every action from a write‑ahead intent with idempotent commits, you will ship buy flows that withstand retries, crashes, and automation. Add a verifiable ledger and exactly‑once refunds at the API boundary, and your agents can buy safely without blowing up your finance team or your PCI scope.