> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zavu.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Ecommerce assistant

> Order tracking, smart recommendations, cart recovery — over WhatsApp.

## Ecommerce assistant

A WhatsApp shopper that does three things really well:

1. **Track orders** — "where's my package?" answered in 1 turn.
2. **Recommend products** — "I need a gift for my sister, \$50 budget".
3. **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

```sh theme={null}
zavu fn init --slug shopper -y
cd shopper
```

## The agent + tools

```ts index.ts theme={null}
import { defineAgent, defineTool, defineFunction } from "@zavu/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-4o-mini",
  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:

```ts theme={null}
import { verifyWebhook } from "@zavu/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

```
👤 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

```
👤 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:

```
🤖 ¡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

```sh theme={null}
zavu fn secrets set SENDER_ID jn76vnxet8g5nq661by3v06y1581bmmn
zavu fn secrets set SHOP_API_URL https://shop.acme.com/api
zavu fn secrets set SHOP_API_KEY ...
zavu fn secrets set SHOP_WEBHOOK_SECRET ...
zavu fn secrets set SLACK_WEBHOOK_URL https://hooks.slack.com/...
zavu 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

```sh theme={null}
# Conversion rate: messages sent → orders created
zavu agents executions --sender $SENDER_ID --json | jq '
  group_by(.status) | map({status: .[0].status, count: length})'

# Token cost per conversation (gpt-4o-mini ≈ $0.0002 / 3-turn convo)
zavu 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

<CardGroup cols={2}>
  <Card title="Broadcasts" icon="bullhorn" href="/guides/broadcasts/overview">
    Black Friday: broadcast a 20% off code to your list. The agent picks up
    when customers reply with questions.
  </Card>

  <Card title="Templates" icon="file-lines" href="/guides/whatsapp/templates/overview">
    For the initial "your order is shipped" notification (outside the 24h
    window), use a WhatsApp template instead of the agent.
  </Card>

  <Card title="Contacts" icon="address-book" href="/guides/contacts/overview">
    Store `lastOrderId`, `lifetimeValue`, `preferredCategory` on contacts.
    The agent reads them automatically via `includeContactMetadata: true`.
  </Card>

  <Card title="Sub-accounts" icon="sitemap" href="/guides/sub-accounts/overview">
    Run separate agents for different storefronts under the same parent
    Zavu account.
  </Card>
</CardGroup>

## Next

<CardGroup cols={2}>
  <Card title="Define tools in depth" icon="wrench" href="/guides/functions/defining-tools">
    JSON schemas, error handling, the LLM's selection logic.
  </Card>

  <Card title="Triggers" icon="bolt" href="/guides/functions/triggers">
    Native event subscriptions without HTTP.
  </Card>
</CardGroup>
