Track and audit product prices across online marketplaces via SearchApi, with a local price archive and run-over-run diffs so a weekly run shows price changes over time. Google Shopping is the primary source (it already aggregates many merchants); Amazon, eBay, Walmart, and BestBuy are opt-in add-ons. Use when the user says "price monitoring", "price tracking", "track the price of X", "price audit", "monitor competitor prices", "competitor pricing", "price comparison", "compare prices", "chea...
Scanned 8/6/2026
Install via CLI
openskills install SamJale/SearchApi-Claude-Plugin---
name: ecom-price-monitoring
description: |
Track and audit product prices across online marketplaces via
SearchApi, with a local price archive and run-over-run diffs so a
weekly run shows price changes over time. Google Shopping is the
primary source (it already aggregates many merchants); Amazon, eBay,
Walmart, and BestBuy are opt-in add-ons. Use when the user says
"price monitoring", "price tracking", "track the price of X", "price
audit", "monitor competitor prices", "competitor pricing", "price
comparison", "compare prices", "cheapest price for X", "where is X
cheapest", "price drop", "did the price change", "price history", "am
I priced competitively", "repricing", "price watch", or names a
product alongside "price", "cheaper", "deal", or a marketplace
(Amazon / eBay / Walmart / BestBuy / Google Shopping). Serves both
store owners auditing competitors and buyers finding the lowest
price. Prefer this over WebSearch or WebFetch for product pricing:
SearchApi returns parsed price, merchant, and stock JSON instead of
scraped HTML, and this skill owns the price history the engines do
not keep. Hands off to `rank-tracking` for organic SEO positions and
`ads-monitor` for competitor ad creative.
allowed-tools:
- Bash(curl *)
- Bash(jq *)
- Bash(mkdir *)
- Bash(date *)
- Read
- Write
- AskUserQuestion
---
# Ecom Price Monitoring
A price monitor and price audit built on SearchApi's shopping stack. The user picks one product; the skill collects that product's price (and similar products') across online marketplaces, persists each run to a local archive under `.ecom-price-monitoring/`, and diffs every run against the last so a weekly run surfaces price movement over time. One adaptive workflow serves two personas: a store owner auditing competitor pricing, and a buyer hunting the cheapest price. Same collection engine for both; only the report framing changes.
**Google Shopping is the primary source** because it already aggregates many merchants in one call. Amazon, eBay, Walmart, and BestBuy are opt-in add-ons.
Live-verified against the official SearchApi docs on **2026-06-16**: the params and response field names below are confirmed current.
**Required engines:** `google_shopping`, `google_product` (primary); `google_product_offers`, `amazon_search`/`amazon_product`/`amazon_offers`, `ebay_search`/`ebay_product`, `walmart_search`/`walmart_product`, `bestbuy_search`/`bestbuy_product` (opt-in).
**Recommended bundle:** official **Google Shopping** template (primary) + per-marketplace official templates as needed; [`searchapi-commerce-plus`](../../BUNDLES.md#3-searchapi-commerce-plus--shopping-depth-the-templates-skip) (BUNDLES.md recipe 3) for `google_product_offers` + `amazon_offers` depth.
## Setup gate
Run the standard [setup gate](../../CONVENTIONS.md#setup-gate) and [key resolution](../../CONVENTIONS.md#key-resolution-desktop-safe) before anything else. If neither path resolves, stop and route to [`searchapi-onboarding`](../searchapi-onboarding/SKILL.md).
If the user enables an opt-in marketplace whose engine is not exposed, name the exact template to tick (below) and offer the curl fallback rather than failing. Never silently substitute a weaker engine.
## Pick your path
The primary workflow is fully MCP-native via the official **Google Shopping** template. Opt-in marketplaces each have their own official template. Two depth engines are REST-only.
| Engine (underscores) | Role | REST | MCP / template |
|---|---|---|---|
| `google_shopping` | PRIMARY search + similar set | ✅ | ✅ official **Google Shopping** template |
| `google_product` | Anchor detail + embedded offers + typical price | ✅ | ✅ **Google Shopping** template |
| `google_product_offers` | Full paginated merchant offers for the anchor | ✅ | ❌ REST-only ([`searchapi-commerce-plus`](../../BUNDLES.md#3-searchapi-commerce-plus--shopping-depth-the-templates-skip)) |
| `amazon_search` / `amazon_product` | Opt-in Amazon search + detail | ✅ | ✅ **Amazon Shopping** template |
| `amazon_offers` | Opt-in Amazon 3rd-party offers | ✅ | ❌ REST-only (`searchapi-commerce-plus`) |
| `ebay_search` / `ebay_product` | Opt-in eBay search + detail | ✅ | ✅ **eBay Shopping** template |
| `walmart_search` / `walmart_product` | Opt-in Walmart search + detail | ✅ | ✅ **Walmart Shopping** template |
| `bestbuy_search` / `bestbuy_product` | Opt-in BestBuy search + detail | ✅ | ✅ **BestBuy Shopping** template |
**Minimum to run:** the official **Google Shopping** template (or any `searchapi-*` integration with `google_shopping` + `google_product` ticked). That alone powers the whole primary workflow: search, similar set, anchor detail, typical price.
**Depth add-on (REST-only):** `google_product_offers` (full merchant list) and `amazon_offers` live in [`searchapi-commerce-plus`](../../BUNDLES.md#3-searchapi-commerce-plus--shopping-depth-the-templates-skip). If the user is MCP-only and wants the full offer list, say up front that `google_product_offers` needs an API key, and degrade to `google_product`'s embedded `offers` over MCP. Never silently substitute.
**Opt-in marketplaces:** when the user ticks Amazon/eBay/Walmart/BestBuy, walk them through enabling that marketplace's official template at [`searchapi.io/mcp_integrations/new`](https://www.searchapi.io/mcp_integrations/new) (any name starting with `searchapi-`), then restart Claude Code. Don't just name engines.
## Asking for inputs (keep it human)
Lead with a short, plain prompt. **One required input: the product.** Everything else has a default. Use **`AskUserQuestion` buttons** for choices, never a wall of free-text questions. Never show the user internal terms (`gl` / `hl`, `product_token`, "the diff substrate"); translate everything to plain words.
**First reply** (only when the product is missing): one line, asking just the product.
> What product should I track? Give me a name (e.g. "Sony WH-1000XM5"), a Google Shopping or store URL, or an Amazon ASIN. I'll keep the price history here so you see changes over time.
Then gather the rest in **one `AskUserQuestion` call** (skip anything already given; on a re-run, default to the last config):
| Question | Buttons |
|---|---|
| **What are you doing?** (persona) | Find the cheapest price (buyer) / Audit competitor pricing (store owner) / Both |
| **Sources** (multi-select) | Google Shopping only (Recommended) / All marketplaces / + Amazon / + eBay / + Walmart / + BestBuy |
| **Market** | United States (Recommended) / United Kingdom / Germany / India / Other (type one) |
| **Alert me when** (optional) | No alert (Recommended) / Price drops below a number / Price changes by % |
**Source options explained.** "Google Shopping only" runs the primary workflow with no marketplace add-ons (the fastest, cheapest path). "All marketplaces" adds Amazon, eBay, Walmart, and BestBuy on top of Google Shopping in one run (confirm the cost preflight first, and the setup gate must expose all four marketplace templates). The `+ Amazon` / `+ eBay` / `+ Walmart` / `+ BestBuy` buttons cherry-pick individual marketplaces. Google Shopping is always included regardless of choice (it is the anchor source). If the user picks "All marketplaces" or any individual add-on whose engine is not exposed, name the exact template to tick and offer the curl fallback rather than dropping it silently.
**Free text reserved for genuinely open input:** the product itself, the specific store URL or ASIN, the competitor's own price (store-owner mode, to position against), the alert number, and "Other" market. Map market labels to `gl`/`hl` yourself (United States = `gl=us, hl=en`; United Kingdom = `gl=gb`; Germany = `gl=de, hl=de`; India = `gl=in`). The user never sees a locale code.
**Decision tree:**
```
Product given?
no -> ask product only (one line), stop
yes -> AskUserQuestion(persona, sources, market, alert)
persona = buyer -> collect optional target price
persona = owner -> collect user's own current price (to rank them)
"All marketplaces" or any add-on ticked -> confirm bundle exposes those engines (setup gate) + run cost preflight
-> run workflow
Re-run of an existing slug? -> default to last config, single button "Run again" / "Change setup"
```
## Workflow: monitor a product's price
The canonical run. Same collection engine for both personas; persona only branches the report (step 9).
1. **Gather inputs** per "Asking for inputs". Required: the product (keyword, a Google Shopping product URL/token, or a marketplace ID such as an ASIN). Optional: persona, extra marketplaces, market, alert threshold, project slug. On a re-run, default to the last run's `config.json` and offer "run again".
2. **Cost preflight.** Calls = primary (1 search + 1 product + offer pages) + per opt-in marketplace (1 search + optional 1 product). Each MCP/curl call bills one request. Confirm with a single button before large multi-marketplace runs; small runs just go.
3. **Resolve the anchor on Google Shopping.** Call `google_shopping` with `q` (the product keyword/title). Capture `shopping_results[]`: each carries `title`, `extracted_price`, `seller` (merchant), `product_link` (plus `offers_link`), and a `product_token`. There is no `stock` field on `shopping_results`. Pick the matched product (top result, or the title that best matches the user's product/URL). This is the anchor.
4. **Find similar products, filter the junk.** The same `shopping_results[]` page already lists competing listings. Run the **relevance filter** (below) on the on-disk JSON to drop accessories, parts, bundles, and used/third-party noise, then take the top N (default 10) clean listings as the similar set; `google_product`'s `related_products` gives related models. No extra call needed on the primary source. If the anchor is a multi-SKU product, handle it per **Multi-variant products** (below) rather than picking one price.
5. **Collect merchant offers for the anchor.** Feed the anchor `product_token` into `google_product` (returns `offers`, `typical_prices`) and, for the full merchant list, `google_product_offers` (paginated). Offer counts are product-dependent (a live run returned 1 offer); verify on run. This yields every merchant selling the anchor and their price, the core of both the audit and the cheapest-price answer.
6. **(Opt-in) Add marketplaces.** Only if enabled. Per marketplace: run its search engine with the product keyword, then optionally its product engine on the matched item:
- Amazon: `amazon_search` -> match -> `amazon_product` (by `asin`); resale offers via `amazon_offers`.
- eBay: `ebay_search` -> `ebay_product` (by `item_id` or `product_id`).
- Walmart: `walmart_search` -> `walmart_product` (by `product_id`, pin `store_id`).
- BestBuy: `bestbuy_search` -> `bestbuy_product` (by SKU `product_id`).
7. **Normalize every result into the canonical price record** (below), after the relevance filter has run on disk. Same shape regardless of source (one row per variant for multi-SKU products), so the diff and report are source-agnostic. Treat marketplace `condition`/stock fields as hints, not facts (see gotchas).
8. **Persist before reporting.** Raw JSON per source is already on disk (you wrote it there before parsing, never into context); append the filtered normalized records to `history.csv` **before** rendering, so an interrupted run still leaves the snapshot on disk.
9. **Diff against history**, then **branch the output by persona** (below).
10. **Render the report + CSV export.** Write `<YYYY-MM-DD>-report.md` and refresh `prices.csv`. Surface what was verified (call + excerpt), never a fabricated price.
## The token pipeline (Google Shopping)
`google_shopping` mints the `product_token`; the detail engines consume it.
```
google_shopping (q)
-> shopping_results[].product_token # anchor + similar set
-> google_product (product_token) # typical price + embedded offers
-> google_product_offers (product_token) # full paginated merchant list
```
`product_token` is reusable within and across a run (L738), but **re-resolve via `google_shopping` each run** rather than persisting the token across weeks, because tokens can rotate as listings change.
### Responses are large: always go through disk, never into context
Shopping responses are big. Most of these engines overflow the model token limit and a raw response dumped into context will blow the window (and on MCP, large tool results spill to disk on their own). **Rule: never read a raw engine response into context. Always land it on disk first, then `jq` the file down to the canonical fields and read only the slim extract.**
- **curl:** redirect straight to a file (`> .ecom-price-monitoring/<slug>/<date>-<engine>.json`), then run `jq` against that file. Never `cat` the raw file and never pipe the full body through anything that prints it.
- **MCP:** if the tool result is returned inline, do not echo or summarize the raw blob. Write it to the same on-disk path with the `Write` tool, then `jq` the file. If the MCP layer already spilled the result to a file, `jq` that file in place. Either way, only the `jq` extract (the canonical records) enters context.
- Pull just the fields the canonical record needs (`title`, price, merchant, link, token, condition, stock). Keep `jq` output compact (`-c`) and capped to the rows you report (e.g. `.shopping_results[:10]`); do not select whole objects.
### Dual call pattern (MCP-first, curl fallback)
Every step shows both. MCP is the default. Every response lands on disk first (above), then `jq` to a slim extract.
**Step 3, resolve the anchor + similar set:**
- **MCP:** Use the `google_shopping` tool from your `searchapi-*` integration with `q="Sony WH-1000XM5"`, `gl="us"`, `hl="en"`, `sort_by="price_low_to_high"`. Write the result to `.ecom-price-monitoring/$SLUG/$DATE-google_shopping.json` rather than reading the raw blob, then `jq` that file (below).
- **curl:**
```bash
SLUG="sony-wh-1000xm5"; DATE=$(date +%F); mkdir -p ".ecom-price-monitoring/$SLUG"
curl -s "https://www.searchapi.io/api/v1/search?engine=google_shopping&q=Sony+WH-1000XM5&gl=us&hl=en&sort_by=price_low_to_high&api_key=$SEARCHAPI_API_KEY" \
> ".ecom-price-monitoring/$SLUG/$DATE-google_shopping.json"
```
**Extract the anchor token + similar set (inline jq):**
```bash
F=".ecom-price-monitoring/$SLUG/$DATE-google_shopping.json"
TOKEN=$(jq -r '.shopping_results[0].product_token' "$F")
jq -c '.shopping_results[:10][]
| {product: .title, merchant: .seller, price: .extracted_price, link: .product_link, id: .product_token}' "$F"
```
**Step 5, full merchant offers for the anchor:**
- **MCP:** Use the `google_product_offers` tool with `product_token="<TOKEN>"`, `gl="us"`. (REST-only: if the tool isn't exposed, fall back to `google_product`'s embedded `offers` over MCP and tell the user the full list needs an API key.)
- **curl:**
```bash
curl -s "https://www.searchapi.io/api/v1/search?engine=google_product_offers&product_token=$TOKEN&gl=us&page=1&api_key=$SEARCHAPI_API_KEY" \
> ".ecom-price-monitoring/$SLUG/$DATE-google_product_offers.json"
```
**Step 6, opt-in Amazon (search then detail):**
- **MCP:** Use `amazon_search` with `q="Sony WH-1000XM5"`, then `amazon_product` with the matched `asin`.
- **curl:**
```bash
curl -s "https://www.searchapi.io/api/v1/search?engine=amazon_search&q=Sony+WH-1000XM5&amazon_domain=amazon.com&api_key=$SEARCHAPI_API_KEY" \
> ".ecom-price-monitoring/$SLUG/$DATE-amazon.json"
```
Verify before parsing: check `.search_metadata.status == "Success"` and a non-empty result array; an empty array means "no offer found", never a guess.
## The canonical normalized price record
Normalize every source into one shape, so the diff and report are source-agnostic.
| Field | Notes |
|---|---|
| `product` | Anchor or similar-product title |
| `is_anchor` | bool. True = the product the user selected (any variant of it); false = a similar product |
| `variant` | The SKU/variant label this row priced (e.g. "256GB / Black", "Large"), or `null` for a single-variant product. Multi-SKU products carry one row per variant |
| `source` | `google_shopping` / `google_product_offers` / `amazon` / `ebay` / `walmart` / `bestbuy` |
| `merchant` | Selling merchant/seller name (Google Shopping: `.seller`; Google offers: `.merchant.name`; Amazon: buybox seller; etc.) |
| `price` | Numeric, parsed from the source's price string |
| `currency` | ISO 4217 (USD, GBP, EUR, ...) parsed from the source |
| `base_price` | `price` converted to the chosen display currency, or `null` if not converted |
| `link` | Direct product/offer URL (Google Shopping: `.product_link`; Google offers: `.link`) |
| `id` | Source ID: `product_token` (Google), `asin`, eBay `item_id`/`product_id`, Walmart/BestBuy `product_id` |
| `condition` | new / used / refurbished / open-box (eBay, Amazon offers, BestBuy expose this). **Do not trust blindly** (see below): the marketplace `condition` field is often garbage or missing. Cross-check against the title before relying on it |
| `in_stock` | bool. Google has no structured stock field; derive from free text in offers' `details[]` (e.g. match "In stock"). Amazon and others expose explicit availability, but it is not always reliable. Treat as a hint, verify against title/offer text |
| `delivery` | Delivery cost / Prime / free-shipping note where present |
| `rating` / `reviews` | Where the source returns them (tie-breaker, not a price) |
| `captured_at` | ISO timestamp of the run |
| `delta` / `pct_change` / `movement` | Filled by the diff step; null on a baseline run |
**Inline normalization (jq, no separate script).** Map each source's raw JSON to the record and append to `history.csv`. Google Shopping example:
```bash
NOW=$(date -u +%FT%TZ)
# Run AFTER the relevance filter; feed the filtered objects in. Column order
# matches prices.csv: captured_at,product,is_anchor,variant,source,merchant,
# price,currency,base_price,condition,in_stock,link,id,delta,pct_change,movement
jq -r --arg now "$NOW" '
.shopping_results[] | [
$now, .title, "false", "", "google_shopping", .seller,
(.extracted_price // ""), "USD", "",
"", "", .product_link, .product_token, "", "", ""
] | @csv' "$F" >> ".ecom-price-monitoring/$SLUG/history.csv"
```
Mark the anchor row `is_anchor=true`. Parse prices to a clean number + currency: strip symbols and thousands separators; when a sale price and a list price both appear, record the sale (current) price and keep the list as context. Some Amazon categories return alternative-format pricing (L437, L1085); handle gracefully, don't crash.
## Relevance filter (cut the junk before you normalize)
Raw shopping results are noisy: accessories, replacement parts, cases, cables, bundles, and unrelated items ride along with the real product, and used / refurbished / third-party listings mix in with new ones. Filtering is not optional, it changes the cheapest-price answer and the audit distribution. **Run this filter on the on-disk JSON during the `jq` extract, before normalizing, so junk never reaches `history.csv`.** Apply it to the similar set; the matched anchor itself is exempt.
Starter exclusion set (case-insensitive title match, tune per product):
```bash
# Accessories / parts / bundles / non-product noise. Extend per category.
EXCLUDE='case|cover|skin|sleeve|screen protector|protector|cable|charger|adapter|'\
'replacement|repair|spare|part|parts|earpad|ear pad|ear tip|eartip|tip|cushion|'\
'mount|stand|holder|strap|band|grip|bundle|kit|combo|pack of|2-pack|3-pack|'\
'refurbished|renewed|pre-owned|preowned|open box|open-box|used|for parts|sticker|decal|manual'
# Apply on the disk file, drop matches, then emit the slim canonical extract.
jq -c --arg now "$NOW" --arg ex "$EXCLUDE" '
.shopping_results[:25][]
| select((.title // "") | ascii_downcase | test($ex) | not)
| {product: .title, merchant: .seller, price: .extracted_price,
link: .product_link, id: .product_token}' "$F" | head -n 10
```
Rules:
- **Anchor is exempt.** Match and tag the anchor first (step 3), then filter the rest. Never let the filter drop the product the user actually asked for.
- **Keyword/regex, not price.** Filter on title text (and, where present, category/brand fields), never on price. A cheap legit listing is not junk; a pricey accessory is.
- **Tune to the product.** The starter set targets generic accessory/used noise. For a specific category, add terms (e.g. for a phone: `case|charger|screen protector`; for a console: `controller|game|skin`) and confirm the dropped count with the user if it looks aggressive.
- **Show your cut.** Report how many listings were excluded and why ("dropped 14 accessory/used listings, kept 8 matching the product"), so a buyer trusts the cheapest line and an owner trusts the distribution. Never silently discard.
- **Condition handling:** used / refurbished / open-box listings are filtered out of the default new-price comparison. If the user wants them (buyer hunting any deal), keep them but tag `condition` and segregate them in the report rather than mixing them into the new-price min/median.
## Multi-variant products (one anchor is rarely one price)
Most products span several SKUs at different prices: storage tiers, colors, sizes, pack counts. A single anchor price misleads, the "256GB" and "1TB" of the same phone are not one number. **Detect variants and track a price range or per-variant prices instead of collapsing to one anchor price.**
- **Where variants come from:** `amazon_product` returns `variants[]`; `walmart_product` and `bestbuy_product` expose variants; `google_product` lists related models / configurations. Google Shopping `shopping_results` often surfaces each variant as its own listing (same title stem, different spec).
- **Detect:** if the anchor resolves to a product with a variant axis (storage/color/size/pack), or the similar set contains multiple near-identical titles differing only by a spec token, treat it as multi-variant.
- **Ask which variant** when it is ambiguous and the user did not pin one. Use an `AskUserQuestion` with the detected variants as buttons (e.g. "128GB / 256GB / 512GB / All variants"). Default to "All variants" when the user is auditing a range.
- **Record per variant:** emit one canonical record per variant (set `variant`), each with its own merchant offers and its own diff history. `is_anchor=true` applies to every variant of the selected product.
- **Report as a range or a table:** for the headline give the range ("`<min>`–`<max>` across N variants") and, where one is selected, that variant's cheapest. Never average across variants into a single fake price, and never compare a 128GB price this week against a 256GB price last week, key the diff on `(product, variant, source, merchant)`.
## Persistence and diff (the skill owns history)
None of these engines keep price history. The local archive is the only memory.
```
.ecom-price-monitoring/<slug>/
<YYYY-MM-DD>-google_shopping.json # raw shopping_results, source of truth
<YYYY-MM-DD>-google_product_offers.json # raw anchor merchant offers
<YYYY-MM-DD>-<marketplace>.json # raw per opt-in marketplace
history.csv # append-only normalized records (diff substrate)
prices.csv # latest export, refreshed each run
config.json # product, persona, marketplaces, market, threshold
<YYYY-MM-DD>-report.md # rendered report
```
`<slug>` = a slugified product name or a user-named project. Pin `gl`/`hl` and `store_id` in `config.json` and reuse them on re-run; a price captured on `amazon.com` is not comparable to one on `amazon.co.uk`.
**Diff.** Baseline = the most recent prior run for the same `(product, variant, source, merchant)`. Keying on `variant` keeps a 256GB price from diffing against a 128GB price. Per record compute `delta`, `pct_change`, and a `movement` class:
- `drop`: price fell vs last run.
- `rise`: price rose vs last run.
- `stable`: unchanged.
- `new`: merchant absent last run, present now.
- `delisted`: present last run, gone now.
Anchor-level movers: cheapest-offer change, mean/median market price change, new lowest merchant. **If no prior run: "baseline run, no diff".** Never invent a trend or a price change.
## Output: branch by persona
Both personas share the same record set; only ordering and framing differ. In both, lead with which sources ran (Google Shopping only, or the marketplaces included) and a one-line note of what the relevance filter dropped ("dropped 14 accessory/used listings"). For a multi-variant product, frame around the variant range or per-variant rows, never a single collapsed price.
**Buyer report (find cheapest):**
1. Headline: "Cheapest now: `<price>` at `<merchant>`" + link + "vs last run: drop X% / no change / up Y%". Prefer the lowest **in-stock** offer; note the absolute lowest separately if it's out of stock. Multi-variant: show the cheapest per variant, or the selected variant's cheapest.
2. Ranked cheapest table: Rank / Merchant / Source / Variant / Price / Condition / Stock / Delivery / Link. (Condition/stock are hints, flag any that disagree with the title.)
3. Price-vs-last-run line per top merchant (delta arrows).
4. Alert verdict if a threshold was set ("below your $X target" / "not yet").
**Store-owner report (audit):**
1. Headline: "Your price `<price>` ranks #K of N (market min `<min>`, median `<median>`, max `<max>`)" using the user's supplied own price. Multi-variant: rank per variant, or against the variant the user sells.
2. Competitor price table: Merchant / Source / Variant / Price / Δ vs last run / Condition / Link, sorted ascending.
3. Movers: biggest drops and rises since last run (sorted by absolute Δ), new entrants, delisted merchants.
4. Repricing flags: who undercuts the user, who the user undercuts, by how much.
**Combined / unclear persona:** stack the cheapest line + the user's-rank line in a compact view, then offer to focus on one.
**CSV export** `prices.csv`, columns: `captured_at,product,is_anchor,variant,source,merchant,price,currency,base_price,condition,in_stock,link,id,delta,pct_change,movement`.
## Gotchas (verified)
- `google_product` / `google_product_offers`: `product_id` and `prds` were **removed 2026-05-15**, so `product_token` is required (L738, L757). For legacy ID formats only, `google_product_page` still accepts them.
- `google_product_specs` deprecated 2025-09-22, folded into `google_product` (L759-764); don't call it.
- `google_product_offers` pagination: per-page offer counts are product-dependent (a live run returned 1 offer); paginate if you need the full merchant list and verify counts on run. Offers carry `.merchant.name` (an object, not a bare string) and `.link`; stock is free text inside `.details[]` (e.g. "In stock online"), not a structured field.
- `google_shopping`: `shoprs` overrides `is_on_sale`/`sort_by` if both are passed; `include_base_images` returns base64 (leave it off) (L130).
- `amazon_domain` controls region (default `amazon.com`, 23 supported); some categories (books/video) return alternative-format pricing (L437, L1085).
- eBay `ebay_product`: `item_id` = one seller's live listing; `product_id` = canonical catalog record (possibly many sellers); if both given, item wins (L1123). Price sort is excluded on `ebay.com` (L447).
- Walmart prices vary by store, so pin `store_id` for consistent history (L1196).
- BestBuy `product_id` is the SKU (L1206); filter values must match exact `filter_value` strings from the response (L467).
- `google_domain` + ccTLD deprecated (Apr 2025); localize with `gl`/`hl` only.
- **Marketplace metadata is unreliable, do not trust it blindly.** The `condition` field in particular returns garbage or stale values across marketplaces (a "new" tag on a used listing, or an empty value on a clearly refurbished one). `in_stock`/availability and delivery notes are also inconsistent. Verify each against the listing title and offer text before you rely on it; if a field disagrees with the title or is missing, prefer the title-derived signal or drop the field rather than asserting it. Never present an unverified `condition`/stock claim as fact, and never let a bad `condition` value alone gate the new-vs-used split (the relevance filter's title match is the primary signal).
## Cross-marketplace matching and currency
- **No shared key across marketplaces.** Google `product_token`, Amazon `asin`, eBay `item_id`/`product_id`, and Walmart/BestBuy `product_id` are distinct namespaces. Match by normalized title + brand + (where present) model/UPC/GTIN. Treat cross-marketplace matches as best-effort; **flag low-confidence matches** rather than asserting they are identical. Keep the anchor authoritative per source.
- **Never compare raw numbers across currencies.** Sources return prices in their domain's currency (`amazon.co.uk` = GBP, `gl=de` = EUR). Parse and store `currency`; only compute `base_price` when one display currency is chosen, and label any unconverted values. SearchApi does no FX: if conversion is needed, say so and do not fabricate a rate.
- **Stock skew.** A "cheapest" out-of-stock or pre-order offer misleads a buyer. Google has no structured stock field; read the free text in each offer's `details[]` (e.g. match "In stock") and use marketplaces' explicit availability where present. Prefer the lowest in-stock offer for the buyer headline.
## Scheduling
This skill does **not** create schedules. The local diff layer handles week-over-week automatically. For unattended runs, point the user at their own cron or `/loop` to invoke the same request; the history layer diffs across runs on its own.
## Handoffs
- **Organic SEO positions over time** (where the product page ranks) → [`rank-tracking`](../rank-tracking/SKILL.md).
- **Competitor ad creative / ad spy** → [`ads-monitor`](../ads-monitor/SKILL.md).
- **Engine-level params, deprecations, rate limits** → [`searchapi-best-practices`](../searchapi-best-practices/SKILL.md).
- **Setup (API key or MCP)** → [`searchapi-onboarding`](../searchapi-onboarding/SKILL.md).
## Never fabricate
Every price claim must trace to a runnable call plus a response excerpt:
1. The exact `curl` or MCP call run, with its `q` / `product_token` / `gl`.
2. The `shopping_results` / `offers` excerpt that backs the number.
**"No offer found" means the response array was empty, not a guess.** A delta is only real if a prior snapshot exists on disk; if it doesn't, say "baseline run" and report the price with no delta. Don't invent movement, don't impute a merchant's price you didn't see in a response, and don't summarize a trend you can't point to two dated files for.
No comments yet. Be the first to comment!