Auto-Update Amazon Affiliate Top 10 Lists With Real Data
Build an amazon affiliate product feed that rebuilds your best-of roundups from Amazon's own ranked charts, so no page cites a delisted ASIN or stale price.

Thirdwatch's Amazon Best Sellers Scraper reads Amazon's own ranked Best Sellers and New Releases charts and returns rank, ASIN, title, price, rating, review count, image URL and product URL for up to 100 positions per chart. Schedule it, write the rows into your CMS, and a "best air fryers of 2026" page rebuilds itself from live rank data instead of from a spreadsheet you last touched in March. Built for affiliate and content publishers.
Why build an amazon affiliate product feed instead of curating by hand
Roundup pages rot faster than any other kind of affiliate content. A "best X of 2026" post is a list of specific ASINs at specific prices, and every one of those facts has a shelf life measured in weeks. Products get delisted, prices move, a model gets replaced by its successor, and the page keeps ranking while quietly sending readers to dead links and wrong numbers. The traffic looks fine in analytics right up until conversion falls off a cliff.
The obvious fix is Amazon's own Product Advertising API, and for many publishers it is not available. Amazon requires an Associates account with qualifying sales before it will issue PA-API keys, and new accounts start at one request per second with a cumulative ceiling of 8,640 requests per day, scaling only with shipped revenue. That is a chicken-and-egg problem: the quota you need to build good pages is gated behind the sales those pages would generate.
Amazon's public Best Sellers charts have no such gate. They are recomputed continuously from real order volume, they exist for every browse node, and they are exactly the ranking a "top 10" page is claiming to reflect.
How does this compare to the alternatives?
Every route to the same rows has a different failure mode. Hand-curation fails silently. A DIY scraper fails the week Amazon changes its card markup, usually while you are on holiday. The Product Advertising API is the most authoritative source and the hardest one to get into, and its quota model punishes exactly the sites that publish a lot of pages. A generic scraping API returns HTML and leaves the parsing, rank integrity and pagination to you.
| Approach | Cost model | Reliability | Setup time | Maintenance |
|---|---|---|---|---|
| Hand-curated spreadsheet | Your time | Decays from day one | Hours per page | Continuous, manual |
| DIY Python scraper | Servers + proxies | Breaks on markup changes | 2-4 days | Ongoing, yours |
| Product Advertising API | Free, quota-gated | High when you qualify | Days, plus approval | Quota management |
| Generic scraping API | Per request | Fetches, does not parse | 1-2 days | Parser is yours |
| Thirdwatch Actor | Pay per result | Managed, rank-verified | Under 10 minutes | None |
How to build an auto-updating Amazon affiliate top list in 4 steps
Which Amazon chart should the page be built from?
Pick the chart whose scope matches your page's promise. A page titled "best kitchen products" maps to the kitchen department chart; a page titled "best espresso machines" should map to the espresso browse node, not to the whole department. The categories field accepts a plain slug, a slug/nodeId pair for a subcategory chart, or a full chart URL pasted from your browser.
{
"categories": [
"kitchen",
"electronics/172623",
"https://www.amazon.com/gp/bestsellers/pet-supplies"
],
"listType": "bestSellers",
"country": "us",
"maxResults": 20,
"proxyConfiguration": { "useApifyProxy": true }
}Set maxResults to roughly double the length of your list. A "top 10" page wants 20 rows so you have replacements when a couple are unusable — no price, wrong variant, or a product you would not recommend.
How do I pull the chart on a schedule?
Run the Actor from a scheduled job and read the dataset when it finishes. This example pulls three charts in one call and groups the rows by the page each will feed.
from collections import defaultdict
from apify_client import ApifyClient
client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("thirdwatch/amazon-bestsellers-scraper").call(run_input={
"categories": ["kitchen", "electronics/172623", "pet-supplies"],
"listType": "bestSellers",
"country": "us",
"maxResults": 20,
"proxyConfiguration": {"useApifyProxy": True},
})
by_page = defaultdict(list)
for row in client.dataset(run["defaultDatasetId"]).iterate_items():
by_page[row["source_query"]].append(row)
for slug, rows in by_page.items():
rows.sort(key=lambda r: r["rank"])
print(slug, len(rows), "rows, top rank:", rows[0]["title"][:60])source_query returns the exact categories entry that produced the row, which makes it a stable join key back to your page even when you mix slugs, node IDs and full URLs in one run. Wire the same call into an Apify schedule and it runs nightly without you.
How do I turn the rows into rendered list items?
Treat the row as a rendering payload, not as analytics. Five fields carry the whole card: rank for ordering, title for the heading, image_url for the thumbnail, product_url for the link, and price_text for the displayed price. Append your Associates tag at render time so the tag lives in one place rather than in a thousand stored URLs.
ASSOC_TAG = "yoursite-20"
def render_card(row):
if not row.get("title") or not row.get("product_url"):
return None
link = f"{row['product_url']}?tag={ASSOC_TAG}"
price = row.get("price_text") or "Check current price"
if row.get("price_type") == "from":
price = f"from {price}"
rating = f"{row['rating']} stars" if row.get("rating") else "No rating yet"
reviews = row.get("reviews_count_text") or "0"
return (
f"### {row['rank']}. [{row['title']}]({link})\n"
f"![{row['title']}]({row['image_url']})\n\n"
f"**{price}** — {rating} from {reviews} reviews\n"
)
cards = [c for c in (render_card(r) for r in rows[:10]) if c]
print("\n".join(cards))Do not store price_text in your page body as literal text. Store the whole row as data and render on request, or you have rebuilt the stale-price problem in a database.
How do I stop a delisted ASIN from staying on the page?
Render from the latest run, never from an accumulated list. A product that has been delisted, suppressed or knocked out of the top 100 simply stops appearing in the chart, so a page built from the most recent snapshot drops it automatically. Keep the previous snapshot only to detect and log the change.
import json, pathlib
def refresh(slug, rows, snapshot_dir="snapshots"):
path = pathlib.Path(snapshot_dir) / f"{slug}.json"
previous = json.loads(path.read_text()) if path.exists() else []
prev_asins = {r["asin"] for r in previous}
live_asins = {r["asin"] for r in rows}
dropped = prev_asins - live_asins
added = live_asins - prev_asins
if dropped:
print(f"{slug}: removing {len(dropped)} ASINs no longer charting: {sorted(dropped)}")
if added:
print(f"{slug}: {len(added)} new entrants")
path.parent.mkdir(exist_ok=True)
path.write_text(json.dumps(rows, indent=2))
return rows # publish these, not the unionThe dropped-ASIN log is also the raw material for a genuinely useful editorial note: "we removed the X after it left the category top 100 in June" reads as editorial rigour, and it is free.
How do I handle rows with no price or a "from" price?
Handle them as editorial decisions, because that is what they are. Amazon shows some chart entries as "N offers from $X" and shows some — subscriptions, certain grocery and marketplace items — with no price at all. The Actor reports both faithfully rather than hiding them: price_type is from in the first case with offers_count populated, and price is null in the second.
def price_label(row):
if row.get("price") is None:
return "Check price on Amazon" # never invent a number
if row.get("price_type") == "from":
n = row.get("offers_count")
suffix = f" ({n} offers)" if n else ""
return f"From {row['price_text']}{suffix}"
return row["price_text"]A hard rule worth encoding: if price is null, the page says "check price" and links out. Printing a number Amazon is not currently showing is how affiliate pages earn FTC complaints and reader distrust at the same time.
Sample output
Each dataset item is one ranked product on one chart at one moment. Here are two rows from a US Books chart, trimmed to the fields a roundup page actually renders plus the provenance fields you will want when debugging.
[
{
"rank": 1,
"asin": "1668236516",
"title": "Theo of Golden: A Novel",
"by_line": "Allen Levi",
"product_format": "Paperback",
"price": 14.98,
"price_text": "$14.98",
"price_type": "list",
"offers_count": null,
"currency": "USD",
"rating": 4.7,
"reviews_count": 153523,
"reviews_count_text": "153,523",
"product_url": "https://www.amazon.com/dp/1668236516",
"image_url": "https://images-na.ssl-images-amazon.com/images/I/81P0NvoRrWL._AC_UL300_.jpg",
"list_type": "bestSellers",
"category_slug": "books",
"category_url": "https://www.amazon.com/gp/bestsellers/books",
"source_query": "books",
"page": 1,
"country": "us",
"scraped_at": "2026-07-27T22:55:24+00:00"
},
{
"rank": 14,
"asin": "B0EXAMPLE9",
"title": "Example Stainless Steel Air Fryer, 6 Qt",
"by_line": "",
"price": 89.99,
"price_text": "$89.99",
"price_type": "from",
"offers_count": 4,
"currency": "USD",
"rating": 4.4,
"reviews_count": 8241,
"product_url": "https://www.amazon.com/dp/B0EXAMPLE9",
"image_url": "https://images-na.ssl-images-amazon.com/images/I/71EXAMPLE._AC_UL300_.jpg",
"list_type": "bestSellers",
"category_slug": "kitchen",
"source_query": "kitchen",
"page": 1,
"country": "us",
"scraped_at": "2026-07-27T22:55:31+00:00"
}
]rank comes from Amazon's own rank metadata rather than from the order cards happen to render in, so your numbering matches the chart even when a card loads late. by_line and product_format populate on Books, Music and Video charts and are empty on hard-goods charts — build your template to tolerate both. scraped_at is what you show as "prices checked on", and category_url is the link a sceptical reader can follow to verify your list.
Common pitfalls
One hundred ranks is the ceiling. Amazon publishes only the top 100 positions of any chart, so maxResults caps there. If your page needs more depth, use subcategory browse nodes — each has its own independent top 100.
Chart data is not product detail. Rows carry rank, ASIN, title, one price, rating, review count and image. There are no bullet points, specs, variants or seller names, so plan on writing your own copy and pulling detail separately if a comparison table needs it.
New Releases charts are thin on ratings. Products days old have no reviews yet; a UK New Releases chart in testing returned ratings on 56% of rows. That is real, not a parsing gap, so make ratings optional in the template.
Slugs are marketplace-specific. electronics on amazon.com is computers on amazon.de. An unknown slug logs a warning and returns zero rows for that entry rather than failing the run — check row counts per source_query before publishing.
Charts are point-in-time. Amazon recomputes them roughly hourly, so two runs minutes apart can legitimately differ. Snapshot on a schedule and publish the snapshot. The Actor handles proxy rotation, retries and rank integrity so your only remaining job is the editorial one.
Related use cases
- Expose Amazon's top-100 charts as your own cached JSON endpoint — when the roundup grows into a product.
- Compare the same category's top 100 across five marketplaces — for publishers running localised versions of one page.
- Detect new SKU launches from the New Releases chart — the source for a "just launched" section.
- Measure share of shelf inside a category — if a brand ever sponsors your roundup.
- Actor details and input reference, the ecommerce scraping guide, and the full Thirdwatch blog.
Frequently asked questions
Do I need an Amazon Associates account to use this?
Not to get the data. You need an Associates account only to earn commission on the outbound links you render. The Actor reads Amazon's public chart pages, so your feed keeps working even while an Associates application is pending.
How often should a roundup page refresh?
Daily is plenty for most categories, weekly for slow-moving ones. Amazon recomputes Best Sellers roughly hourly, so refreshing faster than daily mostly churns your page for rank noise your readers will never notice.
Can one run cover several roundup pages at once?
Yes. The `categories` input takes a list, and every row carries `category_slug` and `source_query`, so a single scheduled run can feed a dozen pages. Group your slugs by refresh cadence rather than by page.
Will this give me product descriptions and specs for the copy?
No. Chart rows carry rank, ASIN, title, price, rating, review count and image only. Your editorial copy stays yours to write; the feed handles the facts that go stale, not the prose that does not.
What happens when a product on my page gets delisted?
It disappears from Amazon's chart, so the next run simply will not return it. If you render straight from the latest run rather than from a stored list, delisted ASINs fall off your page without anyone filing a ticket.
Related
100 free credits, no credit card.
About 30 real searches. Add the MCP to Claude or Cursor in two minutes.