Why Prices Differ by Location
E-commerce companies routinely adjust prices based on the customer's geographic location. A product that costs $49.99 in the United States might be priced at 59.99 EUR in Germany, 5,499 JPY in Japan, or $39.99 in India. These differences go beyond currency conversion — they reflect regional purchasing power, competitive landscapes, taxes, shipping costs, and deliberate pricing strategies.
For competitive intelligence, understanding these regional price variations is critical. A competitor might be aggressively undercutting you in one market while charging premium prices in another. Without geo-targeted monitoring, you are blind to half the competitive picture. This guide covers how to build a multi-market price monitoring system using geo-targeted proxies. For foundational monitoring architecture, see our guide on monitoring competitor prices automatically.
How Geo-Pricing Works
Websites determine your location through several signals and adjust content accordingly.
| Signal | How It Works | Impact on Pricing |
|---|---|---|
| IP Geolocation | IP address mapped to country/city via databases | Primary factor — determines which regional store/pricing you see |
| Currency/Language | Browser Accept-Language and previous selections | May trigger region-specific catalog and pricing |
| Cookies | Previous region selections stored in session | Overrides IP-based detection on subsequent visits |
| URL Structure | Country-specific domains (amazon.de) or paths (/de/) | Directly determines regional catalog |
| GPS/Device Location | Mobile device location services | Used for hyperlocal pricing (delivery zones) |
Common Geo-Pricing Patterns
- Marketplace localization: Amazon, eBay, and similar platforms operate separate regional storefronts with independent pricing.
- Dynamic geo-pricing: SaaS companies and travel sites adjust prices based on the visitor's country of origin.
- Shipping-adjusted pricing: Products include different shipping costs based on location, changing the effective price.
- Tax-inclusive pricing: European prices typically include VAT, while US prices show pre-tax amounts.
- Purchasing power parity: Some companies offer lower prices in developing markets to maximize accessibility.
Proxy Configuration for Multi-Market Monitoring
The core requirement is residential proxies from each target country. ProxyHat's geo-targeting lets you specify the exact country for each request.
ProxyHat Setup
# US pricing
http://USERNAME-country-US:PASSWORD@gate.proxyhat.com:8080
# German pricing
http://USERNAME-country-DE:PASSWORD@gate.proxyhat.com:8080
# UK pricing
http://USERNAME-country-GB:PASSWORD@gate.proxyhat.com:8080
# Japanese pricing
http://USERNAME-country-JP:PASSWORD@gate.proxyhat.com:8080
# Brazilian pricing
http://USERNAME-country-BR:PASSWORD@gate.proxyhat.com:8080
# City-level targeting for hyperlocal pricing
http://USERNAME-country-US-city-newyork:PASSWORD@gate.proxyhat.com:8080
http://USERNAME-country-DE-city-berlin:PASSWORD@gate.proxyhat.com:8080
For price monitoring across regions, use per-request rotation within each country to avoid detection, but always maintain consistent geo-targeting per marketplace. Check ProxyHat's full location list for 195+ supported countries.
Python Implementation
Here is a multi-market price monitoring system using ProxyHat's Python SDK.
Geo-Targeted Price Scraper
import requests
from bs4 import BeautifulSoup
import json
import time
import random
from dataclasses import dataclass, asdict
from datetime import datetime
USER_AGENTS = [
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/124.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/124.0.0.0 Safari/537.36",
]
PROXY_TEMPLATE = "http://USERNAME-country-{country}:PASSWORD@gate.proxyhat.com:8080"
@dataclass
class GeoPrice:
product_id: str
country: str
price: float | None
currency: str
url: str
in_stock: bool
scraped_at: str
def get_geo_proxy(country_code: str) -> dict:
"""Get proxy configured for a specific country."""
proxy = PROXY_TEMPLATE.format(country=country_code)
return {"http": proxy, "https": proxy}
def scrape_price_for_region(product_url: str, country_code: str,
price_selector: str, currency: str) -> GeoPrice:
"""Scrape a product price from a specific geographic region."""
headers = {
"User-Agent": random.choice(USER_AGENTS),
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": get_accept_language(country_code),
}
proxies = get_geo_proxy(country_code)
try:
response = requests.get(product_url, headers=headers,
proxies=proxies, timeout=30)
response.raise_for_status()
soup = BeautifulSoup(response.text, "html.parser")
price_el = soup.select_one(price_selector)
price = None
if price_el:
price_text = price_el.get_text(strip=True)
cleaned = "".join(c for c in price_text if c.isdigit() or c in ".,")
# Handle European comma as decimal separator
if "," in cleaned and "." in cleaned:
cleaned = cleaned.replace(".", "").replace(",", ".")
elif "," in cleaned:
cleaned = cleaned.replace(",", ".")
price = float(cleaned) if cleaned else None
return GeoPrice(
product_id=product_url,
country=country_code,
price=price,
currency=currency,
url=product_url,
in_stock=True,
scraped_at=datetime.utcnow().isoformat(),
)
except Exception as e:
return GeoPrice(
product_id=product_url,
country=country_code,
price=None,
currency=currency,
url=product_url,
in_stock=False,
scraped_at=datetime.utcnow().isoformat(),
)
def get_accept_language(country_code: str) -> str:
"""Return appropriate Accept-Language for a country."""
lang_map = {
"US": "en-US,en;q=0.9",
"GB": "en-GB,en;q=0.9",
"DE": "de-DE,de;q=0.9,en;q=0.5",
"FR": "fr-FR,fr;q=0.9,en;q=0.5",
"JP": "ja-JP,ja;q=0.9,en;q=0.5",
"BR": "pt-BR,pt;q=0.9,en;q=0.5",
"IN": "en-IN,hi;q=0.9,en;q=0.5",
"IT": "it-IT,it;q=0.9,en;q=0.5",
"ES": "es-ES,es;q=0.9,en;q=0.5",
}
return lang_map.get(country_code, "en-US,en;q=0.9")
Multi-Market Monitor
class MultiMarketMonitor:
"""Monitor prices across multiple geographic markets."""
def __init__(self):
self.markets = {}
self.results = []
def add_market(self, country_code: str, marketplace_url: str,
price_selector: str, currency: str):
"""Register a market for monitoring."""
self.markets[country_code] = {
"url": marketplace_url,
"selector": price_selector,
"currency": currency,
}
def monitor_product(self, product_urls: dict[str, str]) -> list[GeoPrice]:
"""
Monitor a product across all configured markets.
product_urls: {"US": "https://amazon.com/dp/...", "DE": "https://amazon.de/dp/..."}
"""
prices = []
for country, url in product_urls.items():
market = self.markets.get(country)
if not market:
continue
price = scrape_price_for_region(
url, country,
market["selector"],
market["currency"]
)
prices.append(price)
print(f" {country}: {price.currency} {price.price}")
time.sleep(random.uniform(2, 5))
return prices
def compare_prices(self, prices: list[GeoPrice], base_currency_rates: dict) -> dict:
"""Compare prices across markets normalized to USD."""
normalized = {}
for p in prices:
if p.price:
rate = base_currency_rates.get(p.currency, 1.0)
normalized[p.country] = {
"local_price": p.price,
"currency": p.currency,
"usd_equivalent": round(p.price / rate, 2),
}
if not normalized:
return {}
usd_prices = [v["usd_equivalent"] for v in normalized.values()]
cheapest = min(usd_prices)
most_expensive = max(usd_prices)
return {
"prices": normalized,
"cheapest_market": [k for k, v in normalized.items()
if v["usd_equivalent"] == cheapest][0],
"most_expensive_market": [k for k, v in normalized.items()
if v["usd_equivalent"] == most_expensive][0],
"price_spread_pct": round(
(most_expensive - cheapest) / cheapest * 100, 1
) if cheapest > 0 else 0,
}
# Example: Monitor a product across Amazon marketplaces
monitor = MultiMarketMonitor()
# Configure markets
monitor.add_market("US", "amazon.com", "span.a-price-whole", "USD")
monitor.add_market("DE", "amazon.de", "span.a-price-whole", "EUR")
monitor.add_market("GB", "amazon.co.uk", "span.a-price-whole", "GBP")
monitor.add_market("JP", "amazon.co.jp", "span.a-price-whole", "JPY")
# Monitor a specific product
prices = monitor.monitor_product({
"US": "https://www.amazon.com/dp/B0CHX3QBCH",
"DE": "https://www.amazon.de/dp/B0CHX3QBCH",
"GB": "https://www.amazon.co.uk/dp/B0CHX3QBCH",
"JP": "https://www.amazon.co.jp/dp/B0CHX3QBCH",
})
# Compare prices in USD
comparison = monitor.compare_prices(prices, {
"USD": 1.0, "EUR": 0.92, "GBP": 0.79, "JPY": 149.5,
})
print(json.dumps(comparison, indent=2))
Node.js Implementation
A Node.js multi-market monitor using ProxyHat's Node SDK.
const axios = require("axios");
const cheerio = require("cheerio");
const { HttpsProxyAgent } = require("https-proxy-agent");
function getGeoProxy(countryCode) {
return `http://USERNAME-country-${countryCode}:PASSWORD@gate.proxyhat.com:8080`;
}
const LANG_MAP = {
US: "en-US,en;q=0.9",
GB: "en-GB,en;q=0.9",
DE: "de-DE,de;q=0.9,en;q=0.5",
FR: "fr-FR,fr;q=0.9,en;q=0.5",
JP: "ja-JP,ja;q=0.9,en;q=0.5",
};
async function scrapeGeoPrice(url, countryCode, priceSelector, currency) {
const agent = new HttpsProxyAgent(getGeoProxy(countryCode));
try {
const { data } = await axios.get(url, {
httpsAgent: agent,
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": LANG_MAP[countryCode] || "en-US,en;q=0.9",
},
timeout: 30000,
});
const $ = cheerio.load(data);
let priceText = $(priceSelector).first().text().trim();
let price = parseFloat(priceText.replace(/[^0-9.,]/g, "").replace(",", ".")) || null;
return {
country: countryCode,
price,
currency,
url,
inStock: true,
scrapedAt: new Date().toISOString(),
};
} catch (err) {
return { country: countryCode, price: null, currency, url, inStock: false, scrapedAt: new Date().toISOString() };
}
}
async function monitorMultiMarket(productUrls, markets) {
const results = [];
for (const [country, url] of Object.entries(productUrls)) {
const market = markets[country];
if (!market) continue;
const result = await scrapeGeoPrice(url, country, market.selector, market.currency);
results.push(result);
console.log(`${country}: ${result.currency} ${result.price}`);
await new Promise((r) => setTimeout(r, 2000 + Math.random() * 3000));
}
return results;
}
function comparePrices(results, rates) {
const normalized = {};
for (const r of results) {
if (r.price) {
const rate = rates[r.currency] || 1;
normalized[r.country] = {
localPrice: r.price,
currency: r.currency,
usdEquivalent: Math.round((r.price / rate) * 100) / 100,
};
}
}
const usdPrices = Object.values(normalized).map((v) => v.usdEquivalent);
const cheapest = Math.min(...usdPrices);
const mostExpensive = Math.max(...usdPrices);
return {
prices: normalized,
cheapestMarket: Object.keys(normalized).find((k) => normalized[k].usdEquivalent === cheapest),
mostExpensiveMarket: Object.keys(normalized).find(
(k) => normalized[k].usdEquivalent === mostExpensive
),
priceSpreadPct: cheapest > 0 ? Math.round(((mostExpensive - cheapest) / cheapest) * 1000) / 10 : 0,
};
}
// Usage
const markets = {
US: { selector: "span.a-price-whole", currency: "USD" },
DE: { selector: "span.a-price-whole", currency: "EUR" },
GB: { selector: "span.a-price-whole", currency: "GBP" },
JP: { selector: "span.a-price-whole", currency: "JPY" },
};
monitorMultiMarket(
{
US: "https://www.amazon.com/dp/B0CHX3QBCH",
DE: "https://www.amazon.de/dp/B0CHX3QBCH",
GB: "https://www.amazon.co.uk/dp/B0CHX3QBCH",
JP: "https://www.amazon.co.jp/dp/B0CHX3QBCH",
},
markets
).then((results) => {
const comparison = comparePrices(results, { USD: 1.0, EUR: 0.92, GBP: 0.79, JPY: 149.5 });
console.log(JSON.stringify(comparison, null, 2));
});
Multi-Market Monitoring Strategies
Currency Normalization
To compare prices meaningfully, normalize all prices to a base currency. Use a reliable exchange rate API (Open Exchange Rates, ECB rates) and update rates daily. Store both local and normalized prices for accurate historical analysis.
Tax Handling
Prices in different markets include different tax levels:
| Market | Typical Tax Treatment | Consideration |
|---|---|---|
| United States | Prices shown pre-tax | Actual cost varies by state (0-10.25%) |
| European Union | Prices include VAT (19-27%) | Subtract VAT for like-for-like comparison |
| United Kingdom | Prices include 20% VAT | Subtract VAT for net comparison |
| Japan | Prices include 10% consumption tax | Subtract tax for net comparison |
Monitoring Schedule
Not all markets need the same check frequency. Prioritize based on business impact:
- Primary markets: Your main sales regions — check every 1-2 hours.
- Expansion targets: Markets you are entering — check every 4-6 hours.
- Reference markets: Markets for benchmarking only — check daily.
Detecting Geo-Pricing Strategies
With multi-market data, you can identify competitor pricing strategies:
- Uniform global pricing: Same price (after currency conversion) everywhere. Common for digital products.
- PPP-adjusted pricing: Lower prices in lower-income markets. Common for SaaS and software.
- Competition-driven pricing: Prices vary by market based on local competitive pressure.
- Cost-plus pricing: Different prices reflecting different shipping, warehousing, and tax costs.
Key takeaway: Multi-market monitoring reveals pricing strategies invisible to single-market analysis. A competitor offering 30% lower prices in one market signals either aggressive expansion or a different cost structure worth investigating.
Handling Common Challenges
Regional Redirects
Some sites redirect users to regional versions based on IP. With geo-targeted proxies, you want this redirect — it takes you to the correct regional pricing. Do not follow cross-region redirects, as they indicate the IP location does not match the target marketplace.
Content Differences
Product availability varies by region. A product sold on amazon.com might not exist on amazon.de. Handle 404 responses and unavailable products gracefully in your monitoring pipeline.
Scraping Etiquette
When monitoring multiple regions, you are effectively scraping multiple separate websites. Apply best practices for avoiding blocks independently per marketplace. What works for amazon.com may need different tuning for amazon.co.jp.
Data Storage for Multi-Market Data
CREATE TABLE geo_price_history (
id SERIAL PRIMARY KEY,
product_id VARCHAR(100) NOT NULL,
country_code VARCHAR(2) NOT NULL,
local_price DECIMAL(12, 2),
currency VARCHAR(3),
usd_equivalent DECIMAL(12, 2),
exchange_rate DECIMAL(10, 6),
in_stock BOOLEAN,
scraped_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX idx_geo_price_product_country
ON geo_price_history (product_id, country_code, scraped_at DESC);
-- Query: Price spread across markets for a product
SELECT
country_code,
AVG(usd_equivalent) AS avg_usd_price,
MIN(usd_equivalent) AS min_usd_price,
MAX(usd_equivalent) AS max_usd_price
FROM geo_price_history
WHERE product_id = 'B0CHX3QBCH'
AND scraped_at >= now() - INTERVAL '7 days'
GROUP BY country_code
ORDER BY avg_usd_price;
Key Takeaways
- E-commerce prices vary significantly by geography — monitoring one market gives an incomplete competitive picture.
- Geo-targeted residential proxies are essential: use ProxyHat's country-level targeting to access authentic regional pricing.
- Normalize prices to a base currency for meaningful cross-market comparisons.
- Account for tax differences (VAT-inclusive vs pre-tax) when comparing prices.
- Match Accept-Language headers to the target country for accurate results.
- Prioritize monitoring frequency by market importance to optimize proxy usage.
Ready to monitor prices across markets? Start with ProxyHat's residential proxies with 195+ country options and read our e-commerce scraping guide for the full strategy. For real-time capabilities, see our guide on real-time price monitoring infrastructure.






