Proxies for Price Monitoring

Location-accurate prices, sane crawl cadence, and what a catalogue actually costs to track · checked 3 November 2026
Price monitoring

Price data is the most time-sensitive thing a retailer collects, and the easiest to get wrong in a way nobody notices for a month. A price scraped from the wrong city is not a small error; it is a pricing decision made on a number a customer never saw. This page covers the three things that actually decide data quality: the location you ask from, the cadence you run at, and whether you are reading the price the page displays or a number left over in the markup.

Location is part of the price

Marketplaces and large retailers vary availability, delivery estimates, tax display and sometimes the price itself by location. Collecting a single national feed and calling it "the price" produces a number that matches no customer's screen. Target the cities your customers actually buy from, and keep the location in the record.

# one sticky session per city: same shopper, same cart context
import requests

CITIES = [("ca", "losangeles"), ("ny", "newyork"), ("tx", "houston"), ("il", "chicago")]
PWD = "PASSWORD"

for state, city in CITIES:
    user = "ACCOUNT-country-us-state-%s-city-%s-sid-%s01-stime-30" % (state, city, city[:6])
    p = "http://%s:%s@GATEWAY_HOST:PORT" % (user, PWD)
    r = requests.get(product_url, proxies={"http": p, "https": p}, timeout=30)
    print(city, r.status_code, extract_price(r.text))  # helper defined below
Keep the location in the output schema. A price row without country, city and timestamp is not usable for a pricing decision — you cannot tell a real geographic difference from a stale record.

Read the data, not the paint

Most product pages carry the price twice: once in visible markup, once in structured data or an embedded state object. The visible one is often a pre-discount list price, or a placeholder replaced by JavaScript after load. Extracting from the structured block is cheaper and more stable than rendering.

SourceReliabilityCostNotes
JSON-LD offersHighestCheapestStructured by design; currency included
Embedded state (__NEXT_DATA__, window.__STATE__)HighCheapContains variants and stock the markup omits
Visible markupMediumCheapMay be a pre-discount or placeholder value
Rendered DOMHigh10–50x bandwidthUse only when the first three fail
# JSON-LD first, then the embedded state object, then a fallback selector
import json, re

def extract_price(html):
    m = re.search(r'application/ld\+json">(.*?)', html, re.S)
    if m:
        d = json.loads(m.group(1))
        offers = d.get("offers") or {}
        if isinstance(offers, list):
            offers = offers[0]
        return offers.get("price"), offers.get("priceCurrency")
    m = re.search(r'"price"\s*:\s*"?([0-9.]+)', html)
    return (m.group(1), None) if m else (None, None)

Cadence: match it to how prices move

Catalogue sliceFrequencyWhy
Top 500 SKUs by revenueHourlyRepricing happens here first and moves margin
Competitor overlap setEvery 4–6 hoursEnough to catch a same-day response
Long tail (10k+ SKUs)DailyPrices are stable; hourly only adds cost
MAP / reseller checksWeeklyDisputes move slower than prices

Every extra pass costs traffic whether or not the price changed. The useful trick is to diff before you re-crawl: store a hash of the product block, and only re-parse when it changes. On a catalogue with stable prices this typically cuts parsed volume by an order of magnitude even though you still fetch the page.

What a catalogue costs

Assume 100,000 SKUs, one pass per day, 400 KB per page, 95% success rate:

ItemValue
Pages per pass100,000
Attempts at 95% success105,263
Traffic per pass42 GB
Cost per pass at $1/GB$42
Monthly$1,260

At $1/GB that is roughly $42 a day, or $1,260 a month, for a full daily pass. Two levers move that number far more than any unit-price negotiation: read JSON instead of rendering (often halves the bytes), and crawl only the SKUs that matter at hourly frequency while the long tail runs daily.

A monitoring loop that produces decisions, not just rows

  1. Define the record. SKU, retailer, country, city, price, currency, promotion text, timestamp, HTTP status, extraction source. Without the source field you cannot debug a bad number a week later.
  2. Validate before you store. A price that moved 70% in one hour is usually a parse error or a currency switch, not a repricing. Re-fetch once with a new session and compare.
  3. Alert on the second confirmation. Two consecutive passes showing the same change is worth a notification; the first is worth a retry.
  4. Keep an evidence copy. For MAP and channel disputes, store a screenshot hash or the raw HTML fragment alongside the price — a claim needs proof attached to the moment.
  5. Review the target list monthly. Sites add protection; a host that ran fine on datacenter last quarter may need residential today.

Where teams get it wrong

Frequently asked questions

How often should I check competitor prices?

Match the cadence to how often prices actually move. Most retailers reprice once or twice a day, so hourly crawls burn traffic without new information. Track a small set of high-value SKUs hourly and the long tail daily, then raise frequency only when you see real movement in the logs.

Can I see the price a shopper in another city sees?

Yes on the residential pool: target country, state and city in the username, for example country-us-state-ca-city-losangeles. Marketplaces use location for availability, delivery estimates and sometimes price, so a single national view misses the differences that matter.

Why is the price I collect different from what customers report?

Three usual causes: the page served a personalised or A/B variant, you read a pre-discount price from the markup while the visible price is rendered by JavaScript, or you hit a cached version. Pin the session, read the structured data rather than the rendered text where possible, and record the timestamp with every price.

How do I monitor MAP violations?

Collect the reseller page, not just the brand site, from the reseller's own country. Store the price, the currency, the promotion text and a screenshot reference together — a violation claim needs the evidence attached, and prices change faster than disputes are resolved.

Do I need static IPs for price monitoring?

Only if a target requires a logged-in account tied to one address. For public price pages, rotating residential is both cheaper and safer: it spreads requests across the pool instead of concentrating them on a handful of addresses that will get flagged.

What does a 100k-SKU daily crawl cost?

At 400 KB per page and 95% success, about 42 GB a day, so roughly $42 a day at $1/GB. Halving page weight by reading JSON instead of rendering saves more than any rate negotiation.

Run your own target list first

Residential from $1/GB billed by traffic, static ISP from $2/IP per month from 15 IPs. No monthly commitment — measure your success rate before you scale.

Create account