David Lewin David Lewin
E-Commerce, Research & Ads

E-commerce and scraper monitoring: UptimeRobot vs custom scripts for checkout pages, price feeds and cron jobs

Disclosure: some links in this guide are affiliate links. If you sign up through them, I may earn a commission at no extra cost to you. How this site makes money.

Short answer

Use an external service such as UptimeRobot for anything a shopper would notice: storefront uptime, a keyword monitor that alerts when the Add to cart button disappears, an API monitor that checks the product JSON has a price above zero, and status monitors for payment providers. Use your own scripts for what only you can judge: whether a scraper's output is complete and plausible, and whether the checkout flow works end to end. Connect the two with heartbeat monitors: each script pings UptimeRobot only when it succeeds, so a crashed script, a dead server or a run that produced bad data all raise the same alert. Heartbeat, API and SSL monitors need a paid UptimeRobot plan; Solo starts at $9 a month billed yearly.

A store can be "up" and still not take money. The homepage returns 200 while a theme update has removed the Add to cart button, a price feed returns zeros, or the payment provider is down. Price scrapers fail even more quietly: the cron job runs, exits cleanly and writes 400 rows of nothing because the target changed its layout. This guide splits monitoring into what an external service does best and what only your own code can check, then joins them so every failure ends in the same alert.

How this guide was made

UptimeRobot's plans, limits and API fields come from its pricing page, its machine-readable pricing file and its v3 OpenAPI specification, checked in October 2026. The three scripts were tested against a local mock of the UptimeRobot v3 API, heartbeat and Slack-style webhook endpoints, and a mock store, with Playwright driving Microsoft Edge. They weren't run against a live UptimeRobot account. Detection times and costs are our calculations from the published check intervals and prices.

1. What to monitor, and who should do it

Our rule of thumb: if a shopper would notice it, check it from outside; if only you can tell whether it's right, check it in code, then report to the outside. A monitor running on the same server as your store or scraper dies with it, so the alerting has to live somewhere else.

Two layers of monitoring, one place for alerts
Two layers of monitoring, one place for alertsUptimeRobot, outside your servers → Storefront / Add to cart / Product JSON / Payment provider → Your scripts, on your servers → checkout_probe.py / scrape_guard.py → AlertsUptimeRobot, outside your servers: checks what shoppers see, every 60 seconds on paid plansUptimeRobot, outside your serverschecks what shoppers see, every 60 seconds on paid plansStorefront: HTTP status and response timeStorefrontHTTP statusand responsetimeAdd to cart: keyword monitor on a product pageAdd to cartkeywordmonitor on aproduct pageProduct JSON: API monitor: price above zeroProductJSONAPI monitor:price abovezeroPayment provider: status page dependency monitorPaymentproviderstatus pagedependencymonitorYour scripts, on your servers: check what only you can judge, then ping a heartbeatYour scripts, on your serverscheck what only you can judge, then ping a heartbeatcheckout_probe.py: product to cart to checkout, every 15 minutescheckout_probe.pyproduct to cart to checkout,every 15 minutesscrape_guard.py: row counts, missing prices, spikes after each scraper runscrape_guard.pyrow counts, missing prices,spikes after each scraper runAlerts: downtime pages someone; price changes go to a Slack channel onlyAlertsdowntime pages someone; price changes go to a Slack channelonly
Heartbeats join the two layers: a script that stops reporting is treated as down. Hover or tab through the boxes for details.
FailureWhat catches itWhy
Store down, DNS or SSL brokenUptimeRobot HTTP monitor, SSL and domain expiryNeeds to run from outside, from more than one location
Page returns 200 but is brokenUptimeRobot keyword monitorAn error page or a theme without the buy button still returns 200
Prices show as 0 or variants are missingUptimeRobot API monitor with JSON assertionsReads the product JSON your storefront already serves
Payment provider outageUptimeRobot dependency monitorYou can't fix it, but you need to know before support tickets arrive
Checkout flow broken by an app or scriptCustom Playwright probe + heartbeatNeeds a real browser and clicks; an HTTP check can't add to cart
Scraper crashed, cron didn't run, server deadHeartbeat monitorSilence is the signal: no ping, no "OK"
Scraper ran but the data is wrongCustom output guard + heartbeatOnly your code knows what plausible data looks like
Competitor changed a priceYour scraper, reported by the guardA business event, not an outage: don't page anyone for it

2. The UptimeRobot side, as code

Clicking monitors together in a dashboard works for five monitors. Past that, keep them in a file in your repository and sync it with the API, so a new product line or a new store is a pull request. UptimeRobot's v3 API uses a bearer token, and the Platform API is available on every plan, including Free. These are the monitors we'd start with for a Shopify store and one daily price scraper:

[
  {"friendlyName": "Storefront", "type": "HTTP", "url": "https://shop.example.com/",
   "interval": 60, "timeout": 30},
  {"friendlyName": "Product page: Add to cart button", "type": "KEYWORD",
   "url": "https://shop.example.com/products/blue-mug", "interval": 300, "timeout": 30,
   "keywordType": "ALERT_NOT_EXISTS", "keywordCaseType": "CaseInsensitive", "keywordValue": "Add to cart"},
  {"friendlyName": "Product JSON: price above zero", "type": "API",
   "url": "https://shop.example.com/products/blue-mug.js", "interval": 300, "timeout": 30,
   "config": {"apiAssertions": {"logic": "AND", "checks": [
     {"property": "$.price", "comparison": "greater_than", "target": 0},
     {"property": "$.variants", "comparison": "is_not_empty"}]}}},
  {"friendlyName": "Stripe status", "type": "DEPENDENCY", "url": "https://status.stripe.com",
   "interval": 300, "timeout": 30, "config": {"dependency": {"providerUrlname": "stripe"}}},
  {"friendlyName": "Price scraper: daily run", "type": "HEARTBEAT",
   "interval": 86400, "gracePeriod": 3600},
  {"friendlyName": "Checkout probe: every 15 min", "type": "HEARTBEAT",
   "interval": 900, "gracePeriod": 600}
]
  • Keyword monitor with ALERT_NOT_EXISTS. It alerts when "Add to cart" disappears from the HTML. That catches a broken theme, a product that went unavailable and an error page served with status 200. The keyword has to be in the server-rendered HTML; if your theme renders the button with JavaScript, use the API monitor or the checkout probe instead.
  • API monitor on /products/<handle>.js. Shopify serves every product as JSON at that address, with the price in cents. The assertions fail if the price drops to zero or the variants list is empty, which is what a botched CSV import looks like.
  • Dependency monitor. Watches a third-party status page, here Stripe's. This is a paid-plan feature, with 1 to 5 services on Solo.
  • Two heartbeat monitors. The interval is how often a ping is expected and the grace period is how late it may be. A daily scraper with a one-hour grace period alerts about 25 hours after the last good run.

The sync script creates missing monitors, updates changed ones and reports monitors that aren't in the file without deleting them:

#!/usr/bin/env python3
"""Keep UptimeRobot monitors in a JSON file and sync them through the v3 API.
Python 3.9+, standard library only.

  export UPTIMEROBOT_API_KEY=...
  python3 monitors.py monitors.json           # show what would change
  python3 monitors.py monitors.json --apply   # create and update monitors
"""
import json
import os
import sys
import urllib.error
import urllib.request

API = os.environ.get('UPTIMEROBOT_API', 'https://api.uptimerobot.com/v3')
KEY = os.environ['UPTIMEROBOT_API_KEY']


def call(method, url, body=None):
    data = json.dumps(body).encode() if body is not None else None
    headers = {'Authorization': 'Bearer ' + KEY, 'Content-Type': 'application/json'}
    req = urllib.request.Request(url if url.startswith('http') else API + url, data, headers, method=method)
    with urllib.request.urlopen(req, timeout=30) as r:
        return json.load(r)


def existing():
    """All monitors in the account, by friendly name. The API pages through them with nextLink."""
    out, url = {}, '/monitors?limit=200'
    while url:
        page = call('GET', url)
        for m in page['data']:
            out[m['friendlyName']] = m
        url = page.get('nextLink')
    return out


def same(have, want):
    """True if `want` matches `have`, ignoring extra keys the API adds to nested objects."""
    if isinstance(want, dict):
        return isinstance(have, dict) and all(same(have.get(k), v) for k, v in want.items())
    return have == want


def main():
    wanted = json.load(open(sys.argv[1]))
    apply = '--apply' in sys.argv
    try:
        current = existing()
    except urllib.error.HTTPError as e:
        sys.exit('Listing monitors failed: HTTP %d %s' % (e.code, e.read()[:300].decode('utf-8', 'replace')))
    errors = 0
    for spec in wanted:
        name = spec['friendlyName']
        cur = current.get(name)
        try:
            if cur is None:
                print('create  %s (%s)' % (name, spec['type']))
                if apply:
                    m = call('POST', '/monitors', spec)
                    if m.get('heartbeatUrl'):
                        print('        heartbeat URL: ' + m['heartbeatUrl'])
                continue
            if str(cur.get('type')).upper() != spec['type'].upper():
                print('SKIP    %s: type changed to %s; delete it and run again' % (name, spec['type']))
                continue
            diff = {k: v for k, v in spec.items() if k != 'type' and not same(cur.get(k), v)}
            if diff:
                print('update  %s: %s' % (name, ', '.join(sorted(diff))))
                if apply:
                    call('PATCH', '/monitors/%s' % cur['id'], diff)
            else:
                print('ok      %s (%s)' % (name, cur.get('status')))
        except urllib.error.HTTPError as e:
            errors += 1
            print('ERROR   %s: HTTP %d %s' % (name, e.code, e.read()[:300].decode('utf-8', 'replace')))
    # Monitors that exist in the account but not in the file are reported, never deleted
    for name in sorted(set(current) - {s['friendlyName'] for s in wanted}):
        print('extra   %s (not in %s)' % (name, sys.argv[1]))
    sys.exit(1 if errors else 0)


if __name__ == '__main__':
    main()

A dry run against our mock API, with one existing monitor whose interval differs from the file:

$ python3 monitors.py monitors.json
update  Storefront: interval
create  Product page: Add to cart button (KEYWORD)
create  Product JSON: price above zero (API)
create  Stripe status (DEPENDENCY)
create  Price scraper: daily run (HEARTBEAT)
create  Checkout probe: every 15 min (HEARTBEAT)
extra   Old staging (not in monitors.json)

With --apply it prints each heartbeat monitor's URL, which goes into the scripts below. A second run reports every monitor as ok. The comparison ignores extra fields the API adds inside nested objects, so it doesn't report changes that aren't there.

3. The custom side: guard the scraper's output

Most scraper failures aren't crashes. The target changes a class name and the parser returns None for every price, or a geo redirect serves yen instead of dollars and every price jumps by 15,000%. The guard below runs after each scraper run and compares today's output with the previous run.

Dead man's switch: the heartbeat only fires on a healthy run
Dead man's switch: the heartbeat only fires on a healthy runScraper finishes → scrape_guard.py → Healthy / Broken → Crash, dead server or cron never ranScraper finishes: writes prices-today.jsonlScraper finisheswrites prices-today.jsonlscrape_guard.py: row count, missing prices, mass price spikesscrape_guard.pyrow count, missing prices, mass price spikesHealthy: ping the heartbeat, post price changes to SlackHealthyping the heartbeat, postprice changes to SlackBroken: no ping, post FAILED to Slack, exit 1Brokenno ping, post FAILED toSlack, exit 1Crash, dead server or cron never ran: no ping either: UptimeRobot alerts after the interval plus the grace periodCrash, dead server or cron never ranno ping either: UptimeRobot alerts after the interval plus thegrace period
Every failure ends the same way: a missing heartbeat. You don't need a separate alert for each way a job can die.
#!/usr/bin/env python3
"""Check a price scraper's output before you trust it, report price changes,
and ping an UptimeRobot heartbeat only when the run looks healthy.
Python 3.9+, standard library only.

  export HEARTBEAT_URL=https://heartbeat.uptimerobot.com/...   # from the heartbeat monitor
  export ALERT_WEBHOOK=https://hooks.slack.com/services/...    # optional, Slack-style webhook
  python3 scrape_guard.py prices-today.jsonl prices-yesterday.jsonl

Each line of the files is a JSON object with at least "sku" and "price".
"""
import json
import os
import sys
import time
import urllib.request

MIN_ROWS = 0.8      # today needs at least 80% of yesterday's rows
MAX_MISSING = 0.05  # at most 5% of rows without a usable price
CHANGE = 0.05       # report price moves of 5% or more
SPIKE = 0.5         # moves of 50% or more are more often parser bugs than real prices
MAX_SPIKES = 0.1    # if over 10% of rows spike at once, treat the run as broken


def load(path):
    if not os.path.exists(path):
        return {}
    rows = (json.loads(line) for line in open(path, encoding='utf-8') if line.strip())
    return {str(r['sku']): r for r in rows}


def valid(price):
    return isinstance(price, (int, float)) and not isinstance(price, bool) and price > 0


def send(url, payload=None, tries=3):
    data = json.dumps(payload).encode() if payload is not None else None
    req = urllib.request.Request(url, data, {'Content-Type': 'application/json'})
    for i in range(tries):
        try:
            urllib.request.urlopen(req, timeout=20).close()
            return True
        except OSError as e:
            print('%s: %s' % (url.split('?')[0], e), file=sys.stderr)
            time.sleep(2 ** i)
    return False


def main():
    today, prev = load(sys.argv[1]), load(sys.argv[2])
    problems, changes, spikes = [], [], []
    if not today:
        problems.append('no rows in %s' % sys.argv[1])
    elif prev and len(today) < MIN_ROWS * len(prev):
        problems.append('%d rows today, %d yesterday' % (len(today), len(prev)))
    missing = [s for s, r in today.items() if not valid(r.get('price'))]
    if today and len(missing) > MAX_MISSING * len(today):
        problems.append('%d of %d rows have no usable price, e.g. %s'
                        % (len(missing), len(today), missing[:3]))
    for sku, row in today.items():
        old, new = prev.get(sku, {}).get('price'), row.get('price')
        if not (valid(old) and valid(new)):
            continue
        move = (new - old) / old
        if abs(move) >= SPIKE:
            spikes.append('%s %.2f -> %.2f (%+.0f%%, check it)' % (sku, old, new, move * 100))
        elif abs(move) >= CHANGE:
            changes.append('%s %.2f -> %.2f (%+.0f%%)' % (sku, old, new, move * 100))
    if today and len(spikes) > MAX_SPIKES * len(today):
        problems.append('%d prices moved 50%% or more at once: currency, page layout or parser change?'
                        % len(spikes))

    lines = ['Scraper run FAILED: ' + p for p in problems]
    if changes or spikes:
        lines.append('%d price changes:' % (len(changes) + len(spikes)))
        lines += changes[:30] + spikes[:30]
    report = '\n'.join(lines)
    if report:
        print(report)
        if os.environ.get('ALERT_WEBHOOK'):
            send(os.environ['ALERT_WEBHOOK'], {'text': report})
    if problems:
        # No ping: UptimeRobot alerts when the heartbeat is overdue, even if this
        # script, the scraper or the whole server never runs again.
        sys.exit(1)
    if os.environ.get('HEARTBEAT_URL') and not send(os.environ['HEARTBEAT_URL']):
        sys.exit(2)
    print('healthy: %d rows, %d price changes' % (len(today), len(changes) + len(spikes)))


if __name__ == '__main__':
    main()

Run it right after the scraper in the same cron entry, then rotate the files:

15 6 * * * cd /srv/prices && python3 scrape.py > prices-today.jsonl \
  && python3 scrape_guard.py prices-today.jsonl prices-yesterday.jsonl \
  && mv prices-today.jsonl prices-yesterday.jsonl

The && chain matters: a failed guard leaves yesterday's file in place, so tomorrow's run compares against the last good data instead of the broken run. Results from our test, against a 100-SKU baseline:

Test inputResultHeartbeat
3 normal price changes, one 10× jump, one missing priceHealthy; 4 changes posted, the 10× jump flagged "check it"Pinged
30 of 100 prices missing (parser broke)FAILED: 30 of 100 rows have no usable priceNot pinged
Every price × 155 (USD page served in JPY)FAILED: 100 prices moved 50% or more at onceNot pinged
50 rows instead of 100 (pagination broke)FAILED: 50 rows today, 100 yesterdayNot pinged
First run, no previous fileHealthy, 0 changesPinged
Heartbeat endpoint unreachableThree retries, then exit code 2Failed, logged

Tune the thresholds to your catalog. A store running a 60%-off clearance sale will trip the spike rule. That's deliberate: a human should glance at a jump that large before it reaches a repricing rule. If you reprice automatically from scraped data, the guard is the cheapest insurance you'll buy. For the scraper itself, see monitoring Shopify competitor prices and geo-targeted price checks, which explains the currency mix-ups this guard catches.

4. The custom side: a synthetic checkout probe

UptimeRobot checks one URL at a time. It can't add a product to the cart and reach checkout, which is the path that breaks when an app injects a script or a theme update changes a form. A short Playwright script can, and it reports through the same heartbeat mechanism:

#!/usr/bin/env python3
"""Synthetic checkout check: product page -> add to cart -> cart -> checkout page loads.
It never fills in payment details or places an order. Pings a heartbeat on success.

  pip install playwright && playwright install chromium
  export HEARTBEAT_URL=https://heartbeat.uptimerobot.com/...
  python3 checkout_probe.py https://shop.example/products/blue-mug

The default selectors match Shopify's Dawn theme. Override them for other themes.
"""
import os
import sys
import urllib.request

from playwright.sync_api import sync_playwright

ADD = os.environ.get('ADD_SELECTOR', 'button[name="add"]')
CHECKOUT = os.environ.get('CHECKOUT_SELECTOR', 'button[name="checkout"]')
CART = os.environ.get('CART_PATH', '/cart')
READY = os.environ.get('CHECKOUT_READY', 'input[type="email"], input[name="email"]')


def main():
    product = sys.argv[1]
    step = 'open product page'
    with sync_playwright() as p:
        browser = p.chromium.launch(channel=os.environ.get('PW_CHANNEL'))
        page = browser.new_page()
        page.set_default_timeout(30000)
        try:
            page.goto(product, wait_until='domcontentloaded')
            step = 'add to cart'
            page.click(ADD)
            page.wait_for_load_state('networkidle')
            step = 'open cart'
            page.goto(product.split('/products/')[0] + CART, wait_until='domcontentloaded')
            step = 'go to checkout'
            page.click(CHECKOUT)
            step = 'checkout form'
            page.wait_for_selector(READY)
        except Exception as e:
            page.screenshot(path='checkout-fail.png', full_page=True)
            print('FAILED at "%s": %s' % (step, str(e).splitlines()[0]), file=sys.stderr)
            sys.exit(1)  # no heartbeat, so UptimeRobot alerts after the grace period
        finally:
            browser.close()
    if os.environ.get('HEARTBEAT_URL'):
        urllib.request.urlopen(os.environ['HEARTBEAT_URL'], timeout=20).close()
    print('checkout reachable')


if __name__ == '__main__':
    main()

In our test against a mock store, the healthy path reached the checkout form and pinged the heartbeat. A product page without the button failed at "add to cart" after the 30-second timeout, saved a full-page screenshot and sent no ping.

  • It stops at the checkout form. It never enters payment details or places an order. Test payments belong in your payment provider's test mode, not in production monitoring.
  • It adds a cart every 15 minutes. That's about 2,900 abandoned carts a month in your analytics. Filter the probe out by its IP or user agent, and exclude it from abandoned-cart emails; see our abandoned cart workflow.
  • Run it somewhere other than the store's own infrastructure, such as a small VPS that also runs the scraper. Chromium needs roughly 300–500 MB for one page, by our estimate.
  • Selectors are theme-specific. The defaults match Shopify's Dawn theme. Set ADD_SELECTOR, CHECKOUT_SELECTOR and CHECKOUT_READY for other themes and platforms.

5. What UptimeRobot costs for this setup

PlanPriceMonitorsIntervalWhat matters here
Free$0505 minHTTP, keyword, ping and port only. No heartbeat, API, SSL or dependency monitors. UptimeRobot describes it as for hobby and non-profit projects.
Solo$10/month, or $9 billed yearly10 (a 50-monitor tier costs more)60 sAdds heartbeat, API, SSL and domain expiry, DNS, maintenance windows, Slack and Telegram alerts, 1–5 dependency monitors
Team$41/month, or $35 billed yearly10030 sWebhook, Zapier and PagerDuty integrations, 3 login seats, incident comments
Scale$77/month, or $65 billed yearly200 (a 500-monitor tier costs more)15 sEverything in Team, 5 seats, unlimited status pages

Prices from UptimeRobot's pricing page, October 2026. The setup in section 2 uses six monitors, so Solo is the right plan: the free plan has no heartbeat monitors, and without them neither script can raise an alert when it stops running. Webhook alerts need Team, but Solo's Slack integration covers the alerts in this guide.

Worst-case time to detect an outage, by check interval
Worst-case time to detect an outage, by check intervalFree, every 5 minutes: 5 min; Solo, every 60 seconds: 1 min; Team, every 30 seconds: 0.5 min; Scale, every 15 seconds: 0.25 minUptimeRobot planFree, every 5 minutes: 5 minFree, every 5 minutes5 minSolo, every 60 seconds: 1 minSolo, every 60 seconds1 minTeam, every 30 seconds: 0.5 minTeam, every 30 seconds0.5 minScale, every 15 seconds: 0.25 minScale, every 15 seconds0.25 min
Our calculation from the check intervals. On average an outage is detected half an interval after it starts. Add the alert threshold you set: alerting only after two failed checks avoids false alarms but doubles the delay.

Detection time is where the free plan costs money. Example, with your own numbers in place of ours: a store doing $20,000 a month takes about $27 an hour on average. A five-minute interval detects an outage on average 2.5 minutes in, a 60-second interval after 30 seconds. That gap is small per incident; it's the alerting on broken checkouts and silent scrapers, not the faster interval, that pays for Solo.

6. Our opinion: build vs buy

  • Buy the external checks. Multi-location probing, SMS and voice alerts, status pages and SSL expiry are commodity features. Rebuilding them costs more in your time than a year of Solo.
  • Build the data checks. No monitoring service knows that your competitor's prices can't all rise 15,000% overnight. A 100-line guard does.
  • Self-hosted monitoring is a valid choice, but only on a separate server, in a different data center, from what it monitors. Uptime Kuma on a $4–6 VPS works well if someone owns its updates. Then something has to monitor that server too, which is where many teams give up and buy.
  • Don't monitor competitors with keyword monitors. It works for a handful of SKUs: ALERT_NOT_EXISTS on "$24.00" alerts when the price changes. But it hits their site every minute, breaks on A/B tests and currency switching, and mixes business events into your outage alerts. A daily scraper with the guard is politer and more accurate.
  • Route alerts by urgency. Store down or checkout broken: SMS or a phone call. Scraper failed: Slack, someone looks during working hours. Price changes: a Slack channel, never a page.

7. Tools

Top picks

ToolRoleWhy it fits
UptimeRobotExternal uptime, keyword, API, dependency and heartbeat monitoringCovers the shopper-facing checks and gives custom scripts a dead man's switch, with a v3 API on every plan for monitors as code. Free for 50 basic monitors at 5-minute intervals; Solo at $9 a month billed yearly adds the heartbeat and API monitors this guide relies on.

Alternatives: free and open-source tools

We don't earn anything from these links.

  • Uptime Kuma: self-hosted monitoring with HTTP, keyword and push (heartbeat) monitors. Free, but you run and watch the server.
  • Healthchecks: open-source cron job monitoring built around heartbeat pings, also offered as a hosted service.
  • Playwright for Python: the browser automation library used by the checkout probe.

8. Pitfalls

  • Pinging the heartbeat at the start of a job. Ping at the end, and only on success, or a job that hangs halfway reports healthy.
  • A grace period shorter than the job's run time. A scraper that takes 40 minutes on a busy day needs a grace period longer than that, or you'll get false alarms.
  • Monitoring behind the CDN cache only. A cached homepage can stay up while the origin is down. Monitor a product page or the JSON endpoint as well, which are cached less often.
  • Bot protection blocking the monitor. A firewall or bot-protection rule can block UptimeRobot's checks. Allow-list its published IP ranges (api.uptimerobot.com/meta/ips) rather than loosening the rule.
  • Forgetting maintenance windows. Planned theme deployments will trigger the keyword monitor. Paid plans support maintenance windows; use them, or the team learns to ignore alerts.
  • Treating the API key like a read-only token. The main API key can delete monitors. Keep it in a secret store, not in the repository next to monitors.json.
Bottom line

Let UptimeRobot watch what shoppers see: storefront, Add to cart, product JSON and the payment provider. Write small scripts for what only you can judge, the checkout flow and the scraper's data, and have each one ping a heartbeat only on success. That turns crashes, dead servers and silently wrong data into one kind of alert. For a store with a price scraper, that's about six monitors on UptimeRobot's Solo plan at $9 a month billed yearly; the free plan lacks the heartbeat monitors the scripts depend on.

David Lewin
David Lewin

Founder of StealthScale.io and e-commerce infrastructure engineer writing field-tested playbooks on proxies, cloud VPS, anti-detect browsers and SEO.