Commerce

Price monitoring proxies for comparable offers

A price drop is only useful if it belongs to the same product, seller, and offer. Price monitoring proxies let you add regional observations to your authorized data sources. Keep shipping, taxes, stock, and observation time beside the number so your alerts compare like with like.

Updated 16 min readPricing analysts and commerce data teams

Worked example

The cheaper listing costs more

Fictional offers for the same product, in one currency and tax basis. Both are in stock.

The cheaper listing costs more
OfferItem + shippingComparable total
Seller A80 + 1292
Seller B85 + 085
Seller C79 + unknownIncomplete
Illustrative exampleRead the interpretation
Where to start
Country-targeted residential. Use a regional residential sample when the approved storefront varies by network country. An API or feed with an explicit market parameter may already answer the question.
How to handle the session
Use one short sticky session to keep a product and shipping estimate in the same observation.

Set up your first price comparison

  1. Match the product and offer

    Choose a product with a stable identifier and a known variant. Record the seller, condition, fulfillment method, and market. A bundle or marketplace reseller should not silently join the same price series.

  2. Capture the whole purchase condition

    Collect item price, currency, taxes, delivery charges, stock, and any membership or promotion requirement. Use the same approved source and observation window for the first comparison.

  3. Make the alert explain itself

    Store both the previous and new complete record with source links. Suppress price-drop alerts on missing fields, unavailable products, or a seller change. Retain those events as separate review items.

The cheaper listing costs more

Fictional offers for the same product, in one currency and tax basis. Both are in stock.

Seller B has the lower complete total. Seller C belongs in an incomplete queue until its shipping conditions are known; it should not trigger a lowest-price alert.

What to measure: comparable-record rate

Count records with matching product identity and complete offer conditions, divided by all observations. Track freshness separately so an old complete offer does not look current.

Start With a Comparable Offer, Not a Proxy

Define the pricing decision and its authorized source before buying traffic. An official merchant API, seller export, product feed, licensed dataset, or partner report is the first choice when it supplies the identifiers and offer fields the decision needs. A proxy becomes relevant only when a permitted public-page test must vary network origin or region while the other shopper conditions remain controlled.

Google's current Merchant API product guide identifies a merchant's products by content language, feed label, and offer ID, and its example keeps availability, price, currency, condition, and identifiers as separate fields. Google also warns that accepting a product input does not mean the product is approved to appear. That is useful evidence for the shape of a normalized record, not a competitor-price feed and not permission to collect another merchant's pages.

Decision gate before a proxy pilot
QuestionUse firstProxy decision
Your own Merchant Center or seller catalogAuthorized API, export, or feedNo proxy unless a separate approved regional page check fills a named gap
Licensed marketplace or partner dataContracted interface and its quotaUse its market fields before adding a network-origin sample
Approved public offer comparisonWritten source register and bounded URL setTest a proxy only if requested network geography is an experimental variable
Login wall, CAPTCHA, 401, 403, or objectionSource owner, official API, export, or permission reviewStop; a new exit does not create access rights

Normalize Product, Seller, Price Conditions, and Market

A number is not comparable until the sellable item and offer context match. Google's current product data specification treats price and ISO 4217 currency, sale price, availability, condition, product identifiers, and market-facing data as distinct attributes. It also distinguishes a public sale price from membership pricing and requires submitted price and availability to agree with the merchant's landing and checkout surfaces. Use those distinctions as a normalization discipline even when an approved source uses different field names.

Normalized offer record
GroupRequired comparison contextReject or quarantine when
Product identitySource key, stable product ID, offer ID, exact variant, pack size, quantity, and conditionThe title looks similar but the sellable item cannot be matched
Seller and fulfillmentSeller identity, merchant or marketplace fulfillment, stock state, and delivery promiseThe visible number belongs to another seller or fulfillment path
PriceRegular amount, sale amount and dates, membership or coupon condition, shipping, tax treatment, mandatory fees, total, and currencyA component is missing, zero is substituted for unknown, or conditions differ
Market controlsRequested and observed network country, feed label or marketplace, language, delivery assumption, account and membership state, device class, and consent stateA network location is mistaken for a delivery, tax, account, or device input
EvidenceAuthorized source method, retrieval time, HTTP status, parser version, validation state, and redacted evidence referenceA challenge or error page is parsed as an offer

Preserve null as unknown. Store unusable, incomplete, stale, refused, and rate-limited observations alongside usable records so downstream coverage and cost calculations include what the workflow actually encountered.

Choose the Least Complex Databay Route That Fits

Choose by required network origin and location control, then validate the smallest suitable paid order against one authorized source. Databay products use shared pools, bill transferred traffic, and support HTTP proxying, HTTPS CONNECT, and SOCKS5. A new connection may select an available exit, but a fresh or different IP is not guaranteed. Databay does not offer a free trial.

Price-monitoring product and entry-path decision, verified August 29, 2026
Starting optionUse whenControls and session boundarySmallest published entry at review
Direct or official sourceThe approved API, feed, or export already supplies the required market and offer fieldsNo proxy variableNo proxy purchase
DatacenterA permitted endpoint accepts a hosting-network origin and no consumer-ISP signal is requiredCountry or continent; requested sticky session up to 120 minutes, with possible early lossReview the current 10 GB entry package; $12.50 total and 31-day validity at review
Premium ResidentialAn authorized shopper-facing comparison explicitly requires a consumer-ISP origin or finer geographic selectorOne country, state, city, ZIP, coordinate, or ASN selector at a time; requested sticky session up to 120 minutes, with possible early lossReview the current 5 GB PAYG entry; $13.75 starting total and 186-day validity at review
Residential FlexA permitted country- or continent-level ISP-origin sample fits and granular controls are unnecessaryCountry or continent; requested sticky session up to 30 minutes, with possible early lossReview the current 25 GB minimum; $13.75 total and 31-day validity at review

Prices, quantities, totals, and validity can change. The linked pricing pages load the current catalogue and are the purchase authority; if a current rate card is unavailable, wait or request a current quote rather than relying on this dated review. Start with Datacenter only when its origin fits, Premium Residential when its finer controls are genuinely required, and Flex when its larger minimum and coarser controls still match the test.

Separate Geography From Delivery and Session State

Write the hypothesis as a controlled comparison: hold product, seller, variant, fulfillment, delivery assumption, language, account, membership, consent, device, parser, and time window fixed; change only the requested network region. Verify the observed exit before interpreting a difference. A country-targeted connection is evidence from one routed request, not proof of a shopper's residence, tax nexus, delivery eligibility, GPS location, or every price in that market.

Use a rotating connection for independent observations only when the sampling plan calls for it. Use one sticky session for a short, permitted multi-request flow that requires route continuity. Keep the same session across an approved 429 follow-up; changing exits in response to a quota or refusal would corrupt the experiment and can turn rate handling into evasion. Connection reuse can retain an exit, and a requested sticky duration is a maximum request rather than a continuity guarantee.

  1. Confirm current country availability for the chosen product rather than inferring it from a location name.
  2. Create the full product username with the proxy configuration generator.
  3. Verify one benign route with the generated client setup before contacting the approved source.
  4. Keep source scheduling and rate budgets global across workers, sessions, and exits.
  5. Record requested and observed region separately; quarantine an unfulfilled route.

Run the Error-Retaining Example Against Loopback First

The example below requires Node.js 22 or newer and uses only built-in modules and the runtime's fetch implementation. Save it as price-monitor.mjs, then run node price-monitor.mjs --self-test. The self-test binds to 127.0.0.1, contacts no public source and no Databay gateway, returns a 429 followed by a valid fixture, then exercises a 403 and malformed JSON. It prints all three records so errors cannot disappear from the result set.

import { createServer } from 'node:http';
import { setTimeout as delay } from 'node:timers/promises';
import { pathToFileURL } from 'node:url';

const MAX_RETRY_AFTER_MS = 60_000;
const PARSER_VERSION = 'normalized-offer-json-v1';

export function retryAfterMs(value, nowMs = Date.now()) {
  if (!value) return null;
  const input = value.trim();
  if (/^\d+$/.test(input)) {
    const seconds = Number(input);
    return Number.isSafeInteger(seconds) && seconds <= 60 ? seconds * 1_000 : null;
  }
  const retryAt = Date.parse(input);
  if (!Number.isFinite(retryAt)) return null;
  const waitMs = Math.max(0, retryAt - nowMs);
  return waitMs <= MAX_RETRY_AFTER_MS ? waitMs : null;
}

function failure(state, attempts, kind, detail = {}) {
  return { state, offer: null, error: { kind, ...detail }, attempts };
}

async function discardBody(response) {
  if (response.body) await response.body.cancel();
}

function normalizedOffer(payload, context, retrievedAtUtc) {
  return {
    identity: {
      sourceKey: context.sourceKey,
      offerId: payload.offerId ?? null,
      productId: payload.productId ?? null,
      variant: payload.variant ?? null
    },
    seller: {
      id: payload.sellerId ?? null,
      fulfillment: payload.fulfillment ?? null
    },
    availability: payload.availability ?? null,
    price: {
      regularAmountMicros: payload.price?.amountMicros ?? null,
      saleAmountMicros: payload.salePrice?.amountMicros ?? null,
      currency: payload.price?.currencyCode ?? null,
      shippingAmountMicros: payload.shipping?.amountMicros ?? null,
      taxTreatment: payload.taxTreatment ?? 'unknown',
      membership: payload.membership ?? 'public'
    },
    market: { ...context.market },
    evidence: { retrievedAtUtc, responseStatus: 200, parserVersion: PARSER_VERSION }
  };
}

function missingFields(offer) {
  const required = [
    ['identity', 'sourceKey'], ['identity', 'offerId'], ['identity', 'productId'],
    ['seller', 'id'], ['seller', 'fulfillment'], ['price', 'regularAmountMicros'],
    ['price', 'currency'], ['market', 'requestedCountry']
  ];
  return required
    .filter(([group, field]) => offer[group][field] === null || offer[group][field] === '')
    .map(([group, field]) => group + '.' + field);
}

export async function observeOffer({
  sourceUrl,
  context,
  fetchImpl = fetch,
  sleep = delay,
  now = () => Date.now(),
  allowOne429Retry = false
}) {
  const attempts = [];
  for (let number = 1; number <= 2; number += 1) {
    const atUtc = new Date(now()).toISOString();
    let response;
    try {
      response = await fetchImpl(sourceUrl, {
        headers: { accept: 'application/json' },
        redirect: 'manual',
        signal: AbortSignal.timeout(10_000)
      });
    } catch (error) {
      attempts.push({ number, atUtc, status: null, retryAfter: null });
      const kind = error instanceof Error ? error.name : 'UnknownTransportError';
      return failure('transport_error', attempts, kind);
    }

    const retryAfter = response.headers.get('retry-after');
    attempts.push({ number, atUtc, status: response.status, retryAfter });

    if (response.status === 429) {
      const waitMs = retryAfterMs(retryAfter, now());
      await discardBody(response);
      if (allowOne429Retry && number === 1 && waitMs !== null) {
        await sleep(waitMs);
        continue;
      }
      return failure('rate_limited', attempts, 'http_429', { retryAfter });
    }

    if (response.status === 401 || response.status === 403) {
      await discardBody(response);
      return failure('refused', attempts, 'destination_refusal', { status: response.status });
    }

    if (response.status === 407) {
      await discardBody(response);
      return failure('proxy_auth_error', attempts, 'proxy_authentication', { status: 407 });
    }

    if (!response.ok) {
      await discardBody(response);
      return failure('http_error', attempts, 'http_status', { status: response.status });
    }

    let payload;
    try {
      payload = await response.json();
    } catch {
      return failure('parse_error', attempts, 'invalid_json', { status: response.status });
    }

    const offer = normalizedOffer(payload, context, atUtc);
    const missing = missingFields(offer);
    if (missing.length) {
      return {
        state: 'incomplete',
        offer,
        error: { kind: 'missing_fields', fields: missing },
        attempts
      };
    }
    return { state: 'usable', offer, error: null, attempts };
  }
  return failure('rate_limited', attempts, 'retry_budget_exhausted');
}

async function selfTest() {
  let rateHits = 0;
  const server = createServer((request, response) => {
    if (request.url === '/rate-limit' && rateHits++ === 0) {
      response.writeHead(429, { 'Retry-After': '0' });
      response.end('rate limited');
      return;
    }
    if (request.url === '/refused') {
      response.writeHead(403);
      response.end('refused');
      return;
    }
    if (request.url === '/malformed') {
      response.writeHead(200, { 'Content-Type': 'application/json' });
      response.end('{');
      return;
    }
    response.writeHead(200, { 'Content-Type': 'application/json' });
    response.end(JSON.stringify({
      offerId: 'sku12345', productId: 'gtin-0001', variant: 'blue-m',
      sellerId: 'authorized-merchant', fulfillment: 'merchant', availability: 'IN_STOCK',
      price: { amountMicros: '15990000', currencyCode: 'USD' },
      salePrice: null, shipping: null, taxTreatment: 'recorded-separately', membership: 'public'
    }));
  });
  await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
  const address = server.address();
  const base = 'http://127.0.0.1:' + address.port;
  const context = {
    sourceKey: 'local-fixture',
    market: {
      requestedCountry: 'US', contentLanguage: 'en', feedLabel: 'US',
      deliveryAssumption: 'none', accountState: 'signed-out'
    }
  };
  try {
    return [
      await observeOffer({ sourceUrl: base + '/rate-limit', context, allowOne429Retry: true }),
      await observeOffer({ sourceUrl: base + '/refused', context }),
      await observeOffer({ sourceUrl: base + '/malformed', context })
    ];
  } finally {
    await new Promise((resolve, reject) =>
      server.close((error) => error ? reject(error) : resolve())
    );
  }
}

async function main() {
  if (process.argv.includes('--self-test')) return selfTest();
  const sourceUrl = process.env.PRICE_SOURCE_URL;
  if (!sourceUrl) throw new Error('Set PRICE_SOURCE_URL to an approved JSON endpoint.');
  return observeOffer({
    sourceUrl,
    context: {
      sourceKey: process.env.PRICE_SOURCE_KEY ?? 'approved-source',
      market: {
        requestedCountry: process.env.PRICE_MARKET ?? null,
        contentLanguage: process.env.PRICE_LANGUAGE ?? null,
        feedLabel: process.env.PRICE_FEED_LABEL ?? null,
        deliveryAssumption: process.env.PRICE_DELIVERY_ASSUMPTION ?? null,
        accountState: process.env.PRICE_ACCOUNT_STATE ?? 'documented-separately'
      }
    },
    allowOne429Retry: process.env.ALLOW_ONE_429_RETRY === 'true'
  });
}

if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
  console.log(JSON.stringify(await main(), null, 2));
}

The fixture's zero-second Retry-After keeps the self-test fast; production values can be either delay-seconds or an HTTP date. The example accepts only a valid delay no longer than 60 seconds and performs at most one follow-up when ALLOW_ONE_429_RETRY=true. Without that explicit opt-in, or when the header is absent, invalid, or longer, it retains a rate-limited record and stops. Choose the ceiling and retry permission with the source owner; 60 seconds is an example guardrail, not a claim about any marketplace.

Honor 429 and Retry-After Without Changing Identity

RFC 6585 section 4 defines 429 as too many requests in a given time and says the response may include Retry-After. It deliberately does not define whether counting is per resource, server, credential, cookie, or another scope. Therefore one source-wide scheduler (not each proxy, worker, or session) must own the permitted request budget.

RFC 9110 section 10.2.3 permits Retry-After as either an HTTP date or non-negative delay-seconds. Parse both forms, cap automated waiting, retain the original response, and use at most the preapproved retry budget. Do not rotate an exit, alter headers, swap accounts, or increase concurrency after 429. A 401, 403, CAPTCHA, login wall, or explicit objection is a stop-and-review outcome rather than a retry candidate.

Error outcome retained by the example
Observed resultStored stateNext action
429 with approved, bounded Retry-AfterOriginal attempt plus at most one same-policy follow-upWait as directed; do not change identity
429 without a usable Retry-AfterRate-limited record with status and headerStop automatic work and review schedule or quota
401 or 403Refused recordUse the official permission or support path
407Proxy-authentication errorCorrect the issued Databay configuration; do not diagnose it as a destination block
Other non-2xx or transport failureHTTP or transport error with attempt historyInvestigate the failed layer before any bounded replay
2xx with malformed or incomplete offerParse error or incomplete normalized recordQuarantine; never substitute zero or reuse an old price silently

Connect the Tested Pattern to One Approved Route

The loopback run validates the request and classification logic; it does not test a marketplace, extraction parser, Databay credential, route availability, or destination acceptance. Before a real pilot, map the approved source into the normalized JSON contract, add a source-wide scheduler and cache, then configure one client through the Databay proxy configuration generator. Keep credentials in a secret manager and never print the proxy URL or authorization header.

Use the Python Requests setup or cURL setup and diagnostic model to prove gateway authentication against a benign endpoint you control. Then inject that reviewed proxy-aware client into observeOffer as fetchImpl; do not assume the direct self-test is routed. Verify requested versus observed exit once, run the authorized offer endpoint, and preserve the same response classification. A successful diagnostic proves only that one connection worked; it does not guarantee that the source will return, expose, or permit the desired offer.

For a public HTML page, an authorized parser must be versioned and tested separately against retained, permitted fixtures. The example intentionally consumes a normalized JSON shape so it cannot be mistaken for a universal extractor. Layout changes, personalization, stock, promotions, consent state, and experiments can still make two otherwise valid observations incomparable.

Pilot on Cost per Usable Comparable Record

Run the smallest representative pilot that can reject the design: one authorized source, important product and offer shapes, only the markets needed for the decision, a frozen parser version, and one network configuration at a time. Count planned observations, all attempts, bytes, status classes, incomplete offers, location fulfillment, review time, and retained errors. Report latency percentiles with failures separately; do not calculate performance only across successful responses.

Price-monitoring pilot scorecard
MetricDecision formulaRequired disclosure
Usable comparable rateValidated complete offers / eligible planned observationsProduct mix, markets, exclusions, and source
Route fulfillmentCorrectly observed required region / routed attemptsRequested selector, observed method, and unknowns
Rate-limit and refusal rate429, 401, 403, and challenge outcomes / attemptsRetry policy and whether any further request occurred
Freshness complianceUsable offers inside maximum age / usable offersMaximum age and retrieval window
Cost per usable recordSource, proxy, compute, storage, and review cost / usable comparable offersTraffic validity, billed bytes, retries, and reviewer time

Expand only if the chosen network supplies decision value that the approved direct source does not, the workflow meets its predeclared quality and cost gates, and stop behavior is verified. No product class guarantees access, extraction, completeness, a unique exit, or a particular success rate.

Keep Price Monitoring Distinct From Ecommerce and Generic Scraping

This page owns the commercial decision for price monitoring proxies: whether a network-origin variable is needed, which Databay product and smallest paid path fit it, and how to test that choice without dropping errors. The ecommerce proxy guide covers broader catalog QA, localization, storefront testing, seller research, and repricing safeguards. The web-scraping guide covers source approval, request budgets, provenance, and collection design across data types.

All three workflows share a boundary: public display is not blanket permission. RFC 9309 states that robots rules are not access authorization. Review contracts, API terms, robots instructions where applicable, rate guidance, privacy, intellectual-property, competition, retention, and reuse requirements, and keep every request within Databay's Acceptable Use Policy. Stop when authorization, source behavior, or controls change.

When the result looks wrong

A sudden price drop looks too good

The parser may have captured an installment, member price, or different variant.

Next step: Inspect the offer terms and stable product ID before changing a price or sending an alert.

A regional price moves between checks

Delivery address, account state, or currency may have changed.

Next step: Pin those inputs in the observation record and compare within the same market configuration.

Errors disappear from the report

A collector may discard non-200 responses before logging.

Next step: Keep every attempt and its failure state; calculate quality against the complete run.

Sources and further reading

The worked example is an illustrative exercise. Documentation and existing test evidence support the technical guidance; their scope and dates remain attached to the relevant sections.

Choose the network your task needs

Four products, different location controls and session windows. Compare the current package for the route you actually need.

All four products use shared, rotating pools and traffic-based billing. Requested sessions can end early; location selection depends on live route availability. Verify protocols, minimum order, and traffic validity on the product page.

Price Monitoring questions, answered

Do I always need a proxy for price monitoring?
No. Use an authorized API, merchant feed, seller export, licensed dataset, or partner report when it supplies the needed offer and market fields. Add a proxy only for a permitted test in which network origin or region is a required variable.
Which Databay product should a price-monitoring pilot start with?
Start with Datacenter when a hosting-network origin and country-or-continent control fit the approved endpoint. Use Premium Residential only when an ISP-origin or finer geographic selector is required, and Residential Flex when country-or-continent ISP origin fits its larger minimum. Validate the smallest suitable current paid order; no class guarantees acceptance.
What is the smallest paid path for price monitoring?
At the August 29, 2026 review, Datacenter began at 10 GB for $12.50, Premium Residential PAYG at 5 GB for $13.75, and Residential Flex at 25 GB for $13.75. Check the linked live pricing page for the current quantity, total, tax treatment, and validity before purchase. Databay does not offer a free trial.
Should a collector retry HTTP 429?
Only within the source's approved global request budget. Preserve the 429, honor a valid Retry-After date or delay, cap automated waiting, keep the same policy and identity, and allow at most the predeclared retry count. Without an approved retry or usable header, retain the rate-limited result and stop.
Why retain failed and incomplete price observations?
Dropping them inflates apparent coverage and hides parser, access, geography, and cost failures. Keep status class, attempt history, validation state, and redacted evidence so usable-record rate and cost per usable record include the complete planned workload.
Does a residential exit reproduce the price every local shopper sees?
No. It changes a network-origin signal. Delivery address, tax, account, membership, cookies, consent, device, inventory, promotions, seller, fulfillment, and experiments can also affect an offer. Record those controls and describe unknowns.
Does the example guarantee extraction from a marketplace?
No. The self-test uses a local normalized JSON fixture and proves only the request, Retry-After, and error-classification logic. A real source needs explicit authorization, a source-specific parser or API mapping, current route validation, and its own measured pilot.

Put the guide to work.

Build a connection for price monitoring, then check one route before scaling.

Pay as you go. Pricing, order minimums, and traffic validity vary by network.