AX/T/01 — AX/TUTORIALS
Opublikowano: 2 maj 2026 · 18 min czytania

Jak zbudować bota do monitorowania cen konkurencji

Production-grade scraper cen z Playwright, rotacją proxy, alertami Slack i historią zmian. Krok po kroku, z pełnym kodem.

IntermediatePlaywrightNode.js / TypeScriptPostgreSQLSlack webhooksResidential proxy

Monitoring cen konkurencji to chleb powszedni e-commerce. W tym tutorialu zbudujemy production-ready bota który: pobiera ceny 200-500 SKU z 3-5 sklepów konkurencji, wykrywa zmiany cen, zapisuje historię, wysyła alerty na Slack gdy cena spadnie poniżej threshold.

Powiemy NIE rozwiązaniom które padają po 2 dniach: pojedyncza pętla fetch(), jeden IP, brak retry, brak history. Powiemy TAK: idempotency, proxy rotation, schema validation, dead-letter queue.

Co potrzebujesz
  • Node.js 20+ lub Python 3.11+
  • Podstawowa znajomość JS/TS lub Python
  • Dostęp do residential proxy (Smartproxy, Bright Data — od $7/GB)
  • Slack workspace (opcjonalnie, dla alertów)
  • PostgreSQL lub SQLite do historii cen
Kroki
  1. 01

    Setup projektu i zależności

    Inicjalizujemy projekt TypeScript z Playwright i kilkoma helperami:

    mkdir price-monitor && cd price-monitor
    npm init -y
    npm i playwright pg zod pino
    npm i -D typescript @types/node tsx
    npx playwright install chromium

    Czemu te zależności:

    • playwrightbrowser automation z auto-wait
    • pg — PostgreSQL client (lepiej niż SQLite jeśli planujesz scale)
    • zod — runtime schema validation (wykryje gdy parser się rozjedzie)
    • pino — strukturalne logging (JSON-line dla observability)
  2. 02

    Definicja schemy i target config

    Każdy target (sklep) ma własną strukturę — definiujemy to deklaratywnie:

    // config/targets.ts
    export const targets = [
      {
        id: 'allegro',
        baseUrl: 'https://allegro.pl/oferta/',
        selectors: {
          price: '[data-role="app-container"] [aria-label*="cena"]',
          title: 'h1',
          availability: '[data-role="availability"]',
        },
        waitFor: 'h1',
        proxyTier: 'residential',
      },
      // ...
    ];

    Schema dla wyniku (Zod):

    import { z } from 'zod';
    export const PriceData = z.object({
      sku: z.string(),
      targetId: z.string(),
      price: z.number().positive(),
      currency: z.enum(['PLN', 'EUR', 'USD']),
      available: z.boolean(),
      scrapedAt: z.date(),
    });

    Schema validation jest krytyczna — gdy sklep zmieni layout i selector zwróci śmieci, validation zawiedzie zamiast wepchnąć złe dane do bazy.

  3. 03

    Browser context i proxy rotation

    Playwright używa browser contexts — to jak osobne incognito sessions z izolowanymi cookies/storage. Dla każdego SKU tworzymy nowy context z innym proxy:

    // scraper/browser.ts
    import { chromium } from 'playwright';
    
    const proxies = [
      'http://user:pass@proxy1.smartproxy.com:7000',
      'http://user:pass@proxy2.smartproxy.com:7000',
      // ...
    ];
    
    export async function scrapeWithRotation(url, selectors) {
      const proxy = proxies[Math.floor(Math.random() * proxies.length)];
      const browser = await chromium.launch({
        headless: true,
        proxy: { server: proxy },
      });
      const context = await browser.newContext({
        userAgent: getRandomUserAgent(),
        viewport: { width: 1920, height: 1080 },
        locale: 'pl-PL',
      });
      // ... scrape logic
      await browser.close();
    }

    Dla protected sites (Allegro, Amazon) dodaj playwright-extra + puppeteer-extra-plugin-stealth żeby ukryć headless flagi.

  4. 04

    Retry logic i dead-letter queue

    Każdy scrape może paść: timeout, captcha, network error, parser drift. Implementacja exponential backoff:

    async function scrapeWithRetry(target, sku, maxAttempts = 5) {
      let lastError;
      for (let attempt = 1; attempt <= maxAttempts; attempt++) {
        try {
          const data = await scrape(target, sku);
          return PriceData.parse(data); // validate
        } catch (err) {
          lastError = err;
          const delay = Math.min(2 ** attempt * 1000, 30000);
          logger.warn({ sku, attempt, err: err.message });
          await sleep(delay);
        }
      }
      // After 5 fails → dead-letter queue
      await db.query(
        'INSERT INTO scrape_dlq (sku, target, error, failed_at) VALUES ($1, $2, $3, NOW())',
        [sku, target.id, lastError.message]
      );
      throw lastError;
    }

    DLQ z alertem na Slack gdy lista urośnie powyżej threshold — wiesz że coś się popsuło zanim klient zapyta "gdzie moje raporty".

  5. 05

    Storage i wykrywanie zmian cen

    Każdy scrape to nowy wiersz w tabeli historii. Wykrywanie zmiany cena = comparison vs previous:

    CREATE TABLE price_history (
      id BIGSERIAL PRIMARY KEY,
      sku TEXT NOT NULL,
      target_id TEXT NOT NULL,
      price NUMERIC(10,2) NOT NULL,
      currency TEXT NOT NULL,
      available BOOLEAN NOT NULL,
      scraped_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
    );
    CREATE INDEX idx_price_lookup ON price_history (sku, target_id, scraped_at DESC);

    Query do wykrywania spadku:

    SELECT current.sku, current.price, prev.price as prev_price,
           ((current.price - prev.price) / prev.price * 100) as pct_change
    FROM price_history current
    JOIN LATERAL (
      SELECT price FROM price_history
      WHERE sku = current.sku AND target_id = current.target_id
        AND scraped_at < current.scraped_at
      ORDER BY scraped_at DESC LIMIT 1
    ) prev ON true
    WHERE current.scraped_at > NOW() - INTERVAL '1 hour'
      AND ((current.price - prev.price) / prev.price) < -0.05;

    Każdy wiersz w wyniku = spadek >5%. Alert do Slacka.

  6. 06

    Slack alerts i scheduling

    Webhook na Slack (incoming webhook URL z Slack App):

    async function sendSlackAlert(changes) {
      await fetch(process.env.SLACK_WEBHOOK_URL, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          text: `Price drops detected: ${changes.length} SKUs`,
          blocks: changes.map(c => ({
            type: 'section',
            text: { type: 'mrkdwn',
              text: `• ${c.sku} (${c.target_id}): ${c.prev_price} → ${c.price} (${c.pct_change.toFixed(1)}%)`
            }
          }))
        })
      });
    }

    Scheduling: cron w VPS lub BullMQ + Redis dla dystrybuowanej kolejki. Co 30 min:

    // cron: */30 * * * *
    import { scrapeAll } from './scraper';
    import { detectChanges } from './detect';
    import { sendSlackAlert } from './alerts';
    
    (async () => {
      await scrapeAll(targets, getSkuList());
      const changes = await detectChanges();
      if (changes.length > 0) await sendSlackAlert(changes);
    })();
Ile to kosztuje uruchomić

Run cost dla 500 SKU, scrape co 30 min (24/7):

  • Proxy (residential, ~3MB per page × 500 SKU × 48 runs/dzień × 30 dni = 22GB/mies): ~$150-180/mies u Smartproxy
  • VPS (Hetzner CX22, 2vCPU/4GB): €5/mies
  • Postgres (Supabase Free tier wystarczy do 500MB): $0
  • Slack: $0 (free tier)

Total: ~$160-185/mies operating cost dla 500 SKU. Skalowalne liniowo z proxy bandwidth.

Częste pułapki
  • Brak schema validation — gdy sklep zmieni layout, scraper zacznie zapisywać null jako cenę. Twoja analityka się zepsuje cicho.
  • Pojedynczy IP — po 50-200 requestach Cloudflare/Akamai zablokują. Proxy rotation jest wymagana nie opcjonalna.
  • Brak retry logic — transient errors (timeout, network) wywalą całą partię zamiast pojedynczego SKU.
  • Hard-coded selectors w kodzie — gdy się zmienią, musisz redeploy. Lepiej w config file lub bazie.
  • Brak monitoringu success rate — nie wiesz że 30% scrapów zwraca śmieci. Loguj success/fail per target.
Build yourself czy zlecić?

Powyższy stack obsłuży 500-2000 SKU z 3-5 sklepów bez problemu. Dla 10k+ SKU dochodzi: queue system (BullMQ), distributed workers, observability (Grafana + Prometheus), bardziej zaawansowany anti-bot (mobile proxy dla najtrudniejszych targets).

Jeśli twój use-case wymaga scale, compliance (GDPR/RODO), 99.9% uptime SLA, lub po prostu nie masz developera który to ogarnie — napisz do nas. Robimy to dla 14+ klientów w produkcji.

Najczęściej zadawane pytania
Czy monitoring cen konkurencji jest legalny?
Tak, dla publicznie dostępnych danych cenowych — precedens hiQ vs LinkedIn z 2022 potwierdza prawo do scrapingu public data. Jednocześnie naruszasz ToS sklepów (Allegro, Amazon, etc.), więc ryzyko = blokada IP/konta, nie pozew. Compliance-wise: nie scrapuj danych za login wall, nie obciążaj serwera, respektuj robots.txt dla SEO-related crawling.
Ile SKU mogę monitorować jednym botem?
500-2000 SKU z 3-5 sklepów na jednym VPS (Hetzner CX22, 2GB RAM) bez problemu. Powyżej 5000 SKU dochodzi distributed queue (BullMQ + Redis), workers na kilku maszynach, dedicated proxy per worker. Limit nie jest w CPU/RAM tylko w proxy bandwidth.
Co się stanie gdy Allegro/Amazon zmieni layout strony?
Twoje selektory padną — scrape zwróci null/undefined. Dlatego krok 2 (schema validation z Zod) jest krytyczny: catch fails fast zamiast pushować śmieci do bazy. Pod retainer fixujemy w 24-48h. Bez retaineru — sam musisz monitorować success rate i naprawiać.
Czy residential proxy są naprawdę potrzebne?
Dla Allegro/Amazon/eBay — tak. Datacenter proxy są blokowane przez Cloudflare/Akamai/DataDome w 50-200 requestach. Smartproxy/Bright Data residential ~$7-12/GB, miesięcznie $50-200 dla typowego setupu. Dla mniejszych sklepów bez agresywnego anti-bot — datacenter wystarczy.