Agentic commerce is the fastest-growing AI use case in 2026 — users ask their AI to book a flight, order groceries, buy a gift. If your checkout has aggressive captcha, JS-only step transitions, hidden honeypots, or unstable session state, agents abandon before purchase completes. This guide covers the patterns that let legitimate agent purchases through while keeping fraud protection.
/checkout → user fills details, JS swaps content /checkout (same URL, now showing payment) → user fills payment, JS swaps again /checkout (now showing review) <!-- Agent refresh = lose state. Resume = impossible. -->
/checkout/cart /checkout/details /checkout/shipping /checkout/payment /checkout/review /checkout/confirmation/:order-id <!-- Agent can resume any step via URL. Refresh-safe. -->
Field labels, price totals, validation errors — all in HTML response:
<!-- /checkout/shipping -->
<main>
<h1>Shipping address</h1>
<form action="/checkout/shipping" method="POST">
<label for="ship-name">Full name</label>
<input id="ship-name" name="shipName" autocomplete="name" required />
<label for="ship-addr">Street address</label>
<input id="ship-addr" name="shipAddr" autocomplete="street-address" required />
<label for="ship-city">City</label>
<input id="ship-city" name="shipCity" autocomplete="address-level2" required />
<label for="ship-postcode">Postcode</label>
<input id="ship-postcode" name="shipPostcode" autocomplete="postal-code" required />
<button type="submit">Continue to payment</button>
</form>
<aside aria-label="Order summary">
<p>Items: 2</p>
<p>Subtotal: £49.00</p>
<p>Shipping: TBC</p>
</aside>
</main>
// Only challenge when behaviour suggests bot
async function shouldShowCaptcha(req) {
const flags = [];
if (req.session.submitTimes.some(t => t < 1500)) flags.push('too_fast');
if (await checkAbuseIPLookup(req.ip) > 80) flags.push('high_abuse_ip');
if (req.body._honeypot) flags.push('honeypot_filled');
if (req.session.failedAttempts > 3) flags.push('repeated_failures');
return flags.length >= 2; // Only challenge on multiple flags
}
// In route handler
if (await shouldShowCaptcha(req)) {
return res.render('captcha', { ... });
}
// Otherwise proceed
Honest agents complete fast (within range), have clean IPs (datacenter but not abuse), don't trigger honeypots, don't fail repeatedly. They pass through. Fraud bots flag.
<form id="payment-form" action="/checkout/process" method="POST">
<label for="card-element">Card details</label>
<div id="card-element" aria-label="Credit card information">
<!-- Stripe injects labelled iframe here -->
</div>
<label for="postal">Billing postcode</label>
<input id="postal" name="postal" autocomplete="postal-code" />
<button type="submit">Pay £49.00</button>
</form>
<!-- Stripe's iframe has labelled inputs inside; agents in browse mode handle it -->
Some agents (especially those handling repeat user purchases with stored cards) can call your backend directly. Expose a tokenised endpoint:
POST /api/checkout/confirm
{
"cartId": "cart_abc",
"paymentMethod": "pm_xxx", // Stripe PaymentMethod ID
"shipping": { ... }
}
→ 200 { "orderId": "ord_yyy", "status": "confirmed" }
Don't tie cart to a fragile session. Agents may resume from a different IP, after a delay, or with a new browser instance:
// Issue durable cart token, store cart server-side <input type="hidden" name="cartToken" value="cart_v1_abc123" /> // On every step submit, server looks up cart_v1_abc123, validates ownership // via signed user session OR signed token. Both stable across IP/UA changes.
<!-- Bad: error in toast that disappears, JS-only -->
<Toast message="Postcode invalid" />
<!-- Good: error inline, ARIA-connected -->
<label for="ship-postcode">Postcode</label>
<input id="ship-postcode" name="shipPostcode"
aria-invalid="true" aria-describedby="postcode-error" />
<div id="postcode-error" role="alert">
Postcode "SW1Z" is not valid. Try "SW1A 1AA" format.
</div>
<!-- Agent reads, corrects, resubmits -->
test('agent can checkout', async ({ page }) => {
await page.goto('/products/widget');
await page.getByRole('button', { name: 'Add to cart' }).click();
await page.goto('/checkout/cart');
await page.getByRole('link', { name: 'Continue to details' }).click();
await page.getByLabel('Full name').fill('Test User');
await page.getByLabel('Email').fill('test@example.com');
await page.getByRole('button', { name: 'Continue' }).click();
// ... shipping, payment, review
await expect(page).toHaveURL(/\/checkout\/confirmation\//);
});