AI Agents & Automation

One Booking, Four Sources That Have to Agree

Brett Ridenour Brett Ridenour · Published September 2026

A booking lands in production. The confirmation number is in Slack, the operator gets a push notification, the reservations dashboard shows a new row. I could stop there. Everybody else in the tour-ops software space seems to.

I stopped stopping there a while ago, because every one of those signals is coming from the same place: my own application code. If my code is confidently wrong about anything — the price it charged, the account it charged, the email it thinks it sent — everything downstream will confidently agree with it.

So when I want to know if a booking really happened, I check four surfaces. None of them know what the other ones say. They all have to agree.

The four surfaces

1
Database
Supabase: the reservation, the immutable quote version, the payment intent row, the lead that converted.
2
Payment processor
Stripe: charges, application fees, refunds, disputes on the platform account AND on the connected operator account.
3
Application logs
Railway: the actual sequence of events the API emitted — quote → payment intent → reservation → ledger post.
4
Notification delivery
Novu + Resend: the confirmation email actually left the building, and the operator subscriber actually got their push.

Four independent surfaces. Any three agreeing while the fourth disagrees is the interesting case.

Each one lies about something the others can catch:

  • The database shows a status: 'confirmed' reservation. It cannot tell you whether the money actually cleared. Denormalized price columns on the reservation row are literally zero for legitimate bookings on my schema, because pricing lives in the immutable quote version. Read the reservation row alone and you’ll think a real customer paid nothing.
  • Stripe shows the money. It cannot tell you whether the reservation ever got written. It also cannot tell you which of my Stripe accounts it thinks it’s answering about — more on that in a minute.
  • Railway logs show the sequence of events my API emitted. They cannot tell you whether Formance actually accepted the ledger post. Emitting an event and having it land are different verbs.
  • Novu knows the workflow triggered. It doesn’t know whether Resend actually delivered the email. And Resend doesn’t know whether the operator’s inbox provider spam-filed it.

Any single surface, run alone, will happily produce a story where the booking looks fine.

Running them in parallel

The fastest way to run this check is to dispatch four subagents, one per surface, in parallel. Each subagent gets the reservation id (or the customer name, or just “latest”) and a narrow prompt: hit your one surface, report what you see, do not speculate about the others.

That last constraint is the whole point. If the database subagent says “looks good, but I can’t verify the money moved,” that’s exactly what I want. A subagent that hedges into the next surface’s territory ends up producing plausible narrative where there should be raw evidence.

Four surfaces, four commands, no cross-talk

Once all four reports are in, I run one small cross-correlation pass:

  • The idempotency_key on the payment intent row matches the Stripe payment intent id.
  • The Stripe PI id shows up in the Railway logs at the moment of creation and again at capture.
  • The reservation id in the Railway logs matches the row written to the database.
  • The email address in the Novu delivery event matches the customer email on the reservation.

If any one of those four joins fails, the booking is not verified. Doesn’t matter how green the dashboards look.

The pitfall the runbook exists to kill

The very first version of this check almost ended my week.

I have a Stripe account for the payments SaaS I’m building. I also have a Stripe account for an unrelated consulting LLC I use for other work. Different EIN, different bank account, different everything. Both are logged into the Stripe CLI on the same machine at different times.

The runbook now bans the bare CLI entirely. Every Stripe call either passes an explicit --api-key "$KEY" pulled from the production environment at runtime, or it uses a --project-name alias that is pinned to the platform account. The first thing every Stripe subagent does is confirm the account id in the response before trusting any numbers.

There’s a second layer of this that matters for anyone building on Stripe Connect: platform-level list endpoints are empty for direct charges. If you’re a marketplace and your operator uses direct charge topology, stripe charges list at the platform level will show you nothing, forever, no matter how many charges landed. You have to scope with --stripe-account acct_... for every list call. A quiet week narrated from the platform account looks identical to a dead marketplace.

What “no findings” looks like

When the four surfaces agree, the report is boring. One paragraph. Payment intent pi_... for the expected amount was created at 21:34, captured at 21:35, resulted in reservation res_... for the expected product on the expected date, and the confirmation email was delivered to the customer’s Gmail address at 21:35:12. All four surfaces confirm.

That paragraph is the product. Nobody sees it unless they go looking. But every morning I know that the booking that came in overnight is a booking, not four hopeful strings that look like a booking.

Independence beats intelligence. A dumb subagent staring at one surface, forbidden to speculate about the others, is more useful than a smart one that tries to tell the whole story.

— The whole point of the pattern

What I’d change if I started over

Two things.

First, I’d write the four-surface check before I wrote the booking flow itself. Every state the booking passes through leaves a fingerprint on a different surface — the check tells you which fingerprints to look for, which forces you to instrument them properly on the way through. Verification-first design.

Second, I’d have run the wrong-CLI-account footgun a hundred years earlier in a lower-stakes context. Everyone building on Stripe Connect trips over the same thing eventually. The version where you trip over it while investigating a real customer’s money is not the version you want.

The dashboards are lying to somebody today. Whether it’s you depends on whether you looked.