# How do I keep Crawlbase's site tiers from surprising my bill?

Read the tier Crawlbase reports on every billed response (the X-Domain-Complexity header) and send the domains it prices above standard, plus the requests it couldn't complete, to UpRock. Crawlbase sets each site's tier itself, from 1 to 20 times the base rate, and JavaScript doubles it, so at 100,000 pages a month 1,000 pages cost $0.76 on a standard site and up to $30.56 on its top tier with JavaScript. UpRock is $0.50 per 1,000 pages on any site.

Updated 25 Oct 2026. Web page: https://uprock.ai/compare/crawlbase

## On this page

- [What Crawlbase does well](#what-crawlbase-does-well)
- [What the tiers do to a page's price](#what-the-tiers-do-to-a-pages-price)
- [How Crawlbase reports a page it couldn't get](#how-crawlbase-reports-a-page-it-couldnt-get)
- [Crawlbase and UpRock side by side](#crawlbase-and-uprock-side-by-side)
- [The fallback, in TypeScript and Python](#the-fallback-in-typescript-and-python)
- [FAQ](#faq)
- [Other use cases to explore](#explore)
- [Related articles](#related)
- [References](#references)
- [Get started](#get-started)

## What Crawlbase does well

Crawlbase, around since 2017, sells one credit balance across its Crawling API, an asynchronous Enterprise Crawler, a residential proxy endpoint (Smart AI Proxy) and Cloud Storage. The Crawling API renders JavaScript, returns HTML, JSON or GitHub-flavored markdown (with an optional readability pass), takes screenshots and PDFs, and runs more than 100 built-in scrapers that return JSON at the price of a page. There are SDKs for Python, Node, Ruby, PHP, Go, Java and C#, integrations with LangChain, Zapier, n8n and Make, and an MCP server you can run yourself or use hosted. Pay as you go has no monthly fee, bills only successful requests and supports a hard spend cap.

If most of your pages are on ordinary sites and your volume is high, Crawlbase is hard to beat on price. Keep it.

## What the tiers do to a page's price

Pay as you go runs on a graduated ladder: each month, the first 1,000 requests cost $3.00 per 1,000, the next 10,000 $2.00, the next 100,000 $0.60, the next million $0.50, the next 10 million $0.10, falling to $0.02. On top of that, Crawlbase puts every site in one of eight tiers: standard ×1, moderate ×1.5, complex ×2, complex I ×3, complex II ×4.5, extreme ×9, extreme I ×13.5 and extreme II ×20. JavaScript rendering doubles any tier.

Here's the blended cost of 1,000 pages at four monthly volumes, worked out the way Crawlbase's own estimator does it (the month's ladder total, times the tier, times 2 for JavaScript), with prices checked 25 Oct 2026:

| Pages a month | Standard | Standard, JavaScript | Complex, JavaScript (×4) | Extreme, JavaScript (×18) | Extreme II, JavaScript (×40) | UpRock |
|---|---|---|---|---|---|---|
| 10,000 | $2.10 | $4.20 | $8.40 | $37.80 | $84.00 | $0.50 |
| 100,000 | $0.76 | $1.53 | $3.06 | $13.75 | $30.56 | $0.50 |
| 1 million | $0.53 | $1.06 | $2.11 | $9.50 | $21.10 | $0.50 |
| 10 million | $0.15 | $0.29 | $0.59 | $2.65 | $5.89 | $0.50 |

Subscriptions swap the ladder for a monthly block of credits: Starter is $99 for 200,000 (about $0.50 per 1,000), Growth $599 for 2 million (about $0.30). The tiers come out of that block too. A standard page with JavaScript uses 2 credits and an extreme II page with JavaScript 40, so Starter covers 100,000 of the first or 5,000 of the second.

Three things the table can't show. You learn a site's tier from the `X-Domain-Complexity` header of a billed response, unless the site is one of the 356 in Crawlbase's Cookbook, which lists most at one credit and a few at 1.5 to 20. The docs name only three values for that header (standard, moderate and complex) while the pricing page lists eight tiers, so treat anything other than `standard` as a price you didn't plan for. And the pricing page says a tier multiplies "a group's whole ladder total" without spelling out how a month that mixes tiers fills the brackets, so the table assumes every page sits in one tier.

## How Crawlbase reports a page it couldn't get

Crawlbase answers with three signals: the HTTP status of your request, a `cb_status` header with Crawlbase's verdict (formerly `pc_status`, which is still sent and is the only one on a 401), and `original_status` with the site's own answer. A crawl that failed comes back as HTTP 520 and isn't charged. Here's how we'd route each case, from Crawlbase's status-code docs (checked 25 Oct 2026):

| What you see | What it means | Send to UpRock? |
|---|---|---|
| 200, `cb_status` 200, `original_status` 200 | The page | No: use it |
| 200, `cb_status` 200, `original_status` 404 or 410 | The page is gone (billed) | No: it's the site's answer |
| 200, `cb_status` 207 | The site sent an empty body (billed) | Yes (Crawlbase suggests its JavaScript token) |
| 520, `cb_status` 705 or 706 | The site answered 403 or 429 | Yes |
| 520, `cb_status` 503 | Crawlbase recognised a block or challenge page | Yes |
| 520, `cb_status` 704 | The site answered another error, in `original_status` | Only if it's a 5xx |
| 520, `cb_status` 599 or another 5xx to 7xx | An internal or site-specific failure | Yes |
| 520, `cb_status` 595 | A `css_click_selector` wasn't on the page | No: fix the selector |
| 429, 500, 503, 504 | Concurrency limit (20 per token by default), an internal error, unavailable, or timed out | Yes |
| 402, 403 | Your workspace spend cap or account limit | Yes, or raise the cap |
| 401 | A missing or invalid token | Fix the token (the code sends it to UpRock meanwhile) |
| 400 | A bad request | No: fix it |

## Crawlbase and UpRock side by side

| | Crawlbase | UpRock |
|---|---|---|
| Price per 1,000 pages | [$3.00 down to $0.02](https://crawlbase.com/pricing) on the pay-as-you-go ladder, times the site tier and JavaScript (table above); subscriptions $99 to $599 a month | $0.50 by card or USDC, $0.40 with UPT |
| Site tiers | [Eight, from ×1 to ×20](https://crawlbase.com/pricing), set by Crawlbase; JavaScript ×2 | None: every page is one credit |
| Failed requests | [Not billed](https://crawlbase.com/docs/crawling-api); 404 and 410 are | Not documented yet |
| Countries | [22 listed](https://crawlbase.com/docs/crawling-api), and it may override the one you pass | 63, or a region |
| Where requests come from | Residential or datacenter exit nodes, chosen automatically; the docs we read don't say how the residential IPs are sourced | Devices whose owners opted in |
| Scrapers, screenshots, storage, async crawls | Yes | No. One page per call |
| Concurrency | [20 requests per token](https://crawlbase.com/docs/rate-limits) by default | Not published yet |
| MCP server | [Local or hosted](https://crawlbase.com/docs/ai-mcp) | Yes, remote (streamable HTTP) |
| Video | Scrapers for TikTok profiles, products and shop pages | Search public video by moment, $1 per 1,000 searches |
| To try it | 1,000 free credits, up to 5,000 with setup steps, no card | Prepaid credits |

Where Crawlbase is better: very low prices for standard sites at high volume, scrapers and storage on the same balance, failed requests that cost nothing, no monthly minimum, SDKs in seven languages and a free allowance. Where UpRock fits: the domains Crawlbase prices above standard, the requests it couldn't complete, countries outside its list, and a price you know before you send the request.

## The fallback, in TypeScript and Python

Every version ask Crawlbase for markdown in the country you pass, read the three signals and the tier, and keep a set of domains Crawlbase billed above `standard`: from then on, those go straight to UpRock. Failures worth a second network go to UpRock too; a 404, a 410 or a bad request raises an error. They use Crawlbase's normal token, which costs one credit times the tier; add `javascript: "true"` to render pages, which doubles it. The TypeScript version uses Crawlbase's Node SDK, which keeps the headers on any status. The Python version calls the HTTP API with requests, because the Python SDK drops the headers on a non-2xx response. UpRock gets one `POST /v1/crawl` request with your key in an `Authorization: Bearer` header; the MCP and CLI tabs do the same through UpRock's MCP server (TypeScript) or CLI (Python).

### TypeScript (HTTP)

```ts
// npm i crawlbase   (Node 18+)   env: CRAWLBASE_TOKEN, UPROCK_API_KEY
import { CrawlingAPI } from "crawlbase";

// Crawlbase asks for a client timeout of at least 90 s.
const crawlbase = new CrawlingAPI({
  token: process.env.CRAWLBASE_TOKEN ?? "",
  timeout: 90_000,
});

// Crawlbase reports each domain's price tier on billed responses (X-Domain-Complexity).
// A domain billed at any other tier goes straight to UpRock from then on.
const ACCEPTED_TIERS = new Set(["standard"]);
const uprockDomains = new Set<string>(); // save it somewhere if you want it to persist

// HTTP statuses worth a second network: a bad token (401), your spend cap or account
// limit (402, 403), the concurrency limit (429), errors and timeouts (500, 503, 504).
const TRY_UPROCK_HTTP = new Set([401, 402, 403, 429, 500, 503, 504]);

// cb_status (crawlbase.com/docs/status-codes, checked 25 Oct 2026): 704 is the site's own
// error, worth a second try only if it's a 5xx; 595 is a click selector to fix; 503 (a
// block page), 705 (the site said 403), 706 (429) and other 5xx to 7xx codes are worth it.
function worthUpRock(http: number, cb: number, site: number) {
  if (cb === 704) return site >= 500;
  if (cb === 595) return false;
  return cb >= 500 || TRY_UPROCK_HTTP.has(http);
}

async function viaUpRock(url: string, country: string) {
  // One call: UpRock loads the page on a real device in that country, tries a fresh device
  // every 7 s (up to 3 tries) and returns markdown. Failed calls aren't billed.
  const res = await fetch("https://edge.uprock.com/v1/crawl", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.UPROCK_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ url, country }),
    signal: AbortSignal.timeout(90_000),
  });
  const page = await res.json();
  if (!res.ok) throw new Error(`UpRock ${res.status} ${page.code}: ${page.error}`);
  return page.markdown ?? "";
}

async function viaCrawlbase(url: string, country: string) {
  try {
    const res = await crawlbase.get(url, { format: "md", country });
    const header = (name: string) => String(res.headers[name] ?? "");
    return {
      http: Number(res.statusCode),
      cb: Number(header("cb_status") || header("pc_status")), // pc_status: the old name
      site: Number(header("original_status")),
      tier: header("x-domain-complexity") || header("domain_complexity"),
      body: String(res.body),
    };
  } catch (err) {
    console.warn(`Crawlbase unreachable or timed out (${err}); trying UpRock`);
    return null;
  }
}

export async function read(url: string, country = "US") {
  const domain = new URL(url).hostname;
  const r = uprockDomains.has(domain) ? null : await viaCrawlbase(url, country);
  if (r) {
    if (r.tier && !ACCEPTED_TIERS.has(r.tier)) {
      uprockDomains.add(domain); // this page is billed already; the next ones aren't
      console.warn(
        `Crawlbase bills ${domain} as "${r.tier}"; using UpRock from now on`,
      );
    }
    if (r.http === 200 && r.cb === 200) {
      // 404 and 410 are the site's answer (and billed): a second crawler will see the same.
      if (r.site === 404 || r.site === 410)
        throw new Error(`The page is gone (${r.site})`);
      return { via: "crawlbase", markdown: r.body };
    }
    if (r.cb !== 207 && !worthUpRock(r.http, r.cb, r.site))
      throw new Error(
        `Crawlbase ${r.http}, cb_status ${r.cb}: ${r.body.slice(0, 200)}`,
      );
    console.warn(`Crawlbase ${r.http}, cb_status ${r.cb}; trying UpRock`);
  }
  return { via: "uprock", markdown: await viaUpRock(url, country) };
}

// node read.ts https://developer.mozilla.org/en-US/docs/Web/HTTP DE
const [url = "https://developer.mozilla.org/en-US/docs/Web/HTTP", country = "US"] =
  process.argv.slice(2);
const page = await read(url, country);
console.log(page.via, page.markdown.slice(0, 200));
```

### Python (HTTP)

```python
# pip install requests   env: CRAWLBASE_TOKEN, UPROCK_API_KEY
# Calls Crawlbase's HTTP API directly: the Python SDK drops the headers, and with them
# cb_status, on any non-2xx response, which is how a failed crawl arrives.
import os
import sys
from typing import Optional
from urllib.parse import urlparse

import requests

# Crawlbase reports each domain's price tier on billed responses (X-Domain-Complexity).
# A domain billed at any other tier goes straight to UpRock from then on.
ACCEPTED_TIERS = {"standard"}
uprock_domains: set = set()  # save it somewhere if you want it to persist

# HTTP statuses worth a second network: a bad token (401), your spend cap or account
# limit (402, 403), the concurrency limit (429), errors and timeouts (500, 503, 504).
TRY_UPROCK_HTTP = {401, 402, 403, 429, 500, 503, 504}


def worth_uprock(http: int, cb: int, site: int) -> bool:
    # cb_status (crawlbase.com/docs/status-codes, checked 25 Oct 2026): 704 is the
    # site's own error, worth a second try only if it's a 5xx; 595 is a click selector
    # to fix; 503 (a block page), 705 (the site said 403), 706 (429) and other 5xx to
    # 7xx codes are worth it.
    if cb == 704:
        return site >= 500
    if cb == 595:
        return False
    return cb >= 500 or http in TRY_UPROCK_HTTP


def via_uprock(url: str, country: str) -> str:
    # One call: UpRock loads the page on a real device in that country, tries a fresh device
    # every 7 s (up to 3 tries) and returns markdown. Failed calls aren't billed.
    res = requests.post(
        "https://edge.uprock.com/v1/crawl",
        headers={"Authorization": f"Bearer {os.environ['UPROCK_API_KEY']}"},
        json={"url": url, "country": country},
        timeout=90,
    )
    page = res.json()
    if not res.ok:
        raise RuntimeError(f"UpRock {res.status_code} {page.get('code')}: {page.get('error')}")
    return page.get("markdown", "")


def via_crawlbase(url: str, country: str) -> Optional[dict]:
    params = {
        "token": os.environ.get("CRAWLBASE_TOKEN", ""),
        "url": url,
        "format": "md",
        "country": country,
    }
    try:  # Crawlbase asks for a client timeout of at least 90 s
        res = requests.get("https://api.crawlbase.com/", params=params, timeout=90)
    except requests.RequestException as err:
        print(f"Crawlbase unreachable ({err}); trying UpRock", file=sys.stderr)
        return None
    h = res.headers  # case-insensitive
    return {
        "http": res.status_code,
        "cb": int(h.get("cb_status") or h.get("pc_status") or 0),  # pc_status: old name
        "site": int(h.get("original_status") or 0),
        "tier": h.get("x-domain-complexity") or h.get("domain_complexity") or "",
        "body": res.text,
    }


def read(url: str, country: str = "US") -> dict:
    domain = urlparse(url).hostname or ""
    r = None if domain in uprock_domains else via_crawlbase(url, country)
    if r:
        if r["tier"] and r["tier"] not in ACCEPTED_TIERS:
            # This page is billed already; the next ones from this domain aren't.
            uprock_domains.add(domain)
            print(f"Crawlbase bills {domain} as {r['tier']!r}", file=sys.stderr)
        if r["http"] == 200 and r["cb"] == 200:
            if r["site"] in (404, 410):
                # The site's answer (and billed): a second crawler will see the same.
                raise LookupError(f"The page is gone ({r['site']})")
            return {"via": "crawlbase", "markdown": r["body"]}
        if r["cb"] != 207 and not worth_uprock(r["http"], r["cb"], r["site"]):
            raise RuntimeError(f"Crawlbase {r['http']}, cb_status {r['cb']}")
        status = f"{r['http']}, cb_status {r['cb']}"
        print(f"Crawlbase {status}; trying UpRock", file=sys.stderr)
    return {"via": "uprock", "markdown": via_uprock(url, country)}


if __name__ == "__main__":
    # python read.py https://developer.mozilla.org/en-US/docs/Web/HTTP DE
    url = sys.argv[1] if len(sys.argv) > 1 else "https://developer.mozilla.org/en-US/docs/Web/HTTP"
    country = sys.argv[2] if len(sys.argv) > 2 else "US"
    page = read(url, country)
    print(page["via"], page["markdown"][:200])
```

### TypeScript (MCP)

```ts
// npm i crawlbase @modelcontextprotocol/sdk   (Node 18+)   env: CRAWLBASE_TOKEN, UPROCK_API_KEY
import { CrawlingAPI } from "crawlbase";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";

// Crawlbase asks for a client timeout of at least 90 s.
const crawlbase = new CrawlingAPI({
  token: process.env.CRAWLBASE_TOKEN ?? "",
  timeout: 90_000,
});
const uprock = new Client({ name: "crawlbase-fallback", version: "1.0.0" });
await uprock.connect(
  new StreamableHTTPClientTransport(
    new URL("https://mcp.uprock.ai/mcp"),
    { requestInit: { headers: { Authorization: `Bearer ${process.env.UPROCK_API_KEY}` } } },
  ),
);

// Crawlbase reports each domain's price tier on billed responses (X-Domain-Complexity).
// A domain billed at any other tier goes straight to UpRock from then on.
const ACCEPTED_TIERS = new Set(["standard"]);
const uprockDomains = new Set<string>(); // save it somewhere if you want it to persist

// HTTP statuses worth a second network: a bad token (401), your spend cap or account
// limit (402, 403), the concurrency limit (429), errors and timeouts (500, 503, 504).
const TRY_UPROCK_HTTP = new Set([401, 402, 403, 429, 500, 503, 504]);

// cb_status (crawlbase.com/docs/status-codes, checked 25 Oct 2026): 704 is the site's own
// error, worth a second try only if it's a 5xx; 595 is a click selector to fix; 503 (a
// block page), 705 (the site said 403), 706 (429) and other 5xx to 7xx codes are worth it.
function worthUpRock(http: number, cb: number, site: number) {
  if (cb === 704) return site >= 500;
  if (cb === 595) return false;
  return cb >= 500 || TRY_UPROCK_HTTP.has(http);
}

const textOf = (content: unknown) =>
  (content as { type: string; text?: string }[]).find((c) => c.type === "text")?.text ??
  "";

async function viaUpRock(url: string, country: string) {
  const res = await uprock.callTool(
    { name: "crawl_fetch", arguments: { url, country } },
    undefined,
    { timeout: 300_000 },
  );
  if (res.isError) throw new Error(`UpRock: ${textOf(res.content)}`);
  const uri = JSON.parse(textOf(res.content)).content?.markdown?.resource;
  if (!uri) throw new Error(`UpRock: no markdown for ${url}`);
  return textOf(
    (await uprock.callTool({ name: "resource_fetch", arguments: { uri } })).content,
  );
}

async function viaCrawlbase(url: string, country: string) {
  try {
    const res = await crawlbase.get(url, { format: "md", country });
    const header = (name: string) => String(res.headers[name] ?? "");
    return {
      http: Number(res.statusCode),
      cb: Number(header("cb_status") || header("pc_status")), // pc_status: the old name
      site: Number(header("original_status")),
      tier: header("x-domain-complexity") || header("domain_complexity"),
      body: String(res.body),
    };
  } catch (err) {
    console.warn(`Crawlbase unreachable or timed out (${err}); trying UpRock`);
    return null;
  }
}

export async function read(url: string, country = "US") {
  const domain = new URL(url).hostname;
  const r = uprockDomains.has(domain) ? null : await viaCrawlbase(url, country);
  if (r) {
    if (r.tier && !ACCEPTED_TIERS.has(r.tier)) {
      uprockDomains.add(domain); // this page is billed already; the next ones aren't
      console.warn(
        `Crawlbase bills ${domain} as "${r.tier}"; using UpRock from now on`,
      );
    }
    if (r.http === 200 && r.cb === 200) {
      // 404 and 410 are the site's answer (and billed): a second crawler will see the same.
      if (r.site === 404 || r.site === 410)
        throw new Error(`The page is gone (${r.site})`);
      return { via: "crawlbase", markdown: r.body };
    }
    if (r.cb !== 207 && !worthUpRock(r.http, r.cb, r.site))
      throw new Error(
        `Crawlbase ${r.http}, cb_status ${r.cb}: ${r.body.slice(0, 200)}`,
      );
    console.warn(`Crawlbase ${r.http}, cb_status ${r.cb}; trying UpRock`);
  }
  return { via: "uprock", markdown: await viaUpRock(url, country) };
}

// node read.ts https://developer.mozilla.org/en-US/docs/Web/HTTP DE
const [url = "https://developer.mozilla.org/en-US/docs/Web/HTTP", country = "US"] =
  process.argv.slice(2);
try {
  const page = await read(url, country);
  console.log(page.via, page.markdown.slice(0, 200));
} finally {
  await uprock.close();
}
```

### Python (CLI)

```python
# pip install requests; npm i -g uprock   env: CRAWLBASE_TOKEN, UPROCK_API_KEY
# Calls Crawlbase's HTTP API directly: the Python SDK drops the headers, and with them
# cb_status, on any non-2xx response, which is how a failed crawl arrives.
import json
import os
import subprocess
import sys
from typing import Optional
from urllib.parse import urlparse

import requests

# Crawlbase reports each domain's price tier on billed responses (X-Domain-Complexity).
# A domain billed at any other tier goes straight to UpRock from then on.
ACCEPTED_TIERS = {"standard"}
uprock_domains: set = set()  # save it somewhere if you want it to persist

# HTTP statuses worth a second network: a bad token (401), your spend cap or account
# limit (402, 403), the concurrency limit (429), errors and timeouts (500, 503, 504).
TRY_UPROCK_HTTP = {401, 402, 403, 429, 500, 503, 504}


def worth_uprock(http: int, cb: int, site: int) -> bool:
    # cb_status (crawlbase.com/docs/status-codes, checked 25 Oct 2026): 704 is the
    # site's own error, worth a second try only if it's a 5xx; 595 is a click selector
    # to fix; 503 (a block page), 705 (the site said 403), 706 (429) and other 5xx to
    # 7xx codes are worth it.
    if cb == 704:
        return site >= 500
    if cb == 595:
        return False
    return cb >= 500 or http in TRY_UPROCK_HTTP


def via_uprock(url: str, country: str) -> str:
    run = subprocess.run(
        ["uprock", "ai", "crawl", url, "--country", country, "--content"],
        capture_output=True,
        text=True,
        timeout=330,
    )
    if run.returncode != 0:
        raise RuntimeError(f"UpRock: {run.stderr.strip()}")
    return json.loads(run.stdout).get("inlined_markdown", "")


def via_crawlbase(url: str, country: str) -> Optional[dict]:
    params = {
        "token": os.environ.get("CRAWLBASE_TOKEN", ""),
        "url": url,
        "format": "md",
        "country": country,
    }
    try:  # Crawlbase asks for a client timeout of at least 90 s
        res = requests.get("https://api.crawlbase.com/", params=params, timeout=90)
    except requests.RequestException as err:
        print(f"Crawlbase unreachable ({err}); trying UpRock", file=sys.stderr)
        return None
    h = res.headers  # case-insensitive
    return {
        "http": res.status_code,
        "cb": int(h.get("cb_status") or h.get("pc_status") or 0),  # pc_status: old name
        "site": int(h.get("original_status") or 0),
        "tier": h.get("x-domain-complexity") or h.get("domain_complexity") or "",
        "body": res.text,
    }


def read(url: str, country: str = "US") -> dict:
    domain = urlparse(url).hostname or ""
    r = None if domain in uprock_domains else via_crawlbase(url, country)
    if r:
        if r["tier"] and r["tier"] not in ACCEPTED_TIERS:
            # This page is billed already; the next ones from this domain aren't.
            uprock_domains.add(domain)
            print(f"Crawlbase bills {domain} as {r['tier']!r}", file=sys.stderr)
        if r["http"] == 200 and r["cb"] == 200:
            if r["site"] in (404, 410):
                # The site's answer (and billed): a second crawler will see the same.
                raise LookupError(f"The page is gone ({r['site']})")
            return {"via": "crawlbase", "markdown": r["body"]}
        if r["cb"] != 207 and not worth_uprock(r["http"], r["cb"], r["site"]):
            raise RuntimeError(f"Crawlbase {r['http']}, cb_status {r['cb']}")
        status = f"{r['http']}, cb_status {r['cb']}"
        print(f"Crawlbase {status}; trying UpRock", file=sys.stderr)
    return {"via": "uprock", "markdown": via_uprock(url, country)}


if __name__ == "__main__":
    # python read.py https://developer.mozilla.org/en-US/docs/Web/HTTP DE
    url = sys.argv[1] if len(sys.argv) > 1 else "https://developer.mozilla.org/en-US/docs/Web/HTTP"
    country = sys.argv[2] if len(sys.argv) > 2 else "US"
    page = read(url, country)
    print(page["via"], page["markdown"][:200])
```

Tested 25 Oct 2026 against the live UpRock API with an invalid Crawlbase token: Crawlbase answered HTTP 401 with `pc_status: 401` and no `cb_status`, and both versions fell back to UpRock. Without an account we couldn't see a real `X-Domain-Complexity` header, so the tier check follows Crawlbase's docs, and the cost table follows its pricing page and estimator. On 6 Nov 2026 we ran the HTTP and MCP versions again with an UpRock API key in an `Authorization` header: with an invalid Crawlbase key, each fell back to UpRock and got the page.

## FAQ

### How does Crawlbase decide a site's tier?

Itself. Its pricing page says heavily defended sites carry a higher tier, applied automatically, from standard (×1) through moderate, complex, complex I, complex II, extreme and extreme I to extreme II (×20), and JavaScript doubles any tier. Its Cookbook shows the tier for 356 sites, most at one credit, and notes that a tier can change. For any other site, you see the tier in the response after you've been billed.

### Does Crawlbase charge for failed requests?

No. Its docs bill only responses with cb_status 200 or 207 where the site answered 200, 201, a redirect, 404 or 410. A block, a timeout, or a site's own 403, 429 or 5xx costs nothing.

### Why doesn't the Python example use Crawlbase's SDK?

On any non-2xx response, the Python SDK returns only the status code, with empty headers and body. A failed crawl arrives as HTTP 520 with the reason in the cb_status header, so the SDK loses it. We checked the SDK's source and called it with an invalid token, and it returned status 401 with no headers. The Node SDK keeps the headers.

### Which countries can Crawlbase fetch from?

Its Crawling API docs list 22 countries, and say Crawlbase may override the country you pass to pick a proxy for the URL unless you ask support to turn that off. UpRock covers 63 countries, or a region, and loads the page on a device in the country you choose.

### Does Crawlbase have an MCP server?

Yes, an open-source one you run with npx, and a hosted one at mcp.crawlbase.com/mcp that takes your token as a bearer header.

## Other use cases to explore

- [Compare a residential surcharge](https://uprock.ai/compare/scrapfly#what-a-page-costs-by-proxy-pool-and-plan): another crawler where the proxy pool sets the price, from 1 to 30 credits a page.
- [Decide when an unlocker is worth paying for](https://uprock.ai/compare/web-unlocker-apis#which-one-fits): when a plain proxy is enough, when an unlocker earns its price, and when a second network helps.
- [Cap what a single page can cost](https://uprock.ai/compare/scrapingbee#what-a-page-costs-in-credits): one provider's credit table, from 1 to 75 credits a page, and how to stop before the expensive tiers.
- [Search public video by moment](https://uprock.ai/video-semantic-search): find the second something is shown or said, across platforms.

## Related articles

- [How do I add a backup crawler to Firecrawl?](https://uprock.ai/compare/firecrawl.md): When to send a page from Firecrawl to UpRock, published prices side by side, and a tested fallback in TypeScript and Python.
- [How do I add a backup for Tavily Extract?](https://uprock.ai/compare/tavily.md): Retry the URLs in Tavily Extract's failed results on a real device, and read pages as they appear in a chosen country. Prices side by side and tested code.
- [How do I add a fallback for Exa contents errors?](https://uprock.ai/compare/exa.md): Read the per-URL statuses in Exa's /contents response and retry the right ones live on a real device. Error tags explained, prices side by side, tested code.
- [How do I add a backup to Jina Reader, and when is it cheaper?](https://uprock.ai/compare/jina-reader.md): Spot a failed read in Reader's JSON (it still answers 200), retry it on a real device, and see what six real pages cost in Reader tokens next to a flat per-page price.
- [How do I add a hosted fallback to Crawl4AI?](https://uprock.ai/compare/crawl4ai.md): Plug UpRock into Crawl4AI's fallback_fetch_function so blocked pages are fetched on a real device in the country you choose, then run through Crawl4AI's own markdown pipeline.
- [How do I add a backup to Bright Data's Web Unlocker?](https://uprock.ai/compare/bright-data.md): Which Web Unlocker errors to retry, which to send to a second network and which to respect, with Bright Data's published prices next to ours and tested fallback code.
- [How do I add a fallback to an Apify or Crawlee crawler?](https://uprock.ai/compare/apify.md): Hand the requests Crawlee gives up on to a second network with failedRequestHandler, see what a fetched page costs on Apify's own Actors, and run the tested code.
- [When should a Browserbase agent read a page without opening a browser?](https://uprock.ai/compare/browserbase.md): What 1,000 page reads cost on Browserbase three ways, when to send a read to UpRock instead (including when every browser is busy), and tested fallback code.
- [How do I cap ScrapingBee's credit cost and send the rest to a backup?](https://uprock.ai/compare/scrapingbee.md): ScrapingBee charges 1 to 75 credits a page depending on the options. Cap it with Auto-Mode and max_cost, watch the country fallback, and send the rest to UpRock with tested code.
- [Spider costs less per page than UpRock. When is a backup still worth it?](https://uprock.ai/compare/spider.md): Spider is cheaper per page, so keep it as your primary. How Spider reports a page it couldn't get (from our own tests), and tested code that sends only those pages to UpRock.
- [How do I add a fallback to Parallel's Extract API?](https://uprock.ai/compare/parallel.md): Make Parallel Extract report a failed live fetch instead of quietly serving an old cached copy, then retry those URLs on UpRock. Prices side by side and tested code.
- [How do I add a backup to ZenRows?](https://uprock.ai/compare/zenrows.md): Which ZenRows error codes to send to a second network and which to respect, what a page from one country costs once premium proxies are required, and tested fallback code.
- [How do I retry a ScraperAPI batch's failed jobs on another network?](https://uprock.ai/compare/scraperapi.md): Send the jobs ScraperAPI's async batch gives up on to UpRock, see which countries each ScraperAPI plan can target, and compare credit costs. Tested code for both.
- [How do I add a backup to Oxylabs' new Web API?](https://uprock.ai/compare/oxylabs.md): Which Oxylabs Web API failures to send to a second network and which to respect, what a generic page really costs after its October 2026 pricing change, and tested fallback code.
- [How do I add a backup to Context.dev's scrape API?](https://uprock.ai/compare/context-dev.md): Context.dev reports a failed scrape inside an HTTP 200. Which of its error codes to retry on a second network, what a page costs by plan, and tested fallback code.
- [How do I add a backup to Scrapfly instead of paying for residential?](https://uprock.ai/compare/scrapfly.md): A Scrapfly page costs 1 to 30 credits depending on proxy pool and browser. What that is per 1,000 pages on each plan, which errors to send on, and tested code.
- [Which unlocker API fits: Bright Data, Oxylabs, Zyte or UpRock?](https://uprock.ai/compare/web-unlocker-apis.md): Bright Data's Web Unlocker, Oxylabs' Web API and Zyte API priced per 1,000 hard pages, with what each bills and when a second network helps.
- [Compare: all articles](https://uprock.ai/compare.md)

## References

1. [Crawlbase, Pricing (rate card, tiers, subscriptions)](https://crawlbase.com/pricing), checked 25 Oct 2026
2. [Crawlbase docs, Crawling API (countries, formats, headers, billable requests)](https://crawlbase.com/docs/crawling-api), checked 25 Oct 2026
3. [Crawlbase docs, Status codes](https://crawlbase.com/docs/status-codes), checked 25 Oct 2026
4. [Crawlbase docs, Error handling](https://crawlbase.com/docs/errors), checked 25 Oct 2026
5. [Crawlbase docs, Rate limits](https://crawlbase.com/docs/rate-limits), checked 25 Oct 2026
6. [Crawlbase, Cookbook, how the numbers are counted (tiers per site)](https://crawlbase.com/cookbook/how-we-count), checked 25 Oct 2026
7. [Crawlbase docs, MCP server](https://crawlbase.com/docs/ai-mcp), checked 25 Oct 2026
8. [Crawlbase Node SDK on GitHub](https://github.com/crawlbase/crawlbase-node), checked 25 Oct 2026
9. [Crawlbase Python SDK on GitHub](https://github.com/crawlbase/crawlbase-python), checked 25 Oct 2026
10. [UpRock, everything in one file (API, CLI, MCP, pricing)](https://uprock.ai/llms-full.txt), checked 25 Oct 2026

## Get started

Get an API key: https://uprock.ai/app/api-keys

Paste this into your agent:

```text
Set up UpRock for me: read https://uprock.ai/agents.md, connect the UpRock MCP server, and run one test crawl. Tell me what it costs before you spend anything.
```

Setup for every client (MCP, skill, CLI): https://uprock.ai/agents.md
