---
title: 'Reading the signals: Helping your AI-built payment integration understand business intent'
summary: 'The low-level code that makes a payment go through is getting cheap, fast. What is becoming valuable is the judgment above it: knowing what the money is supposed to do and making sure the code obeys it.'
authors:
  - 'David Benchi'
date: '2026-08-14'
topics:
  - 'AI'
  - 'AI Research'
  - 'Payments'
type: 'Blog'
---

Watching an agent build a payment integration is impressive. It is fast, and building has become almost easy. A few prompts, and the `PaymentIntent` is created, the Payment Element renders, the flow runs end to end. The demo passes.

That speed is what developers tell me constantly now: building on Stripe has never been this quick. And they are right. The next step is helping the integration understand what the money is supposed to do, not just how to move it.

I am the Technical Advisor to the Head of Professional Services EMEA, and reviewing integrations for large users is most of my job, so I see where the gaps are. Someone builds an integration, it looks correct, and most of it is. Then they come to us saying it’s not working as expected, even though on paper it should be. The code ran, but the result did not match the business intent behind it.

When the business intent is missing, nothing throws and nothing is logged. The integration simply starts sending signals, money rests in states it should not rest in, and a week later a support ticket lands: "I was charged and got nothing." The code was correct. The business intent behind it had not been made explicit yet. The good news is that these signals are readable, and once you can read them, you can get ahead of them.

## Why payments are different

Payments are different from most software a team builds. The developers usually have strong development skills, but not always payment skills. They work from the API reference and build against it the same way they would build anything else.

Two things are usually missing at once: the payment knowledge, and the business intent behind the money. Put those gaps together and you get code with correct syntax that passes tests, but then behaves in ways nobody decided on in production. It does not crash. It moves money along paths the business never chose. The model was never the constraint. Knowing what the money is supposed to do is the part worthy of your attention, and the part you can add.

That knowledge rarely announces itself as an error. Instead the integration emits signals, small clues in the data that something is off. Each looks minor and unrelated on its own. Read together, against what the money was supposed to do, they point at a single source, the part a developer cannot see without payment knowledge. That is where a review begins, and it is exactly what you can teach the integration to check for itself.

This post walks you through how to read those signals, encode the business intent, and validate every build against it, whether the build came from a person or an agent.

## The PaymentIntent and the business intent

When I start an integration review, I ask for the first fifteen minutes with no code, just the business: what should happen to the money, and what must never happen to it. If I read the code first, I get biased and start reasoning from code to code. I would rather build the end-to-end business version in my head, find the payment pattern that fits it, and only then look at how it becomes a `PaymentIntent`.

That order matters, so enforce  it within your team: define the business intent before the payment intent. The two share a word for a reason. The `PaymentIntent` is the Stripe object that tracks the payment through its lifecycle. The business intent is the set of decisions behind it, the rules the money has to obey. An agent is very good at producing the first. It cannot know the second unless someone tells it.

Here is what that gap looks like in code. A `return_url` is required for any redirect-based payment method, so on its own it tells you nothing. The signal is a flow that redirects every customer away, even card payments that never needed a redirect:

```javascript
// Client. redirect: "always" bounces every customer to the return page,
// even card payments that never needed a redirect.
// redirect: "if_required" would keep those on the page. Why always? And what runs there?
const { error } = await stripe.confirmPayment({
  elements,
  confirmParams: {
    return_url: "https://example.com/order/complete",
  },
  redirect: "always",
});
```

The syntax is correct. But the object cannot tell you why every payment leaves the page. Is fulfillment meant to happen on that return page? Was redirecting everyone a deliberate business decision, or just the default nobody questioned? The business intent behind it is nowhere in the code, and neither the object nor the model can enforce a rule it was never given.

## Signals that the business intent is missing

You have probably seen at least one of these. On their own they look like small, unrelated bugs in different parts of the stack. Put them next to the business intent and they turn out to be the same failure wearing five faces.

**An authorization rate of 43% that was really above 90%.** A retry loop was resubmitting hard declines, `authentication_required`, `incorrect_number`, or `expired_card`, none of which passes on a second attempt. It looks like a processor problem. The cause is retry logic that treats every decline the same instead of branching on the `decline_code`.

**`PaymentIntents` resting in `requires_action`.** A server-side confirm with no client-side `handleNextAction`, so the 3DS step is never triggered and the intent waits for an authentication nobody sent. Money that could have been collected is instead stuck, and nothing errors.

**A `requires_capture` with money held and no order behind it.** The order is created by a client call that runs after the authorization lands. Kill the tab in that window and the money is held with nothing behind it. The customer was charged and got nothing, and the exception tracker is empty.

**Connected accounts that quietly went `restricted`.** No subscription to `account.updated`, so an account hits `requirements.currently_due`, flips to `restricted`, and keeps taking payments into `testmode_charges_only`. Live volume is lost while the code that moves money keeps working.

**Async payment methods refunded like cards.** The integration fulfills off the return URL, ignores `payment_intent.processing`, and never handles the `succeeded` that lands a day or two later. Revenue you already earned, refunded.

Independent signals like these tend to arrive late, after the money has already moved. Five subsystems, five different bugs. None threw an error. None is a syntax problem. Each one is a decision about how the money should behave that nobody actually made. On their own they look unrelated, but read against the business intent they share one source: the payment knowledge that was never written into the code. Once you can read them, you can encode them.

## Encoding the business intent

This part is harder, because the right answer depends on the team, and there is no single best practice. Think about how you already treat code style: a formatter and a linter keep the syntax consistent so anyone can read it. Business intent needs the same discipline, and most teams do not have it yet. Whether you use a well-named function, a comment that states the rule, a pull request linked to a documented requirement, metadata on the object, or a Stripe custom object does not matter. The point is that the decision is written down where the next person, or the next agent, cannot miss it.

Here is the test I use. I ask a developer why a parameter is set the way it is, and I want more than the technical meaning. I want the business needs behind it. Why does every payment redirect? Why do we retry, and which declines? What happens when a connected account becomes restricted? If the answer is "that is just how it was generated," the intent was never encoded.

Metadata is a good habit here, because it makes the intent legible from the outside. Open a `PaymentIntent` in the Dashboard, and if the metadata says which order and decision it belongs to, you understand the business behind it without reading the code:

```javascript
// Server. Put the business context on the object itself.
const order = await orders.createPending(); // your order id, status "pending"

const paymentIntent = await stripe.paymentIntents.create({
  amount,
  currency,
  // Business decision: authorize now, capture only once the order is confirmed.
  // Never hold funds we cannot fulfill.
  capture_method: "manual",
  // Business decision: every authorization must trace back to an order.
  metadata: { order_id: order.id },
});
```

## Validating every build against the intent

We have an agent to build the code. We need an agent to validate it.

The best teams I work with have stopped treating the build agent as the whole story. They run a small system of agents. An orchestrator coordinates the work for the development team. A builder agent writes the code. And a verifier agent runs after the build, with one job: check that what was built actually does what the money is supposed to do.

The verifier is where it comes together. To build one, someone has to encode the business knowledge into it. You cannot check that a terminal decline is never retried, or that no order exists without an authorization behind it, unless someone states those rules. So the verifier forces the business intent to be written down. In most reviews the business owner never stated it up front, which is exactly why nothing encoded it. Building a verifier forces the conversation that should have happened at the start.

The important part is that those rules are code, not a checklist. Here are three of the five signals from earlier, each written down as something a verifier can run on every build. The other two follow the same shape:

```javascript
// Verifier rules, each one a signal that cost a live integration money.
const paymentRules = [
  {
    rule: "Never retry a terminal decline",
    check: (retry) =>
      !["authentication_required", "incorrect_number", "expired_card"]
        .includes(retry.declineCode),
  },
  {
    rule: "A held authorization must have an order behind it",
    check: (pi) =>
      pi.capture_method !== "manual" || Boolean(pi.metadata?.order_id),
  },
  {
    rule: "Async payment methods handle their asynchronous result",
    check: (integration) =>
      integration.subscribesTo("payment_intent.processing") &&
      integration.subscribesTo("payment_intent.succeeded"),
  },
];
```

How these rules run is up to the team, and both common paths work. You can encode them in a skill the verifier agent loads before it reviews a build, so the check happens conversationally as part of the agent workflow. Or you can wire the same rules into automated testing, a CI step that inspects the integration and fails the build when a rule is violated, the same way a linter fails on a style break. The teams getting the most value do both: the skill catches intent gaps while the code is being written, and the test suite enforces the same rules on every merge so nothing regresses later. What matters is not the mechanism but that the rules run on every build automatically, not when someone remembers to look.

None of these came from a specification. Each one came from an integration that looked correct and lost money until someone read the signal and wrote the rule down. That is the point: the rules live where the code can be checked against them, and writing them forces the conversation with the business owner that should have happened first.

And that is the shift: judgment stops being personal and becomes shared. A business team encodes what correct means into the verifier, and every build, whoever or whatever wrote it, is checked against the same rules. None of this removes the human. Encoding the knowledge into an agent does not hand over the decision, it makes it explicit and repeatable. The person still owns what correct means; the agents only enforce it.

## Conclusion

The value is moving. The low-level code that makes a payment go through is getting cheap, fast. What is becoming valuable is the judgment above it: knowing what the money is supposed to do and making sure the code obeys it.

So read the signals your integration is already sending. State the business intent out loud, encode it where the code can be checked against it, and keep a human owning what correct means. Implementing the `PaymentIntent` is getting cheap. Being right about the business intent behind it is now the whole job.
