blog · 10 September 2026 · 8 minutes

Five layers of protection for payments and AI requests: a client case study

Ask an assistant to "build payment processing" and you get code that works. Click the button, pay, get access. Tests pass, the demo looks convincing to the client. Then someone opens devtools and pays a dollar instead of a hundred. Here is what an AI assistant leaves broken in payment handling by default, on a real client project, and what we build around payments and AI requests so it does not happen.

Hole one: the price comes from the client

Here is what gets generated by default if you simply ask for "payment processing":

// do not do this
app.post('/checkout', async (req, res) => {
  const { productId, amount } = req.body;        // amount came from the browser
  const session = await psp.createSession({ amount, currency: 'usd' });
  res.json({ url: session.url });
});

It looks logical enough: the frontend knows the price, so it sends it. But the frontend runs on the buyer's machine, and it takes one line in the browser console to edit. amount: 10000 becomes amount: 100, the payment provider happily charges a dollar, and the product ships.

The fix: the client sends only the id of what it is buying, and the server looks up the price itself.

app.post('/checkout', requireAuth, async (req, res) => {
  const product = await products.get(req.body.product_id);
  if (!product) return res.sendStatus(404);
  // record the order before calling the PSP: we reconcile the webhook against it later
  const order = await orders.create({
    user_id:      req.user.id,
    product_id:   product.id,
    amount_cents: product.price_cents,   // price comes from the database only
    currency:     product.currency,
    status:       'pending',
  });
  const session = await psp.createSession({
    amount:   order.amount_cents,
    currency: order.currency,
    metadata: { order_id: order.id },
  });
  res.json({ url: session.url });
});

Three lines of difference. But as long as the amount comes from req.body, any checks further down the code are pointless.

Hole two: the "thank you" page confirms the payment

The second common pattern: the user returns from the payment provider to /success, and access is granted right there. This breaks both ways. Close the tab right after the charge and the money is gone but access never gets granted, and you never find out. Or open /success directly, skip the payment entirely, and get access for free.

The only source of truth for a payment is the provider's webhook. And receiving it is not enough — you have to verify it: signature, timestamp, idempotency, and the order amount. By default an assistant does, at best, the first item — signature verification. The rest has to be asked for explicitly, one item at a time. Neither hole gets caught by ordinary testing: tests check "paid → got access" and "did not pay → no access", and an attacker is interested in the third path nobody on the review side thought to check.

How we catch these before the client does

On one project with a payment integration, the code was written, tested, and formally worked: money came in, the product shipped. That was enough to hand the project off. We ran the attacker's scenario ourselves instead: a sharp user forges a response from the payment gateway and the server believes it, the product ships for free. Dig deeper, and there is a way to top up a balance without any charge going through, then reach other people's accounts.

The process we now build into projects like this runs in three steps. First, we go through the integration docs and the common vulnerabilities in plain conversation, no code yet — it is easier to keep track of what to look for that way. Then we hand the finished code to an AI agent for a first audit: it checks the logic against current API documentation, and we go through every item in the report and close what it found. Last is a stress test in a clean session, with no memory of earlier chats: the agents get one blunt task — break the code, bypass the payment. On this project they found a gap the first audit had missed.

The clean session matters. An agent that knows "how it was meant to work" defends the design and explains why it is fine. An agent that only sees the code looks for a way to fool it — a completely different role.

Five layers of protection around payments and AI requests

Full protection does not exist. The goal is different: make an attack cost more than whatever it could gain. On the project where we are currently building this out, it comes in five layers.

1. Log everything

Every request, every operation. Looks paranoid right up until the first incident review — without logs there is nothing to review.

2. Block suspicious IPs

Bots, spammers, odd request patterns get cut off automatically, without a manual review of every case.

3. Rate-limit spend per account

A sliding window on request count and on money spent, tracked separately — because 10 expensive AI calls hit the budget harder than 1,000 cheap ones.

4. Real-time alerts

Not "find out from the logs next week" — see the anomaly now, while it can still be stopped.

5. A kill switch

A script that cuts every outbound connection: no data, no API, and a notification lands with us. For the attack the other four layers did not anticipate.

The third layer — spend control — looks like this in practice:

// Count money as well as requests: 10 expensive calls
// hit the wallet harder than 1,000 cheap ones
async function guard(accountId, costCents) {
  const WINDOW = 3600;
  const now = Math.floor(Date.now() / 1000);
  const key = `spend:${accountId}`;
  await redis.zremrangebyscore(key, 0, now - WINDOW);
  await redis.zadd(key, now, `${now}:${crypto.randomUUID()}:${costCents}`);
  await redis.expire(key, WINDOW);
  const entries  = await redis.zrange(key, 0, -1);
  const requests = entries.length;
  const spent    = entries.reduce((s, e) => s + Number(e.split(':')[2]), 0);
  const limit = await limits.get(accountId);
  if (requests > limit.requests_per_hour || spent > limit.cents_per_hour) {
    await accounts.block(accountId, 'anomaly');
    await alerts.send('account_blocked', { accountId, requests, spent });
    return false;
  }
  return true;
}

This fires in two cases: either your client's own customer found a vulnerability, or a stranger is running requests against a paid AI API on someone else's account — they found an endpoint that proxies calls to the model past the interface and its limits, and is using it as a free ride on the model. Either way the response is the same: stop first, investigate after.

A related habit is backups. We take an infrastructure snapshot before any work starts on a server, not after an incident. The reason is simple: a small config change in production can take down a client's live site, and the only thing that turns hours of recovery into minutes is a snapshot taken in advance.

The honest part

AI agents do not replace a pentest. They do not see the full infrastructure, do not know the business context, and confidently say "no vulnerabilities" when they simply found none — every finding still needs a human check. It is a cheap first filter, not a security audit. And the five layers above do not make a product unbreakable. They close the specific places where mistakes happen most often — they do not guarantee the absence of others.

When you do not need this

What to ask a vendor about payment security

Building a product with payments or paid AI features?

A 3–5 day discovery for $170 walks through the architecture and flags the risk before the main build starts. It counts toward the project. AI agents and protective mechanisms start from $1,400.

Telegram · @juzubiyyah