---
title: "Build a Price Tracking Agent in Python with Live Data"
description: "A Python price tracker that checks Google Shopping and eBay once per product per run, stores history in SQLite, flags drops and costs 2 credits a product."
url: https://perceptlayer.com/blog/price-tracking-agent-python
---

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

# Build a Price Tracking Agent in Python with Live Data

17 Sept 2026 Perceptlayer 8 min read

A price tracking agent in Python is a loop that reads a watchlist, asks two live sources for the current lowest price, saves each reading and alerts you when the price falls. This tutorial builds one with the Google Shopping and eBay endpoints, a single Python file and SQLite. Each product costs 2 credits per run, so the monthly bill is a multiplication you can do before you start.

## Key takeaways

-   The tracker makes two calls per product per run: `GET /v1/google-shopping/search` and `GET /v1/ebay/search`, 1 credit each.
-   A title filter and a minimum price per product matter more than any other setting, because search results include accessories and unrelated listings.
-   A drop is a price at least 5% below the average of the last five readings, or at or below your target price.
-   25 products checked 4 times a day cost 25 × 2 × 4 × 30 = 6,000 credits a month.
-   Failed calls cost nothing, but a client timeout can hide a call that succeeded, so the script never retries a timeout automatically.

## What does a price tracking agent have to do?

A tracker has four jobs: fetch the current price for each product, store it with a timestamp, compare it with history, and tell you when something changed. Fetching is the only part that needs live web data. Everything else is a small database and a few lines of arithmetic, which is why this build uses no framework and no model call on the hot path.

We use two sources because they answer different questions. Google Shopping returns offers from many retailers for a query, so it tells you the going retail price. eBay search returns individual listings, so it shows the marketplace price.

| Source | Endpoint | What it gives the tracker | Credits per call |
| --- | --- | --- | --- |
| [Google Shopping](https://perceptlayer.com/apis/google-shopping) | `GET /v1/google-shopping/search` | Retail offers with a numeric price and the seller in `source` | 1 |
| [eBay](https://perceptlayer.com/apis/ebay) | `GET /v1/ebay/search` | Listings with `price.extracted` and `price.currency` | 1 |

The result is a script you can run from cron. Because the loop is deterministic, it is cheap, easy to test, and it never invents a price.

## How do you build the tracker?

You build it in seven steps: get a key, write a watchlist, add a small API client, write one fetch function per source, store readings, detect drops, then run it on a schedule. The code below is one file, `tracker.py`. Each step adds to it, and the pieces concatenate in order into a complete script.

### Step 1: Get a key and install two packages

Create a free account at [/signup](https://perceptlayer.com/signup) to get a key that starts with `sk_live_`. Free accounts get 1,000 credits every month after email verification, with no credit card. Put the key in an environment variable, as the [authentication docs](https://docs.perceptlayer.com/authentication) describe.

```bash
export API_KEY="sk_live_..."
pip install requests
```

SQLite ships with Python, so `requests` is the only dependency. See the [Requests documentation](https://requests.readthedocs.io/en/latest/) if you want to tune sessions or timeouts.

### Step 2: Write the watchlist

Search is fuzzy, so each product carries its own matching rules. `must_include` words must all appear in a result title, `exclude` words drop accessories, and `min_price` removes cases, chargers and parts that match the query by accident. `target` is the price at which you want an alert regardless of history.

Save this as `watchlist.json`:

```json
[
  {
    "name": "iPhone 15 128GB",
    "google_q": "iphone 15 128gb",
    "ebay_q": "iphone 15 128gb unlocked",
    "must_include": ["iphone 15", "128"],
    "exclude": ["case", "cover", "protector", "charger", "cable", "pro", "plus", "max"],
    "min_price": 300,
    "target": 400
  },
  {
    "name": "Keurig K-Cafe",
    "google_q": "keurig k-cafe coffee maker",
    "ebay_q": "keurig k-cafe coffee maker",
    "must_include": ["k-cafe"],
    "exclude": ["pods", "filter", "descaler", "parts"],
    "min_price": 60,
    "target": 120
  }
]
```

These two products are examples. Replace them with yours, and expect to adjust the filters after the first run.

### Step 3: Add an API client that respects the limits

The client sends the key in the `X-API-Key` header, waits when the API says to, and records the credit balance from every response. Rate-limited (429) and temporarily unavailable (503) responses carry a `Retry-After` header with the seconds to wait, and they are failed calls, so retrying them costs nothing extra. See [rate limits](https://docs.perceptlayer.com/rate-limits) for how the per-minute counting works.

```python
import json
import os
import sqlite3
import time
from datetime import datetime, timezone

import requests

BASE = "https://api.perceptlayer.com"
DROP = 0.05            # alert when 5% below the recent average
HISTORY = 5            # readings that make up the average
PAUSE = float(os.environ.get("PAUSE", "1.0"))        # seconds between calls
CREDIT_FLOOR = int(os.environ.get("CREDIT_FLOOR", "50"))

session = requests.Session()
session.headers["X-API-Key"] = os.environ["API_KEY"]
state = {"remaining": None}

def call(path, params, tries=3):
    for _ in range(tries):
        r = session.get(BASE + path, params=params, timeout=60)
        if r.status_code in (429, 503):
            time.sleep(int(r.headers.get("Retry-After", "5")))
            continue
        r.raise_for_status()
        state["remaining"] = int(r.headers.get("X-Credits-Remaining", 0))
        return r.json()
    raise RuntimeError(f"{path}: still limited after {tries} tries")
```

The default `PAUSE` of one second keeps a free account, which gets 1 request a second, under its limit. Paying accounts run at least 5 requests a second, so you can lower it.

### Step 4: Fetch the lowest matching price from each source

Each fetch function calls one endpoint, filters the results with the watchlist rules and returns the cheapest survivor as `(price, title, link)`. Google Shopping results sit in `shopping_results` with a numeric `extracted_price`. The eBay search response nests listings under `data.results`, each with a `price` object.

Here is an excerpt of a recorded response from `GET /v1/ebay/search?q=coffee+maker&page=1&per_page=60`, trimmed with …:

```json
{
  "success": true,
  "data": {
    "query": "coffee maker",
    "page": 1,
    "per_page": 60,
    "has_more": true,
    "results": [
      {
        "product_id": "366702966104",
        "title": "Mr. Coffee 4-in-1 Single-Serve Latte, Iced, and Hot Coffee",
        "link": "https://www.ebay.com/itm/366702966104",
        "price": {
          "raw": "$60.00",
          "extracted": 60,
          "currency": "USD"
        },
        "sponsored": true,
        …
      },
      …
    ],
    …
  },
  …
}
```

The tracker reads `title`, `link`, `price.extracted` and `price.currency`. It also passes the optional boolean `buy_it_now` parameter, because a Buy It Now price is easier to compare over time than a bid. Confirm exactly what the filter does on the [eBay search endpoint page](https://perceptlayer.com/apis/ebay/search), which lists every parameter, including `price_min` and `free_shipping`, and spot-check the titles in your first runs.

```python
def matches(title, item):
    t = title.lower()
    return all(w.lower() in t for w in item["must_include"]) and not any(
        w.lower() in t for w in item.get("exclude", [])
    )

def cheapest(rows, item):
    best = None
    for title, price, link in rows:
        if price is None or price < item["min_price"] or not matches(title, item):
            continue
        if best is None or price < best[0]:
            best = (price, title, link)
    return best

def google_lowest(item):
    data = call(
        "/v1/google-shopping/search",
        {"q": item["google_q"], "gl": "us", "hl": "en"},
    )
    rows = [
        (r.get("title", ""), r.get("extracted_price"), r.get("link"))
        for r in data.get("shopping_results", [])
    ]
    return cheapest(rows, item)

def ebay_lowest(item):
    data = call(
        "/v1/ebay/search",
        {
            "q": item["ebay_q"],
            "buy_it_now": "true",
            "price_min": item["min_price"],
            "per_page": 60,
        },
    )
    rows = []
    for r in data["data"]["results"]:
        price = r.get("price") or {}
        if price.get("currency") == "USD":
            rows.append((r.get("title", ""), price.get("extracted"), r.get("link")))
    return cheapest(rows, item)
```

Both functions cost one credit when they succeed. The Google Shopping call passes `gl=us` and `hl=en`, the same values as the recorded example for that endpoint. Both are optional two-character parameters. This tutorial only tests with those values, so if you change them, check the prices you get back before trusting the history. The eBay function keeps only USD listings, so change that check if you track in another currency.

### Step 5: Store readings and detect drops

Every reading goes into a `prices` table with a UTC timestamp, so history survives restarts. A drop is a price at least 5% below the mean of the previous five readings for the same product and source, or any price at or below your `target`. Comparing against an average rather than the last reading catches slow slides that a one-step comparison misses, and it ignores a single noisy listing.

```python
def open_db(path="prices.db"):
    db = sqlite3.connect(path)
    db.execute(
        "CREATE TABLE IF NOT EXISTS prices ("
        "item TEXT, source TEXT, ts TEXT, price REAL, title TEXT, link TEXT)"
    )
    return db

def record_and_check(db, item, source, hit):
    price, title, link = hit
    prev = [
        row[0]
        for row in db.execute(
            "SELECT price FROM prices WHERE item = ? AND source = ? "
            "ORDER BY ts DESC LIMIT ?",
            (item["name"], source, HISTORY),
        )
    ]
    now = datetime.now(timezone.utc).isoformat(timespec="seconds")
    db.execute(
        "INSERT INTO prices VALUES (?, ?, ?, ?, ?, ?)",
        (item["name"], source, now, price, title, link),
    )
    db.commit()

    reasons = []
    if prev:
        avg = sum(prev) / len(prev)
        if price <= avg * (1 - DROP):
            reasons.append(f"{(1 - price / avg):.0%} below the recent average of {avg:.2f}")
    if price <= item["target"]:
        reasons.append(f"at or below your target of {item['target']}")
    return reasons
```

The [sqlite3 module documentation](https://docs.python.org/3/library/sqlite3.html) covers the `?` placeholders used here. Using them instead of string formatting keeps product names from breaking the query.

### Step 6: Run the loop with a spending guard

The main function walks the watchlist, calls both sources per product, and prints an alert for every drop. It stops early in two cases: the balance in `X-Credits-Remaining` falls below `CREDIT_FLOOR`, or the API returns `insufficient_credits` (402). The service is prepaid and there is no automatic top-up, so running out stops the tracker rather than billing you further.

```python
def alert(item, source, hit, reasons):
    price, title, link = hit
    print(f"DROP {item['name']} [{source}] {price:.2f}: {'; '.join(reasons)}")
    print(f"     {title}\n     {link}")
    # Send to Slack, email or a webhook here.

def run():
    with open("watchlist.json") as f:
        items = json.load(f)
    db = open_db()
    for item in items:
        for source, fetch in (("google-shopping", google_lowest), ("ebay", ebay_lowest)):
            if state["remaining"] is not None and state["remaining"] < CREDIT_FLOOR:
                print(f"Stopping: {state['remaining']} credits left")
                return
            try:
                hit = fetch(item)
            except requests.Timeout:
                # The call may have succeeded and been billed; skip it this run.
                print(f"timeout: {item['name']} [{source}]")
                continue
            except requests.HTTPError as e:
                body = e.response.json().get("error", {})
                print(f"{e.response.status_code} {body.get('code')}: {item['name']} [{source}]")
                if e.response.status_code == 402:
                    return
                continue
            finally:
                time.sleep(PAUSE)
            if hit is None:
                print(f"no match: {item['name']} [{source}]")
                continue
            reasons = record_and_check(db, item, source, hit)
            if reasons:
                alert(item, source, hit, reasons)
    print(f"Done. Credits remaining: {state['remaining']}")

if __name__ == "__main__":
    run()
```

Run it once with `python tracker.py`. The first run only records a baseline, so alerts on that run come only from your `target` prices. Drops against history start on the second run. For the error codes the handler prints, see the [errors reference](https://docs.perceptlayer.com/errors).

### Step 7: Schedule it

Run the script from cron, a systemd timer or any scheduler. This crontab entry runs it every six hours, which is four checks a day:

```bash
0 */6 * * * cd /opt/tracker && API_KEY=sk_live_... python tracker.py >> tracker.log 2>&1
```

Keep the working directory fixed, because `watchlist.json` and `prices.db` are read relative to it. A scheduler that overlaps two runs would double the spend, so keep the interval longer than a run takes.

## How many credits will it use a month?

Each product costs one Google Shopping call and one eBay call per run, so credits a month equal products × 2 × runs a day × 30. Failed calls cost nothing, so retries after a 429 or 503 do not add to the total. The figures below are credits only; [pricing](https://perceptlayer.com/pricing) shows what credits cost and the [credits docs](https://docs.perceptlayer.com/credits) explain how debits work.

| Products | Runs a day | Arithmetic | Credits a month | Fits in |
| --- | --- | --- | --- | --- |
| 10 | 1 | 10 × 2 × 1 × 30 | 600 | The 1,000 free monthly credits |
| 25 | 4 | 25 × 2 × 4 × 30 | 6,000 | The Starter pack's 10,000 |
| 100 | 4 | 100 × 2 × 4 × 30 | 24,000 | The Build pack's 50,000, about two months |

Purchased credits last 12 months, so a pack that outlives one month of tracking is not wasted. Two levers cut the bill. Check slow-moving products less often, and drop a source for products where one source gives no matches. If you want a hard ceiling, the pattern in [AI agent API cost control](https://perceptlayer.com/blog/ai-agent-web-data-costs) shows how to cap spend per run, and the `CREDIT_FLOOR` guard above is the simplest version of it.

## How do you keep a price tracker reliable?

Three problems cause most bad alerts: a wrong match, a duplicate charge and a silent stop. A wrong match is a filter problem, fixed by tightening `must_include`, `exclude` and `min_price` after reading what the first runs matched. A duplicate charge comes from retrying a call whose reply was lost. A silent stop comes from an empty balance or a scheduler that stopped running.

Requests have no idempotency key. A request whose reply was lost, such as a client timeout, may have succeeded and been billed. That is why the script skips a timed-out check instead of repeating it; one missed reading costs less than a doubled one. [AI agent API cost control](https://perceptlayer.com/blog/ai-agent-web-data-costs) covers spend limits that work alongside this.

For a silent stop, check the last line of `tracker.log` or alert when no new rows land in `prices` for a day. Data is returned as found at the time of the call, so a price you read is a price that was listed then, not a guarantee that the item is in stock now. Treat an alert as a prompt to open the link.

## Where does an LLM fit in a price tracking agent?

The scheduled loop does not need a model, and leaving one out keeps each run at exactly 2 credits per product with no token cost. A model earns its place at the edges: turning "watch the cheapest 128GB iPhone 15 under 400" into a watchlist entry, or explaining a drop in plain language when you ask for it.

For that, give the model the same data over MCP. The Perceptlayer MCP server at `https://api.perceptlayer.com/mcp` exposes `search_endpoints`, `get_endpoint` and `call_endpoint`; the first two only look up the catalogue and cost nothing, while `call_endpoint` spends credits on success only. The [AI agents docs](https://docs.perceptlayer.com/ai-agents) show the setup, and [MCP vs Function Calling for Live Web Data in Agents](https://perceptlayer.com/blog/mcp-vs-function-calling-web-data) helps you pick between that and a tool you write yourself. For a Python tool wrapper, see the [LangChain custom tool tutorial](https://perceptlayer.com/blog/langchain-custom-tool-web-data).

Next step: [get a free API key](https://perceptlayer.com/signup), run one call from each endpoint in the playground on the [Google Shopping](https://perceptlayer.com/apis/google-shopping) and [eBay](https://perceptlayer.com/apis/ebay) pages, then export your key as `API_KEY` and run `tracker.py` on your own watchlist.

## APIs in this post

-   [Google Shopping **1 credit per call** · 1 endpoint](https://perceptlayer.com/apis/google-shopping)
-   [eBay **1 credit per call** · 3 endpoints](https://perceptlayer.com/apis/ebay)

## Frequently asked questions

### How much does it cost to track one product?

Each run costs 2 credits per product, one for the Google Shopping search and one for the eBay search. Tracking one product four times a day for 30 days is 1 × 2 × 4 × 30 = 240 credits. Failed calls cost nothing and return `X-Credits-Used: 0`. See [pricing](https://perceptlayer.com/pricing) for what credits cost.

### Can I track only one of the two sources?

Yes. Remove the source you don't need from the tuple in `run()`, and the cost halves to 1 credit per product per run. Google Shopping suits retail prices across sellers. eBay suits marketplace listing prices. Many watchlists need only one, so start with one and add the other if matches are poor.

### What happens when I run out of credits?

The service is prepaid, with no automatic top-up. When the balance runs out, calls return `insufficient_credits` (402) and nothing more is debited. The script exits on a 402, and `CREDIT_FLOOR` stops it earlier, at a balance you choose, using the `X-Credits-Remaining` header from each response.

### How fast can the tracker call the API?

Limits are per account and counted per minute. Free accounts get 1 request a second (60 a minute), which the default one-second `PAUSE` respects. Paying accounts run at least 5 requests a second (300 a minute). A 429 response carries `Retry-After`, and the client waits that many seconds before retrying.

### Why does the tracker sometimes match the wrong product?

Search returns whatever matches the query, including accessories and other models. The title filter, the `exclude` words and `min_price` are what keep those out, so tune them per product after reading the first few runs. Read the `no match` and low-price lines in the log, and tighten the rules until the matched titles are the product you mean.

price tracking python ai agents google shopping ebay

## 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) 19 Oct 2026 Perceptlayer 8 min read
    
    ## [Trim API Responses for LLM Context: Fewer Tokens per Call](https://perceptlayer.com/blog/trim-api-responses-llm-context)
    
    Cut a Google Shopping response from 1,358 to 422 characters by keeping 8 fields. Python code, a measured before and after, and 1 credit per call.
    
3.  [AI agents](https://perceptlayer.com/blog/category/ai-agents) 11 Sept 2026 Perceptlayer 8 min read
    
    ## [OpenAI Agents SDK with a Remote MCP Server for Web Data](https://perceptlayer.com/blog/openai-agents-sdk-mcp-server)
    
    Connect the OpenAI Agents SDK to a Streamable HTTP MCP server with a Bearer header, cap call\_endpoint at 3 calls a question, and count credits per run.
