Ecommerce assistant
Order tracking, smart recommendations, cart recovery — over WhatsApp.
Ecommerce assistant
A WhatsApp shopper that does three things really well:
- Track orders — "where's my package?" answered in 1 turn.
- Recommend products — "I need a gift for my sister, $50 budget".
- Cart recovery — proactive nudges when someone abandons a cart.
This example mixes two patterns: the agent (reactive — replies to inbound) and a trigger (proactive — fires on inventory events).
Setup
terminalnpx zavudev fn init --slug shopper -y cd shopper
The agent + tools
index.tsimport { defineAgent, defineTool, defineFunction } from "@zavudev/functions" import Zavudev from "@zavudev/sdk" const zavu = new Zavudev({ apiKey: process.env.ZAVU_API_KEY!, baseURL: process.env.ZAVU_API_BASE_URL, }) defineAgent({ senderId: process.env.SENDER_ID!, name: "Shop Helper", provider: "zavu", model: "openai/gpt-5.6-luna", channels: ["whatsapp"], prompt: `Eres el asistente de Acme Shop en WhatsApp. Tu trabajo: - Ayudar a los clientes a encontrar productos (call recommend). - Decirles el estado de sus pedidos (call lookup_order). - Tomar el pedido si quieren comprar directo desde el chat (call create_order). Tono: - Amistoso, breve. - SIEMPRE incluye el precio cuando recomiendes un producto. - Si no encuentras lo que piden, ofrece alternativas, nunca digas "no tengo". Reglas: - Nunca inventes IDs, precios o stock — usa las tools. - Si el cliente pregunta por algo no relacionado a la tienda, dile: "Para eso te paso con un agente humano" y llama escalate.`, }) // 1) Order lookup defineTool({ name: "lookup_order", description: "Get the status of a customer's order. Use when they ask about delivery, " + "tracking, where their package is.", parameters: { type: "object", properties: { orderId: { type: "string", description: "Order number like ACME-12345." }, }, required: ["orderId"], }, handler: async ({ orderId }, ctx) => { const order = await fetch( `${process.env.SHOP_API_URL}/orders/${orderId}`, { headers: { authorization: `Bearer ${process.env.SHOP_API_KEY}` } } ).then((r) => (r.ok ? r.json() : null)) if (!order) return { error: "not_found", message: `No encuentro el pedido ${orderId}.` } if (order.customerPhone !== ctx?.contactPhone) { return { error: "not_yours", message: "Ese pedido está vinculado a otro número." } } return { orderId: order.id, status: order.status, // "processing" | "shipped" | "out_for_delivery" | "delivered" eta: order.estimatedDeliveryAt, tracking: order.trackingUrl, items: order.items.map((i) => `${i.qty}× ${i.name}`), summary: order.status === "shipped" ? `Tu pedido fue despachado. Llega aprox ${formatDay(order.estimatedDeliveryAt)}. Tracking: ${order.trackingUrl}` : order.status === "delivered" ? `Ya fue entregado el ${formatDay(order.deliveredAt)}. ¿Algún problema?` : `Estado: ${order.status}. ETA: ${formatDay(order.estimatedDeliveryAt)}`, } }, }) // 2) Product recommendation defineTool({ name: "recommend", description: "Search the catalog and return 3-5 products matching the customer's needs. " + "Use when they describe what they want (e.g. 'a gift for my dad under $80, " + "he likes camping').", parameters: { type: "object", properties: { query: { type: "string", description: "Customer's request, paraphrased." }, maxPrice: { type: "number", description: "Budget cap in USD." }, category: { type: "string", description: "Filter by category if obvious." }, }, required: ["query"], }, handler: async ({ query, maxPrice, category }) => { const res = await fetch(`${process.env.SHOP_API_URL}/search`, { method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${process.env.SHOP_API_KEY}`, }, body: JSON.stringify({ query, filters: { ...(maxPrice ? { priceLte: maxPrice } : {}), ...(category ? { category } : {}), inStock: true, }, limit: 5, }), }).then((r) => r.json()) return { results: res.products.map((p) => ({ id: p.id, name: p.name, price: `$${p.price}`, url: `${process.env.SHOP_API_URL}/p/${p.slug}`, whyGoodFit: p.relevanceReason, // your search engine's explanation })), count: res.products.length, } }, }) // 3) Create an order from chat defineTool({ name: "create_order", description: "Place an order on behalf of the customer. ONLY call after they explicitly " + "confirmed the items and shipping address. Always confirm the total before " + "calling.", parameters: { type: "object", properties: { items: { type: "array", items: { type: "object", properties: { productId: { type: "string" }, quantity: { type: "number" }, }, required: ["productId", "quantity"], }, }, shippingAddress: { type: "string", description: "Full address line." }, paymentMethod: { type: "string", enum: ["card_on_file", "cash_on_delivery"], }, }, required: ["items", "shippingAddress", "paymentMethod"], }, handler: async ({ items, shippingAddress, paymentMethod }, ctx) => { const res = await fetch(`${process.env.SHOP_API_URL}/orders`, { method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${process.env.SHOP_API_KEY}`, }, body: JSON.stringify({ items, shippingAddress, paymentMethod, customerPhone: ctx?.contactPhone, source: "whatsapp", }), }).then((r) => r.json()) if (res.error) { return { error: res.error, message: res.message } } return { orderId: res.id, total: `$${res.total}`, eta: res.estimatedDeliveryAt, summary: `Pedido ${res.id} creado por $${res.total}. ` + `Envío a "${shippingAddress.slice(0, 40)}…". ` + `Llega aprox ${formatDay(res.estimatedDeliveryAt)}.`, } }, }) // 4) Escalation defineTool({ name: "escalate", description: "Forward this conversation to a human agent. Call when the customer needs " + "help outside shopping (returns disputes, account issues, complaints).", parameters: { type: "object", properties: { reason: { type: "string" } }, required: ["reason"], }, handler: async ({ reason }, ctx) => { await fetch(process.env.SLACK_WEBHOOK_URL!, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ text: `:wave: Escalation from ${ctx?.contactPhone}: ${reason}`, }), }) return { escalated: true, summary: "Te paso con un agente. Respuesta en ~10 min." } }, }) // PROACTIVE: cart recovery trigger. // // When your shop backend sends a `cart.abandoned` event to your Zavu function // (via your own webhook → custom event → trigger fanout), the agent fires off // a nudge message. // // For this to work, you'd: // 1. Have your shop POST to your Zavu function's HTTP URL (httpEnabled=true) // with the abandoned-cart payload. // 2. Format and send a reminder via the SDK. export default defineFunction(async (event, ctx) => { // Handle non-standard event from your shop: shop posts directly to fn URL. // event.body is a stringified JSON body when called via the public HTTP path. const body = typeof event.body === "string" ? JSON.parse(event.body) : event if (body.type !== "cart.abandoned") return { ok: true } const { customerPhone, items, cartUrl, total } = body await zavu.messages.send({ to: customerPhone, channel: "whatsapp", "Zavu-Sender": process.env.SENDER_ID!, text: `¡Hola! Vi que dejaste algo en tu carrito 🛒\n\n` + items.slice(0, 3).map((i: any) => `• ${i.name}`).join("\n") + (items.length > 3 ? `\n• +${items.length - 3} más` : "") + `\n\nTotal: $${total}\n${cartUrl}\n\n¿Querés que te ayude a finalizar?`, }) return { ok: true, nudged: true } }) function formatDay(iso: string) { return new Date(iso).toLocaleDateString("es-ES", { weekday: "long", day: "numeric", month: "short", }) }
For cart recovery via the function's public URL, you need httpEnabled: true —
but the function is then publicly addressable. Two ways to secure:
Option A: HMAC the cart webhook
Make your shop sign payloads:
TypeScriptimport { verifyWebhook } from "@zavudev/functions" export default defineFunction(async (event) => { if (typeof event.body !== "string") return { ok: true } const sig = event.headers?.["x-shop-signature"] if (!verifyWebhook(event.body, sig, process.env.SHOP_WEBHOOK_SECRET!)) { return { statusCode: 401, body: '{"error":"invalid_signature"}' } } // ... })
Option B: Use Zavu triggers instead
If your shop is also "inside Zavu" (rare for ecommerce, common for messaging), you can use a custom event type. For most shops, Option A is the answer.
Sample conversations
Order tracking
Code👤 dónde está mi pedido ACME-9421? 🤖 [lookup_order(orderId="ACME-9421")] 🤖 Tu pedido fue despachado. Llega aprox lunes 18 nov. Tracking: https://tracker.com/ACME-9421-XYZ ¿Algo más?
Discovery → purchase
Code👤 busco un regalo para mi hermana, le gusta cocinar, hasta $60 🤖 [recommend(query="kitchen gift for sister", maxPrice=60, category="kitchen")] 🤖 Te muestro 3 opciones: 🥘 Olla de hierro fundido 4Qt — $54 — perfecto para guisos https://shop.com/p/olla-4qt 🔪 Set de 3 cuchillos japoneses — $48 — calidad pro https://shop.com/p/knife-set 📚 Libro "Salt Fat Acid Heat" — $32 — el clásico moderno https://shop.com/p/sfah ¿Cuál te gusta? 👤 la olla, mándamela a Av Corrientes 1234, Buenos Aires 🤖 ¿Te confirmo el pedido? • Olla de hierro fundido 4Qt — $54 Envío a Av Corrientes 1234, Buenos Aires — gratis Total: $54 con tarjeta guardada. ¿Confirmamos? 👤 sí 🤖 [create_order(...)] 🤖 ¡Listo! Pedido ACME-9425 creado por $54. Envío a "Av Corrientes 1234, Buenos Aires…". Llega aprox jueves 21 nov.
Proactive cart recovery
The shop's backend posts to https://fn.zavu.dev/<project>/shopper 30 min
after a customer abandons checkout. The function sends the nudge:
Code🤖 ¡Hola! Vi que dejaste algo en tu carrito 🛒 • Olla de hierro fundido 4Qt • Set de 3 cuchillos japoneses Total: $102 https://shop.com/cart/r/abc123 ¿Querés que te ayude a finalizar? 👤 sí, hacé el pedido 🤖 [recommend ... or directly create_order if confirmed]
Deploy
terminalnpx zavudev fn secrets set SENDER_ID jn76vnxet8g5nq661by3v06y1581bmmn npx zavudev fn secrets set SHOP_API_URL https://shop.acme.com/api npx zavudev fn secrets set SHOP_API_KEY ... npx zavudev fn secrets set SHOP_WEBHOOK_SECRET ... npx zavudev fn secrets set SLACK_WEBHOOK_URL https://hooks.slack.com/... npx zavudev deploy
If you want cart recovery, also enable HTTP on the function (via dashboard
Function detail page → toggle "HTTP enabled") or pass httpEnabled: true at
first init. Note that this exposes the public URL — make sure your HMAC
verification is in place.
Metrics worth watching
terminal# Conversion rate: messages sent → orders created npx zavudev agents executions --sender $SENDER_ID --json | jq ' group_by(.status) | map({status: .[0].status, count: length})' # Token cost per conversation (gpt-5.6-luna ≈ $0.0004 / 3-turn convo) npx zavudev agents executions --sender $SENDER_ID --json | jq ' [.[].cost] | add' # Cart recovery effectiveness — track which abandoned carts converted # (your own analytics, since the agent doesn't see this)
Composition with other Zavu primitives
Black Friday: broadcast a 20% off code to your list. The agent picks up when customers reply with questions.
For the initial "your order is shipped" notification (outside the 24h window), use a WhatsApp template instead of the agent.
Store lastOrderId, lifetimeValue, preferredCategory on contacts.
The agent reads them automatically via includeContactMetadata: true.
Run separate agents for different storefronts under the same parent Zavu account.