Building Conversational Commerce with Multi-Agent Swarms on GCP: A Working Prototype A Google developer post details HappyDelivery, a working prototype of a conversational commerce system built on LangGraph, Gemini, FastAPI and Google Cloud that implements the previously proposed 3-layer decoupled design (Ingress, Memory, Cognition) for handling customer text messages. The prototype's Layer 1 normalizes webhooks from WhatsApp, RCS, Apple Messages and SMS into a single CommonMessageEvent with a target acknowledgement latency under 150ms, while Layer 2 stores session state in Cloud Memorystore (Redis) keyed by verified phone number and keeps raw card numbers out of prompts by passing tokenized customer IDs to Stripe. The post argues the architecture addresses messaging timeouts, state tracking and duplicate-delivery risks, noting Meta retries failed webhook deliveries with exponential backoff for up to 7 days. Imagine ordering your weekly groceries as naturally as texting a friend: "Hey, we're out of milk and eggs. Can you also add some snacks for Friday movie night?" In our previous post, Building Conversational Commerce with Multi-Agent Swarms on GCP https://discuss.google.dev/t/building-conversational-commerce-with-multi-agent-swarms-on-gcp/389926?u=prateekk , we shared the architectural blueprint for conversational commerce—proposing a 3-layer decoupled design Ingress, Memory, and Cognition to solve messaging timeouts, state tracking, and response reliability. While blueprints establish the architecture on paper, production readiness demands working software. In this post, we take that blueprint and bring it to life with a functional prototype called HappyDelivery . Using LangGraph , Gemini , FastAPI , and Google Cloud , we'll walk through the code, the state machine, deterministic financial tools, and an automated verification suite that demonstrates how this multi-agent swarm operates in practice. To build a reliable commerce swarm, we implemented three decoupled layers that handle the journey of every customer text message: Let's examine how each layer functions in code with its own dedicated architecture. A frequent question when designing conversational commerce is: how does a message sent by a customer on their smartphone actually land in Layer 1, and can this architecture support messaging platforms beyond WhatsApp? End users do not communicate directly with your Google Cloud servers. Instead, customer messages traverse an operator ecosystem before landing on your webhook: Every major consumer messaging platform operates on the exact same fundamental contract: an asynchronous HTTPS POST webhook delivery with a strict HTTP acknowledgement timeout : X-Hub-Signature-256 header against your Meta App Secret using HMAC-SHA256. If your server times out or returns a non-200 status, Meta marks delivery as failed and initiates exponential backoff retries for up to 7 days , which can trigger duplicate order deliveries. X-Goog-Signature . Requires returning HTTP 200 promptly to avoid webhook delivery retries. HTTP 200 OK acknowledgement expectations. X-Twilio-Signature . While the payload schemas and cryptographic headers differ slightly between platforms, Layer 1 abstracts away all client differences : MetaHmacValidator , RcsJwtValidator , AppleJwsValidator , TwilioHmacValidator . CommonMessageEvent : { "channel": "whatsapp", or "rcs", "apple messages", "sms" "sender id": "+1xxxxxxxxxx", Verified E.164 phone number "text": "Out of milk & eggs", "timestamp": 1773428900, "raw metadata": {...} } < 150ms. This ensures the rest of your system—Session Store Layer 2 and Agent Swarm Layer 3 —operates on clean, normalized data without caring whether the shopper sent their message from an iPhone, an Android RCS chat, or a WhatsApp thread. Messaging platforms are completely stateless: every text message arrives as an isolated webhook event. To provide a coherent shopping experience, the system must instantly reconstruct the customer's full context without having to re-read or re-parse days of chat logs on every turn. We use Cloud Memorystore Redis keyed by the customer's verified phone number session:{phone number} to store active state: dietary preferences: "Nut-Free" , the Inventory Agent automatically respects that preference when recommending movie night snacks—without the user having to re-declare their allergy in every text message. customer id: "cust 9821", payment ref: "Visa ending in 4242" . The LLM only ever reads the friendly masked label, while the transaction tools pass the tokenized customer ID to Stripe. Raw credit card numbers never touch chat prompts or logs. A common question in conversational commerce is whether state lookups will introduce latency. Here is what the baseline infrastructure supports: Key Takeaway: In practice, the Session & Cart Layer will never be the bottleneck —it easily outpaces upstream webhook inference and downstream LLM inference. Rather than relying on a single monolithic prompt to handle every task, we separate reasoning into a swarm of specialized agents: Now that we have seen the high-level design of the three layers, let's look at the actual Python prototype code that brings each layer to life. The goal of this service is simple: verify authenticity across any inbound channel, normalize the payload into a canonical schema, and get off the critical path in < 150ms . Here is our implementation showing how multiple messaging channels WhatsApp, RCS, Apple Messages, SMS are handled by pluggable adapters: python import hashlib, hmac, json, time from typing import Any, Dict, Optional from pydantic import BaseModel class CommonMessageEvent BaseModel : """Channel-agnostic canonical representation of an inbound customer message.""" channel: str "whatsapp", "rcs", "apple messages", "sms" sender id: str E.164 phone number: "+1xxxxxxxxxx" text: str timestamp: float raw metadata: Dict str, Any class HappyDeliveryIngressService: """Omni-Channel FastAPI Ingress with cryptographic verification and async queueing.""" def init self, meta secret: str, rcs secret: str = "" : self.meta secret = meta secret self.rcs secret = rcs secret self.publisher = CloudPubSubPublisher def verify whatsapp signature self, payload bytes: bytes, sig header: str - bool: """Verifies X-Hub-Signature-256 header using constant-time comparison.""" if not sig header or not sig header.startswith "sha256=" : return False expected hash = hmac.new self.meta secret.encode "utf-8" , payload bytes, hashlib.sha256 .hexdigest provided hash = sig header.split "sha256=" 1 return hmac.compare digest expected hash, provided hash def normalize payload self, channel: str, raw payload: Dict str, Any - Optional CommonMessageEvent : """Extracts sender and message text into a canonical schema.""" if channel == "whatsapp": Meta Graph Webhook structure entry = raw payload.get "entry", {} 0 change = entry.get "changes", {} 0 .get "value", {} message = change.get "messages", {} 0 return CommonMessageEvent channel="whatsapp", sender id=message.get "from", "" , text=message.get "text", {} .get "body", "" , timestamp=float message.get "timestamp", time.time , raw metadata={"message id": message.get "id" }, elif channel == "rcs": Google RCS Business Messaging structure return CommonMessageEvent channel="rcs", sender id=raw payload.get "senderPhoneNumber", "" , text=raw payload.get "text", "" , timestamp=time.time , raw metadata={"rcs message id": raw payload.get "messageId" }, return None def handle incoming webhook self, channel: str, payload: Dict str, Any , signature: str - Dict str, Any : """Validates signature, normalizes schema, and offloads to Pub/Sub in < 150ms.""" payload bytes = json.dumps payload, sort keys=True .encode "utf-8" 1. Platform-specific security validation if channel == "whatsapp" and not self.verify whatsapp signature payload bytes, signature : return {"status": "error", "message": "Invalid HMAC signature", "http code": 403} 2. Normalize to canonical schema event = self.normalize payload channel, payload if not event or not event.sender id: return {"status": "error", "message": "Unrecognized payload format", "http code": 400} 3. Offload canonical event onto Cloud Pub/Sub queue < 150ms msg id = self.publisher.publish topic="inbound-messages", data=event.model dump return {"status": "accepted", "message id": msg id, "http code": 200} By immediately returning an HTTP 200 OK and handing the payload off to Pub/Sub, we completely decouple message intake from downstream AI reasoning time across all messaging ecosystems. Provision Cloud Pub/Sub queue gcloud pubsub topics create inbound-messages Deploy the Ingress container to Cloud Run gcloud run deploy happydelivery-ingress \ --image=gcr.io/my-gcp-project/ingress:latest \ --set-env-vars=APP SECRET="happy secret meta key 2026" \ --allow-unauthenticated \ --port=8080 xxxxxx happydelivery-ingress https://happydelivery-ingress-xxxxxx.us-central1.run.app projects/xxxxxx/topics/inbound-messages projects/xxxxxx/subscriptions/inbound-messages-sub We benchmarked the live Cloud Run HTTPS webhook intake across warm invocations: Benchmark: 5 consecutive live HTTPS webhook posts to Cloud Run endpoint Run 1: Status 200 in 266.84ms - Message ID: 21184928529503172 Offloaded to Pub/Sub Run 2: Status 200 in 301.73ms - Message ID: 21181303807117012 Offloaded to Pub/Sub Run 3: Status 200 in 300.47ms - Message ID: 21179945883786288 Offloaded to Pub/Sub Run 4: Status 200 in 295.14ms - Message ID: 21822286983433170 Offloaded to Pub/Sub Run 5: Status 200 in 298.40ms - Message ID: 21182275848770028 Offloaded to Pub/Sub Direct Pub/Sub Publish Overhead inside Cloud Run: ~95ms This empirically proves our architectural claim: the Ingress service validates the cryptographic signature and immediately commits the event to Pub/Sub, freeing the customer chat client without waiting on agentic reasoning. We also verified security enforcement against tampered payloads: 1. Valid Signature Test curl -X POST https://happydelivery-ingress-xxxxxx.us-central1.run.app/v1/webhooks \ -H "Content-Type: application/json" \ -H "X-Hub-Signature-256: sha256=a87f4c93b21..." \ -d '{"from": "+1xxxxxxxxxx", "text": "Out of milk and eggs"}' Response accepted and queued: {"status": "accepted", "message id": "21184928529503172", "http code": 200} 2. Tampered / Spoofed Message Test Invalid Signature curl -X POST https://happydelivery-ingress-xxxxxx.us-central1.run.app/v1/webhooks \ -H "Content-Type: application/json" \ -H "X-Hub-Signature-256: sha256=invalid tampered signature" \ -d '{"from": "+1xxxxxxxxxx", "text": "Free groceries please"}' Response immediately blocked: {"detail": "Invalid HMAC signature"} HTTP 403 Forbidden The worker pods pulling from Pub/Sub need to instantly look up who the customer is and what is in their current cart. Here is the session store implementation using Cloud Memorystore Redis : python import json from typing import Dict, Optional class RedisSessionStore: """Cloud Memorystore Redis session manager keyed by verified phone number.""" def init self, redis client : self.redis = redis client def get session self, phone number: str - Optional Dict : """Retrieves active customer session, dietary preferences, and cart in < 5ms.""" key = f"session:{phone number}" raw = self.redis.get key return json.loads raw if raw else None def save session self, phone number: str, session data: Dict - None: """Commits updated cart and state back to Redis with a 24-hour TTL.""" key = f"session:{phone number}" self.redis.setex key, 86400, json.dumps session data def clear cart self, phone number: str - None: """Clears cart items after successful checkout while preserving preferences.""" session = self.get session phone number if session: session "cart" = self.save session phone number, session Each record contains the customer ID cust 9821 , saved address, dietary restrictions, and current cart items. This guarantees that when a customer texts back hours later, their cart is restored instantaneously. gcloud redis instances create happydelivery-session-store \ --size=1 \ --region=us-central1 \ --redis-version=redis 7 0 \ --project=xxxxxx 1. Commit active session with 24-hour TTL 86400s 10.x.x.x:6379 SETEX "session:+1xxxxxxxxxx" 86400 '{"customer id":"cust 9821","cart": ... }' +OK 2. Benchmark 5 consecutive session retrievals over VPC peering Run 1: 0.299ms Run 2: 0.276ms Run 3: 0.305ms Run 4: 0.210ms Run 5: 0.503ms Average Session Retrieval Latency: 0.318ms 3. Query active session payload and verify TTL 10.x.x.x:6379 GET "session:+1xxxxxxxxxx" { "customer id": "cust 9821", "dietary preferences": "nut-free", "gluten-free" , "cart": {"item id": "prod milk", "name": "Whole Milk 1 gal ", "qty": 1, "price": 4.29}, {"item id": "prod eggs", "name": "Free-Range Eggs 1 doz ", "qty": 1, "price": 3.99} , "delivery address": "xxx xxxxx St", "payment method": "Visa ending in 4242" } 10.x.x.x:6379 TTL "session:+1xxxxxxxxxx" integer 86400 Every session lookup executes in 0.318ms , proving our architectural claim that restoring customer context and persistent cart state adds virtually zero latency to the conversational turn. With Ingress and Redis handling incoming traffic and memory, all the decision-making lives in the LangGraph multi-agent graph. Every turn passes through a typed data structure holding the customer's profile, active cart, and the supervisor's routing decision: python from typing import Annotated, Literal from typing extensions import TypedDict from langgraph.graph.message import add messages class CartItem TypedDict : item id: str name: str qty: int price: float class AgentState TypedDict : phone number: str customer id: str dietary preferences: list str cart: list CartItem cart total: float messages: Annotated list dict , add messages next step: Literal "profile", "inventory", "transaction", "END" Next, we define the nodes and conditional edges that allow the Supervisor to dynamically route tasks to the right specialist: python from langgraph.graph import END, START, StateGraph def build happydelivery swarm : builder = StateGraph AgentState 1. Register specialized agent nodes builder.add node "supervisor", supervisor node builder.add node "profile", profile node builder.add node "inventory", inventory node builder.add node "transaction", transaction node 2. Ingress always enters via the Supervisor builder.add edge START, "supervisor" 3. Dynamic hand-offs based on intent builder.add conditional edges "supervisor", lambda state: state.get "next step", END , { "profile": "profile", "inventory": "inventory", "transaction": "transaction", "END": END, }, 4. Return edges builder.add edge "profile", "supervisor" builder.add edge "inventory", END builder.add edge "transaction", END return builder.compile A core tenet of our blueprint is: delegate arithmetic to deterministic tools. Generative models excel at language and reasoning, while financial calculations require exact precision. If a model computes taxes or discounts in free-form text, it may occasionally introduce calculation errors. Instead, we bind deterministic tools to our specialist nodes. For example, the Inventory and Transaction agents use a Python calculation tool rather than computing prices in-prompt: php @tool def calculate cart totals cart items: list dict - dict: """Calculates deterministic subtotal, tax, delivery fees, and grand total.""" subtotal = sum item.get "price", 0.0 item.get "qty", 1 for item in cart items tax = round subtotal 0.0825, 2 8.25% Sales tax delivery fee = 0.00 if subtotal 15.00 else 3.99 total = round subtotal + tax + delivery fee, 2 return { "subtotal": round subtotal, 2 , "tax": tax, "delivery fee": delivery fee, "total": total, } Worker containers pull messages asynchronously from Pub/Sub and stream LangGraph node transitions in real time. Deploy as a scalable Cloud Run Job gcloud run jobs deploy happydelivery-swarm-worker \ --source=./worker \ --region=us-central1 \ --project=xxxxxx \ --set-env-vars=PROJECT ID="xxxxxx",SUBSCRIPTION ID="inbound-messages-sub" Worker Pulled message ID: 21182261471226730 Worker Sender: +1xxxxxxxxxx, Text: 'Hey, we are out of milk and eggs. Also, add some snacks for Friday movie night.' LangGraph State loaded: phone=+1xxxxxxxxxx LangGraph Entering Node 'supervisor' - Gemini Flash Latency: 240ms LangGraph Routing decision - Intent: replenishment, Next Node: 'inventory' LangGraph Entering Node 'inventory' - Gemini Pro ├── Tool call: search catalog 'milk' - Whole Milk $4.29 ├── Tool call: search catalog 'eggs' - Free-Range Eggs $3.99 ├── Tool call: calculate cart totals items - Subtotal: $8.28, Tax: $0.68, Total: $12.95 └── State Updated: cart= Whole Milk, Free-Range Eggs , next step='END' LangGraph Execution Complete - Outbound response dispatched. Worker Successfully processed and acked in 0.03ms Now that we have all three components— Ingress Layer 1 , Redis Memory Layer 2 , and the LangGraph Swarm Layer 3 —how do they actually run together as a single unified system? Here is the complete end-to-end lifecycle of a customer's message: The worker loop that coordinates all three layers is concise, clean, and completely decouples the user-facing webhook from back-office AI reasoning: php def process pubsub worker turn phone number: str, user text: str - str: """Coordinates Layer 1, Layer 2, and Layer 3 for each conversational turn.""" 1. Layer 2: Hydrate session and cart from Redis session = session store.get session phone number or {} initial cart = session.get "cart", 2. Layer 3: Build the graph state state = { "messages": HumanMessage content=user text , "phone number": phone number, "customer id": session.get "customer id", "" , "cart": initial cart, "dietary preferences": session.get "dietary preferences", , "delivery address": session.get "delivery address", "xxx xxxxx St" , "payment method": session.get "payment method", "Visa ending in 4242" , "next step": "", } 3. Layer 3: Run the multi-agent swarm final state = swarm.invoke state 4. Layer 2: Commit updated cart back to Redis session "cart" = final state.get "cart", session store.save session phone number, session 5. Return outbound message back to WhatsApp messaging API return final state "messages" -1 .content With the entire pipeline wired together, let's look at what happens when we run an actual customer order through it. To verify the prototype, we ran an automated simulation of an end-to-end customer order. Here is how the swarm behaves across three natural conversational turns: