July 30, 2026/1710 words/9 min read
Building Agentic Commerce in a Weekend
Giving ChatGPT a safe way to pay.
I manage Verifone Factoring at work, a BNPL product. It is one of the cleverest pieces of payments tech I have ever worked with. A merchant first stores a customer's card or SEPA direct debit mandate and gets back a proprietary token for that payment method. When the customer buys something later, the merchant sends that token in an API call. There is no need to collect the card or mandate again.
The lifecycle API is what makes it special. For a card payment, the merchant can authorize it, capture it, retrieve its current state and refund it through separate endpoints. A SEPA payment works against the saved mandate instead and invoice payments are available through the same API surface. Everything is abstracted away and each operation takes a known reference and returns a payment state, so the merchant always has something concrete to inspect or act on next.
Making a payment is an API call, exactly what makes the product interesting for an agent.
For this experiment, I only need four payment operations, authorize, capture, get status and refund. The caller works with references and proprietary payment-method tokens, the product handles the payment details and lifecycle. That makes it a boring machine for moving money, which is one of the nicest things one can say about payments infra. A client asks for a specific operation, the product enforces the rules around it, then the resulting state can be inspected later. It had this shape long before anyone called software agentic!
The idea for the experiment came while I was watching a developer on Twitch build something similar. He had a catalogue and he had given an agent tools. He got the agent almost close enough to a complete purchase, but it kept failing when it came to the payment itself. He was doing the right things with the wrong product, it felt like I was watching someone hammer a nail with a shoe. Sure, with enough effort and hammering technique, the nail will eventually go in, but it's difficult and wasteful and not very secure.
Thing is, I already have a hammer! Stored instruments mean an agent does not need to collect payment credentials, and tokens keep those credentials out of the model's context. Authorize, capture, refund and get status already look suspiciously like agent tools. I just wanted to see how little I needed to put between the product and ChatGPT, so I built a small commerce platform around our test environment. The platform has a catalogue, a customer with a saved test card and enough of an order model to connect a product to a payment. I then added a few products and wrote the remote MCP server to expose four commerce operations.
search_products(query, maximum_price)
get_product(product_id)
prepare_order(product_id, quantity)
pay_order(order_id)I connected the MCP server to ChatGPT through developer mode. OpenAI surfaces these connections under ChatGPT Plugins, which makes the naming a little confusing. I did not even have to publish a plugin. ChatGPT connected directly to my remote MCP server, discovered its tools and could call them from an ordinary conversation.
Then I gave ChatGPT the request below.
Find me a laptop stand from the shop. Keep it under €50.
It searched the catalogue, picked a stand within budget and prepared an order. It showed me the product and final amount, then waited for approval. I approved it. ChatGPT called pay_order, the platform resolved the saved test card, then the order came back paid.
It was a fucking breeze!
The ease comes from the work below the model, ChatGPT only has to deal with the intent, products and orders. Since Verifone Factoring supplies a narrow, tokenized way to move money, the model never needs to know how card or direct debit processing works.
I wrote the whole thing, MCP server included, in TypeScript. I considered Rust, I like it, but making a small commerce demo borrow-check its way to completion would have been painful.
Each MCP tool is just a Zod schema describing its inputs plus a handler, and TypeScript lets me use the same types for database rows, API payloads and those schemas. That removes a whole category of translation mistakes around amounts, currencies, payment states and so on. Money can just stay an integer number of minor units paired with an ISO currency code, and no floating-point prices travel between tools.
I also used Hono instead of Next.js because there is barely a website there. The useful surface is a catalogue API, an MCP endpoint and the payment webhook, so with Hono I can put all three in one small Cloudflare Worker without bringing a rendering framework into a project that has almost nothing to render.
Cloudflare was the experiment inside the experiment. I normally reach for Vercel, almost by instinct at this point, but Workers fit a remote MCP server way better. A Worker has a public HTTPS endpoint, starts quickly and needs no application process kept alive. The /mcp route uses Cloudflare's createMcpHandler with Streamable HTTP, with the four commerce tools registered on it. OAuth maps the ChatGPT connection to my test customer, while the Verifone Factoring API credentials stay in Worker secrets.
I also designed the MCP layer to be stateless. A tool call can not depend on whichever edge isolate happens to receive the previous one. Products, customers and orders live in D1. A Durable Object, keyed by order ID, coordinates each payment. That last decision ended up being a really good call.
I kept arbitrary amounts out of the tool surface as well. All ChatGPT has to do is search products by criteria, then pass a product reference to the platform. The platform loads the current price, calculates the total and returns an order summary. The model can choose a product, but it has no way to decide what that product costs.
pay_order accepts an order ID, not an amount, currency and free-text description. The Worker loads the order, checks that it is still payable and resolves the customer's saved payment-method token outside the model's context. It then routes the request to the Durable Object for that order.
Every attempt to pay the same order reaches the same coordinator. Before calling Verifone Factoring, the object moves the order from ready to processing and stores an idempotency key. If ChatGPT calls the tool twice, or two requests fire at nearly the same time, the second request finds the existing attempt instead of creating another one.
The object calls the payments API and writes the payment reference back to D1. Later webhook updates go through the same object, giving authorization responses and asynchronous state changes one place to meet.
ChatGPT never sees the card details and it also cannot change the amount between presenting the order and paying it. The payment stays attached to an order the platform priced and I approved.
"Buy me a laptop stand" hides several decisions. ChatGPT has permission to search the catalogue and discretion to choose a product within my budget, but needs my approval before paying for the order. One unrestricted buy_something tool would have made the demo shorter, but the payment model much worse.
The platform has to enforce those boundaries even when the prompt describes them. "Do not spend more than €50" helps ChatGPT search, but the server still has to reject an order over €50. Asking the model to request approval is useful, but a production payment flow needs evidence of that approval outside the model's own claim that it received one.
OpenAI's MCP guidance treats approval as part of a tool call and recommends it for sensitive actions. That protects the moment before the tool runs. The payments platform still has to carry the approved amount, order and payment method through the financial operation.
A saved card token answers which instrument the platform may use, but does not prove that I want this product, from this merchant, for this amount, now. The merchant API credential identifies the platform calling Verifone Factoring. It does not prove what authority I delegated to ChatGPT. A production system needs evidence for each of those facts.
Google's Agent Payments Protocol uses signed mandates to describe that authority. Stripe's Shared Payment Tokens bind agentic payments to limited-use credentials without exposing the underlying payment method. Their implementations differ, but both separate access to a payment method from permission to make a particular purchase.
Agents also really love trying again after a tool returns an error. Payments contain plenty of responses that mean the request may have succeeded, even though the caller never received a clean answer. Retrying an authorization with a new idempotency key can create a second hold. Retrying a capture before checking state is even worse.
The Durable Object has to own that recovery logic, so the same order always produces the same idempotency key. If the payment call times out, the object checks the existing payment before attempting another operation. ChatGPT receives a structured result such as paid, declined or processing. It can report the state, but it cannot decide whether to repeat a financial operation.
The audit record includes my instruction, the product ChatGPT selected, the order I approved, the saved-instrument reference, the idempotency key and the payment result. Support or risk can reconstruct the purchase from those facts without asking the model why it thought a laptop stand was a good idea.
After the demo, I looked at how other teams were approaching the same problem. KAMIYO starts with wallets, keeping custody with the user while giving an agent bounded spending power. Slash starts with cards, exposing card creation, spend controls and payments through MCP. I started with payment methods a merchant already has on file. All three approaches give the agent bounded authority. That should be the secure default.
The Cloudflare experiment worked better than I expected. Catalogue reads and MCP tools stay stateless and each payment order gets a small, strongly consistent coordinator when it needs one. I do not keep an MCP session alive, run a permanent application server or bolt a distributed lock onto the database.
If I take the demo further, the first thing I'd add is an authority object that carries the approved order, amount and payment method through the payment attempt and every later webhook.
That was as far as I got over one weekend, so good enough.