An Airline in the Chat Window: Building an Agentic Commerce Accelerator on ACP and MCP
We thought the hard part of agentic commerce was teaching an AI to sell a flight: search, seats, add-ons, payment, ticket. It wasn't: the final commit on the repository reads "add critical payment URL handling instructions to booking and checkout tools," because the hard part was teaching the model to hand the customer the bill.
The Bet
Two protocols landed in the same window. OpenAI shipped the Agentic Commerce Protocol and Instant Checkout, letting ChatGPT discover and buy from merchants inside a conversation. Anthropic shipped the Model Context Protocol, letting Claude reach external systems through tools. Both point at the same future: the chat window becomes a place where people search and buy.
We didn't want to write a take about it. We wanted working code. So we built an agentic commerce accelerator (an internal platform of ours, not client work) designed to drop a merchant of almost any vertical (airlines, hotels, retail) into those chat surfaces. ChatGPT enters through ACP; Claude enters through MCP. We don't know how big this channel gets. The way to find out where it breaks is to build the hardest version and watch what cracks.
What We Actually Built
The deepest vertical is airline booking, because a flight is close to the hardest commerce object there is. Not a "find me a flight to Goa" demo. The real domain. One-way, round-trip, and multi-city itineraries modeled as flight segments. Per-aircraft seat maps with premium and exit-row pricing. Passenger types (adult, child, infant), each carrying passport details, frequent-flyer numbers, and special requests. Add-ons across baggage, meals, priority boarding, lounge access, and insurance, plus bundled value packs. PNRs, 6-character confirmation codes, 13-digit ticket numbers. Post-booking modifications, cancellations, refunds, e-ticket PDFs. If a real carrier's app does it, the accelerator models it.
The stack is deliberately boring, because boring is what survives production. Node 22 and TypeScript, Fastify 5 for the API, Prisma over PostgreSQL 15 for bookings, Redis 7 with Redlock for distributed seat locks, BullMQ for background jobs, the Stripe SDK for payments, AWS SES for e-ticket delivery, Pino for logs, Vitest for tests. The MCP server runs on the official @modelcontextprotocol/sdk, validates every input with zod, and speaks both stdio and Streamable HTTP.
The shape that matters is the two doors. ChatGPT comes in through the ACP REST surface. Claude (or any MCP client) comes in through the MCP server. Both doors hit identical guarantees, because the guarantees live in one place underneath them.
This is our second run at agentic commerce. A 2025 retail discovery MVP on AWS taught us the operational gaps: metering, security, retrieval quality. This accelerator is the protocol-first rebuild, and we went airline-deep on purpose. If the architecture holds for flights, hotels and retail are downhill from there.

The First Attempt: ACP in a Weekend
The first pass was a 2-product in-memory store: a catalog endpoint, a cart, an order endpoint, a /.well-known/acp.json discovery file, and a payment link minted through Stripe or Razorpay with a mock URL as fallback. It demoed beautifully. You could talk to it, add a thing to a cart, and get a checkout link back.
What it didn't have is exactly the list the real protocol demands. No idempotency. No webhook signature verification. No checkout-session lifecycle. No delegated payments. No MCP server at all. It was a storefront-shaped object, not a protocol implementation.
A weekend gets you a demo. The protocol surface that survives a real AI assistant is roughly 10x that, not 10x the cleverness, 10x the surface area. The interesting work is all in the parts a demo lets you skip.
The Checkout Session Is a State Machine
Here is what Instant Checkout actually asks a merchant to stand up. Checkout-session endpoints: create, retrieve, update, complete, cancel. A discovery document at /.well-known/acp.json. A delegate_payment endpoint that mints single-use vault tokens. Stripe webhooks with signature verification. On Fastify that means preserving the raw request body before any parser touches it, a gotcha that costs an afternoon the first time. And a required header set on every call (API-Version, Idempotency-Key, Request-Id, an RFC 3339 Timestamp), all echoed back on the response.
The center of it is the checkout session, and the session is a state machine: not_ready_for_payment → ready_for_payment → in_progress → completed | canceled. We enforce it with an explicit transition map where terminal states have empty transition lists. The model can ask for anything it likes; the machine only permits legal moves. You cannot complete a session that was never ready, and you cannot revive one that was canceled.
Then the boring, load-bearing rules. Idempotency keys are UUIDs of at least 16 characters, held for 24 hours. A replayed key returns the existing session instead of booking a second seat: the difference between a flaky network and a double charge. Sessions expire after 15 minutes and return 410 Gone afterward. Every amount is stored in minor units (paise, cents), never floats. Totals travel as an array with display text the model can read aloud, like "Tax (18% GST)."
None of this is visible to the model. The conversation is the easy surface. Agentic commerce correctness lives below it, in the state machine the assistant never sees.
The state machine, the idempotency rules, and the 15-minute clock each earned their place by preventing one specific double-booking.
12 MCP Tools and One Humbling Commit
The MCP server exposes 12 tools, and naming them is the fastest way to show the funnel: search_flights, search_multi_city_flights, get_flight_details, check_flight_availability, get_available_seats, get_addons, create_booking_checkout, select_seats, get_checkout_session, complete_booking, check_payment_status, and get_order_details. Search to ticket, every step a tool.
The architectural decision underneath them: the MCP server contains no business logic. It's a thin client over the REST API, so the state machine, the seat locks, and the idempotency rules apply identically whether the caller arrived as ChatGPT through ACP or as Claude through MCP. Two doors, one set of invariants. Zod validates every tool input; stdio serves local clients and Streamable HTTP serves remote ones.
Then the humbling part. Everything passed the JSON-RPC test script. Tools returned correct data, sessions advanced correctly, payments completed in test mode. But in live conversation the model would complete a checkout and simply not show the customer the payment link. It had done its job, the booking was assembled. Why mention a URL? The fix, and the literal last commit on the repository, was adding explicit payment-URL handling instructions to the booking and checkout tool descriptions. A tool description is not documentation: it's user-interface copy aimed at a model. Anything you need the assistant to surface to the human has to be written into the tool contract, because the model treats the description as its instructions.
One more layer the demo skipped: auth. Each platform gets its own bearer key (separate credentials for OpenAI, Claude, and internal callers), an optional CIDR allowlist for OpenAI's ranges, rate limiting, and the usual helmet and CORS hardening. The caller is never the end user. It's a platform acting on a user's behalf, and the trust model has to say so.
The MCP spec tells you what the protocol does. It doesn't tell you how assistants actually drive your tools, or why ChatGPT and Claude invoke them differently: MCP Protocol in Practice.
Product Feeds Assume Products Sit Still
Discovery in ChatGPT runs on product feeds, and the feed spec is built for retail. You ship shards (Parquet, gzipped JSONL, or CSV) capped around 500k items, refreshed daily at minimum. Each item carries the expected fields: id, title, description, URL, brand, image, a price with an ISO 4217 currency, an availability enum, a seller name, target countries. Eligibility flags decide whether an item can be searched, checked out, or advertised. Seller terms and a privacy policy are required before checkout is allowed. Variants hang off a group id. A Feeds and Promotions API exists as the push alternative to file drops.
That model quietly assumes your products sit still. A lamp is a lamp tomorrow. An airline's "product" is a fare on a specific flight-date under continuous pricing. It may not survive the gap between two feed refreshes, and its availability isn't an enum: it's a decaying curve that bottoms out the moment the flight departs. A daily snapshot of perishable inventory is stale before it lands.
Our answer (and this is design thinking, not shipped code; the accelerator doesn't publish feeds) is to split the layers. The feed carries only the durable objects: routes, fare families, the brand, the booking entry point. Real-time truth enters at session time, through the search and availability tools, with a price re-validation the moment the checkout session is created. The feed makes you discoverable; the tools make you correct. Conflate the two and an assistant ends up confidently selling a seat that sold an hour ago.
What do you put in a daily feed when your inventory expires in minutes?
Fronting a Navitaire-Backed Carrier
The accelerator runs on seeded inventory. The obvious next question is what it takes to put a real carrier behind it, so we did the integration design against Navitaire New Skies, the passenger service system class behind a large share of the world's low-cost carriers. To be clear about what this section is: this is design, not shipped code. We have not connected a live carrier. We worked out what connecting one would demand.
The central clash is sessions. New Skies is stateful. You assemble a booking server-side in what it calls "booking-in-state," holding inventory as you add passengers, seats, and ancillaries, then commit. Those sessions time out in 15–20 minutes, and when a hold expires the inventory is released with no webhook to tell you; you find out by polling PNR status. ACP, meanwhile, hands you its own 15-minute checkout session with its own clock. The real design problem is reconciling two session lifecycles that tick independently: map the ACP states onto the PSS booking states, and decide exactly what happens when the carrier's hold dies 40 seconds before the assistant's complete call arrives. That hold-expiry race is the integration in miniature.
The rest of the gotchas rhyme with it. Availability calls against a PSS are expensive, so carriers run caching layers your integration has to respect rather than hammer. Ancillary pricing is dynamic, which forces a price re-validation between the moment a fare is shown and the moment it's paid. Seat assignment is a concurrency problem with real money on it, which is exactly why distributed seat locks already exist in the accelerator. Fare rules arrive as opaque codes that need mapping tables to become human-readable. Sandbox and certification environments are vendor-provisioned, on the vendor's calendar. And version upgrades are multi-month efforts: one Indian carrier's publicly documented PSS upgrade reportedly ran around 10 months and thousands of test scenarios. Whatever you build here inherits that cadence.

A stateless protocol in front of a stateful reservation system is two clocks that disagree.
Payments in India: RBI, UPI, and GST
ACP's payment model is clean. A Stripe Shared Payment Token (scoped, single-use, time-limited) is handed from the assistant to the merchant, who charges it server-side. We implemented delegate_payment to mint exactly that kind of single-use vault token, and in Stripe's test mode it behaves precisely as the spec promises.
Then you bring it to India. The RBI tokenization mandate means merchants can't store raw card data outside the networks and issuers. Card transactions require a mandatory second factor of authentication. And UPI, which dominates digital payment volume here, has no delegated-token equivalent at all; it's an app-to-app or QR handoff, not a token you charge in the background. A delegation model designed around a US card stack doesn't map cleanly onto any of that.
The practical path runs through PSP-native flows (Razorpay, PayU, Juspay) and payment links or a UPI handoff. That's why the accelerator implements both delegate_payment and a payment-URL flow: in an Indian checkout, the link the model surfaces to the customer often is the checkout. Build only the delegated-token path and you've built for a market you're not in.
GST is the other half, and it's a schema problem, not an afterthought. A GSTIN has to be captured at booking time. Passenger air transport sits under SAC 996425 for scheduled services, e-invoicing obligations apply, and most carriers won't let you add a GSTIN retroactively. The checkout session needs a GSTIN field before payment, not bolted on after. It's why the totals array carries readable tax lines in the first place. Underneath it sits an unresolved commercial question the protocols don't answer: when a purchase happens inside someone else's chat window, who is the merchant of record: the airline, with the platform as a technology provider, or the platform itself, as a reseller? That one isn't an engineering problem, and it's worth saying out loud.
The most interesting payments problem in agentic commerce isn't fraud: it's jurisdiction.
Honest Status: Protocol-Complete, Demo-Tested, Not Live
What's proven: the full ACP surface Instant Checkout requires is implemented and exercised: checkout sessions, delegated payments, the discovery document, signed webhooks. The MCP server is driven by real MCP clients and by a full JSON-RPC booking-flow test script that runs search through ticket. Unit, integration, and end-to-end suites run on Vitest.
What's not: there is no live ChatGPT merchant listing. That requires merchant approval and conformance testing, which is a process, not a line of code. There is no live carrier or PSS connection; that work is the integration design above, not a running link. Flight data is seeded. Stripe runs in test mode. The multi-tenant scaffolding currently defaults to a single tenant.
We publish this anyway because the protocol surface is the durable asset. Listings open and close, approval queues move, specs get revised. The state machines, the idempotency rules, the tool-description discipline and the dual payment paths all hold regardless of when any particular merchant program lets you in.
What This Teaches
The protocol surface is 10x the demo. Idempotency, signature verification, session lifecycle, delegated payments: every one of them is invisible in a weekend demo and mandatory in production shape. Budget for the parts the demo let you skip.
Put the guarantees below the protocol layer. One REST core owns the state machine, the locks, and the idempotency guarantees; ACP and MCP are thin doors onto it. Two protocols, one set of invariants, is the only version of this that stays correct as surfaces multiply.
Tool descriptions are user interface. The model reads the description as its instructions. Anything the assistant must surface to the human (a payment link, a warning, a next step) belongs in the tool contract.
Specs encode someone's business model. ACP's feed model assumes retail SKUs that sit still; airlines sell perishable fares under continuous pricing. Read a spec by working out whose business it was written for, then design around the gap.
Payments are jurisdictional. Delegated tokens work in a US card stack. In India, RBI rules and UPI's dominance mean you ship the payment-URL path too. There is no global checkout; there are local ones you have to build for.
If you're weighing an agentic channel of your own, start with three questions:
- If an AI assistant drove a purchase on your platform today, which endpoint breaks first, and would you learn about it from a log, or from a customer?
- Which of your "products" survive a daily feed refresh, and which expire in minutes?
- Who is the merchant of record when the buyer never visits your site?
The protocols will keep changing. The invariants underneath them won't.
Content on this page may not be reproduced, distributed, or republished without prior written permission. Sharing links is encouraged. See our Terms of Use for details.