AI Shopping Agent: Compare Prices Across Marketplaces
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.
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; 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:
{
"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:
{
"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:
{
"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, Walmart API and AliExpress API 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.
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.
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, 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".
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 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.
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 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 explain the balance, and pricing has what packs cost. For the same idea through an MCP client instead of your own loop, the AI agents (MCP) docs show the server setup, and MCP vs function calling explains what changes.
APIs in this post
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.