Skip to main content

Shopping Agent

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.

1 min read
View MarkdownEdit on GitHub
TIME
~15 min
STACK
MCPor Node.js SDK
TOOLS
codespar_shopcodespar_walletcodespar_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.

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.

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.

Prerequisites

  • A CodeSpar API key. New accounts get a test-environment project plus a csk_test_* key auto-created at signup: 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:

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

For the SDK path:

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():

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 bucketWhat happenedWhat the agent should do
Retriable (transient store or worker failure)The store's checkout flow hiccuped mid-driveStart a fresh action=checkout; the old session stays terminal
No shippingThe store cannot ship to the buyer's addressNot retriable as-is. Fix the vaulted address or pick another store
Identity requiredThe store's checkout hit an identity wall and needs real buyer detailsVault the shopper profile once via codespar_manage_connections (save_profile: CEP, email, CPF), then retry
Not connectedMercado Livre buys run on the buyer's own account, which is not linked yetRun 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

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.

Next steps

Shopping Agent | CodeSpar