cdpfleet Docs GitHub Dashboard

Cases / #50 · 2026-10-15 · Easy

A price watcher in one program: two snapshots minutes apart, diffed, billed only while reading

Take a snapshot of a catalogue price and a live market price, wait two minutes, take another and diff them field by field. Each snapshot is its own short session, so a 2-minute watch costs seconds of browser time — and an idle session would have ended anyway.

Chromium

Run on production on 2026-10-15: ✓ Node.js ✓ Python ✓ Java ✓ C# ✓ Go

The problem

Price and stock monitoring means reading the same pages again and again and reporting what changed. The tempting version keeps one browser open and re-reads the page every few minutes — which bills the whole time, and on cdpfleet ends after the 60-second idle timeout anyway. How do you structure a watcher so it pays only for the reading, and how do you tell a real change from noise?

What we used, and why

WhatWhy
One session per snapshotLaunch, read every target, browser.close(). Between snapshots nothing runs and nothing is billed.
page.evaluate(read) per targetEach target has its own small read function: the price and stock text of a product page, the amount from a JSON price API.
A field-by-field diffCompare the two snapshots per field and add the percentage change for numeric prices.
WAIT_SECONDS = 120Stands in for your scheduler's interval (cron, a queue) so the case can be verified in one run.

How it works

  1. Snapshot 1: one short session reads a product page on books.toscrape.com and the Bitcoin spot price from Coinbase's public API.
  2. Wait two minutes with no session open.
  3. Snapshot 2 the same way, then diff the two and report what changed.

The code

The same program in five languages (also on GitHub, with the raw output). Set these environment variables first:

// npm install [email protected]
// env: CDPFLEET_API_KEY, PROXY_URL
import { chromium } from 'playwright';

const KEY = process.env.CDPFLEET_API_KEY;
const WAIT_SECONDS = 120; // between the two snapshots — in a real watcher, your scheduler's interval

// What to watch: a catalogue page (its price only changes when the shop changes it) and a live
// market price that moves by the second.
const TARGETS = [
  { name: 'Book price', url: 'https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html',
    read: () => ({ price: document.querySelector('.product_main .price_color').textContent.trim(), stock: document.querySelector('.product_main .availability').textContent.trim().replace(/\s+/g, ' ') }) },
  { name: 'Bitcoin spot (USD)', url: 'https://api.coinbase.com/v2/prices/BTC-USD/spot',
    read: () => ({ price: JSON.parse(document.body.innerText).data.amount }) },
];

// 503 means the fleet is momentarily busy: wait Retry-After and try again.
async function launch(body) {
  for (let attempt = 1; ; attempt++) {
    const res = await fetch('https://starter.cdpfleet.com/chromium/session', {
      method: 'POST',
      headers: { 'x-api-key': KEY, 'content-type': 'application/json' },
      body: JSON.stringify(body),
    });
    if (res.ok) return res.json();
    if (res.status !== 503 || attempt === 5) throw new Error(`launch ${res.status} ${await res.text()}`);
    await new Promise((r) => setTimeout(r, (Number(res.headers.get('retry-after')) || 5) * 1000));
  }
}

// One snapshot = one short session: launch, read every target, close. Nothing is billed while
// waiting, and an idle session would end after 60 s anyway.
async function snapshot() {
  const t = Date.now();
  const { wsUrl } = await launch({ proxy: process.env.PROXY_URL, headless: true, record: false });
  const browser = await chromium.connect(wsUrl, { headers: { 'x-api-key': KEY } });
  try {
    const page = await browser.newPage();
    const values = {};
    for (const target of TARGETS) {
      for (let attempt = 1; ; attempt++) { // proxies drop a tunnel now and then
        try {
          await page.goto(target.url, { timeout: 45000 });
          values[target.name] = await page.evaluate(target.read);
          break;
        } catch (err) {
          if (attempt === 3) throw err;
        }
      }
    }
    return { at: new Date().toISOString(), seconds: Math.round((Date.now() - t) / 100) / 10, values };
  } finally {
    await browser.close();
  }
}

// Field-by-field diff of two snapshots, with the percentage for numeric prices.
function diff(a, b) {
  return TARGETS.map(({ name }) => {
    const changes = Object.keys(a.values[name]).filter((k) => a.values[name][k] !== b.values[name][k]).map((k) => {
      const [x, y] = [a.values[name][k], b.values[name][k]];
      const [nx, ny] = [x, y].map((v) => Number(String(v).replace(/[^0-9.]/g, '')));
      return { field: k, before: x, after: y, ...(nx && ny ? { percent: Math.round(((ny - nx) / nx) * 1e4) / 100 } : {}) };
    });
    return { target: name, changed: changes.length > 0, changes, now: b.values[name] };
  });
}

const first = await snapshot();
await new Promise((r) => setTimeout(r, WAIT_SECONDS * 1000));
const second = await snapshot();
console.log(JSON.stringify({
  wait_seconds: WAIT_SECONDS,
  billed_seconds: Math.round((first.seconds + second.seconds) * 10) / 10,
  first_at: first.at, second_at: second.at,
  diff: diff(first, second),
}, null, 2));

What we got

TargetChanged?BeforeAfter
Book pricenoprice: £51.77; stock: In stock (22 available)same
Bitcoin spot (USD)yes (+0.05%)price: 81676.54price: 81715.885

From the Node.js run on 2026-10-15. IP addresses are replaced with placeholders (203.0.113.x); equal addresses stay equal. The other languages produced the same findings.

Raw output (Node.js)
{
  "wait_seconds": 120,
  "billed_seconds": 12.9,
  "first_at": "2026-10-15T01:39:52.935Z",
  "second_at": "2026-10-15T01:41:58.330Z",
  "diff": [
    {
      "target": "Book price",
      "changed": false,
      "changes": [],
      "now": {
        "price": "£51.77",
        "stock": "In stock (22 available)"
      }
    },
    {
      "target": "Bitcoin spot (USD)",
      "changed": true,
      "changes": [
        {
          "field": "price",
          "before": "81676.54",
          "after": "81715.885",
          "percent": 0.05
        }
      ],
      "now": {
        "price": "81715.885"
      }
    }
  ]
}

Takeaways