# Amazon Buy Box + Offers Agent (`buzzidata/amazon-buybox-offers-agent`) Actor

One ASIN check → Buy Box winner, stock, landed-cost offers. MCP-ready JSON. Errors never charged.

- **URL**: https://apify.com/buzzidata/amazon-buybox-offers-agent.md
- **Developed by:** [Ian Baxter](https://apify.com/buzzidata) (community)
- **Categories:**
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $20.00 / 1,000 asin checks

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Amazon Buy Box + Offers Agent

**One ASIN check → Buy Box winner, stock, landed-cost offers. MCP-ready JSON. Errors never charged.**

Apify Store Actor (`amazon-buybox-offers-agent`) published by **buzzidata**. This repository is the source scaffold. It is **not** published to the Apify Store from this PR.

Give the Actor one or more ASINs (or `/dp/` URLs). For each marketplace × ASIN it returns a single JSON snapshot: who owns the Buy Box, whether the listing looks in stock, and competing offers with **landed price** (item + shipping). Failed checks are written with a typed error (`BLOCKED`, `RATE_LIMIT`, `PARSE`, `NETWORK`) and are **never** billed.

### How to run for money (proxy)

Amazon.com blocks datacenter and AUTO Apify Proxy groups. **Unblocker is the recommended Amazon.com production default on Free and paid Apify.** Residential US is a fallback when you need sticky IPs (glow ZIP cookies) or the account has no Unblocker units.

| Setting | Default | Why |
| --- | --- | --- |
| Apify Proxy | **On** | Free-plan proxy only works from Actors on the platform, not from your laptop |
| `proxyGroup` | **`UNBLOCKER`** | Production default. Build 0.1.5 with Unblocker + ZIP **92618** returned real Buy Boxes (`B014I8SSD0` Amazon.com $4.80, `B07FM5HLMF` YETI Authorized $35 FBA, `B004U3Y8OM` Amazon.com $8.99). Residential alone was mostly `RATE_LIMIT`. |
| Country | **Omitted** on Unblocker; **`US`** on residential | Country targeting shrinks the Unblocker pool. Residential US + California (`CA`) matches ZIP **92618** |
| Concurrency | **1** (Actor default) | Higher parallelism burns proxy budget and raises `RATE_LIMIT` |
| `postalCode` | **92618** unless you need another ZIP | Location cookies + glow address-change run before `/dp/` |

The Console form and empty API input (`{}` or `{ "useApifyProxy": true }`) default to Unblocker. The proxy widget may still show RESIDENTIAL because Apify MCP cannot put `UNBLOCKER` on `apifyProxyGroups`; **`proxyGroup` overrides it**. Set `useApifyProxy: false` only for local debugging.

#### When to select Residential (fallback)

Use **`proxyGroup: RESIDENTIAL`** when you need sticky US IPs so glow ZIP cookies hold across home → address-change → `/dp/` → AOD, or if the account has no `PROXY_UNBLOCKER` units.

| | Unblocker (default) | Residential (fallback) |
| --- | --- | --- |
| Select | `proxyGroup: "UNBLOCKER"` | `proxyGroup: "RESIDENTIAL"` |
| Sticky IP | **No** — glow ZIP cookies may not hold | Yes (~30 min) |
| Country / CA | **Omitted** (country shrinks the bypass pool) | Forced US (+ CA) |
| Billing | **10 Unblocker units** per successful request | Residential **GB** |
| Free plan | Separate Unblocker unit allotment (often large on Free) | ~20 GB/mo residential credit |

**MCP clients:** Apify MCP hardcodes `proxyConfiguration.apifyProxyGroups` to `["RESIDENTIAL","DATACENTER"]` and **rejects `UNBLOCKER` on that field**. Default `proxyGroup` is already `UNBLOCKER`. To force the fallback:

```json
{ "asins": ["B014I8SSD0"], "proxyGroup": "RESIDENTIAL", "dryRun": true }
```

Console users whose account has `PROXY_UNBLOCKER` can also pick Unblocker on the proxy widget; either path is enough.

**Free-plan credits.** Watch [Proxy usage](https://console.apify.com/proxy/usage). Cheerio stays on the HTTP path (no images/JS). Each ASIN still fetches glow home + address-change + `/dp/` + optional AOD; CAPTCHA retries download HTML again. Unblocker bills those hops in units (roughly several successful requests per ASIN before retries).

Do **not** turn on a browser crawler for production. Playwright is intentionally not the default.

### Why this Actor

- Pricing / MAP teams who need the live Buy Box seller, not just a list price
- 3P sellers tracking whether Amazon or a competitor holds the box
- Agents and MCP clients that want one structured record per ASIN, not HTML

### Features

- Cheerio + Crawlee (no browser in v1; Playwright only if a later revision needs JS-hydrated Buy Box)
- Default **Unblocker** Apify Proxy (`proxyGroup: UNBLOCKER`) when proxy input is missing or empty — recommended Amazon.com production default on Free/paid Apify
- Fallback **`proxyGroup: RESIDENTIAL`** for sticky US IPs (`RESIDENTIAL` + `US` / `CA`). MCP-safe: the proxy-widget group enum cannot accept UNBLOCKER
- Session pool with rotate-on-CAPTCHA / throttle (retries before writing `BLOCKED` / `RATE_LIMIT`)
- Concurrency 1, polite same-domain delay, up to 6 request retries / 8 session rotations
- ASINs or product URLs (`/dp/`, `/gp/product/`, `/gp/aw/d/`, `?asin=`)
- Marketplaces: `com` (default), `co.uk`, `de`
- Optional extra marketplaces for the same ASINs
- Deliver-to postal code (default **92618**, Irvine CA) so Amazon shows a real Buy Box
- All Offers Display (AOD) panel for landed-cost offers
- Pay-per-event: `asin_check` only on **successful** snapshots
- `dryRun` skips charging so local `apify run` is free
- `monitorMode` is a **v1 stub** (one-shot snapshot; no watch state)

### Input

| Field | Type | Default | Notes |
| --- | --- | --- | --- |
| `asins` | `string[]` | — | 10-character ASINs |
| `productUrls` | `string[]` | — | Amazon URLs; ASIN extracted from `/dp/` (and related paths) |
| `marketplace` | `com` | `co.uk` | `de` | `com` | Primary storefront |
| `alsoCheckMarketplaces` | same enum\[] | `[]` | Extra storefronts; each pair is its own output row |
| `includeAllOffers` | boolean | `true` | Fetch AOD and return competing offers |
| `maxOffers` | integer | `20` | Cap per snapshot (1–50) |
| `postalCode` | string | `92618` | Deliver-to ZIP / postal code. Alias: `zipCode` |
| `proxyGroup` | `RESIDENTIAL` | `UNBLOCKER` | `UNBLOCKER` | Recommended Amazon.com production default. Select `RESIDENTIAL` as a sticky-IP fallback |
| `proxyCountry` | `US` | `GB` | `DE` | `US` | Used when `proxyConfiguration` has no country and group is residential. amazon.com needs `US`. Ignored for Unblocker |
| `monitorMode` | boolean | `false` | Stub in v1 |
| `dryRun` | boolean | `false` | Write items, do not charge |
| `proxyConfiguration` | proxy | **widget RESIDENTIAL + US** | Console widget (MCP-safe). Real default is `proxyGroup=UNBLOCKER`, which overrides this |

#### Why `postalCode` defaults to 92618

Amazon's product page **Buy Box, shipping eligibility, and landed price are location-specific**. If deliver-to is unset (or a proxy IP geolocates somewhere the item cannot ship), the HTML often still has title / brand / image but:

- `buyBox` is missing
- availability becomes “This item cannot be shipped to your selected delivery location…”
- `#availability` may also contain All Offers Display (AOD) JavaScript, which older parsers treated as `availabilityText`

The Actor sets Amazon's glow / address-change location (plus `lc-acb*` / `i18n-prefs` / `sp-cdn` cookies) to the given postal code **before** fetching `/dp/` pages. Default **92618** is Irvine, CA — the locked test geography for this Actor. Use a local postcode for `co.uk` / `de`.

At least one parseable ASIN (from `asins` or `productUrls`) is required.

Sample input lives at [`test/input.sample.json`](test/input.sample.json). Fixture ASINs (wins / contested / messy) are listed in [`test/asins.txt`](test/asins.txt).

```json
{
    "asins": ["B014I8SSD0", "B004U3Y8OM"],
    "productUrls": ["https://www.amazon.com/dp/B014I8SSD0"],
    "marketplace": "com",
    "includeAllOffers": true,
    "maxOffers": 20,
    "postalCode": "92618",
    "proxyGroup": "UNBLOCKER",
    "proxyCountry": "US",
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "US"
    },
    "dryRun": true
}
```

### Output

One dataset item per **marketplace × ASIN**.

| Field | Description |
| --- | --- |
| `asin`, `marketplace`, `url` | Identity of the check |
| `title`, `brand`, `image` | Product facts |
| `buyBox` | `{ sellerName, sellerId, isAmazon, fulfilledBy, price }` |
| `inStock`, `availabilityText` | Best-effort stock. `availabilityText` is visible copy only (AOD JS is stripped) |
| `cannotShip` | `true` when Amazon says the item cannot ship to the selected postal code |
| `offers[]` | Landed-cost rows (`price`, `shipping`, `landedPrice`, seller, condition) |
| `lowestOffer` | Cheapest landed offer among `offers` |
| `scrapedAt` | ISO-8601 timestamp |
| `ok` | `true` only when the snapshot parsed |
| `error`, `errorDescription` | Typed failure; `null` on success |

#### Error codes

| Code | Meaning |
| --- | --- |
| `BLOCKED` | CAPTCHA, robot check, or automated-access interstitial |
| `RATE_LIMIT` | HTTP 429/503 or throttle copy |
| `PARSE` | HTML loaded but title/Buy Box could not be extracted (or ASIN missing from input) |
| `NETWORK` | Timeouts, DNS, connection errors, other 4xx/5xx |

### PPE pricing

Configure these events on the Actor's monetization tab (not charged until you enable PPE in Console). The code already respects the contract:

| Event | When it fires | When it does **not** fire |
| --- | --- | --- |
| `asin_check` | After a snapshot with `ok: true` is pushed | `ok: false` errors; `dryRun: true`; invalid input that never became a snapshot |
| `apify-actor-start` | Platform synthetic (if you leave it enabled) | — |

`Actor.pushData(item, 'asin_check')` is used only for successful items. Error rows use `Actor.pushData(item)` with no event name.

Suggested list price (set in Console, not in this repo): on the order of **$0.01–$0.03 per successful `asin_check`**, depending on proxy mix. Errors are a cost you absorb — this Actor keeps concurrency at 1 and defaults to Unblocker. Do not change PPE prices from this repository.

`dryRun: true` is the supported way to exercise the Actor locally or in CI without billing.

### How to run locally

Requires Node.js 20+ and the [Apify CLI](https://docs.apify.com/cli).

```bash
npm install
npm test
```

```bash
## copy the sample input, then run
cp test/input.sample.json storage/key_value_stores/default/INPUT.json
## or:
apify run --input-file=test/input.sample.json
```

`test/input.sample.json` sets `dryRun: true` so a local run never attempts `asin_check` charges.

Without Apify Proxy credentials, Amazon will often return `BLOCKED` or `RATE_LIMIT` after retries are exhausted. That is expected locally: the Actor still writes a typed error item and does not charge. **Free-plan residential proxy only works from a platform run**, not from `apify run` on a laptop.

### Project layout

```text
.actor/
  actor.json              # name, memory limits, schema pointers
  input_schema.json
  dataset_schema.json
  output_schema.json
  Dockerfile
src/
  main.js                 # Crawlee + PPE orchestration
  amazon.js               # selectors, ASIN extraction, Buy Box / AOD parsers
  proxy.js                # Unblocker default + residential-US fallback ProxyConfiguration
  retry.js                # BLOCKED / RATE_LIMIT session-rotation helpers
test/
  amazon.test.js          # extractAsin + fixture parse tests
  proxy.test.js           # proxy defaults + retry/session helpers
  asins.txt
  input.sample.json
  fixtures/               # product, AOD, location-blocked, AOD-JS-polluted HTML
```

Amazon CSS selectors and URL builders live **only** in `src/amazon.js`.

### Risks and limitations

- **Amazon ToS.** Scraping Amazon storefronts may violate Amazon's terms. Use this Actor only where you have a lawful basis. This is not an official Amazon API.
- **Blocking.** Datacenter IPs are frequently CAPTCHA'd. This Actor defaults to Apify **Unblocker** (residential US is a fallback) and retries CAPTCHA / throttle pages with a new session before writing `BLOCKED` / `RATE_LIMIT`. Cheerio does not execute JavaScript; some A/B Buy Box layouts hydrate late and will surface as `PARSE`.
- **Selector drift.** Amazon A/B-tests `#buybox`, `#merchantInfoFeature_feature_div`, `#tabular-buybox`, and AOD markup. Selectors are centralized in `amazon.js` so they can be patched without touching crawl/PPE code.
- **Stock is heuristic.** `inStock` is derived from availability copy (`In Stock`, `Currently unavailable`, `Auf Lager`, …), not warehouse quantity.
- **Deliver-to is required for a real Buy Box.** Default ZIP is **92618** (Irvine, CA). A missing or out-of-area location is modeled as `cannotShip: true` with `inStock: false`; product identity is still returned when it parsed. Tax and Prime benefits are not modeled. Shipping is parsed from AOD / delivery strings when present; missing shipping is treated as `0`.
- **monitorMode is a stub.** v1 does not store previous winners or emit change events.
- **Do not publish from this PR.** Store listing, PPE prices, and `apify push` are out of scope here.

### MCP / agent use

Each dataset item is a flat-enough JSON object with `ok`, typed `error`, and nested `buyBox` / `offers`. Point an MCP client at the default dataset (`{{links.apiDefaultDatasetUrl}}/items`) or run the Actor via the Apify MCP server and read items directly. Failed checks remain in the dataset so an agent can retry only `BLOCKED` / `RATE_LIMIT` without being billed for those attempts.

Default `proxyGroup` is `UNBLOCKER`. MCP clients can omit it or pass `proxyGroup: "UNBLOCKER"` explicitly. Do not put `UNBLOCKER` on `proxyConfiguration.apifyProxyGroups` — MCP input validation only allows `RESIDENTIAL` and `DATACENTER` on that field. Use `proxyGroup: "RESIDENTIAL"` for the sticky-IP fallback.

### License

Apache-2.0

# Actor input Schema

## `asins` (type: `array`):

Amazon ASINs to check (10-character identifiers). Duplicate ASINs are checked once per marketplace.

## `productUrls` (type: `array`):

Amazon product URLs. The Actor extracts the ASIN from `/dp/`, `/gp/product/`, `/gp/aw/d/`, or an `asin=` query parameter.

## `marketplace` (type: `string`):

Primary Amazon marketplace TLD. Used for every ASIN unless listed again under Also check marketplaces.

## `alsoCheckMarketplaces` (type: `array`):

Extra marketplaces to snapshot for the same ASINs. Each marketplace × ASIN pair is one output row.

## `includeAllOffers` (type: `boolean`):

When enabled, the Actor also requests Amazon's All Offers Display (AOD) panel and returns landed-cost offers (price + shipping).

## `maxOffers` (type: `integer`):

Maximum offers to keep per marketplace × ASIN snapshot, sorted by landed price.

## `monitorMode` (type: `boolean`):

v1 stub. When true the Actor still performs a one-shot snapshot and does not persist watch state or diffs. Reserved for scheduled Buy Box change detection.

## `postalCode` (type: `string`):

Deliver-to location for Buy Box, shipping eligibility, and landed-cost offers. Applied with Amazon location cookies and the glow address-change request before product pages are fetched. Amazon hides the Buy Box or shows “cannot be shipped to your selected delivery location” when deliver-to is unset or outside the item’s ship area. Default 92618 is Irvine, CA — the locked test geography. Accepts US ZIP (e.g. 92618) and alphanumeric UK/DE postcodes. Alias: `zipCode` in JSON input is treated the same.

## `dryRun` (type: `boolean`):

Parse and write dataset items as usual, but never charge the `asin_check` PPE event. Use this for local `apify run` and pricing tests.

## `proxyGroup` (type: `string`):

Which Apify Proxy product to use. Default UNBLOCKER — recommended Amazon.com production default on Free and paid Apify (CAPTCHA bypass, no sticky IP, billed in Unblocker units; Free plans often include a large Unblocker allotment). Select RESIDENTIAL as a fallback when you need sticky US IPs so deliver-to ZIP cookies hold (billed per GB). MCP clients must use this field — Apify MCP hardcodes proxyConfiguration.apifyProxyGroups to RESIDENTIAL|DATACENTER and rejects UNBLOCKER there.

## `proxyCountry` (type: `string`):

ISO country for Apify Proxy when `proxyConfiguration` has no country and `proxyGroup` is RESIDENTIAL. Amazon.com requires US residential IPs. Default US; California (`CA`) is also applied in code to match ZIP 92618. Ignored for UNBLOCKER (country targeting shrinks the bypass pool).

## `proxyConfiguration` (type: `object`):

Console widget stays MCP-safe RESIDENTIAL|DATACENTER. The real Amazon.com production default is proxyGroup=UNBLOCKER, which overrides this widget. Pick RESIDENTIAL on proxyGroup as a fallback for sticky US IPs. Console users with PROXY\_UNBLOCKER can also pick Unblocker here; MCP clients cannot — set proxyGroup=UNBLOCKER instead. Datacenter / AUTO IPs are CAPTCHA'd. Empty input is filled in code. Set useApifyProxy=false only for local debugging. Free-plan residential credit is currently 20 GB/mo; Unblocker is a separate unit allotment (recommended default).

## Actor input object example

```json
{
  "asins": [
    "B014I8SSD0",
    "B00MNV8E0C",
    "B004U3Y8OM"
  ],
  "productUrls": [
    "https://www.amazon.com/dp/B014I8SSD0"
  ],
  "marketplace": "com",
  "alsoCheckMarketplaces": [],
  "includeAllOffers": true,
  "maxOffers": 20,
  "monitorMode": false,
  "postalCode": "92618",
  "dryRun": false,
  "proxyGroup": "UNBLOCKER",
  "proxyCountry": "US",
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "US"
  }
}
```

# Actor output Schema

## `results` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "asins": [
        "B014I8SSD0",
        "B00MNV8E0C",
        "B004U3Y8OM"
    ],
    "productUrls": [
        "https://www.amazon.com/dp/B014I8SSD0"
    ],
    "postalCode": "92618",
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "US"
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("buzzidata/amazon-buybox-offers-agent").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "asins": [
        "B014I8SSD0",
        "B00MNV8E0C",
        "B004U3Y8OM",
    ],
    "productUrls": ["https://www.amazon.com/dp/B014I8SSD0"],
    "postalCode": "92618",
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "US",
    },
}

# Run the Actor and wait for it to finish
run = client.actor("buzzidata/amazon-buybox-offers-agent").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "asins": [
    "B014I8SSD0",
    "B00MNV8E0C",
    "B004U3Y8OM"
  ],
  "productUrls": [
    "https://www.amazon.com/dp/B014I8SSD0"
  ],
  "postalCode": "92618",
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "US"
  }
}' |
apify call buzzidata/amazon-buybox-offers-agent --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,buzzidata/amazon-buybox-offers-agent"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/cYVaf0ggCXXhS2Uqd/builds/M1kIePFhGNmS6sWrT/openapi.json
