---
title: "AI Shopping Agent: Compare Prices Across Marketplaces"
description: "A Python AI shopping agent that searches Google Shopping, Walmart and AliExpress, normalises offers into one table and stops at a credit cap of 6 per question."
url: https://perceptlayer.com/blog/ai-shopping-agent-compare-prices
---

[Blog](https://perceptlayer.com/blog) / [AI agents](https://perceptlayer.com/blog/category/ai-agents)

# AI Shopping Agent: Compare Prices Across Marketplaces

16 Jul 2026 Perceptlayer 7 min read

An AI shopping agent takes a product name, searches several marketplaces, and returns the offers side by side in one table. This tutorial builds one in Python with Claude: it searches Google Shopping, Walmart and AliExpress through one generic `call_endpoint` tool, records each offer through a second tool with a fixed schema, and refuses any data call past a credit cap. A typical question costs 3 credits, one search per marketplace, with a hard cap of 6.

## Key takeaways

-   Three searches answer most questions: `/v1/google-shopping/search` ( `q`), `/v1/walmart/search` ( `query`) and `/v1/aliexpress/search` ( `query`, `page`, `sort`), 1 credit each.
-   The model normalises the offers itself through a `record_offer` tool with a fixed schema, so the code doesn't depend on three different response shapes.
-   The recorded Google Shopping response gives `extracted_price` as a number; the recorded Walmart response gives `price` with a separate `currency`. None of the three recorded search responses includes a shipping cost, so shipping stays empty unless the data states it.
-   The credit cap lives in code: every `call_endpoint` adds its `X-Credits-Used`, and calls past the cap return an error to the model instead of reaching the API.
-   300 questions a month at 3 credits each is 900 credits, inside the 1,000 free monthly credits. Claude's own tokens are billed separately by Anthropic.

## What does the agent do?

You ask it "compare prices for an Instant Pot 6 quart". It runs one search per marketplace, reads the results, picks the offers that match the product, and records each one with its marketplace, title, price, currency, link and stock status. Your code then prints the offers sorted by price. If the first search comes back empty or ambiguous, the model may refine it once, but only within the cap.

The design splits the work by what each side does well. The model reads messy, differently shaped JSON and decides which results are the same product. The code owns everything that must be exact: which endpoints the model may call, how many credits it may spend, and the final table. That split is why the agent needs only two tools, however many marketplaces you add.

You need Python 3.10 or newer, `pip install anthropic requests`, an Anthropic API key in `ANTHROPIC_API_KEY`, and a Perceptlayer key in `API_KEY`. [Get a free API key](https://perceptlayer.com/signup); verifying your email gives you 1,000 free credits a month.

## Step 1: Which endpoints does the agent search?

One search endpoint per marketplace, each 1 credit per successful call. Their required parameters differ, which is why the model needs to know them: AliExpress search requires `page` and `sort` as well as the query. The table lists what the agent sends.

| Marketplace | Endpoint ID | Path | Required parameters |
| --- | --- | --- | --- |
| Google Shopping | `google-shopping.search` | `/v1/google-shopping/search` | `q` (up to 500 characters) |
| Walmart | `walmart.search` | `/v1/walmart/search` | `query` (up to 500 characters) |
| AliExpress | `aliexpress.search` | `/v1/aliexpress/search` | `query`, `page`, `sort` ( `default`, `orders`, `price_asc`, `price_desc`) |

Google Shopping results come from many shops, so one search already compares sellers. In the recorded response for `q=iphone 15&gl=us&hl=en`, the first two results are from "Walmart - Wireless Source" at `extracted_price` 393.73 and "Best Buy" at 441:

```json
{
  "shopping_results": [
    {
      "position": 1,
      "title": "Apple iPhone 15",
      "source": "Walmart - Wireless Source",
      "rating": 4.7,
      "price": "$393.73",
      "extracted_price": 393.73,
      "link": "https://www.google.com/shopping/product/4263606017411565992",
      …
    },
    {
      "position": 2,
      "title": "Apple iPhone 15 128GB",
      "source": "Best Buy",
      "price": "$441.00",
      "extracted_price": 441,
      …
    }
  ],
  …
}
```

Walmart's recorded search for `air fryer` returns `results` with a numeric `price`, a `currency`, `seller_name`, `availability_status` and `product_page_url`, plus a `sponsored` flag that's `true` on both of the first two results:

```json
{
  "data": {
    "search_metadata": { "query": "air fryer", "total_items": 72, … },
    "results": [
      {
        "us_item_id": "14678059894",
        "title": "Chefman Air Fryer 1500W, 6 Quart, 4 Functions with Touch Controls, Stainless Steel",
        "price": 59.98,
        "currency": "USD",
        "seller_name": "Walmart.com",
        "availability_status": "IN_STOCK",
        "product_page_url": "https://www.walmart.com/ip/14678059894",
        "sponsored": true,
        …
      },
      …
    ],
    …
  }
}
```

AliExpress's recorded search for `query=wireless earbuds&page=1&sort=orders` returns `results` with a `price` object that carries its own `currency`, plus an `original_price` and `discount_percent` for the advertised discount:

```json
{
  "data": {
    "query": "wireless earbuds",
    "page_size": 60,
    "total_results": 16557,
    "results": [
      {
        "product_id": "3256812408115438",
        "title": "HTC NE79 TWS Bluetooth 6.0 Earbuds AI Translation 135 Languages ENC Noise Cancelling HiFi Stereo Headphones with LED Display",
        "url": "https://www.aliexpress.com/item/3256812408115438.html",
        "price": { "amount": 4.35, "currency": "USD", "formatted": "US $4.35" },
        "original_price": { "amount": 17.31, "currency": "USD", "formatted": "US $17.31" },
        "discount_percent": 74,
        "selling_points": ["Fastest delivery in 6 days"],
        "is_sponsored": false,
        …
      },
      …
    ]
  }
}
```

`selling_points` mentions delivery time, not a shipping cost, so `shipping` stays null for these rows too. The [Google Shopping API](https://perceptlayer.com/apis/google-shopping), [Walmart API](https://perceptlayer.com/apis/walmart) and [AliExpress API](https://perceptlayer.com/apis/aliexpress) pages list every parameter.

## Step 2: How do you define the two tools?

`call_endpoint` takes an endpoint ID and its parameters, the same shape as the `call_endpoint` tool on Perceptlayer's MCP server, so the model's behaviour carries over if you later move it to an MCP client. `record_offer` takes one normalised offer. Its schema is the table you want at the end, so the model's job is to fill it in.

```python
TOOLS = [
    {
        "name": "call_endpoint",
        "description": (
            "Run one marketplace search and return its JSON. Allowed endpoints: "
            "google-shopping.search (params: q, optional gl, hl), "
            "walmart.search (params: query, optional sort, min_price, max_price), "
            "aliexpress.search (params: query, page, sort; sort is one of "
            "default, orders, price_asc, price_desc). Each successful call costs 1 credit."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "endpoint": {"type": "string", "enum": ["google-shopping.search", "walmart.search", "aliexpress.search"]},
                "params": {"type": "object"},
            },
            "required": ["endpoint", "params"],
        },
    },
    {
        "name": "record_offer",
        "description": (
            "Record one offer that matches the requested product. Use only values present "
            "in the search results. Leave shipping and in_stock null unless the data states them."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "marketplace": {"type": "string", "enum": ["google_shopping", "walmart", "aliexpress"]},
                "seller": {"type": ["string", "null"]},
                "title": {"type": "string"},
                "price": {"type": "number"},
                "currency": {"type": ["string", "null"]},
                "shipping": {"type": ["number", "null"]},
                "in_stock": {"type": ["boolean", "null"]},
                "link": {"type": "string"},
            },
            "required": ["marketplace", "title", "price", "link"],
        },
    },
]
```

The `enum` on `endpoint` means the model can only name the three searches. The instruction to leave `shipping` null unless the data states it is the honest part: none of the recorded search responses carry a shipping cost, and a model asked to fill every field will otherwise guess.

## Step 3: How do you run the endpoints under a credit cap?

Map each endpoint ID to its path, call the API, and keep a running total from the `X-Credits-Used` header. Before each call, check the total against the cap; past it, return an error the model can read instead of calling the API. Failed calls return `X-Credits-Used: 0`, so they don't count against the cap.

```python
import json
import os

import requests

BASE = "https://api.perceptlayer.com"
HEADERS = {"X-API-Key": os.environ["API_KEY"]}
PATHS = {
    "google-shopping.search": "/v1/google-shopping/search",
    "walmart.search": "/v1/walmart/search",
    "aliexpress.search": "/v1/aliexpress/search",
}
CREDIT_CAP = 6

class Budget:
    def __init__(self, cap):
        self.cap, self.spent = cap, 0

    def call(self, endpoint, params):
        if endpoint not in PATHS:
            return f"Error unknown_endpoint: use one of {', '.join(PATHS)}.", True
        if self.spent >= self.cap:
            return f"Error credit_cap: {self.spent} of {self.cap} credits used. Stop searching and record the offers you have.", True
        r = requests.get(BASE + PATHS[endpoint], params=params, headers=HEADERS, timeout=90)
        if r.status_code != 200:
            err = r.json()["error"]
            return f"Error {err['code']}: {err['message']}", True
        self.spent += int(r.headers["X-Credits-Used"])
        return r.text[:60000], False
```

The response is cut at 60,000 characters to keep one large result from filling the model's context; the cut drops the end of the response. Error results start `Error <code>:` with the API's own message, which tells the model what went wrong and what to do next. Anthropic's tool use docs recommend exactly that: return failures as a `tool_result` with `is_error` set, and make the message say what went wrong and what to try ( [Claude Docs](https://platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls), 2026).

## Step 4: How do you write the agent loop?

A standard tool-use loop. Send the question with both tools, run every `tool_use` block Claude returns, send all the results back in one user message, and repeat until Claude stops calling tools. `call_endpoint` goes through the budget; `record_offer` appends to a list and returns "recorded".

```python
import anthropic

SYSTEM = (
    "You compare prices for one product across Google Shopping, Walmart and AliExpress. "
    "Search each marketplace once with call_endpoint. Refine a search only if its results "
    "don't contain the product. Then call record_offer for each matching offer, up to 3 per "
    "marketplace, using only values from the results. Skip accessories and different models. "
    "Finish with one sentence naming the cheapest matching offer."
)

def run_agent(question):
    client = anthropic.Anthropic()
    budget, offers = Budget(CREDIT_CAP), []
    messages = [{"role": "user", "content": question}]
    while True:
        response = client.messages.create(
            model="claude-opus-5-5", max_tokens=16000,
            system=SYSTEM, tools=TOOLS, messages=messages,
        )
        if response.stop_reason == "refusal":
            raise RuntimeError("The model declined this request.")
        messages.append({"role": "assistant", "content": response.content})
        calls = [b for b in response.content if b.type == "tool_use"]
        if not calls:
            break
        results = []
        for call in calls:
            if call.name == "call_endpoint":
                text, is_error = budget.call(call.input["endpoint"], call.input.get("params", {}))
            else:
                offers.append(call.input)
                text, is_error = "recorded", False
            results.append({"type": "tool_result", "tool_use_id": call.id,
                            "content": text, "is_error": is_error})
        messages.append({"role": "user", "content": results})
    summary = "".join(b.text for b in response.content if b.type == "text")
    return offers, summary, budget.spent
```

Two details keep it correct. All tool results for one turn go back in a single user message, one per `tool_use` block and matched by `tool_use_id`, as Anthropic's [parallel tool use docs](https://platform.claude.com/docs/en/agents-and-tools/tool-use/parallel-tool-use) require (2026). And the loop ends when a response contains no `tool_use` blocks, so a model that hits the cap simply records what it has and answers.

## Step 5: How do you print the comparison?

Sort the recorded offers by price and print one row each. Add the shipping cost to the price only when it's known, and say so in the column header, so nobody reads an item price as a delivered price.

```python
def print_table(offers):
    rows = sorted(offers, key=lambda o: o["price"] + (o.get("shipping") or 0))
    print(f"{'marketplace':16} {'price':>9} {'ship':>6} {'cur':4} {'stock':6} title")
    for o in rows:
        ship = "" if o.get("shipping") is None else f"{o['shipping']:.2f}"
        stock = {True: "yes", False: "no"}.get(o.get("in_stock"), "?")
        print(f"{o['marketplace']:16} {o['price']:>9.2f} {ship:>6} {(o.get('currency') or ''):4} {stock:6} {o['title'][:60]}")

if __name__ == "__main__":
    import sys
    offers, summary, spent = run_agent(" ".join(sys.argv[1:]) or "Compare prices for a 6 quart air fryer")
    print_table(offers)
    print(summary)
    print(f"Perceptlayer credits used: {spent}")
```

Run it with `python shop.py "Instant Pot Vortex 4 quart air fryer"`. You get up to nine rows, three per marketplace, sorted by price, then the model's one-sentence answer and the credits the run spent. An empty `ship` column means the source didn't state shipping, and `?` under stock means the same for availability. We ran this code with the model and API calls mocked, which checks the loop, the cap and the table; we haven't run it live against both APIs for this article, so expect to adjust the prompt after your first real runs.

## What should you check before trusting the table?

Three things, all visible in the recorded data. First, currencies: Walmart returns a `currency` field, while Google Shopping's `extracted_price` is a bare number, with the currency only visible in the `price` text ( `$393.73` in the recording, searched with `gl=us`). AliExpress's `price` carries its own `currency` ( `USD` in the recording). Only compare rows with the same currency. Second, sponsored results: both of the first two Walmart results in the recording are `sponsored: true`, so the first result isn't necessarily the cheapest or the most relevant.

Third, matching. "6 quart air fryer" matches many products, and the model decides which offers are the same item. Ask for a specific model name or number when you need like-for-like prices, and spot-check the links. For tracking prices over time rather than answering one question, the [price tracking agent tutorial](https://perceptlayer.com/blog/price-tracking-agent-python) stores results per day.

## What does each question cost?

Each successful search is 1 credit, and `record_offer` costs nothing because it never calls the API. A question that searches each marketplace once is 3 credits; one that refines all three searches hits the cap at 6. Failed calls are free and don't count toward the cap.

| Workload | Credits per question | Credits a month |
| --- | --- | --- |
| 300 questions, one search per marketplace | 3 | 300 × 3 = 900 |
| 300 questions, every search refined (cap) | 6 | 300 × 6 = 1,800 |
| 3,000 questions, one search per marketplace | 3 | 3,000 × 3 = 9,000 |

The first row fits inside the 1,000 free monthly credits, and the third inside the Starter pack's 10,000. These are Perceptlayer credits only; Claude's tokens are billed by Anthropic. The [credits docs](https://docs.perceptlayer.com/credits) explain the balance, and [pricing](https://perceptlayer.com/pricing) has what packs cost. For the same idea through an MCP client instead of your own loop, the [AI agents (MCP) docs](https://docs.perceptlayer.com/ai-agents) show the server setup, and [MCP vs function calling](https://perceptlayer.com/blog/mcp-vs-function-calling-web-data) explains what changes.

## APIs in this post

-   [Google Shopping **1 credit per call** · 1 endpoint](https://perceptlayer.com/apis/google-shopping)
-   [Walmart **1 credit per call** · 2 endpoints](https://perceptlayer.com/apis/walmart)
-   [AliExpress **1 credit per call** · 9 endpoints](https://perceptlayer.com/apis/aliexpress)

## Frequently asked questions

### How much does one price comparison cost?

Three credits when the agent searches each marketplace once, and never more than the 6-credit cap, because the code refuses calls past it. Recording offers costs nothing. Failed calls cost nothing either. Claude's tokens are billed separately by Anthropic, so the full cost of a question is the credits plus the model usage.

### Why not normalise the responses in code?

You can: the recorded responses show `extracted_price` on Google Shopping, `price` on Walmart and `price.amount` on AliExpress. Letting the model fill a fixed `record_offer` schema avoids writing and maintaining three parsers, and lets it skip accessories and different models, which a parser can't judge. The code still controls the output shape, because the tool schema is the table.

### Does the agent include shipping costs?

Only when a source states them. None of the recorded search responses include a shipping cost, so the prompt tells the model to leave `shipping` null rather than guess. AliExpress has a separate `/v1/aliexpress/product-shipping` endpoint with `product_id` and `country`; adding it as a fourth allowed endpoint costs 1 credit per product checked.

### Can I run this agent through MCP instead?

Yes. Add Perceptlayer's MCP server to an MCP client and give the model the same system prompt; its `call_endpoint` tool takes the same endpoint IDs and parameters. What you lose is the hard cap, since the client, not your code, runs the calls. Keep your own loop when the cap has to hold.

### What happens when the cap is reached?

The next `call_endpoint` returns an error result, `Error credit_cap`, without calling the API, and the model is told to record the offers it already has. The run ends with a partial table rather than overspending. Raise `CREDIT_CAP` if your questions routinely need refined searches.

ai agents shopping price comparison tool calling python

## More from the blog

1.  [AI agents](https://perceptlayer.com/blog/category/ai-agents) 23 Oct 2026 Perceptlayer 8 min read
    
    ## [Build a Market Research Agent with Search and Trends Data](https://perceptlayer.com/blog/market-research-agent-search-trends)
    
    Wire Google Search, Trends and News into one Python agent, cap it at 8 credits per question, and see a recorded Trends response.
    
2.  [AI agents](https://perceptlayer.com/blog/category/ai-agents) 22 Jul 2026 Perceptlayer 11 min read
    
    ## [LangChain Custom Tool for Live Web Data: Python Tutorial](https://perceptlayer.com/blog/langchain-custom-tool-web-data)
    
    Build a LangChain tool in Python for Google Search and News: typed arguments, retries on 429, 502 and 503 only, and a per-run credit cap.
    
3.  [AI agents](https://perceptlayer.com/blog/category/ai-agents) 12 Oct 2026 Perceptlayer 7 min read
    
    ## [MCP Server Too Many Tools: Keeping Agent Context Small](https://perceptlayer.com/blog/mcp-too-many-tools-context)
    
    One tool per endpoint for 1,659 endpoints is about 567,000 characters of schema. A 10-step checklist to keep an MCP server's tool list small.
