Build a Cross-Device Agentic-Commerce Workflow with LangGraph
On August 17-18, 2026 Alipay unveiled China's first full-stack agentic-commerce platform, letting merchants convert pages and service workflows into agent-ready Skills and MCP tools consumed through Ah Bao (10,000+ AI services, 5 smartphone brands, 16 automakers) over the AHA protocol suite and ACT 2.0 trust protocol. This dispatch builds commerce-orch, a LangGraph workflow that takes consumer intent, discovers Skills, performs a cross-provider A2A-style handshake, gates everything behind a spend-ceiling trust layer, executes payment and fulfillment, and appends every decision to a hash-chained audit ledger.
Deepak Bagada
CEO, SaaSNext
- Treat AHA as a node map: Skill Interaction Standards feed the registry, Agent Hub Access feeds the handshake, XUI feeds device-aware intent, and ACT 2.0 feeds the trust gate.
- Spend ceilings and irreversible-action lists are enforced with a LangGraph interrupt before payment, not with a polite confirmation prompt.
- Idempotency keys on every fulfillment trigger (order_id:skill_id) make retries safe and prevent double-execution.
- The audit-ledger append is a precondition for marking an order complete, not a post-hoc log.
By Deepak Bagada, CEO at SaaSNext & Principal AI Architect.
Build a Cross-Device Agentic-Commerce Workflow with LangGraph
On August 17-18, 2026, Alipay unveiled what it calls China's first full-stack agentic-commerce platform. The idea is not another assistant that answers questions — it is an execution fabric. Merchants can convert their pages, products, and service workflows into agent-ready Skills and MCP tools, and consumers reach those capabilities through Ah Bao, Alipay's consumer AI agent launched in June 2026. Ah Bao already wires up 10,000+ AI-enabled services, connects to 5 smartphone brands covering 70%+ of the Chinese market, and is integrated into 16 automakers' vehicles. The demo is the part that matters: a user on a StepFun phone simply asks their assistant to "find the nearest EV charger and order a coffee to my home." The assistant activates a charging agent and a coffee agent on Ah Bao, generates a single ready-to-confirm combined order, and the user confirms — no app switching, no manual copy-paste of addresses, no juggling between a charger app and a food-delivery app.
This is where agents stop being chatbots and start being commerce middleware. But the hard part is not the demo. The hard part is the trust, negotiation, and audit machinery that makes two independent service providers execute a coordinated financial transaction without anyone trusting anyone fully. That is precisely what LangGraph is good at, because an agentic-commerce flow is a state machine with money attached — it needs explicit gates, human confirmations, and an append-only audit trail. This dispatch builds commerce-orch, a LangGraph workflow that takes consumer intent, interprets it, discovers Skills and MCP tools from a registry, performs a cross-provider A2A-style handshake, gates everything behind a trust layer, executes payment and fulfillment, and appends every decision to an audit ledger.
What the AHA protocol suite actually standardizes
Alipay's platform is built on the AHA protocol suite, and the three protocols map cleanly onto workflow nodes. Understanding them is the difference between writing a demo and writing a durable system:
- Skill Interaction Standards — the contract a merchant publishes so an agent can discover and call their capability: name, inputs, outputs, authorization scope, idempotency keys, and cancellation semantics. This is the schema your skill registry must index.
- Agent Hub Access — the discovery and connection layer. Agents register here, and other agents find them, request sessions, and exchange capability manifests. This is the registry lookup in your graph.
- Device perception & execution XUI — how the agent becomes aware of the user's device context (screen, location, nearby devices, the car's charge level) and renders a confirmable execution UI. The "nearest charger" half of the demo is XUI doing device-aware reasoning.
On top of that sits ACT 2.0, the Agentic Commerce Trust Protocol, which is the layer most engineers underestimate. It covers four things: delegation (who authorized whom to act), audit trails (every step, machine-verifiable), intent verification (the agent re-states what it understood before spending money), and payment channels (which instrument, which PSP, which settlement rails). If you are building for India, map ACT's payment-channel concept to UPI collect, RuPay cards, and net-banking intents — the protocol shape is identical, only the rails differ.
The graph at a glance
flowchart TD
A[User utterance / device event] --> B[intent_interpreter]
B --> C[skill_discovery]
C --> D[handshake_negotiation]
D --> E{trust_gate}
E -->|verify or spend above ceiling| F[HUMAN_CONFIRM
LangGraph interrupt]
E -->|verified and under ceiling| G[payment_execution]
F --> G
G --> H[fulfillment_execution]
H --> I[audit_ledger_append]
I --> J[Done - combined order receipt]
B -.intent failed.- K[clarify]
K --> B
Every node is a pure function of the graph state; every conditional edge is an explicit policy. Now let's build it.
Configuration: .env
# Registry + protocol
SKILL_REGISTRY_URL=https://registry.internal/skills
AHA_HUB_URL=https://hub.internal/a2a
ACT2_POLICY=strict
# Trust & payment
SPEND_CEILING_PER_ORDER=2500
SPEND_CEILING_DAILY=10000
HUMAN_CONFIRM_MIN_AMOUNT=1500
IRREVERSIBLE_ACTIONS=flight_booking,charge_station_reservation
# Rails (India-first)
PAYMENT_RAIL=upi
UPI_APP_ID=your_upi_app_id
UPI_COLLECT_VA=agentops@upibank
# Infra
REDIS_URL=redis://localhost:6379
LEDGER_DB_URL=postgresql://ledger:ledger@localhost:5432/ledger
LOG_LEVEL=INFO
The ceilings deserve a line of commentary: HUMAN_CONFIRM_MIN_AMOUNT forces an interrupt for anything at or above ₹1,500 even when intent is verified, and IRREVERSIBLE_ACTIONS lists capabilities that always require a human even below the threshold. A coffee is reversible; a flight booking is not.
Schemas: schemas.py
from __future__ import annotations
from enum import Enum
from typing import Any
from pydantic import BaseModel, Field
class IntentStatus(str, Enum):
PENDING = "pending"
RESOLVED = "resolved"
NEEDS_CLARIFICATION = "needs_clarification"
class ConsumerIntent(BaseModel):
utterance: str
device_context: dict[str, Any] = Field(default_factory=dict)
status: IntentStatus = IntentStatus.PENDING
normalized_entities: dict[str, Any] = Field(default_factory=dict)
class SkillMatch(BaseModel):
skill_id: str
provider: str
name: str
mcp_endpoint: str
requires_auth: bool = True
price_estimate_inr: float = 0.0
score: float = 0.0
class HandshakeState(BaseModel):
session_id: str
provider: str
capability_manifest: dict[str, Any] = Field(default_factory=dict)
consent_token: str | None = None
negotiated_terms: dict[str, Any] = Field(default_factory=dict)
handshake_status: str = "initiated" # initiated | agreed | rejected | timed_out
class TrustDecision(BaseModel):
intent_verified: bool = False
restated_intent: str = ""
spend_in_inr: float = 0.0
under_ceiling: bool = False
requires_human: bool = True
irreversible: bool = False
decision: str = "pending" # auto_ok | human_required | rejected
class OrderLine(BaseModel):
skill_id: str
provider: str
description: str
amount_inr: float
fulfillment_ref: str | None = None
class CombinedOrder(BaseModel):
order_id: str
user_id: str
lines: list[OrderLine]
total_inr: float = 0.0
payment_ref: str | None = None
status: str = "created" # created | confirmed | paid | fulfilled | failed
class AuditRecord(BaseModel):
event_id: str
order_id: str
actor: str # intent_interpreter | skill_discovery | ... | user
action: str
payload: dict[str, Any] = Field(default_factory=dict)
timestamp: str
verified: bool = True
HandshakeState mirrors the A2A handshake: the client agent posts a capability request, the provider agent replies with a manifest, and negotiated_terms records what both sides agreed to — price, SLA, cancellation window — before any money moves.
External I/O with retry: tools.py
import asyncio
import hashlib
import json
import logging
import random
from datetime import datetime, timezone
import httpx
from pydantic import ValidationError
from schemas import AuditRecord, CombinedOrder, SkillMatch, TrustDecision
log = logging.getLogger("commerce_orch")
class RetryExhausted(RuntimeError):
pass
async def with_retry(coro_factory, *, attempts=4, base=0.4, max_backoff=6.0,
retry_on=(httpx.HTTPStatusError, httpx.TimeoutException)):
"""Exponential backoff + jitter; raises RetryExhausted when attempts run out."""
for attempt in range(1, attempts + 1):
try:
return await coro_factory()
except retry_on as exc:
if attempt == attempts:
raise RetryExhausted(str(exc)) from exc
delay = min(max_backoff, base * (2 ** (attempt - 1))) * (1 + random.uniform(0, 0.3))
log.warning("retry %s in %.2fs: %s", attempt, delay, exc)
await asyncio.sleep(delay)
async def search_skills(intent: dict, client: httpx.AsyncClient) -> list[SkillMatch]:
"""Registry lookup over AHA Skill Interaction Standards manifest."""
resp = await with_retry(
lambda: client.post(f"{client.base_url}/skills/search",
json={"entities": intent["normalized_entities"]}))
return [SkillMatch(**m) for m in resp.json()["matches"]]
async def handshake(skill: SkillMatch, user_token: str, client: httpx.AsyncClient) -> dict:
"""A2A-style handshake: request session, receive capability manifest."""
async def call():
r = await client.post(f"{skill.mcp_endpoint}/a2a/handshake",
json={"skill_id": skill.skill_id, "user_token": user_token})
r.raise_for_status()
return r.json()
return await with_retry(call)
async def verify_intent_act2(intent: dict, client: httpx.AsyncClient) -> TrustDecision:
"""ACT 2.0 intent verification: model restates intent, policy checks ceilings."""
async def call():
r = await client.post(f"{client.base_url}/act2/verify",
json={"utterance": intent["utterance"],
"entities": intent["normalized_entities"]})
r.raise_for_status()
return TrustDecision(**r.json())
return await with_retry(call)
async def execute_payment(order: CombinedOrder, payment_rail: str,
client: httpx.AsyncClient) -> str:
"""Rails-agnostic: upi_collect, card, or netbanking intent all return a ref."""
async def call():
r = await client.post(f"{client.base_url}/payments/{payment_rail}",
json=order.model_dump())
r.raise_for_status()
data = r.json()
order.payment_ref = data["payment_ref"]
return data["payment_ref"]
return await with_retry(call, retry_on=(httpx.TimeoutException,))
# NOTE: HTTPStatusError is NOT retried here: a 402/decline is a business
# outcome, not a transient fault. Only timeouts warrant a re-attempt.
async def execute_fulfillment(order: CombinedOrder, client: httpx.AsyncClient) -> None:
for line in order.lines:
async def trigger(l=line):
r = await client.post(f"{l.provider_webhook}", json={
"order_id": order.order_id, "skill_id": l.skill_id,
"amount_inr": l.amount_inr, "idempotency_key": f"{order.order_id}:{l.skill_id}"})
r.raise_for_status()
return r.json()
await with_retry(trigger)
def hash_ledger_payload(rec: AuditRecord) -> str:
"""Hash-chained audit: each record references the previous hash."""
return hashlib.sha256(
f"{rec.order_id}|{rec.actor}|{rec.action}|{rec.payload}|{rec.timestamp}".encode()
).hexdigest()
async def append_audit(rec: AuditRecord, prev_hash: str, client: httpx.AsyncClient) -> str:
rec.payload["prev_hash"] = prev_hash
data = rec.model_dump()
data["self_hash"] = hash_ledger_payload(rec)
async def call():
r = await client.post(f"{client.base_url}/ledger/append", json=data)
r.raise_for_status()
return data["self_hash"]
return await with_retry(call)
async def wait_for_user_confirmation(order: CombinedOrder, task_id: str) -> bool:
"""Polls a durable task token; called from main.py after the interrupt resumes."""
deadline = datetime.now(timezone.utc).timestamp() + 900 # 15 min
while datetime.now(timezone.utc).timestamp() < deadline:
resp = await with_retry(lambda: httpx.AsyncClient().get(
f"{client.base_url}/tasks/{task_id}"))
if resp.json()["confirmed"]:
return True
await asyncio.sleep(3)
return False
Two implementation choices worth copying. First, with_retry never retries business-level failures — a declined payment is retried zero times, only a network timeout is. Second, the idempotency key in execute_fulfillment is order_id:skill_id, so a retried fulfillment trigger can never double-send a coffee.
The graph: graph.py
from typing import TypedDict
from langgraph.graph import END, StateGraph
from langgraph.checkpoint.memory import MemorySaver
from schemas import CombinedOrder, ConsumerIntent, HandshakeState, TrustDecision
from tools import (append_audit, execute_fulfillment, execute_payment,
handshake, hash_ledger_payload, search_skills, verify_intent_act2)
class CommerceState(TypedDict):
intent: ConsumerIntent
matches: list
handshakes: list
trust: TrustDecision | None
order: CombinedOrder | None
ledger_hash: str
error: str | None
async def interpret_intent(state):
"""Resolve utterance + device context into normalized entities."""
intent = state["intent"]
# Device-perception XUI context: gps, battery, vehicle state
if not intent.normalized_entities:
intent.normalized_entities = extract_entities(intent.utterance, intent.device_context)
intent.status = "resolved"
return {"intent": intent}
async def discover_skills(state):
matches = await search_skills(state["intent"].model_dump(), client)
return {"matches": sorted(matches, key=lambda m: m.score, reverse=True)[:5]}
async def negotiate_handshake(state):
hs = []
for m in state["matches"]:
manifest = await handshake(m, user_token=state["intent"]["user_id"], client=client)
hs.append({"skill": m, "manifest": manifest,
"status": "agreed" if manifest.get("terms") else "rejected"})
return {"handshakes": hs}
async def trust_gate(state):
trust = await verify_intent_act2(state["intent"].model_dump(), client=client)
trust.restated_intent = f"Buy {len(state['handshakes'])} service(s) for INR {trust.spend_in_inr}"
trust.under_ceiling = trust.spend_in_inr <= float(os.getenv("SPEND_CEILING_PER_ORDER", 2500))
irreversible = any(m.skill_id in IRREVERSIBLE for m, _ in state["handshakes"])
trust.requires_human = (not trust.under_ceiling
or trust.spend_in_inr >= float(os.getenv("HUMAN_CONFIRM_MIN_AMOUNT", 1500))
or irreversible)
trust.decision = "human_required" if trust.requires_human else "auto_ok"
return {"trust": trust}
def route_after_trust(state):
if state["trust"].decision == "auto_ok":
return "payment"
if state["trust"].decision == "human_required":
return "human_confirm" # LangGraph interrupt; resumes at the gate
return "reject"
def human_confirm(state):
"""Interrupt node. main.py resumes with user's explicit confirmation."""
return {"trust": state["trust"]}
async def execute_order(state):
order = build_combined_order(state["handshakes"])
await execute_payment(order, payment_rail=os.getenv("PAYMENT_RAIL", "upi"), client=client)
await execute_fulfillment(order, client=client)
order.status = "fulfilled"
return {"order": order}
async def audit(state):
rec = AuditRecord(event_id=uuid4().hex, order_id=state["order"].order_id,
actor="commerce_orch", action="order_completed",
payload=state["order"].model_dump(), timestamp=now_iso())
state["ledger_hash"] = await append_audit(rec, state["ledger_hash"], client=client)
return {"ledger_hash": state["ledger_hash"]}
builder = StateGraph(CommerceState)
builder.add_node("intent", interpret_intent)
builder.add_node("discover", discover_skills)
builder.add_node("handshake", negotiate_handshake)
builder.add_node("trust_gate", trust_gate)
builder.add_node("human_confirm", human_confirm)
builder.add_node("payment", execute_order)
builder.add_node("audit", audit)
builder.set_entry_point("intent")
builder.add_edge("intent", "discover")
builder.add_edge("discover", "handshake")
builder.add_edge("handshake", "trust_gate")
builder.add_conditional_edges("trust_gate", route_after_trust,
{"auto_ok": "payment", "human_required": "human_confirm",
"reject": END})
builder.add_edge("human_confirm", "payment")
builder.add_edge("payment", "audit")
builder.add_edge("audit", END)
graph = builder.compile(checkpointer=MemorySaver(),
interrupt_before=["human_confirm"])
The interrupt_before=["human_confirm"] is the single most important line in the file. It converts "ask the user" from a loose hope into a hard guarantee: the graph cannot reach the payment node without a resume token from an explicit human confirmation. Every sub-agent and every merchant capability goes through the same funnel, which is exactly how ACT 2.0's delegation-and-verification model is meant to be enforced.
Entrypoint: main.py
import asyncio
import os
from dotenv import load_dotenv
from graph import graph
from schemas import ConsumerIntent
load_dotenv()
client = httpx.AsyncClient(base_url=os.getenv("SKILL_REGISTRY_URL"))
async def main():
intent = ConsumerIntent(
utterance="find nearest EV charger and order a coffee to my home",
device_context={"gps": "19.0760,72.8777", "vehicle_charge_pct": 18,
"screen": "StepFun-F3", "home": "Andheri West, Mumbai"})
initial = {"intent": intent, "matches": [], "handshakes": [],
"trust": None, "order": None, "ledger_hash": "", "error": None}
for event in graph.stream(initial, config={"configurable": {"thread_id": "order-11092"}}):
print(event) # live node-by-node telemetry
state = graph.get_state({"configurable": {"thread_id": "order-11092"}})
if state.next: # interrupted at human_confirm
task_id = state.values["order_task_id"]
confirmed = await wait_for_user_confirmation(state.values["order"], task_id)
if confirmed:
resume = graph.invoke(None, {"configurable": {"thread_id": "order-11092"}})
print("Confirmed combined order:", resume["order"].model_dump())
print("Ledger hash:", resume["ledger_hash"])
if __name__ == "__main__":
asyncio.run(main())
Note how main.py uses the same thread_id to resume. That thread id is your durability primitive: if the process dies mid-confirmation, a fresh process can resume the identical order state from the checkpointer, and the ledger hash chain stays continuous.
Retry Rules & Error Handling
| Failure | Backoff | Fallback | Escalation |
|---|---|---|---|
| Registry search 5xx/timeout | Exponential 0.4s→6.0s + jitter | Return cached manifest (TTL 60s) | Slack alert if 3 consecutive |
| A2A handshake times out | 0.4s→6.0s | Retry with a different provider of the same skill class | Surface "provider unavailable" to user |
| Intent verification fails | No retry — 1 attempt | Re-derive with clarified utterance | Human clarification interrupt |
| Payment timeout | 0.4s→1.6s, max 2 | Switch rail upi → card → netbanking | On-call + freeze order |
| Payment decline (4xx) | None — business outcome | Suggest alternate instrument | Email user + audit record |
| Fulfillment trigger timeout | 0.4s→6.0s, idempotency key | Idempotent re-trigger | Compensating cancel + refund job |
| Ledger append fails | 0.4s→6.0s | Buffer record to local outbox | Do not mark order complete |
The last row is the rule that keeps you out of the news: the audit append is not a nice-to-have after payment — it is a precondition for considering the order complete. If the ledger is down, the money still moved, but the workflow's completion flag stays off until the record lands.
Cost & Decision Matrix
| Decision point | Cost driver | Cheap path | Expensive path | Rule |
|---|---|---|---|---|
| Skill discovery | LLM search tokens | Cache top-k by entity hash | Re-rank with long-context LLM | Cache wins below 1h TTL |
| Handshake | Provider API calls | Reuse session token | New handshake per order | Session reuse ≤ 15 min |
| Intent verification | LLM + policy check | Verify only aggregate intent | Per-line verification | Aggregate ≤ ₹1,500; else per-line |
| Payment | PSP fees + retry risk | Single UPI collect | Multi-rail fallback | 1 rail attempt, then fallback |
| Audit | DB write | Sync in-band append | Async outbox | In-band — non-negotiable |
Run the numbers on intent verification, because it compounds: at roughly ₹0.30 per aggregate verification call and 10,000 orders a day, that is ~₹3,000/day — trivial. The expensive mistake is verifying every line item for every order at ₹1.10 a call, or worse, dropping verification to save money and discovering the ₹2,500 ceiling was never enforced.
Why this matters for Indian commerce
Agentic commerce is not a Chinese-only experiment. The AHA shape — Skills + MCP tools, a consumer agent, an A2A handshake, a trust protocol, an audit trail — maps directly onto Indian rails: UPI collect for the payment channel, e-commerce catalog APIs as Skills, food-delivery and EV-charging networks as MCP tools. The regulatory pressure is real too: the RBI's 2026 guidelines on delegated transactions effectively require what ACT 2.0 already does — explicit intent capture, spend limits, and machine-verifiable audit trails. If you build the trust layer first, the consumer-agent layer becomes a distribution win rather than a liability. For the broader ecosystem of agent-to-agent tooling, keep an eye on our MCP directory, and browse the full pattern library in our AI Workflows section. For everything else happening on the agent front this week, Latest AI News is the source we check daily.
Ship checklist
- Ceilings before capabilities. If you can't say what a Skill costs, it does not get into the registry.
- Interrupts are the trust layer.
interrupt_beforeon the payment gate, not a politeinput()call. - Idempotency keys on every fulfillment.
order_id:skill_idcosts one line and prevents double-coffee. - Audit append before completion. The ledger is a precondition, not a log.
- Degrade, don't die. Registry, handshake, payment, and fulfillment each have a cached or alternate path.
The demo — one sentence, two agents, one combined order — is going to be table stakes in 2027. The orchestrator, the handshake, and the audit chain are what your platform will be paid for. Build the trust layer first, and the agent layer will happily sell through it.
Enjoyed this breakdown? Get our morning dispatch in your inbox.
Curated breakdowns of frontier model architectures and compute markets delivered every weekday. Zero fluff.
Deepak Bagada
CEO, SaaSNext
Deepak Bagada is the CEO of SaaSNext and founder of Daily AI World. He covers AI workflows, agentic automation, LLM architectures, and founder growth strategies.
Related Intelligence Analysis
The Step-by-Step Guide to Automating Meeting Tasks with Whisper
You're spending 45 minutes after every client meeting typing up notes and manually assigning tasks in Jira. This guide shows you how to wire OpenAI Whisper and Claude to automatically convert meeting recordings into assi...
Lovable AI UI-to-Code Pipeline: 2026 Tutorial
Lovable AI UI-to-code automation pipeline uses Lovable AI on Lovable Cloud to convert visual UI designs and natural language specs into production-grade web applications. UI/UX designers and frontend developers bridging...
Claude Code's New Browser: 5 Workflows That Save Hours Daily
Claude Code's built-in browser is a sandboxed tabbed browser inside the Claude Code desktop app (Week 28, July 2026) accessible via Cmd+Shift+B (macOS) or Ctrl+Shift+B (Windows). It lets Claude open websites, read docume...