How to Scrape Shopify Stores in 2026: Products, Prices, and Inventory

There are over 4.8 million active Shopify stores worldwide. If you’re doing competitor research, building a price comparison tool, tracking product trends, or sourcing for dropshipping, Shopify stores are a goldmine of structured product data.

The best part? Shopify has a little-known public endpoint that makes scraping significantly easier than most ecommerce platforms.

The Shopify products.json Endpoint

Every Shopify store exposes a public JSON API at /products.json. No authentication needed:

https://store-name.myshopify.com/products.json
https://custom-domain.com/products.json

This returns up to 250 products per page with full details: titles, descriptions, prices, variants, images, inventory availability, and more.

Basic Shopify Scraper

Here’s a straightforward Python scraper that pulls every product from a Shopify store:

import requests
import time
import csv

def scrape_shopify_store(domain: str) -> list[dict]:
    """Scrape all products from a Shopify store."""
    products = []
    page = 1

    while True:
        url = f"https://{domain}/products.json?limit=250&page={page}"

        response = requests.get(url, timeout=30, headers={
            "User-Agent": "Mozilla/5.0 (compatible; ProductResearch/1.0)"
        })

        if response.status_code == 430:
            print(f"Rate limited on page {page}, waiting 30s...")
            time.sleep(30)
            continue

        if response.status_code != 200:
            print(f"Got status {response.status_code} on page {page}")
            break

        data = response.json()
        batch = data.get("products", [])

        if not batch:
            break

        for product in batch:
            for variant in product.get("variants", []):
                products.append({
                    "title": product["title"],
                    "product_type": product.get("product_type", ""),
                    "vendor": product.get("vendor", ""),
                    "variant": variant.get("title", "Default"),
                    "price": variant.get("price"),
                    "compare_at_price": variant.get("compare_at_price"),
                    "sku": variant.get("sku"),
                    "available": variant.get("available", False),
                    "created_at": product.get("created_at"),
                    "updated_at": product.get("updated_at"),
                    "tags": ", ".join(product.get("tags", [])),
                    "image": product.get("images", [{}])[0].get("src", ""),
                    "url": f"https://{domain}/products/{product['handle']}",
                })

        print(f"Page {page}: {len(batch)} products")
        page += 1
        time.sleep(1)

    return products

# Scrape a store
products = scrape_shopify_store("allbirds.com")
print(f"Total variants: {len(products)}")

Exporting to CSV

def export_to_csv(products: list[dict], filename: str = "products.csv"):
    """Export scraped products to CSV."""
    if not products:
        return

    with open(filename, "w", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(f, fieldnames=products[0].keys())
        writer.writeheader()
        writer.writerows(products)

    print(f"Exported {len(products)} products to {filename}")

export_to_csv(products)

Scraping Multiple Stores for Competitive Analysis

For comparing competitors, batch scrape and save each store separately:

import json
from pathlib import Path

COMPETITORS = [
    "allbirds.com",
    "gymshark.com",
    "fashionnova.com",
    "colourpop.com",
    "kyliecosmetics.com",
]

def scrape_competitors(domains: list[str], output_dir: str = "data"):
    """Scrape multiple Shopify stores and save results."""
    Path(output_dir).mkdir(exist_ok=True)

    for domain in domains:
        print(f"nScraping {domain}...")
        try:
            products = scrape_shopify_store(domain)

            output_file = Path(output_dir) / f"{domain.replace('.', '_')}.json"
            with open(output_file, "w") as f:
                json.dump(products, f, indent=2)

            print(f"  Saved {len(products)} variants to {output_file}")
        except Exception as e:
            print(f"  Failed: {e}")

        time.sleep(5)  # Be nice between stores

scrape_competitors(COMPETITORS)

Detecting if a Site Runs Shopify

Not sure if a website is on Shopify? Check programmatically:

def is_shopify(domain: str) -> bool:
    """Detect if a website runs on Shopify."""
    try:
        resp = requests.get(
            f"https://{domain}/products.json?limit=1",
            timeout=10,
            allow_redirects=True,
        )
        if resp.status_code == 200:
            data = resp.json()
            return "products" in data
    except Exception:
        pass

    # Fallback: check for Shopify headers
    try:
        resp = requests.head(f"https://{domain}", timeout=10)
        return "shopify" in resp.headers.get("x-shopify-stage", "").lower() 
            or "shopify" in resp.headers.get("server", "").lower()
    except Exception:
        return False

# Test some domains
for domain in ["allbirds.com", "nike.com", "gymshark.com"]:
    print(f"{domain}: {'Shopify' if is_shopify(domain) else 'Not Shopify'}")

Handling Common Challenges

Rate Limiting (HTTP 430)

Shopify returns status 430 when you hit their rate limit. Solutions:

  1. Add delays — 1-2 seconds between pages, 5+ seconds between stores
  2. Use ScraperAPI — rotates IPs automatically so you spread requests across many addresses
  3. Use ThorData residential proxies — a pool of real residential IPs makes your requests look like normal browser traffic
def scrape_with_proxy(domain: str, proxy_url: str) -> list[dict]:
    """Scrape with rotating proxy to avoid rate limits."""
    products = []
    page = 1

    while True:
        url = f"https://{domain}/products.json?limit=250&page={page}"

        response = requests.get(url, proxies={
            "http": proxy_url,
            "https": proxy_url,
        }, timeout=30)

        if response.status_code != 200:
            break

        data = response.json()
        batch = data.get("products", [])
        if not batch:
            break

        products.extend(batch)
        page += 1
        time.sleep(1)

    return products

Stores That Block products.json

Some merchants disable the public endpoint. In that case:

Getting Collection and Category Data

def scrape_collections(domain: str) -> list[dict]:
    """Get all product collections/categories."""
    url = f"https://{domain}/collections.json"
    response = requests.get(url, timeout=30)

    if response.status_code != 200:
        return []

    collections = response.json().get("collections", [])
    return [{
        "title": c["title"],
        "handle": c["handle"],
        "products_count": c.get("products_count"),
        "url": f"https://{domain}/collections/{c['handle']}",
    } for c in collections]

Building a Price Monitor

The real power is in running this daily to track price changes:

import sqlite3
from datetime import datetime

def init_db(db_path: str = "prices.db"):
    conn = sqlite3.connect(db_path)
    conn.execute("""
        CREATE TABLE IF NOT EXISTS price_history (
            domain TEXT,
            sku TEXT,
            title TEXT,
            price REAL,
            compare_at_price REAL,
            available BOOLEAN,
            scraped_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        )
    """)
    return conn

def record_prices(conn, domain: str, products: list[dict]):
    for p in products:
        conn.execute(
            "INSERT INTO price_history (domain, sku, title, price, compare_at_price, available) VALUES (?, ?, ?, ?, ?, ?)",
            (domain, p["sku"], p["title"], float(p["price"]),
             float(p["compare_at_price"]) if p["compare_at_price"] else None,
             p["available"])
        )
    conn.commit()

# Run daily via cron
conn = init_db()
products = scrape_shopify_store("competitor.com")
record_prices(conn, "competitor.com", products)

Set this up as a cron job and you have a full competitor price tracker running for free.

Legal Notes

The products.json endpoint serves publicly available data that Shopify intentionally exposes. That said:

  • Don’t hammer stores with high-frequency requests
  • Don’t scrape and republish product descriptions (copyright)
  • Don’t use scraped data for fake reviews or misleading comparisons
  • Respect rate limits — the 430 status exists for a reason

Conclusion

Shopify’s public JSON API makes it one of the easiest ecommerce platforms to scrape. For stores that block the endpoint, tools like ScraperAPI and ThorData proxies handle the heavy lifting. And if you’d rather skip the code, Apify has cloud-based Shopify scrapers ready to go.

Happy scraping — let me know in the comments what you’re building!

The Rise of the AI Worm: How Self-Replicating Prompts Threaten Multi-Agent Systems

For decades, the term “computer worm” meant malicious code exploiting binary vulnerabilities. From the 1988 Morris Worm to modern ransomware, we’ve been in a constant arms race.

But as we move from simple chatbots to complex Multi-Agent Systems (MAS), a new, more insidious threat has emerged: the AI Worm.

Unlike traditional malware, these “digital parasites” don’t target your source code. They target the very fabric of AI communication: language.

What exactly is an AI Worm?

An AI worm is a piece of self-replicating prompt malware. It’s a malicious instruction embedded within an innocuous-looking email or document.

When an AI agent processes this data, the prompt does two things:

  1. Tricks the agent into performing an unwanted action (like exfiltrating data).
  2. Compels the agent to replicate and spread that same instruction to other agents or systems.

This isn’t science fiction. Researchers have already demonstrated this with Morris II, a zero-click worm that targets generative AI ecosystems.

The Anatomy of a Self-Replicating Prompt

How does a string of text become a virus? It happens in three stages:

1. Replication

The attacker crafts a prompt that forces the LLM to include the malicious instruction in its own output. Think of it like a “jailbreak” that survives a summary. If an agent summarizes an infected document, the summary itself now contains the malware.

2. Propagation

This is where the “worm” part comes in. AI agents are often connected to tools such as email clients, Slack, or databases. The replicated prompt instructs the compromised agent to use these tools to send the malware to new targets.

Example: An AI email assistant summarizes an infected message and then forwards that summary to everyone in your contact list.

3. Payload

The final goal. This could be anything from stealing sensitive PII to launching automated spam campaigns. This often uses Indirect Prompt Injection (IPI), where the malware is hidden in data the AI processes naturally, making it incredibly hard to detect.

Why Multi-Agent Systems (MAS) are Vulnerable

In a MAS, agents collaborate and share information autonomously. This interconnectedness is a double-edged sword.

  • Trust Assumptions: Developers often assume internal agent-to-agent communication is safe. If one agent is compromised, the infection can cascade through the entire system.
  • Agentic RAG: Retrieval-Augmented Generation allows agents to pull data from external sources (web, emails, docs). This creates a massive attack surface for malicious prompts to enter the system.
  • Tool Access: Modern agents have “hands”, they can send emails, update databases, or even trigger financial transactions. An AI worm uses these hands to spread itself.

The Enterprise Risk: Zero-Click Infections

The scariest part? Zero-click infections.

Unlike traditional phishing, where a human has to click a link, an AI worm can spread without any human interaction. If your agent is set to automatically process incoming support tickets or emails, it can become infected and start propagating the malware the moment it reads the text.

This leads to:

  • Data Exfiltration: Sensitive customer or company data sent to unauthorized recipients.
  • Poisoned Knowledge Bases: Malicious prompts subtly altering stored info, leading to flawed business decisions.
  • Automated Spam/Misinformation: Your own agents being used to damage your brand reputation.

How to Secure Your Agentic Workflows

Building a secure MAS requires moving beyond traditional code-centric defenses. Here are some practical best practices:

1. Treat All LLM Outputs as Untrusted

Never assume an agent’s output is safe just because it’s “internal.” Implement rigorous input/output sanitization. Scan for known malicious patterns or unexpected commands before any agent-generated text is acted upon.

2. The Principle of Least Privilege

Give your agents only the tools they absolutely need. An email summarizer doesn’t need the ability to send emails or modify your database.

3. Human-in-the-Loop (HITL)

For high-stakes actions, like financial transactions or communicating with external clients, always require a human “circuit breaker” to approve the action.

4. Sandbox Your Agents

Isolate agents and their LLMs in sandboxed environments. If one agent gets infected, the sandbox prevents the malware from spreading laterally to the rest of your infrastructure.

Securing the Future

The future of AI security is the security of language. As we entrust more of our business logic to autonomous agents, we need specialized layers that can monitor and protect these linguistic interactions.

Solutions like NeuralTrust are designed for this exact purpose—providing the visibility and control needed to detect indirect prompt injections and stop self-replicating prompts before they can do damage.

Are you building with multi-agent systems? How are you handling prompt security? Let’s discuss in the comments!

5 “Tell Me About Yourself” Patterns That Turn Interviews Into Technical Conversations

Most developers answer “tell me about yourself” like a resume. That kills the interview in 60 seconds. Here are 5 patterns that turn it into a technical discussion about your strongest work.

1. Replace Titles With Impact Statements

Most answers start with a job title. That adds zero signal.

Before

I am a Senior Frontend Developer with 7 years of experience in React and TypeScript.

After

I build customer-facing dashboards for a fintech product processing $2M in daily transactions.

The second version compresses domain, scale, and responsibility into one sentence. You remove 6 words and add actual signal.

2. Anchor Your Story in One Measurable Result

Listing technologies is noise. One quantified result is sticky.

Before

I work with React, Node.js, PostgreSQL, Redis, Docker, and AWS.

After

Last year I rebuilt our checkout flow using React Server Components and reduced time-to-interactive from 3.2s to 900ms, increasing conversion by 12%.

Now the interviewer has something to ask about. Numbers trigger follow-up questions. No one asks about your tech list.

3. Use Architecture as a Hook, Not a Buzzword

Mentioning architecture without context sounds generic. Tie it to a constraint.

Before

I worked on microservices and scalable systems.

After

I redesigned our order pipeline to handle Black Friday traffic by moving to a queue-based architecture with Redis, cutting API latency from 1200ms to 180ms under load.

This creates a natural follow-up path into system design. If you want to go deeper into backend bottlenecks, this pairs well with the patterns in the Node.js memory leak debugging strategies that prevent production crashes.

4. Match Your Achievement to the Role’s Core Problem

Most candidates reuse the same story everywhere. That is a mistake.

Before

I built a social media app with React and Firebase.

After (for a performance-heavy role)

I optimized a real-time analytics dashboard rendering 5,000 data points at 60fps using virtualization and WebSocket streaming.

Same developer. Different positioning. The second answer aligns with what the company actually needs.

5. Close With a Technical Alignment, Not a Career Goal

“Looking for growth” is a dead sentence. Show direct overlap.

Before

I am looking for new challenges and opportunities to grow.

After

I saw your team is moving to Next.js with server components, and that is exactly the architecture I shipped in production last year.

This tells the interviewer you read the job description and already solved their problem somewhere else.

6. Control the Next Questions With Intentional Hooks

Your intro determines the next 10 minutes. Use that.

Before

I have experience across frontend and backend systems.

After

I led a migration from Angular to React that reduced bundle size by 40% and improved load time by 2 seconds across 200K users.

Now the interviewer will ask:

  • how you handled the migration
  • how you measured performance
  • what broke during rollout

You just turned random questions into a deep dive on your strongest project.

7. Compress Everything Into 45 to 60 Seconds

Long answers signal lack of prioritization.

Before

I started programming in university, then worked at three companies, learned multiple stacks, contributed to different projects, and...

After

I build high-performance React applications. Recently I rebuilt a checkout flow that improved conversion by 12%. I saw your team is solving similar performance problems, and that is exactly where I add value.

Three sentences. Clear structure. No filler.

You do not need a better resume to pass interviews. You need a better first 60 seconds. Pick one project, attach numbers, align it to the role, and let the interviewer follow your lead.

How Ubuntu 24.04’s PEP 668 Broke Our Month-End Automation — SSH Multi-Node Workaround

How Ubuntu 24.04’s PEP 668 Broke Our Month-End Automation — SSH Multi-Node Workaround

Background

We run automation across multiple nodes in our home lab. At month-end, a script called monthly-endofmonth.sh handles time-tracking, invoice submission, and form filling automatically.

This month, it exploded on first run.

What Happened

The error surfaced like this:

error: externally-managed-environment

× This environment is externally managed
╰─> To install Python packages system-wide, either use a
    distribution package (apt install python3-xxx) or create
    a virtual environment (python3 -m venv PATH)...

Running pip install playwright on Ubuntu 24.04 got blocked by PEP 668.

PEP 668, adopted in 2022, changes how Python package management works. On Ubuntu 24.04 (Noble), direct pip install to the system Python is forbidden. The goal is to prevent apt-managed Python and pip from conflicting and breaking the system.

Previously, pip install --break-system-packages could force through. Ubuntu 24.04 blocks even that in many cases.

The Real Root Cause: Phase 0 and Production Ran on Different Nodes

PEP 668 itself is well-known. The real problem was something else.

Phase 0 (pre-flight check) ran on our infra server — a node with playwright, openpyxl, and google-auth already set up. Everything passed.

But the actual month-end job was designed to run on a different Ubuntu 24.04 node that had none of those libraries.

Result: Phase 0 all green → production blew up immediately.

This environment mismatch is a classic multi-node trap. The infra equivalent of “works on my machine.”

The Fix: Route Python Jobs via SSH to the Prepared Node

We changed the approach: delegate Python execution to the infra server via SSH.

The key change in monthly-endofmonth.sh:

# Before: local execution on Ubuntu 24.04 node (blocked by PEP 668)
python3 /mnt/shared/scripts/make_timesheet.py

# After: run on infra server via SSH
ssh user@infra-server "python3 /mnt/shared/scripts/make_timesheet.py"

The infra server has a fixed LAN IP, and all scripts are on an NFS mount (/mnt/shared/), so both nodes access the same files regardless of which machine runs the command.

Post-fix results:

Task Result
Timesheet xlsx generation ⚠️ SameFileError (caused by running twice same day; file itself was OK)
HajimariWORKS time entry ✅ All 21 days saved
Pasture NOB DATA ✅ All days entered
Pasture Optage ✅ All days entered

Lessons Learned

1. Run Phase 0 Checks on the Production Node

“It worked somewhere” doesn’t mean “it works everywhere.” Dependencies, Python environments, and file paths vary per node. Phase 0 should run on the actual node that will execute production jobs.

2. Move Work to the Prepared Environment, Not the Other Way Around

Fighting PEP 668 is the wrong battle. Moving the job to an already-configured environment is faster and safer.

Using venv is also an option, but in a multi-node NFS setup, managing venv paths across machines gets messy. SSH-delegating to a single trusted node is simpler.

3. Test Month-End Scripts Mid-Month

Testing one day before month-end is too late. Next month, we plan to add a --dry-run pass around the 25th to catch environment issues early.

Bonus: The SameFileError Bug

We found a bonus bug during debugging. Running make_timesheet.py twice on the same day produces:

shutil.SameFileError: '/mnt/shared/.../timesheet.xlsx' and '/mnt/shared/.../timesheet.xlsx' are the same file

shutil.copy() was passed the same path as both source and destination. A simple existence check before copying will fix it — planned for next month’s run.

Summary

  • Ubuntu 24.04 enforces PEP 668, blocking system-wide pip install
  • Multi-node setups need explicit awareness of which node runs which job
  • SSH-delegating to a prepared environment is a clean, pragmatic solution
  • Month-end automation needs mid-month dry-run testing, not last-minute manual checks

How to Scrape Fiverr in 2026: Freelancer Listings, Prices, and Reviews

Fiverr is a goldmine of freelance market data — gig prices, seller ratings, delivery times, service categories, and buyer reviews. Whether you’re researching freelance pricing trends, building a competitor analysis tool, or studying the gig economy, scraping Fiverr gives you structured data that their platform doesn’t expose through any public API.

In this guide, I’ll show you how to scrape Fiverr gig listings, seller profiles, and reviews with Python. I’ll cover what works, what breaks, and how to handle Fiverr’s anti-bot measures.

What Data Can You Extract from Fiverr?

Here’s what’s available on public Fiverr pages:

  • Gig listings — title, description, pricing tiers (Basic/Standard/Premium), delivery time
  • Seller profiles — username, level (New/Level 1/2/Top Rated), response time, country, member since
  • Reviews — star rating, review text, buyer country, date
  • Category data — subcategories, number of services available
  • Search results — gigs ranked by relevance/best selling/newest for any keyword

Step 1: Understanding Fiverr’s URL Structure

Fiverr search URLs are straightforward:

https://www.fiverr.com/search/gigs?query=web+scraping&source=top-bar&ref_ctx_id=...&page=1

Key parameters:

  • query — search keywords
  • page — pagination (starts at 1)
  • category_id — filter by category
  • delivery_time — filter by delivery speed

Individual gig pages follow the pattern:

https://www.fiverr.com/{seller_username}/{gig_slug}

Step 2: Scraping Search Results

Fiverr’s search results render with JavaScript, but the initial HTML contains enough structured data to get started. Here’s a scraper for gig listings:

import requests
from bs4 import BeautifulSoup
import json
import time
import random
import csv

def scrape_fiverr_search(query, pages=3):
    """Scrape Fiverr search results for gig listings."""

    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                       "AppleWebKit/537.36 (KHTML, like Gecko) "
                       "Chrome/124.0.0.0 Safari/537.36",
        "Accept-Language": "en-US,en;q=0.9",
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
        "Referer": "https://www.fiverr.com/",
    }

    all_gigs = []

    for page in range(1, pages + 1):
        url = f"https://www.fiverr.com/search/gigs?query={query}&page={page}"

        response = requests.get(url, headers=headers, timeout=30)

        if response.status_code != 200:
            print(f"Page {page}: Status {response.status_code}")
            continue

        soup = BeautifulSoup(response.text, "html.parser")

        # Fiverr embeds gig data in JSON within the page
        # Look for the Next.js data script
        for script in soup.select('script[id="__NEXT_DATA__"]'):
            try:
                data = json.loads(script.string)
                gigs_data = (
                    data.get("props", {})
                    .get("pageProps", {})
                    .get("searchResults", {})
                    .get("gigs", [])
                )

                for gig in gigs_data:
                    parsed = {
                        "title": gig.get("title"),
                        "seller": gig.get("seller", {}).get("username"),
                        "seller_level": gig.get("seller", {}).get("level"),
                        "seller_country": gig.get("seller", {}).get("country"),
                        "price": gig.get("price"),
                        "currency": gig.get("currency"),
                        "rating": gig.get("rating"),
                        "reviews_count": gig.get("reviews_count"),
                        "delivery_time": gig.get("delivery_time"),
                        "gig_url": f"https://www.fiverr.com{gig.get('url', '')}",
                    }
                    all_gigs.append(parsed)

            except (json.JSONDecodeError, TypeError, KeyError):
                continue

        # Fallback: parse HTML directly if JSON isn't available
        if not all_gigs:
            gig_cards = soup.select('.gig-card-layout')
            for card in gig_cards:
                gig = {}

                title_el = card.select_one('.text-display-7')
                gig["title"] = title_el.get_text(strip=True) if title_el else None

                seller_el = card.select_one('.seller-name')
                gig["seller"] = seller_el.get_text(strip=True) if seller_el else None

                price_el = card.select_one('.price')
                gig["price"] = price_el.get_text(strip=True) if price_el else None

                rating_el = card.select_one('.rating-score')
                gig["rating"] = rating_el.get_text(strip=True) if rating_el else None

                link_el = card.select_one('a[href*="/"]')
                gig["gig_url"] = f"https://www.fiverr.com{link_el['href']}" if link_el else None

                if gig["title"]:
                    all_gigs.append(gig)

        print(f"Page {page}: Found {len(all_gigs)} gigs total")
        time.sleep(random.uniform(3, 6))

    return all_gigs

# Usage
gigs = scrape_fiverr_search("web+scraping", pages=3)
print(f"nFound {len(gigs)} gigs")
for g in gigs[:5]:
    print(f"  {g['title'][:60]} — ${g['price']} — ★{g['rating']}")

Step 3: Scraping Individual Gig Pages

To get detailed gig information including all pricing tiers:

def scrape_gig_details(gig_url):
    """Extract detailed data from a single Fiverr gig page."""

    headers = {
        "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
                       "AppleWebKit/537.36 (KHTML, like Gecko) "
                       "Chrome/124.0.0.0 Safari/537.36",
        "Accept-Language": "en-US,en;q=0.9",
    }

    response = requests.get(gig_url, headers=headers, timeout=30)
    soup = BeautifulSoup(response.text, "html.parser")

    details = {}

    # Try extracting from embedded JSON first
    for script in soup.select('script[id="__NEXT_DATA__"]'):
        try:
            data = json.loads(script.string)
            gig_data = data.get("props", {}).get("pageProps", {}).get("gig", {})

            details["title"] = gig_data.get("title")
            details["description"] = gig_data.get("description")
            details["category"] = gig_data.get("category", {}).get("name")
            details["subcategory"] = gig_data.get("sub_category", {}).get("name")

            # Pricing tiers
            packages = gig_data.get("packages", [])
            for pkg in packages:
                tier = pkg.get("title", "unknown").lower()
                details[f"price_{tier}"] = pkg.get("price")
                details[f"delivery_{tier}"] = pkg.get("delivery_time")
                details[f"revisions_{tier}"] = pkg.get("revisions")
                details[f"description_{tier}"] = pkg.get("description")

            # Seller info
            seller = gig_data.get("seller", {})
            details["seller_username"] = seller.get("username")
            details["seller_level"] = seller.get("level")
            details["seller_country"] = seller.get("country")
            details["seller_response_time"] = seller.get("response_time")
            details["seller_member_since"] = seller.get("member_since")

            # Stats
            details["rating"] = gig_data.get("rating")
            details["reviews_count"] = gig_data.get("reviews_count")
            details["orders_in_queue"] = gig_data.get("orders_in_queue")

        except (json.JSONDecodeError, TypeError):
            continue

    return details

# Usage
gig = scrape_gig_details("https://www.fiverr.com/someuser/some-gig")
print(json.dumps(gig, indent=2))

Step 4: Scraping Seller Reviews

Reviews are critical for understanding service quality:

def scrape_gig_reviews(gig_url, pages=3):
    """Scrape reviews for a specific Fiverr gig."""

    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                       "AppleWebKit/537.36 Chrome/124.0.0.0 Safari/537.36",
        "Accept-Language": "en-US,en;q=0.9",
    }

    all_reviews = []

    for page in range(1, pages + 1):
        url = f"{gig_url}?page={page}#reviews"

        response = requests.get(url, headers=headers, timeout=30)
        soup = BeautifulSoup(response.text, "html.parser")

        review_items = soup.select('.review-item')

        for item in review_items:
            review = {}

            # Reviewer name and country
            name_el = item.select_one('.reviewer-name')
            review["reviewer"] = name_el.get_text(strip=True) if name_el else None

            country_el = item.select_one('.reviewer-country')
            review["country"] = country_el.get_text(strip=True) if country_el else None

            # Star rating
            stars = item.select('.star-rating .filled')
            review["stars"] = len(stars) if stars else None

            # Review text
            text_el = item.select_one('.review-description')
            review["text"] = text_el.get_text(strip=True) if text_el else None

            # Date
            date_el = item.select_one('.review-date')
            review["date"] = date_el.get_text(strip=True) if date_el else None

            # Price paid (sometimes visible)
            price_el = item.select_one('.review-price')
            review["price_paid"] = price_el.get_text(strip=True) if price_el else None

            if review["text"]:
                all_reviews.append(review)

        time.sleep(random.uniform(2, 4))

    return all_reviews

Step 5: Category Browsing

To map the entire Fiverr marketplace structure:

def scrape_category(category_slug, subcategory_slug=None, pages=2):
    """Browse Fiverr by category to find top gigs."""

    if subcategory_slug:
        url = f"https://www.fiverr.com/categories/{category_slug}/{subcategory_slug}"
    else:
        url = f"https://www.fiverr.com/categories/{category_slug}"

    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                       "AppleWebKit/537.36 Chrome/124.0.0.0 Safari/537.36",
    }

    response = requests.get(url, headers=headers, timeout=30)
    soup = BeautifulSoup(response.text, "html.parser")

    # Extract subcategories if on main category page
    subcategories = []
    for link in soup.select('a[href*="/categories/"]'):
        href = link.get("href", "")
        text = link.get_text(strip=True)
        if text and category_slug in href:
            subcategories.append({"name": text, "url": href})

    return {
        "subcategories": subcategories,
        "url": url,
    }

# Example: browse programming category
cats = scrape_category("programming-tech", "web-scraping")

Handling Anti-Bot Protection

Fiverr uses Cloudflare and custom anti-bot measures. Here’s what you’ll face:

  1. Cloudflare challenges — JavaScript challenges that block simple HTTP requests
  2. Rate limiting — aggressive throttling after repeated requests
  3. Browser fingerprinting — detection of automated browsers

For scraping at scale, you’ll need a scraping API that handles these challenges. ScraperAPI manages proxy rotation, CAPTCHA solving, and browser rendering automatically:

SCRAPER_API_KEY = "your_api_key"

def scrape_with_api(url):
    """Use ScraperAPI to handle anti-bot protection."""
    api_url = f"http://api.scraperapi.com?api_key={SCRAPER_API_KEY}&url={url}&render=true"

    response = requests.get(api_url, timeout=60)

    if response.status_code == 200:
        return BeautifulSoup(response.text, "html.parser")
    else:
        print(f"ScraperAPI error: {response.status_code}")
        return None

# Use it in place of direct requests
soup = scrape_with_api("https://www.fiverr.com/search/gigs?query=data+scraping")

This is significantly easier than managing your own proxy infrastructure, especially for Cloudflare-protected sites like Fiverr.

Building a Market Research Dataset

Here’s how to combine everything into a market research pipeline:

def build_market_dataset(queries, output_file="fiverr_market_data.csv"):
    """Build a dataset of gig listings across multiple search terms."""

    all_gigs = []

    for query in queries:
        print(f"nSearching for: {query}")
        gigs = scrape_fiverr_search(query, pages=2)

        for gig in gigs:
            gig["search_query"] = query
            all_gigs.append(gig)

        time.sleep(random.uniform(5, 10))

    # Save to CSV
    if all_gigs:
        keys = all_gigs[0].keys()
        with open(output_file, "w", newline="", encoding="utf-8") as f:
            writer = csv.DictWriter(f, fieldnames=keys)
            writer.writeheader()
            writer.writerows(all_gigs)

        print(f"nSaved {len(all_gigs)} gigs to {output_file}")

    return all_gigs

# Research the web scraping niche
queries = [
    "web+scraping",
    "data+scraping",
    "web+crawler",
    "scraping+bot",
    "api+scraping",
]

dataset = build_market_dataset(queries)

Practical Use Cases

What can you actually do with Fiverr data?

  • Pricing research — understand market rates for any service category
  • Competitor analysis — see how top sellers position their gigs
  • Trend monitoring — track which services are growing in demand
  • Quality assessment — analyze review patterns to find reliable sellers
  • Niche discovery — find underserved categories with high demand

Limitations and Honest Assessment

  1. Cloudflare is tough. Plain requests will get blocked quickly. You need either a scraping API or Playwright with stealth plugins.
  2. Fiverr’s frontend changes often. The __NEXT_DATA__ JSON structure can change between deployments. Build your parser to handle missing fields gracefully.
  3. No official API for this data. Fiverr’s API is only for sellers managing their own gigs. There’s no public API for browsing the marketplace.
  4. Rate limit strictly. Fiverr will ban your IP fast if you’re aggressive. Keep delays between requests at 3+ seconds minimum.
  5. Respect the platform. Don’t scrape personal data, don’t spam sellers, and don’t use the data for anything that violates Fiverr’s Terms of Service.

Pre-Built Solutions

If you’d rather skip the scraping infrastructure entirely, there are pre-built Fiverr scrapers available on platforms like Apify. These handle anti-bot protection, proxy rotation, and data formatting out of the box — just configure your search parameters and get structured JSON output.

Wrapping Up

Fiverr scraping is a great way to understand the freelance marketplace at scale. The combination of __NEXT_DATA__ JSON extraction and a scraping API like ScraperAPI for anti-bot handling gives you a reliable pipeline.

Start small with a single search query, verify your selectors work, then scale up gradually. The freelance economy generates fascinating data — pricing trends, demand shifts, and service quality patterns that aren’t visible from casual browsing.

Got questions about scraping other freelance platforms? Drop a comment below.

How to Scrape Booking.com in 2026: Hotel Data, Prices, and Reviews

Booking.com holds one of the richest datasets in the travel industry — hotel listings, nightly rates, guest reviews, availability calendars, and property photos across millions of properties worldwide. Whether you’re building a price comparison tool, analyzing travel trends, or doing market research for the hospitality industry, Booking.com data is incredibly valuable.

But scraping it? That’s where things get interesting.

In this guide, I’ll walk you through scraping Booking.com hotel data with Python — what works, what doesn’t, and the anti-bot challenges you’ll face in 2026.

What Data Can You Extract from Booking.com?

Before writing any code, let’s map out what’s available:

  • Hotel listings — name, star rating, address, coordinates, property type
  • Pricing — nightly rates, total stay cost, taxes, discounts
  • Availability — room types, dates available, occupancy limits
  • Reviews — guest scores, review text, reviewer country, review date
  • Photos — property images, room photos
  • Amenities — WiFi, parking, breakfast, pool, etc.

The search results page is the easiest entry point. You search by location and dates, and Booking returns a paginated list of properties with pricing.

Step 1: Understanding the Search URL Structure

Booking.com search URLs follow a predictable pattern:

https://www.booking.com/searchresults.html?ss=Paris&checkin=2026-04-15&checkout=2026-04-18&group_adults=2&no_rooms=1

Key parameters:

  • ss — search query (city, region, or hotel name)
  • checkin / checkout — dates in YYYY-MM-DD format
  • group_adults — number of guests
  • no_rooms — number of rooms
  • offset — pagination (25 results per page, so offset=25 for page 2)

Step 2: Basic Scraper with requests + BeautifulSoup

Here’s a starting point that extracts hotel names and prices from search results:

import requests
from bs4 import BeautifulSoup
import json
import time
import random

def scrape_booking_search(location, checkin, checkout, pages=3):
    """Scrape Booking.com search results for hotel listings."""

    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                       "AppleWebKit/537.36 (KHTML, like Gecko) "
                       "Chrome/124.0.0.0 Safari/537.36",
        "Accept-Language": "en-US,en;q=0.9",
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
    }

    hotels = []

    for page in range(pages):
        offset = page * 25
        url = (
            f"https://www.booking.com/searchresults.html"
            f"?ss={location}&checkin={checkin}&checkout={checkout}"
            f"&group_adults=2&no_rooms=1&offset={offset}"
        )

        response = requests.get(url, headers=headers, timeout=30)

        if response.status_code != 200:
            print(f"Page {page + 1}: Got status {response.status_code}")
            continue

        soup = BeautifulSoup(response.text, "html.parser")

        # Booking.com uses data attributes on property cards
        property_cards = soup.select('[data-testid="property-card"]')

        for card in property_cards:
            hotel = {}

            # Hotel name
            title_el = card.select_one('[data-testid="title"]')
            hotel["name"] = title_el.get_text(strip=True) if title_el else None

            # Price
            price_el = card.select_one('[data-testid="price-and-discounted-price"]')
            hotel["price"] = price_el.get_text(strip=True) if price_el else None

            # Review score
            score_el = card.select_one('[data-testid="review-score"]')
            hotel["review_score"] = score_el.get_text(strip=True) if score_el else None

            # Link to property page
            link_el = card.select_one('a[data-testid="title-link"]')
            hotel["url"] = link_el["href"] if link_el else None

            # Address / distance
            distance_el = card.select_one('[data-testid="distance"]')
            hotel["distance"] = distance_el.get_text(strip=True) if distance_el else None

            if hotel["name"]:
                hotels.append(hotel)

        print(f"Page {page + 1}: Found {len(property_cards)} properties")

        # Be respectful — random delay between requests
        time.sleep(random.uniform(2, 5))

    return hotels

# Usage
results = scrape_booking_search("Paris", "2026-04-15", "2026-04-18")
print(f"Total hotels found: {len(results)}")

for hotel in results[:5]:
    print(f"  {hotel['name']} — {hotel['price']} — Score: {hotel['review_score']}")

Step 3: Extracting Detailed Hotel Data

Once you have property URLs, you can scrape individual hotel pages for deeper data:

def scrape_hotel_details(hotel_url):
    """Extract detailed information from a Booking.com hotel page."""

    headers = {
        "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
                       "AppleWebKit/537.36 (KHTML, like Gecko) "
                       "Chrome/124.0.0.0 Safari/537.36",
        "Accept-Language": "en-US,en;q=0.9",
    }

    response = requests.get(hotel_url, headers=headers, timeout=30)
    soup = BeautifulSoup(response.text, "html.parser")

    details = {}

    # Property name and type
    name_el = soup.select_one('h2.pp-header__title')
    details["name"] = name_el.get_text(strip=True) if name_el else None

    # Address
    address_el = soup.select_one('[data-node_tt_id="location_score_tooltip"]')
    details["address"] = address_el.get_text(strip=True) if address_el else None

    # Overall review score
    score_el = soup.select_one('[data-testid="review-score-component"]')
    details["score"] = score_el.get_text(strip=True) if score_el else None

    # Description
    desc_el = soup.select_one('[data-testid="property-description"]')
    details["description"] = desc_el.get_text(strip=True) if desc_el else None

    # Amenities / facilities
    amenities = []
    for item in soup.select('[data-testid="facility-group-icon"]'):
        text = item.find_parent().get_text(strip=True)
        if text:
            amenities.append(text)
    details["amenities"] = amenities

    # Try to extract structured data from JSON-LD
    for script in soup.select('script[type="application/ld+json"]'):
        try:
            ld_data = json.loads(script.string)
            if ld_data.get("@type") == "Hotel":
                details["star_rating"] = ld_data.get("starRating", {}).get("ratingValue")
                details["coordinates"] = {
                    "lat": ld_data.get("geo", {}).get("latitude"),
                    "lng": ld_data.get("geo", {}).get("longitude"),
                }
                details["aggregate_rating"] = ld_data.get("aggregateRating", {}).get("ratingValue")
        except (json.JSONDecodeError, TypeError):
            continue

    return details

The Anti-Bot Problem (This Is Where It Gets Hard)

Here’s the honest truth: Booking.com has some of the most aggressive anti-bot protection in the travel industry. In 2026, you’ll face:

  1. Akamai Bot Manager — sophisticated browser fingerprinting that detects headless browsers
  2. CAPTCHA challenges — triggered after just a few requests from datacenter IPs
  3. Rate limiting — aggressive throttling that returns 429 or redirects to CAPTCHA pages
  4. Dynamic rendering — prices and availability often load via JavaScript after the initial page load

With plain requests, you’ll get blocked within 10-20 requests from a datacenter IP. This is where residential proxies become essential.

Using Residential Proxies for Reliable Scraping

Residential proxies route your requests through real consumer IP addresses, making your traffic look like normal browsing. For Booking.com specifically, this is not optional — it’s required for any meaningful data collection.

ThorData offers residential proxies with geo-targeting, which is particularly useful for Booking.com since prices vary by the visitor’s location:

import requests

PROXY_URL = "http://YOUR_USER:[email protected]:9000"

proxies = {
    "http": PROXY_URL,
    "https": PROXY_URL,
}

def scrape_with_proxy(url, headers):
    """Make a request through residential proxy."""
    try:
        response = requests.get(
            url,
            headers=headers,
            proxies=proxies,
            timeout=30,
        )
        return response
    except requests.exceptions.ProxyError as e:
        print(f"Proxy error: {e}")
        return None

# Geo-target to see prices in a specific currency
# ThorData supports country-level targeting

Why geo-targeting matters for Booking.com: Hotels show different prices based on where you’re browsing from. A hotel in Paris might show €120/night to a French visitor but €135 to someone browsing from the US. If you’re doing price comparison, you need to control which country your requests come from.

Handling JavaScript-Rendered Content

Some pricing data on Booking.com loads dynamically. For those cases, you’ll need a browser automation tool:

from playwright.sync_api import sync_playwright
import json

def scrape_with_browser(location, checkin, checkout):
    """Use Playwright for JS-rendered Booking.com pages."""

    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        context = browser.new_context(
            user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                       "AppleWebKit/537.36 Chrome/124.0.0.0 Safari/537.36",
            viewport={"width": 1920, "height": 1080},
            locale="en-US",
        )

        page = context.new_page()

        url = (
            f"https://www.booking.com/searchresults.html"
            f"?ss={location}&checkin={checkin}&checkout={checkout}"
            f"&group_adults=2&no_rooms=1"
        )

        page.goto(url, wait_until="networkidle")

        # Wait for price elements to render
        page.wait_for_selector('[data-testid="price-and-discounted-price"]', timeout=15000)

        # Extract data from the rendered page
        hotels = page.evaluate("""
            () => {
                const cards = document.querySelectorAll('[data-testid="property-card"]');
                return Array.from(cards).map(card => ({
                    name: card.querySelector('[data-testid="title"]')?.textContent?.trim(),
                    price: card.querySelector('[data-testid="price-and-discounted-price"]')?.textContent?.trim(),
                    score: card.querySelector('[data-testid="review-score"]')?.textContent?.trim(),
                }));
            }
        """)

        browser.close()
        return hotels

Extracting Review Data

Guest reviews are one of the most valuable parts of Booking.com data. Each property has a reviews page:

def scrape_reviews(property_id, pages=5):
    """Scrape guest reviews for a specific Booking.com property."""

    reviews = []

    for page in range(pages):
        offset = page * 25
        # Reviews are loaded via an internal API endpoint
        url = (
            f"https://www.booking.com/reviewlist.html"
            f"?pagename={property_id}&offset={offset}&rows=25"
        )

        response = requests.get(url, headers=headers, proxies=proxies, timeout=30)
        soup = BeautifulSoup(response.text, "html.parser")

        for review_el in soup.select('.review_item'):
            review = {}

            score = review_el.select_one('.review-score-badge')
            review["score"] = score.get_text(strip=True) if score else None

            title = review_el.select_one('.review_item_header_content')
            review["title"] = title.get_text(strip=True) if title else None

            positive = review_el.select_one('.review_pos')
            review["positive"] = positive.get_text(strip=True) if positive else None

            negative = review_el.select_one('.review_neg')
            review["negative"] = negative.get_text(strip=True) if negative else None

            date_el = review_el.select_one('.review_item_date')
            review["date"] = date_el.get_text(strip=True) if date_el else None

            reviews.append(review)

        time.sleep(random.uniform(2, 4))

    return reviews

Saving to CSV

import csv

def save_to_csv(hotels, filename="booking_hotels.csv"):
    """Save scraped hotel data to CSV."""
    if not hotels:
        return

    keys = hotels[0].keys()
    with open(filename, "w", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(f, fieldnames=keys)
        writer.writeheader()
        writer.writerows(hotels)

    print(f"Saved {len(hotels)} hotels to {filename}")

Limitations and Honest Assessment

Let me be upfront about the challenges:

  1. Booking.com actively fights scraping. Their anti-bot is among the best. Expect to invest in residential proxies and browser fingerprint management.
  2. Prices are session-dependent. The same hotel can show different prices based on cookies, login status, and browsing history. Getting accurate price data requires careful session management.
  3. Selectors change frequently. Booking.com updates their frontend regularly. Your selectors will break — budget time for maintenance.
  4. Scale is expensive. Between proxy costs and the slow pace required to avoid detection, scraping thousands of properties daily requires real infrastructure investment.
  5. Legal considerations. Booking.com’s ToS prohibit automated scraping. Use the data responsibly, respect rate limits, and consider whether their affiliate API might meet your needs instead.

When to Use the Booking.com Affiliate API Instead

Before building a scraper, check if the Booking.com Affiliate Partner API gives you what you need. It provides:

  • Hotel search and availability
  • Pricing data
  • Property details and photos

The API is free for affiliates and doesn’t require proxy infrastructure. The trade-off is that you’re limited to their data format and rate limits, and you need to apply for partner access.

Summary

Scraping Booking.com is doable but challenging. For small-scale research (a few hundred properties), the approach above works. For production-scale data collection, you’ll need residential proxies, browser automation, and a maintenance plan for when selectors inevitably break.

The key insight: start with their API if it meets your needs. Only build a scraper if you need data the API doesn’t provide — like competitor pricing from the guest perspective, or historical price trends.

Happy scraping, and remember: always be respectful of the sites you scrape. Rate-limit your requests and don’t hammer their servers.

How I Built an AI Job Board That Auto-Updates from 164+ Companies

Нефункциональные требования

The Problem

If you’re looking for an AI engineering role in 2026, you’re probably checking 10-20 individual career pages. Anthropic’s Greenhouse page, OpenAI’s careers site, DeepMind’s jobs board, Cohere’s Lever page…

General job boards like LinkedIn and Indeed have AI roles, but the filtering is terrible. Search “AI engineer” and you get results for “AI-powered customer service” and “engineer at an AI startup doing nothing related to AI.”

I wanted a single page with every AI/ML/LLM role from every major AI company, updated automatically.

So I built LLMHire.

Architecture

Stack: Next.js 14 (App Router) + Supabase (PostgreSQL) + Vercel (hosting + crons)

The core idea is simple: most tech companies use one of three ATS platforms — Greenhouse, Ashby, or Lever. Each exposes a public API for their job listings. I built adapters for each.

The Fetcher Pattern

Each ATS has a fetcher module:

  • Greenhouse: GET https://boards-api.greenhouse.io/v1/boards/{company}/jobs
  • Ashby: POST https://jobs.ashbyhq.com/api/non-auth-job-posting
  • Lever: GET https://api.lever.co/v0/postings/{company}

Each fetcher normalizes the response into a common schema:

  • title, company, location, department
  • remote/hybrid/onsite classification
  • role type (ML Engineer, LLM Engineer, Research Scientist, etc.)
  • posting date, description, apply URL

Auto-Categorization

Job title → role type mapping uses pattern matching:

  • Contains “LLM” or “NLP” or “language model” → LLM Engineer
  • Contains “ML” or “machine learning” → ML Engineer
  • Contains “research” + “scientist” → AI Research
  • Contains “infrastructure” or “platform” → AI Infrastructure
  • Contains “prompt” → Prompt Engineer

This gets ~90% accuracy. Edge cases get manually reviewed.

Cron Jobs

Vercel cron jobs run every 4 hours (6x/day):

  1. Fetch all jobs from all companies
  2. Deduplicate against existing listings (by company + title + location hash)
  3. Mark stale listings (>30 days without refresh) for review
  4. Update search index

The Numbers

  • 130+ companies tracked
  • 670+ active positions
  • ~50-100 new/updated listings per day
  • Average fetch cycle: ~45 seconds for all companies

What the Data Shows

After tracking the AI job market for a few weeks:

  1. “LLM Engineer” is a real role now — distinct from ML Engineer, with different skill requirements
  2. Remote is declining — major labs want hybrid/onsite
  3. AI Infrastructure is booming — fastest growing subcategory
  4. Prompt Engineering peaked — being absorbed into broader roles

What’s Next

  • Workable integration (another ATS with significant AI company coverage)
  • Salary data aggregation
  • Weekly AI jobs newsletter
  • Blog with hiring market analysis

Try It

LLMHire is completely free. No accounts, no fees, no signup required to browse. If you have feedback or companies I should add, drop a comment.