---
title: Shopping Agent
description: A buy-side agent that searches a real store, drives the store's checkout, and pays the resulting Pix from its governed wallet under a signed mandate.
---

<MetaStrip items={[
  { label: "TIME", value: "~15 min" },
  { label: "STACK", value: (<><ServerChip name="MCP" accent /><span style={{ color: "var(--color-fd-muted-foreground)", fontSize: 12 }}>or Node.js SDK</span></>) },
  { label: "TOOLS", value: (<><ServerChip name="codespar_shop" /><ServerChip name="codespar_wallet" /><ServerChip name="codespar_pay" /></>) },
]} />

Build an agent that shops on the buy side: it searches a real store's catalog, starts the store's actual checkout, waits for the store to mint its payable Pix copia-e-cola, and settles it from the agent's governed wallet. The spend only executes inside a **signed mandate**, so caps, allowlists, and expiry are enforced server-side, not trusted to the model.

What you'll build:

- Search live, in-stock offers at a real store (Cobasi, Animale, Lojas Pompeia via guest checkout, or Mercado Livre via the buyer's connected account).
- Start an async checkout session and poll it to completion.
- Pay the store's real Pix code from the wallet, gated by the mandate.

## The flow

Checkout is **async** by design: driving a real store checkout takes longer than an agent tool-call timeout, so `codespar_shop` returns a session id immediately and you poll for the terminal state.

```text
1. codespar_shop  action=search           → { products: [ { variants: [ { sku_id } ] } ] }
2. codespar_shop  action=checkout         → { checkout_session_id, status: "in_progress" }
3. codespar_shop  action=checkout_status  → poll until terminal:
     ready_for_payment  → carries pix_copia_e_cola + total_minor
     canceled           → carries error (see reason handling below)
4. codespar_wallet action=balance         → confirm the wallet can cover total_minor
5. codespar_pay   method=pix              → settle pix_copia_e_cola under the mandate
```

Both terminal states are final. Polling a terminal session returns the same payload, so the loop is safe to re-enter.

<Callout>
**Pass the SKU, not the product.** Search results carry the buyable unit as `variants[].sku_id`; checkout takes it as the item's `variant_id`. The product-level `id` is not buyable, and passing it is the single most common shopping-agent mistake.
</Callout>

## Prerequisites

- A CodeSpar API key. New accounts get a `test`-environment project plus a `csk_test_*` key auto-created at signup: [Dashboard → API Keys](https://codespar.dev/dashboard/api-keys).
- Either an MCP client (Claude Code, Codex CLI, Cursor, VS Code) or the Node.js SDK.

For the MCP path, add the server once:

```bash
claude mcp add codespar --env CODESPAR_API_KEY=csk_test_your_key -- npx -y @codespar/mcp serve
```

For the SDK path:

```bash
npm install @codespar/sdk
```

## Step 1: Drive it in natural language (MCP)

With the MCP server installed, the whole loop is a conversation. Ask your agent:

> "Search Cobasi for ração golden retriever 15kg and show me the top options."

The agent calls `codespar_shop` `action=search` and gets back live, buyable offers with titles, prices, and variant ids. Then:

> "Buy the first one."

The agent starts the store's real checkout (`action=checkout`), polls `action=checkout_status`, and receives the store's actual Pix copia-e-cola when the session reaches `ready_for_payment`. Finally:

> "What's my wallet balance?" ... "Pay this checkout with my wallet."

That is `codespar_wallet` `action=balance` followed by `codespar_pay` with the checkout's Pix code. An out-of-policy payment is blocked by the mandate, and every step lands in the audit ledger.

## Step 2: The same loop in code (SDK)

There is no typed wrapper for `codespar_shop` yet, so call it via `session.execute()`:

```typescript title="shopping-agent.ts"
import { CodeSpar } from "@codespar/sdk";

const codespar = new CodeSpar({ apiKey: process.env.CODESPAR_API_KEY! });

async function buy(merchant: string, query: string) {
  const session = await codespar.create("buyer_123");

  try {
    // 1) search the store's live catalog
    const found = await session.execute("codespar_shop", {
      action: "search",
      merchant,
      query,
      limit: 5,
    });
    const offer = found.products[0];
    if (!offer) throw new Error("no offers found");

    // 2) start the store's real checkout (async)
    // pass variants[].sku_id from search as the item's variant_id
    const started = await session.execute("codespar_shop", {
      action: "checkout",
      merchant,
      items: [{ variant_id: offer.variants[0].sku_id, quantity: 1 }],
    });

    // 3) poll until terminal
    let status = started;
    while (status.status === "in_progress") {
      await new Promise((r) => setTimeout(r, 3000));
      status = await session.execute("codespar_shop", {
        action: "checkout_status",
        checkout_session_id: started.checkout_session_id,
      });
    }

    if (status.status === "canceled") {
      throw new Error(`checkout canceled: ${status.error}`);
    }

    // 4) confirm the wallet covers the total
    const wallet = await session.execute("codespar_wallet", { action: "balance" });
    if (wallet.balance < status.total_minor) {
      throw new Error("insufficient wallet balance, top up with codespar_wallet action=receive");
    }

    // 5) pay the store's Pix from the governed wallet, gated by the mandate
    return await session.execute("codespar_pay", {
      method: "pix",
      recipient: status.pix_copia_e_cola,
      amount: status.total_minor,
    });
  } finally {
    await session.close();
  }
}

buy("cobasi", "ração golden retriever 15kg").then(console.log);
```

The payment step is a separate tool with separate governance on purpose: `codespar_shop` never moves money. It only mints the store's payable instrument, and `codespar_pay` settles it under the mandate.

## Step 3: Handle a canceled checkout

A checkout that fails after start does not throw. It arrives as a `canceled` session with an `error` explaining why. Branch on the reason in four buckets:

| Reason bucket | What happened | What the agent should do |
|---|---|---|
| Retriable (transient store or worker failure) | The store's checkout flow hiccuped mid-drive | Start a fresh `action=checkout`; the old session stays terminal |
| No shipping | The store cannot ship to the buyer's address | Not retriable as-is. Fix the vaulted address or pick another store |
| Identity required | The store's checkout hit an identity wall and needs real buyer details | Vault the shopper profile once via [`codespar_manage_connections`](/docs/concepts/meta-tools/manage-connections) (`save_profile`: CEP, email, CPF), then retry |
| Not connected | Mercado Livre buys run on the buyer's own account, which is not linked yet | Run the one-time connected login (`connect_start` / `connect_finish`), then retry |

Only the first bucket is worth an automatic retry loop. The other three need a state change (address, vaulted identity, connected account) before a retry can succeed, so surface them to the user instead of burning attempts.

## Honest sandbox note

<Callout type="info">
Search and checkout run against **real store catalogs** today: the offers, prices, and the minted Pix copia-e-cola are the store's own. Paying a real store's Pix code with **real money** requires production rails on your account. In the `test` environment the full loop executes end to end against sandbox rails, so you can build and demo the whole agent before going to production.
</Callout>

## Next steps

<NextStepsGrid items={[
  { label: "CONCEPT", title: "codespar_shop", description: "The buy-side primitive: search, async checkout sessions, status polling.", href: "/docs/concepts/meta-tools/shop" },
  { label: "CONCEPT", title: "codespar_wallet", description: "Balance, statement, top-up. The governed wallet your agent spends from.", href: "/docs/concepts/meta-tools/wallet" },
  { label: "CONCEPT", title: "codespar_pay", description: "Spending out of the wallet, gated by the signed mandate.", href: "/docs/concepts/meta-tools/pay" },
  { label: "CONCEPT", title: "Shopper identity", description: "Vaulted checkout identity so stores get real buyer details.", href: "/docs/concepts/shopper-identity" },
  { label: "QUICKSTART", title: "Buyer agent", description: "The MCP-first version of this loop, no code required.", href: "/docs/quickstart-buyer" },
]} />
