David Lewin David Lewin
E-Commerce, Research & Ads

Turn competitor product pages into clean JSON for an LLM: Firecrawl vs DIY Playwright

Disclosure: some links in this guide are affiliate links. If you sign up through them, I may earn a commission at no extra cost to you. How this site makes money.

Short answer

Firecrawl takes a URL and a JSON schema, renders the page, and returns the product as structured JSON in one API call, for 5 credits a page (1 for the scrape plus 4 for the JSON format). 10,000 product pages a month is 50,000 credits, which fits the Standard plan at $83 a month billed yearly. A DIY pipeline with Playwright and Beautiful Soup costs little to run, but you still need an LLM call to turn the cleaned text into JSON, proxies for protected stores and a few hours a month of upkeep: by our estimate about $180 to $490 a month for the same 10,000 pages. DIY wins when you scrape a few sites with good schema.org data, or at very high volume.

A competitor's product page is mostly noise for a language model: navigation, cookie banners, tracking scripts, recommendation carousels and kilobytes of markup around a price, a name and a spec table. Feed raw HTML to an LLM and you pay for tokens that carry no information, and the model may pick the wrong price. What you want is one clean JSON record per product, in a fixed schema, that you can compare, store or hand to a model to write descriptions. This guide compares two ways to get there: Firecrawl's API, which renders the page and returns JSON in your schema, and a DIY pipeline built on Playwright and Beautiful Soup.

How this guide was made

Firecrawl prices, credit rules and API options come from its pricing page and documentation, checked in October 2026. Both scripts below were tested on our machine: the Firecrawl client against a mock of the /v2/scrape endpoint that checks the request body and returns rate-limit, temporary and out-of-credit errors, and the DIY pipeline in a real browser against a local product page whose price loads with JavaScript. Neither was tested against live stores. DIY running costs are our estimates.

1. The pipeline

From product page to structured data
From product page to structured dataRaw product page → Firecrawl API → Structured JSON → LLM / DatabaseRaw product page: HTML, JavaScript, banners, tracking and carouselsRaw product pageHTML, JavaScript, banners, tracking and carouselsFirecrawl API: Renders JavaScript, picks a proxy, strips boilerplate and fills your JSON schemaFirecrawl APIRenders JavaScript, picks a proxy, strips boilerplate and fillsyour JSON schemaStructured JSON: name, brand, sku, price, currency, in_stock, specsStructured JSONname, brand, sku, price, currency, in_stock, specsLLM: Spec comparisons, product descriptions, summariesLLMSpec comparisons, productdescriptions, summariesDatabase: Price history, catalog matching, alertsDatabasePrice history, catalogmatching, alerts
One API call replaces the browser, the HTML cleanup and the extraction prompt. Hover or tab through the boxes for details.

In the DIY version, the middle box becomes three steps you run yourself: render with Playwright, reduce the HTML with Beautiful Soup, then call an LLM to fill the schema.

2. Firecrawl: one call per page

Firecrawl's /v2/scrape endpoint takes a URL and a list of formats. The json format takes a JSON schema and an optional prompt, and returns the extracted object under data.json. Standard library only:

#!/usr/bin/env python3
"""Turn product pages into JSON records with Firecrawl's JSON format. Python 3.9+, standard library only.

  export FIRECRAWL_API_KEY=fc-...
  python3 fc_extract.py urls.txt products.jsonl
"""
import json
import os
import sys
import time
import urllib.error
import urllib.request
from concurrent.futures import ThreadPoolExecutor

API = os.environ.get('FIRECRAWL_API_URL', 'https://api.firecrawl.dev/v2/scrape')
KEY = os.environ['FIRECRAWL_API_KEY']
WORKERS = 5  # keep at or below your plan's concurrent requests

PROMPT = 'Extract the main product on this page. Use the current selling price, not a crossed-out one.'
SCHEMA = {
    'type': 'object',
    'properties': {
        'name': {'type': 'string'},
        'brand': {'type': 'string'},
        'sku': {'type': 'string'},
        'price': {'type': 'number'},
        'currency': {'type': 'string', 'description': 'ISO 4217 code, e.g. USD'},
        'in_stock': {'type': 'boolean'},
        'specs': {'type': 'object', 'additionalProperties': {'type': 'string'},
                  'description': 'Technical specifications as name: value pairs'},
        'description': {'type': 'string', 'description': 'Product description, at most 3 sentences'},
    },
    'required': ['name', 'price', 'currency'],
}


def extract(url, retries=3):
    body = json.dumps({
        'url': url,
        'formats': [{'type': 'json', 'schema': SCHEMA, 'prompt': PROMPT}],
        'maxAge': 0,  # the default serves cached pages up to 2 days old: wrong for prices
    }).encode()
    headers = {'Authorization': 'Bearer ' + KEY, 'Content-Type': 'application/json'}
    req = urllib.request.Request(API, body, headers)
    for attempt in range(retries + 1):
        try:
            with urllib.request.urlopen(req, timeout=120) as r:
                data = json.load(r)['data']
                status = data.get('metadata', {}).get('statusCode')
                return {'url': url, 'status': status, **data.get('json', {})}
        except urllib.error.HTTPError as e:
            # 429: rate or concurrency limit; 5xx: temporary.
            # 402 (out of credits) and other 4xx errors won't fix themselves.
            if e.code not in (429, 500, 502, 503) or attempt == retries:
                print('%s: HTTP %d %s' % (url, e.code, e.read()[:200]), file=sys.stderr)
                return None
            time.sleep(2 ** attempt)


def main():
    urls = [line.strip() for line in open(sys.argv[1]) if line.strip()]
    with ThreadPoolExecutor(WORKERS) as pool, open(sys.argv[2], 'a', encoding='utf-8') as out:
        ok = 0
        for rec in pool.map(extract, urls):
            if rec:
                out.write(json.dumps(rec, ensure_ascii=False) + '\n')
                ok += 1
    print('%d of %d pages extracted' % (ok, len(urls)), file=sys.stderr)


if __name__ == '__main__':
    main()

Each line of the output file is one product, ready for a database or a prompt:

{"url": "https://shop.example/p/1", "status": 200, "name": "Mug", "price": 24.0, "currency": "USD", "in_stock": true, "specs": {"Capacity": "350 ml"}}
  • maxAge: 0. By default Firecrawl may return a cached copy of the page up to two days old. That's fine for specs and descriptions. For prices and stock, always fetch fresh.
  • Proxies. The default auto mode retries hard sites with enhanced proxies, and the docs say an enhanced request costs the same 1 credit as a basic one. Set location with a country code to see a market's local prices. Our geo price check guide explains why location changes prices.
  • onlyMainContent. On by default, it strips navigation and footers before extraction. If a field that sits in a sidebar, such as a spec table, comes back empty, set it to false.
  • Concurrency. Set WORKERS to your plan's concurrent request limit: 2 on the free plan, 5 on Hobby, 25 on Standard. The script retries 429 and temporary errors and stops on 402 (out of credits).

3. DIY: Playwright plus Beautiful Soup

The DIY version renders the page in your own headless browser, keeps the schema.org Product JSON-LD if there is any, and reduces the rest of the page to plain text, which is what you send to the LLM:

#!/usr/bin/env python3
"""Render product pages with Playwright and reduce them to JSON-LD plus clean text for an LLM.

  pip install playwright beautifulsoup4 && playwright install chromium
  python3 diy_extract.py urls.txt pages.jsonl
"""
import json
import re
import sys

from bs4 import BeautifulSoup
from playwright.sync_api import sync_playwright

MAX_CHARS = 12000  # roughly 3,000 tokens of English text


def reduce(html):
    soup = BeautifulSoup(html, 'html.parser')
    products = []
    for tag in soup.find_all('script', type='application/ld+json'):
        try:
            data = json.loads(tag.string or '')
        except ValueError:
            continue
        for item in data if isinstance(data, list) else data.get('@graph', [data]):
            if 'Product' in str(item.get('@type')):
                products.append(item)
    for tag in soup(['script', 'style', 'noscript', 'svg', 'iframe', 'nav', 'header', 'footer', 'form']):
        tag.decompose()
    text = re.sub(r'\n\s*\n+', '\n', soup.get_text('\n', strip=True))
    return products, text[:MAX_CHARS]


def main():
    urls = [line.strip() for line in open(sys.argv[1]) if line.strip()]
    with sync_playwright() as p, open(sys.argv[2], 'a', encoding='utf-8') as out:
        browser = p.chromium.launch()
        page = browser.new_page()
        # Skip images, fonts and video: they don't hold product data
        page.route(re.compile(r'\.(png|jpe?g|webp|gif|woff2?|mp4)(\?|$)'), lambda r: r.abort())
        for url in urls:
            try:
                page.goto(url, wait_until='networkidle', timeout=45000)
            except Exception as e:
                print('%s: %s' % (url, e), file=sys.stderr)
                continue
            products, text = reduce(page.content())
            rec = {'url': url, 'jsonld': products, 'text': text}
            out.write(json.dumps(rec, ensure_ascii=False) + '\n')
        browser.close()


if __name__ == '__main__':
    main()

In our test, a product page whose price was injected by JavaScript after 300 ms came out as six lines: the name, "$24.00 In stock" and the spec table. Navigation, header, footer and scripts were gone. On a real store, expect a few thousand tokens per page after reduction.

That's where the DIY work starts, not ends:

  • You still need the extraction step. The text has to go to an LLM with your schema to become JSON, and that's an API bill or a GPU of your own. Our Ollama and vLLM guide covers running an open model on your own server.
  • Use JSON-LD first. When a page's Product JSON-LD already has the price and currency, you can skip the LLM for those fields. Many stores publish it for search engines, but it's often incomplete: the test page above had the name and SKU in JSON-LD and the price only in the visible text.
  • Browsers, proxies and blocks are yours. Bot-protected stores need residential proxies and stealth patches. See ZenRows vs self-hosted Playwright for what that costs, and the Playwright cluster guide for running browsers at scale.
  • Selectors are the cheap alternative to an LLM, but they break when the site changes. The Zyte auto-extraction guide compares the two.

4. What Firecrawl costs

Firecrawl charges 1 credit to scrape a page and 4 more for the JSON format, so a structured product record is 5 credits. Markdown output without JSON is 1 credit. Failed scrapes are free, but pages that return an error status such as 403 or 404 still cost 1 credit.

PlanPriceCredits per monthConcurrent requestsProduct pages as JSON (5 credits)
Free$01,0002200
Hobby$19 a month, or $16 billed yearly5,00051,000
Standard$83 a month, billed yearly100,0002520,000
Growth$333 a month, billed yearly500,00050100,000
Scale$599 a month, billed yearly1,000,000100200,000

Prices from Firecrawl's pricing page, October 2026. Extra credits on paid plans come in $5 packs: 1,000 credits on Hobby, 2,000 on Standard, 2,500 on Growth and 5,000 on Scale. Firecrawl also offers an optional prompt-injection check on LLM formats for 4 more credits, worth turning on when the pages come from sites you don't control.

5. Cost comparison: 10,000 product pages a month

CostFirecrawlDIY Playwright + Beautiful Soup
Plan or server$83, Standard plan billed yearly (50,000 of its 100,000 credits)$24, a 4 GB DigitalOcean Droplet
Text to JSONIncluded in the 5 credits$3 to $30: about 3,000 input tokens per page, 30 million tokens a month, at $0.10 to $1.00 per million for a small model (our estimate)
Proxies for protected storesIncluded$0 to $140: up to 2 MB per page at up to $7 per GB (our estimate)
Keeping it workingNone3 to 6 hours a month at $50 an hour, $150 to $300 (our estimate)
Monthly total$83about $180 to $490

The DIY total leaves out the one-off build, which we'd estimate at 10 to 20 hours. On Hobby with monthly billing, the same 10,000 pages cost $19 plus 45 packs of extra credits, $244 in all, so a steady workload belongs on Standard.

Monthly cost: 10,000 product pages as structured JSON
Monthly cost: 10,000 product pages as structured JSONFirecrawl Standard, billed yearly: 83 $/month; Firecrawl Hobby plus extra credits: 244 $/month; DIY, low estimate: 177 $/month; DIY, high estimate: 494 $/monthFirecrawlDIY Playwright + Beautiful SoupFirecrawl Standard, billed yearly: 83 $/monthFirecrawl Standard, billed yearly$83Firecrawl Hobby plus extra credits: 244 $/monthFirecrawl Hobby plus extra credits$244DIY, low estimate: 177 $/monthDIY, low estimate$177DIY, high estimate: 494 $/monthDIY, high estimate$494
Firecrawl at list prices. DIY figures are our estimates: a $24 VPS, LLM extraction at $0.10 to $1.00 per million input tokens, residential bandwidth for protected stores and 3 to 6 hours of upkeep a month at $50 an hour.

When DIY wins

  • A handful of stores with complete JSON-LD. If price, currency, stock and SKU are all in the Product JSON-LD, you need no LLM and no selectors, and a small server covers it. Our Shopify price monitoring guide is an example.
  • Very high volume. One million structured pages a month is 5 million credits, five times the Scale plan. At that size, your own browsers and a self-hosted model can cost less, if you have the people to run them.
  • Logged-in or interactive pages, or data you can't send to a third party.

6. Which one to pick

Your situationBest fit
Many different competitor sites, schema-valid JSON for an LLM or databaseFirecrawl with the json format
You already run your own LLM and only need clean textFirecrawl markdown at 1 credit, or DIY if the sites aren't protected
A few stores with complete Product JSON-LDDIY, no LLM needed
Millions of pages a month, in-house scraping teamDIY on your own servers

7. Tools

Top pick

ToolRoleWhy it fits
FirecrawlWeb pages to LLM-ready JSON and markdownOne call renders the page, handles proxies and returns JSON in your schema, at 5 credits per structured page. Enhanced proxies cost no extra, and the free plan's 1,000 credits are enough to test 200 of your own product pages.

Alternatives: open-source tools

We don't earn anything from these links.

  • Playwright for Python: the headless browser in the DIY pipeline.
  • Beautiful Soup: HTML parsing and cleanup.
  • Scrapy: a full crawling framework with scheduling, retries and pipelines, for large DIY crawls of sites that don't need a browser.
  • Crawl4AI: an open-source crawler that outputs LLM-friendly markdown, if you want a DIY tool closer to what Firecrawl does.

8. Pitfalls

  • Cached prices. Leaving Firecrawl's default maxAge on a price monitor gives you prices up to two days old. Set it to 0.
  • Trusting the JSON without checks. Validate every record before it reaches your database: price is a positive number, currency is a three-letter code, required fields exist. LLM extraction can misread a crossed-out price or a bundle price.
  • Paying for dead URLs. 403 and 404 pages still cost a credit. Remove them from your lists.
  • Prompt injection. Pages from competitors are untrusted input. Text hidden in a page can try to steer the model that reads it. Use Firecrawl's injection check, or keep the extraction model's output strictly limited to your schema.
  • Ignoring a site's terms. Scrape public pages at a polite rate and check the site's terms of service.
Bottom line

For turning many different competitor product pages into schema-valid JSON, Firecrawl does the rendering, proxies, cleanup and extraction in one call at 5 credits a page: 10,000 pages a month fit the $83 Standard plan, billed yearly. A DIY Playwright and Beautiful Soup pipeline is cheap to run but still needs an LLM, proxies and upkeep, about $180 to $490 a month by our estimate. Build it yourself when a few stores publish complete JSON-LD, or when you scrape at a volume where your own infrastructure pays for itself.

David Lewin
David Lewin

Founder of StealthScale.io and e-commerce infrastructure engineer writing field-tested playbooks on proxies, cloud VPS, anti-detect browsers and SEO.