Amazon Web Services published a piece a few weeks ago called From Connected to Resilient: Cloud-Native Payment Connectivity on AWS. It's a deep dive into hardening AWS PrivateLink and Resource Gateway for payment networks that speak ISO 8583 over long-lived TCP sessions. Not exactly light reading, but one detail stuck with me: a Network Load Balancer's idle timeout for TLS listeners is fixed at 350 seconds, and if your client's TCP keepalive interval is longer than that, the NLB will quietly kill the connection. Neither side gets an error. The client just... stops hearing back.
The article frames this as a resilience problem, and it is one. But I kept thinking about it from the other side. If a payment request goes out and the connection dies before the response comes back, what does the client actually know? Nothing. The charge might have gone through. It might not have. Until someone checks, both are simultaneously true.
That's a physics joke I couldn't resist building.
Schrödinger's Payment
I built a small app that reproduces this exact failure mode and then makes you live in it. You set an idle timeout and a keepalive interval, send a payment, and watch a heartbeat animation play out in real time. If the keepalive pulses arrive often enough, the connection survives and you get a clean ACK. If they don't, the connection resets right on schedule, and the payment falls into superposition. The backend, unaware anyone left, keeps processing and commits the charge anyway. The client just never finds out.
Turn the keepalive interval up past the idle timeout and you get to watch the reset happen.
The interesting part isn't the outage
Anyone who's built a retry mechanism knows the fix for "did my request actually go through": idempotency keys. Retry with the same key, and a well-built server returns the original result instead of charging twice. I built that path first, and it works exactly as expected.
The part I actually wanted to explore was what happens when the client doesn't retry cleanly. Say the mobile app restarted, or the request got routed through a different channel, and it generates a brand-new idempotency key for what is, to a human, obviously the same purchase. Now the server sees two different keys, the same order, and has to guess whether this is a legitimate second attempt or someone about to get charged twice.
I didn't want to hardcode that guess as a rule. So instead of a simple "same amount within N seconds = safe," the app hands the situation to Claude through Amazon Bedrock's Converse API. It gets the full record: both attempts, their amounts, timestamps, and any note the retrying party attached. Then it has to decide whether to block it as a duplicate, allow it as a distinct transaction, or, if it isn't confident either way, hand the decision to a human instead of guessing.
That last option turned out to be the whole point. When I retried with the same amount and no explanation, the agent blocked it outright at 85% confidence, citing the identical amount and a two-second gap. But when I changed the retry amount and added a note like "customer isn't sure if the amount was right, asked to double check," it stopped short and asked for a human instead. The amount mismatch could mean a legitimate correction, but the short elapsed time and unacknowledged first attempt kept it from being sure. That's roughly the judgment call a fraud analyst makes, minus the human.
Where it actually broke
I deployed the first version to Vercel and ran through the same flow with curl to sanity-check it end to end. Sending a payment and retrying with a new key worked as a pair of requests. Then I called the approve endpoint right after, and got back "no record pending approval found." The record I'd just created, seconds earlier, was gone.
The app's ledger, standing in for the payment backend's source of truth, lives as an in-memory Map in a Node process. That's fine locally, where next dev runs everything as one long-lived process. On Vercel, /api/pay, /api/resolve, /api/approve, and /api/ledger each compile into their own serverless function. They don't share memory at all; I'd built four backends that couldn't talk to each other and only noticed because I happened to test the full sequence with curl instead of clicking through the UI once and calling it done.
I merged all four into a single dynamic route, /api/action/[type], so at least they'd be the same function. That closes most of the gap, since a single function's warm instance does hold state between requests, but it isn't a real fix. Vercel doesn't guarantee that every request lands on the same warm instance. A genuinely production-grade version of this would need actual persistent storage: Vercel KV, DynamoDB, anything that isn't a Map in Lambda memory. For a demo meant to show off reasoning, not infrastructure, I decided that was an honest place to stop. The live version is a simulation, not a payment backend.
Try it
The demo runs in English and Japanese, and it's live at schrodinger-payment.vercel.app. It's a Next.js app with API routes calling Claude through Amazon Bedrock's Converse API, deployed on Vercel with a scoped IAM user that can only invoke Bedrock models and nothing else. Every screenshot above was captured against the real deployment, agent reasoning included. Nothing here is scripted.
If you've read the AWS article this is riffing on, Pattern A (three-layer keepalive alignment) is the one this whole thing reproduces. Patterns B through D, covering graceful maintenance windows and tenant isolation, are still on my list to turn into something equally over-engineered.








Top comments (0)