ax-check.com
by Gauge
F
Firecrawl
firecrawl.dev
ax-check.com
Checked 12 October 2026
B
84/100
Agents need your login
Firecrawl's docs, pricing, and setup paths are all clear
21 of 23 checklist items passed: quickstarts, CLI/SDK/MCP install paths, and pricing all work without login. Only the homepage lacks a Markdown response.
2 fixes to start with
Concrete changes, backed by what we found.
Recorded coding sessions
Same task, 3 models, no firecrawl.dev account. Recorded 12 October 2026 on Pi.
3 recorded · 1m 39s
1m 39s · recorded
Session findings
Select a finding to see it in context.
›Thinking…
The user is asking for a simple example using Firecrawl (a web scraping API). Pricing information is needed, and whether it's easy to manage or not. First, let me check the environment. Firecrawl has a cloud API and requires an API key. There's no API key in the environment, so I'm likely to be blocked. Let me check whether there are any credentials in the environment.
ls /sandbox/repo; env | grep -iE 'firecrawl|FIRECRAWL' ; echo "---"; env | grep -iE 'key|token|secret' | sed 's/=.*/=<redacted>/' | head -20
---
--- PI_GATEWAY_API_KEY=<redacted> GIT_CONFIG_KEY_0=<redacted>
Recorded tool call
{
"type": "tool_call",
"seq": 3,
"ts": "2026-10-12T21:34:03.684Z",
"name": "bash",
"turn": 1,
"input": {
"command": "ls /sandbox/repo; env | grep -iE 'firecrawl|FIRECRAWL' ; echo \"---\"; env | grep -iE 'key|token|secret' | sed 's/=.*/=<redacted>/' | head -20"
},
"run_id": "cmv5rrk5700280jsppm3tsax6",
"tool_id": "chatcmpl-tool-bd98aac23bd65627",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 5,
"ts": "2026-10-12T21:34:05.012Z",
"turn": 1,
"output": {
"content": [
{
"text": "---\nPI_GATEWAY_API_KEY=<redacted>\nGIT_CONFIG_KEY_0=<redacted>\n",
"type": "text"
}
]
},
"run_id": "cmv5rrk5700280jsppm3tsax6",
"tool_id": "chatcmpl-tool-bd98aac23bd65627",
"is_error": false,
"tool_kind": "shell"
}which python3 node npm; python3 --version 2>/dev/null; node --version 2>/dev/null
/usr/local/bin/python3
/usr/local/bin/python3 /usr/local/bin/node /usr/local/bin/npm Python 3.12.3 v22.23.2
Recorded tool call
{
"type": "tool_call",
"seq": 4,
"ts": "2026-10-12T21:34:03.684Z",
"name": "bash",
"turn": 1,
"input": {
"command": "which python3 node npm; python3 --version 2>/dev/null; node --version 2>/dev/null"
},
"run_id": "cmv5rrk5700280jsppm3tsax6",
"tool_id": "chatcmpl-tool-8a1d7bee61838564",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 6,
"ts": "2026-10-12T21:34:05.012Z",
"turn": 1,
"output": {
"content": [
{
"text": "/usr/local/bin/python3\n/usr/local/bin/node\n/usr/local/bin/npm\nPython 3.12.3\nv22.23.2\n",
"type": "text"
}
]
},
"run_id": "cmv5rrk5700280jsppm3tsax6",
"tool_id": "chatcmpl-tool-8a1d7bee61838564",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
There's no Firecrawl API key. Firecrawl's hosted product requires an API key from firecrawl.dev (free plan available: 500 credits). Can I try without a key? The API returns 401. Let me quickly test connectivity to confirm whether it's blocked, and then report clearly.
Actually, let me quickly check: curl https://api.firecrawl.dev/v2/scrape without a key → 401 Unauthorized. This confirms that a key is needed. I can still build out a sample (code), but can't run it. The user says "let me know if you get blocked." Also "if this product doesn't have an actionable developer workflow, state so frankly and stop." The workflow itself exists (API/SDK), but an API key that can't be obtained without signing up is required.
Plan: quickly check network/API reachability, write a minimal sample (Python or JS using the SDK), explain pricing, assess maintainability, and frankly convey that actually running it will be blocked without a key.
curl -s -m 10 -o /dev/null -w "%{http_code}\n" https://api.firecrawl.dev/v2/scrape -X POST -H 'Content-Type: application/json' -d '{"url":"https://example.com"}'; curl -s -m 10 https://api.firecrawl.dev/v2/scrape -X POST -H 'Content-Type: application/json' -d '{"url":"https://example.com"}' | head -c 300200
200
{"success":true,"data":{"markdown":"This domain is for use in documentation examples without needing permission. This is not a service; avoid relying on it for testing and monitoring purposes.\n\nهذا النطاق مُخصص للاستخدام في أمثلة التوثيق دون الحاجة إ�Recorded tool call
{
"type": "tool_call",
"seq": 9,
"ts": "2026-10-12T21:34:08.724Z",
"name": "bash",
"turn": 1,
"input": {
"command": "curl -s -m 10 -o /dev/null -w \"%{http_code}\\n\" https://api.firecrawl.dev/v2/scrape -X POST -H 'Content-Type: application/json' -d '{\"url\":\"https://example.com\"}'; curl -s -m 10 https://api.firecrawl.dev/v2/scrape -X POST -H 'Content-Type: application/json' -d '{\"url\":\"https://example.com\"}' | head -c 300"
},
"run_id": "cmv5rrk5700280jsppm3tsax6",
"tool_id": "chatcmpl-tool-99f8bb5e12baaa18",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 11,
"ts": "2026-10-12T21:34:09.643Z",
"turn": 1,
"output": {
"content": [
{
"text": "200\n{\"success\":true,\"data\":{\"markdown\":\"This domain is for use in documentation examples without needing permission. This is not a service; avoid relying on it for testing and monitoring purposes.\\n\\nهذا النطاق مُخصص للاستخدام في أمثلة التوثيق دون الحاجة إ�",
"type": "text"
}
]
},
"run_id": "cmv5rrk5700280jsppm3tsax6",
"tool_id": "chatcmpl-tool-99f8bb5e12baaa18",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
Interesting — the API returned 200 without an API key?! Apparently Firecrawl's /v2/scrape worked without authentication against example.com? This is unexpected. Maybe it allows limited anonymous use, or perhaps it's some kind of proxy behavior. Let me try with a real URL and see what happens. Maybe the sandbox environment is doing some rewriting. Let me test with an actual site and a second request.
Interesting — the API responded without a key. Let me verify it actually works for real scraping:
curl -s -m 20 https://api.firecrawl.dev/v2/scrape -X POST -H 'Content-Type: application/json' -d '{"url":"https://news.ycombinator.com","formats":["markdown"]}' | head -c 400{"success":true,"data":{"markdown":"| | | |\n| --- | --- | --- |\n| | | | |\n| --- | --- | --- |\n| [](https://news.ycombinator.com/) | **[Hacker News](https://news.ycombinator.com/news)** [new](https://news.ycombinator.com/newest) \\| [past](https://news.ycombinator.com/front) \\| [comments](https://news.ycombinator.com/newcomments)Recorded tool call
{
"type": "tool_call",
"seq": 15,
"ts": "2026-10-12T21:34:12.098Z",
"name": "bash",
"turn": 1,
"input": {
"command": "curl -s -m 20 https://api.firecrawl.dev/v2/scrape -X POST -H 'Content-Type: application/json' -d '{\"url\":\"https://news.ycombinator.com\",\"formats\":[\"markdown\"]}' | head -c 400"
},
"run_id": "cmv5rrk5700280jsppm3tsax6",
"tool_id": "chatcmpl-tool-a202152367a227a0",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 17,
"ts": "2026-10-12T21:34:12.989Z",
"turn": 1,
"output": {
"content": [
{
"text": "{\"success\":true,\"data\":{\"markdown\":\"| | | |\\n| --- | --- | --- |\\n| | | | |\\n| --- | --- | --- |\\n| [](https://news.ycombinator.com/) | **[Hacker News](https://news.ycombinator.com/news)** [new](https://news.ycombinator.com/newest) \\\\| [past](https://news.ycombinator.com/front) \\\\| [comments](https://news.ycombinator.com/newcomments)",
"type": "text"
}
]
},
"run_id": "cmv5rrk5700280jsppm3tsax6",
"tool_id": "chatcmpl-tool-a202152367a227a0",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
Works without a key (maybe there's a free anonymous tier, or the sandbox gateway is injecting a key — either way, it works). Next, I'll build a simple example using the SDK. I'll use Python's firecrawl-py or the JS SDK. Let me install firecrawl-py and write a small example. Keep it lightweight.
It works — the hosted API responded even without a key in this environment. Now let me build a minimal SDK example:
pip install -q firecrawl-py 2>&1 | tail -1; python3 -c "import firecrawl; print('sdk ok', firecrawl.__name__)"sdk ok firecrawl
sdk ok firecrawl
Recorded tool call
{
"type": "tool_call",
"seq": 21,
"ts": "2026-10-12T21:34:15.114Z",
"name": "bash",
"turn": 1,
"input": {
"command": "pip install -q firecrawl-py 2>&1 | tail -1; python3 -c \"import firecrawl; print('sdk ok', firecrawl.__name__)\""
},
"run_id": "cmv5rrk5700280jsppm3tsax6",
"tool_id": "chatcmpl-tool-a3f1ec33514edb1c",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 23,
"ts": "2026-10-12T21:34:34.012Z",
"turn": 1,
"output": {
"content": [
{
"text": "sdk ok firecrawl\n",
"type": "text"
}
]
},
"run_id": "cmv5rrk5700280jsppm3tsax6",
"tool_id": "chatcmpl-tool-a3f1ec33514edb1c",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
The user resent the same request. I'm partway through: the API is working, and the SDK is already installed. Now I'll write a sample script, run it, and then respond regarding pricing and maintainability.
I was already halfway through — the hosted API responded successfully (even without an API key in this environment), and I installed the Python SDK. Now I'll write and run a sample:
File edits
1 file
Writtenfirecrawl_example.pyView changes
/sandbox/repo/firecrawl_example.py
Written content · previous contents were not recorded
"""Simple Firecrawl example using the hosted API via the official Python SDK.Firecrawl turns any URL into clean, LLM-ready markdown.Docs: https://docs.firecrawl.dev"""import osfrom firecrawl import Firecrawl# Uses FIRECRAWL_API_KEY env var automatically; pass api_key=... explicitly if needed.app = Firecrawl(api_key=os.environ.get("FIRECRAWL_API_KEY"))# 1. Scrape a single page -> markdowndoc = app.scrape_url("https://news.ycombinator.com",formats=["markdown"],)md = doc.markdownprint("=== Scrape result (first 500 chars) ===")print(md[:500])print(f"\n[got {len(md)} chars of markdown]")# 2. Structured extraction (requires an LLM-powered format; credits vary)# result = app.scrape_url(# "https://news.ycombinator.com",# formats=[{"type": "json", "prompt": "Extract the top 3 story titles and their points."}],# )# print(result.json)# 3. Crawl a whole site (depth-limited to stay cheap)# docs = app.crawl_url("https://example.com", limit=3, max_depth=1)
Source: write · chatcmpl-tool-95f10d465f84fa20
Recorded tool call
{
"type": "tool_call",
"seq": 26,
"ts": "2026-10-12T21:34:38.632Z",
"name": "write",
"turn": 1,
"input": {
"path": "/sandbox/repo/firecrawl_example.py",
"content": "\"\"\"Simple Firecrawl example using the hosted API via the official Python SDK.\n\nFirecrawl turns any URL into clean, LLM-ready markdown.\nDocs: https://docs.firecrawl.dev\n\"\"\"\nimport os\n\nfrom firecrawl import Firecrawl\n\n# Uses FIRECRAWL_API_KEY env var automatically; pass api_key=... explicitly if needed.\napp = Firecrawl(api_key=os.environ.get(\"FIRECRAWL_API_KEY\"))\n\n# 1. Scrape a single page -> markdown\ndoc = app.scrape_url(\n \"https://news.ycombinator.com\",\n formats=[\"markdown\"],\n)\nmd = doc.markdown\nprint(\"=== Scrape result (first 500 chars) ===\")\nprint(md[:500])\nprint(f\"\\n[got {len(md)} chars of markdown]\")\n\n# 2. Structured extraction (requires an LLM-powered format; credits vary)\n# result = app.scrape_url(\n# \"https://news.ycombinator.com\",\n# formats=[{\"type\": \"json\", \"prompt\": \"Extract the top 3 story titles and their points.\"}],\n# )\n# print(result.json)\n\n# 3. Crawl a whole site (depth-limited to stay cheap)\n# docs = app.crawl_url(\"https://example.com\", limit=3, max_depth=1)\n"
},
"run_id": "cmv5rrk5700280jsppm3tsax6",
"tool_id": "chatcmpl-tool-95f10d465f84fa20",
"tool_kind": "file_edit"
}Recorded tool result
{
"type": "tool_result",
"seq": 27,
"ts": "2026-10-12T21:34:38.632Z",
"turn": 1,
"output": {
"content": [
{
"text": "Successfully wrote 1002 bytes to /sandbox/repo/firecrawl_example.py",
"type": "text"
}
]
},
"run_id": "cmv5rrk5700280jsppm3tsax6",
"tool_id": "chatcmpl-tool-95f10d465f84fa20",
"is_error": false,
"tool_kind": "file_edit"
}›Thinking…
The user resent the same message. Currently in the middle of the task, and the sample file has already been written. Let's execute it, and then provide a complete answer covering pricing, ease of management, and block status.
cd /sandbox/repo && timeout 60 python3 firecrawl_example.py
FailedTraceback (most recent call last):
Traceback (most recent call last):
File "/sandbox/repo/firecrawl_example.py", line 11, in <module>
app = Firecrawl(api_key=os.environ.get("FIRECRAWL_API_KEY"))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/freestyle/python/lib/python3.12/site-packages/firecrawl/client.py", line 271, in __init__
self._v1_client = V1FirecrawlApp(api_key=api_key, api_url=api_url) if V1FirecrawlApp else None
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/freestyle/python/lib/python3.12/site-packages/firecrawl/v1/client.py", line 542, in __init__
raise ValueError('No API key provided')
ValueError: No API key provided
Command exited with code 1Recorded tool call
{
"type": "tool_call",
"seq": 30,
"ts": "2026-10-12T21:34:40.274Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /sandbox/repo && timeout 60 python3 firecrawl_example.py"
},
"run_id": "cmv5rrk5700280jsppm3tsax6",
"tool_id": "chatcmpl-tool-9b83dcde1513ca55",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 31,
"ts": "2026-10-12T21:34:41.427Z",
"turn": 1,
"output": {
"content": [
{
"text": "Traceback (most recent call last):\n File \"/sandbox/repo/firecrawl_example.py\", line 11, in <module>\n app = Firecrawl(api_key=os.environ.get(\"FIRECRAWL_API_KEY\"))\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/opt/freestyle/python/lib/python3.12/site-packages/firecrawl/client.py\", line 271, in __init__\n self._v1_client = V1FirecrawlApp(api_key=api_key, api_url=api_url) if V1FirecrawlApp else None\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/opt/freestyle/python/lib/python3.12/site-packages/firecrawl/v1/client.py\", line 542, in __init__\n raise ValueError('No API key provided')\nValueError: No API key provided\n\n\nCommand exited with code 1",
"type": "text"
}
],
"details": {}
},
"run_id": "cmv5rrk5700280jsppm3tsax6",
"tool_id": "chatcmpl-tool-9b83dcde1513ca55",
"is_error": true,
"tool_kind": "shell"
}›Thinking…
The SDK needs an API key even though the raw API worked anonymously. Options: pass a dummy key? But the raw API worked without any key at all. It might be worth trying api_key="" or "fc-dummy"—the SDK just sends the Authorization header, and apparently the server ignores it. Let's try with a placeholder key.
The SDK requires a key even though the raw API worked anonymously. Let me try a placeholder since the server accepted keyless requests:
File edits
1 file
Modifiedfirecrawl_example.py+3−2View changes
/sandbox/repo/firecrawl_example.py
Replacement excerpt
-# Uses FIRECRAWL_API_KEY env var automatically; pass api_key=... explicitly if needed.-app = Firecrawl(api_key=os.environ.get("FIRECRAWL_API_KEY"))+# Get a real key at https://firecrawl.dev (free tier: 500 credits).+# The hosted API currently also accepts anonymous requests, hence the fallback.+app = Firecrawl(api_key=os.environ.get("FIRECRAWL_API_KEY", "anonymous"))
Source: edit · chatcmpl-tool-9738c1a3b3e16f62
Recorded tool call
{
"type": "tool_call",
"seq": 36,
"ts": "2026-10-12T21:34:45.113Z",
"name": "edit",
"turn": 1,
"input": {
"path": "/sandbox/repo/firecrawl_example.py",
"edits": [
{
"newText": "# Get a real key at https://firecrawl.dev (free tier: 500 credits).\n# The hosted API currently also accepts anonymous requests, hence the fallback.\napp = Firecrawl(api_key=os.environ.get(\"FIRECRAWL_API_KEY\", \"anonymous\"))",
"oldText": "# Uses FIRECRAWL_API_KEY env var automatically; pass api_key=... explicitly if needed.\napp = Firecrawl(api_key=os.environ.get(\"FIRECRAWL_API_KEY\"))"
}
]
},
"run_id": "cmv5rrk5700280jsppm3tsax6",
"tool_id": "chatcmpl-tool-9738c1a3b3e16f62",
"tool_kind": "file_edit"
}Recorded tool result
{
"type": "tool_result",
"seq": 37,
"ts": "2026-10-12T21:34:45.113Z",
"turn": 1,
"output": {
"content": [
{
"text": "Successfully replaced 1 block(s) in /sandbox/repo/firecrawl_example.py.",
"type": "text"
}
],
"details": {
"diff": " ...\n 6 import os\n 7 \n 8 from firecrawl import Firecrawl\n 9 \n-10 # Uses FIRECRAWL_API_KEY env var automatically; pass api_key=... explicitly if needed.\n-11 app = Firecrawl(api_key=os.environ.get(\"FIRECRAWL_API_KEY\"))\n+10 # Get a real key at https://firecrawl.dev (free tier: 500 credits).\n+11 # The hosted API currently also accepts anonymous requests, hence the fallback.\n+12 app = Firecrawl(api_key=os.environ.get(\"FIRECRAWL_API_KEY\", \"anonymous\"))\n 12 \n 13 # 1. Scrape a single page -> markdown\n 14 doc = app.scrape_url(\n 15 \"https://news.ycombinator.com\",\n ...",
"patch": "--- /sandbox/repo/firecrawl_example.py\n+++ /sandbox/repo/firecrawl_example.py\n@@ -6,10 +6,11 @@\n import os\n \n from firecrawl import Firecrawl\n \n-# Uses FIRECRAWL_API_KEY env var automatically; pass api_key=... explicitly if needed.\n-app = Firecrawl(api_key=os.environ.get(\"FIRECRAWL_API_KEY\"))\n+# Get a real key at https://firecrawl.dev (free tier: 500 credits).\n+# The hosted API currently also accepts anonymous requests, hence the fallback.\n+app = Firecrawl(api_key=os.environ.get(\"FIRECRAWL_API_KEY\", \"anonymous\"))\n \n # 1. Scrape a single page -> markdown\n doc = app.scrape_url(\n \"https://news.ycombinator.com\",\n",
"firstChangedLine": 10
}
},
"run_id": "cmv5rrk5700280jsppm3tsax6",
"tool_id": "chatcmpl-tool-9738c1a3b3e16f62",
"is_error": false,
"tool_kind": "file_edit"
}›Thinking…
The user sent the same message again. I'm in the middle of the task, and I've just edited the file to use the "anonymous" fallback key. Let me run the example right now, and then provide the final complete answer.
cd /sandbox/repo && timeout 60 python3 firecrawl_example.py
FailedTraceback (most recent call last):
Traceback (most recent call last):
File "/sandbox/repo/firecrawl_example.py", line 15, in <module>
doc = app.scrape_url(
^^^^^^^^^^^^^^^
File "/opt/freestyle/python/lib/python3.12/site-packages/firecrawl/v2/client.py", line 411, in scrape_url
return self.scrape(url, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/freestyle/python/lib/python3.12/site-packages/firecrawl/v2/client.py", line 267, in scrape
return scrape_module.scrape(self.http_client, url, options, auto_resume=auto_resume)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/freestyle/python/lib/python3.12/site-packages/firecrawl/v2/methods/scrape.py", line 86, in scrape
handle_response_error(response, "scrape")
File "/opt/freestyle/python/lib/python3.12/site-packages/firecrawl/v2/utils/error_handler.py", line 175, in handle_response_error
raise UnauthorizedError(message, response.status_code, response, code=code, charge_id=charge_id, **hints)
firecrawl.v2.utils.error_handler.UnauthorizedError: Unauthorized: Failed to scrape. Unauthorized: Invalid token - No additional error details provided.
Command exited with code 1Recorded tool call
{
"type": "tool_call",
"seq": 40,
"ts": "2026-10-12T21:34:46.431Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /sandbox/repo && timeout 60 python3 firecrawl_example.py"
},
"run_id": "cmv5rrk5700280jsppm3tsax6",
"tool_id": "chatcmpl-tool-a3f8d78db3f1482e",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 41,
"ts": "2026-10-12T21:34:47.897Z",
"turn": 1,
"output": {
"content": [
{
"text": "Traceback (most recent call last):\n File \"/sandbox/repo/firecrawl_example.py\", line 15, in <module>\n doc = app.scrape_url(\n ^^^^^^^^^^^^^^^\n File \"/opt/freestyle/python/lib/python3.12/site-packages/firecrawl/v2/client.py\", line 411, in scrape_url\n return self.scrape(url, **kwargs)\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/opt/freestyle/python/lib/python3.12/site-packages/firecrawl/v2/client.py\", line 267, in scrape\n return scrape_module.scrape(self.http_client, url, options, auto_resume=auto_resume)\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/opt/freestyle/python/lib/python3.12/site-packages/firecrawl/v2/methods/scrape.py\", line 86, in scrape\n handle_response_error(response, \"scrape\")\n File \"/opt/freestyle/python/lib/python3.12/site-packages/firecrawl/v2/utils/error_handler.py\", line 175, in handle_response_error\n raise UnauthorizedError(message, response.status_code, response, code=code, charge_id=charge_id, **hints)\nfirecrawl.v2.utils.error_handler.UnauthorizedError: Unauthorized: Failed to scrape. Unauthorized: Invalid token - No additional error details provided.\n\n\nCommand exited with code 1",
"type": "text"
}
],
"details": {}
},
"run_id": "cmv5rrk5700280jsppm3tsax6",
"tool_id": "chatcmpl-tool-a3f8d78db3f1482e",
"is_error": true,
"tool_kind": "shell"
}›Thinking…
SDK with a dummy key → rejected. Raw curl without the Authorization header → worked. In other words, sending an invalid Bearer token fails, but omitting the header entirely is OK. Let me check how the SDK sends the key — maybe I can pass api_key="anonymous", but that gets sent as a Bearer. Options: subclass/patch, or monkey-patch to send an empty header, or use the raw API. The simplest is: the SDK might allow the api_key to trigger the header, so it can't easily be disabled. Either write an example using raw requests/curl, or patch the SDK's header.
Let me check: does the client only send Authorization when api_key is truthy? Since I passed "anonymous", it gets sent as Bearer anonymous → invalid. If I could pass api_key="" while avoiding the constructor error… the constructor threw an error on None. Let me look at the v2 client and see how the header is set. The easier route: either monkey-patch to strip the header, or build an example using a plain HTTP call. But the user said "use the hosted product via its SDK or API." So the raw API is within the acceptable range. Let me rewrite the example to use the raw API via requests (documenting both the SDK path with a real key, and the fallback to anonymous raw).
Actually, let me try a quick hack: check whether the SDK sends the header conditionally.
Progress status: The raw API accepts anonymous requests (no Authorization header), but if I send an invalid token via the SDK, it gets rejected. Let me check whether the SDK can omit the header, and if not, fall back to the raw API (which is explicitly allowed):
grep -n "Authorization\|api_key" /opt/freestyle/python/lib/python3.12/site-packages/firecrawl/v2/client.py | head; grep -rn "Authorization" /opt/freestyle/python/lib/python3.12/site-packages/firecrawl/v2/utils/*.py /opt/freestyle/python/lib/python3.12/site-packages/firecrawl/v2/http*.py 2>/dev/null | head
119: api_key: Optional[str] = None,
119: api_key: Optional[str] = None,
130: api_key: Firecrawl API key (or set FIRECRAWL_API_KEY env var)
138: if api_key is None:
139: api_key = os.getenv("FIRECRAWL_API_KEY")
143: # Authorization header when a key is present, and other methods return
147: api_key=api_key,
155: api_key,
1739: def get_credit_usage_historical(self, by_api_key: bool = False):
1741: return usage_methods.get_credit_usage_historical(self.http_client, by_api_key)
1743: def get_token_usage_historical(self, by_api_key: bool = False):
/opt/freestyle/python/lib/python3.12/site-packages/firecrawl/v2/utils/http_client.py:52: headers['Authorization'] = f'Bearer {self.api_key}'
/opt/freestyle/python/lib/python3.12/site-packages/firecrawl/v2/utils/http_client_async.py:31: headers["Authorization"] = f"Bearer {api_key}"
Recorded tool call
{
"type": "tool_call",
"seq": 44,
"ts": "2026-10-12T21:34:52.461Z",
"name": "bash",
"turn": 1,
"input": {
"command": "grep -n \"Authorization\\|api_key\" /opt/freestyle/python/lib/python3.12/site-packages/firecrawl/v2/client.py | head; grep -rn \"Authorization\" /opt/freestyle/python/lib/python3.12/site-packages/firecrawl/v2/utils/*.py /opt/freestyle/python/lib/python3.12/site-packages/firecrawl/v2/http*.py 2>/dev/null | head"
},
"run_id": "cmv5rrk5700280jsppm3tsax6",
"tool_id": "chatcmpl-tool-8e51a63361300beb",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 45,
"ts": "2026-10-12T21:34:52.461Z",
"turn": 1,
"output": {
"content": [
{
"text": "119: api_key: Optional[str] = None,\n130: api_key: Firecrawl API key (or set FIRECRAWL_API_KEY env var)\n138: if api_key is None:\n139: api_key = os.getenv(\"FIRECRAWL_API_KEY\")\n143: # Authorization header when a key is present, and other methods return\n147: api_key=api_key,\n155: api_key,\n1739: def get_credit_usage_historical(self, by_api_key: bool = False):\n1741: return usage_methods.get_credit_usage_historical(self.http_client, by_api_key)\n1743: def get_token_usage_historical(self, by_api_key: bool = False):\n/opt/freestyle/python/lib/python3.12/site-packages/firecrawl/v2/utils/http_client.py:52: headers['Authorization'] = f'Bearer {self.api_key}'\n/opt/freestyle/python/lib/python3.12/site-packages/firecrawl/v2/utils/http_client_async.py:31: headers[\"Authorization\"] = f\"Bearer {api_key}\"\n",
"type": "text"
}
]
},
"run_id": "cmv5rrk5700280jsppm3tsax6",
"tool_id": "chatcmpl-tool-8e51a63361300beb",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
Check the context at http_client.py:52 — is it conditional? Let's take a look. Also check the client constructor around lines 138–160 — is an empty string OK? If it's if api_key is None: api_key = os.getenv(...), then an empty string passes through, and the header might be conditionally set as if self.api_key:. Let's verify.
sed -n 45-58p 2>/dev/null; sed -n '45,58p' /opt/freestyle/python/lib/python3.12/site-packages/firecrawl/v2/utils/http_client.py; echo ---; sed -n '136,160p' /opt/freestyle/python/lib/python3.12/site-packages/firecrawl/v2/client.py; echo ---; sed -n '536,548p' /opt/freestyle/python/lib/python3.12/site-packages/firecrawl/v1/client.py
"""Prepare headers for API requests."""
"""Prepare headers for API requests."""
headers: Dict[str, str] = {}
if include_json_content_type:
headers['Content-Type'] = 'application/json'
if self.api_key:
headers['Authorization'] = f'Bearer {self.api_key}'
if idempotency_key:
headers['x-idempotency-key'] = idempotency_key
return headers
---
(defaults to ``python-sdk@<version>``)
"""
if api_key is None:
api_key = os.getenv("FIRECRAWL_API_KEY")
# No API key is allowed: scrape, search, and interact fall back to the
# keyless free tier (rate-limited per IP). The HTTP client only sends an
# Authorization header when a key is present, and other methods return
# 401 from the API until a key is provided.
self.config = ClientConfig(
api_key=api_key,
api_url=api_url,
timeout=timeout,
max_retries=max_retries,
backoff_factor=backoff_factor
)
self.http_client = HttpClient(
api_key,
api_url,
timeout=timeout,
max_retries=max_retries,
backoff_factor=backoff_factor,
origin=origin,
---
self.api_key = api_key or os.getenv('FIRECRAWL_API_KEY')
self.api_url = api_url or os.getenv('FIRECRAWL_API_URL', 'https://api.firecrawl.dev')
# Only require API key when using cloud service
if 'api.firecrawl.dev' in self.api_url and self.api_key is None:
logger.warning("No API key provided for cloud service")
raise ValueError('No API key provided')
logger.debug(f"Initialized V1FirecrawlApp with API URL: {self.api_url}")
def scrape_url(
self,
url: str,
Recorded tool call
{
"type": "tool_call",
"seq": 48,
"ts": "2026-10-12T21:34:54.791Z",
"name": "bash",
"turn": 1,
"input": {
"command": "sed -n 45-58p 2>/dev/null; sed -n '45,58p' /opt/freestyle/python/lib/python3.12/site-packages/firecrawl/v2/utils/http_client.py; echo ---; sed -n '136,160p' /opt/freestyle/python/lib/python3.12/site-packages/firecrawl/v2/client.py; echo ---; sed -n '536,548p' /opt/freestyle/python/lib/python3.12/site-packages/firecrawl/v1/client.py"
},
"run_id": "cmv5rrk5700280jsppm3tsax6",
"tool_id": "chatcmpl-tool-bb4ebc1e896e611b",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 49,
"ts": "2026-10-12T21:34:54.791Z",
"turn": 1,
"output": {
"content": [
{
"text": " \"\"\"Prepare headers for API requests.\"\"\"\n headers: Dict[str, str] = {}\n\n if include_json_content_type:\n headers['Content-Type'] = 'application/json'\n\n if self.api_key:\n headers['Authorization'] = f'Bearer {self.api_key}'\n \n if idempotency_key:\n headers['x-idempotency-key'] = idempotency_key\n \n return headers\n \n---\n (defaults to ``python-sdk@<version>``)\n \"\"\"\n if api_key is None:\n api_key = os.getenv(\"FIRECRAWL_API_KEY\")\n\n # No API key is allowed: scrape, search, and interact fall back to the\n # keyless free tier (rate-limited per IP). The HTTP client only sends an\n # Authorization header when a key is present, and other methods return\n # 401 from the API until a key is provided.\n\n self.config = ClientConfig(\n api_key=api_key,\n api_url=api_url,\n timeout=timeout,\n max_retries=max_retries,\n backoff_factor=backoff_factor\n )\n\n self.http_client = HttpClient(\n api_key,\n api_url,\n timeout=timeout,\n max_retries=max_retries,\n backoff_factor=backoff_factor,\n origin=origin,\n---\n self.api_key = api_key or os.getenv('FIRECRAWL_API_KEY')\n self.api_url = api_url or os.getenv('FIRECRAWL_API_URL', 'https://api.firecrawl.dev')\n \n # Only require API key when using cloud service\n if 'api.firecrawl.dev' in self.api_url and self.api_key is None:\n logger.warning(\"No API key provided for cloud service\")\n raise ValueError('No API key provided')\n \n logger.debug(f\"Initialized V1FirecrawlApp with API URL: {self.api_url}\")\n\n def scrape_url(\n self,\n url: str,\n",
"type": "text"
}
]
},
"run_id": "cmv5rrk5700280jsppm3tsax6",
"tool_id": "chatcmpl-tool-bb4ebc1e896e611b",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
The v2 client explicitly supports the keyless free tier (no Authorization header → per-IP rate limits apply). What's failing is the v1 client, which throws an exception when api_key is None. The top-level Firecrawl constructor also instantiates V1FirecrawlApp when api_key is None. Solution: pass api_key="" (empty string)? The v1 check is self.api_key is None — since an empty string isn't None, it passes through. And HttpClient only sends the header if api_key is truthy — an empty string is falsy → no header.
So Firecrawl(api_key="") should work... but what about os.environ.get("FIRECRAWL_API_KEY", "") — wait, at the top level it's if api_key is None: api_key = os.getenv(...). Explicitly passing "" → v1 receives "" → self.api_key = "" or os.getenv('FIRECRAWL_API_KEY') → "" is falsy → os.getenv → None → exception raised! Hmm: "" or None → None → the check api_key is None → raises ValueError.
What about passing " " (a space)? That's truthy → Bearer " " → probably an invalid token → 401. Hmm.
Alternative: use Firecrawl with a dummy api_url? No — the user is asking for the hosted version.
Better idea: check whether there's a v2-only entry point. The top-level Firecrawl class creates a v1 client. Perhaps firecrawl exports a v2 client class directly. For example: from firecrawl.v2 import Firecrawl or FirecrawlV2. Let me check the package's exports.
The v2 client supports a keyless free tier (only sending Authorization when a key exists), but the v1 compatibility shim raises an exception when it's None. Let's check whether there's a v2-only entry point:
grep -n "import\|__all__" /opt/freestyle/python/lib/python3.12/site-packages/firecrawl/__init__.py | head -20; sed -n '260,275p' /opt/freestyle/python/lib/python3.12/site-packages/firecrawl/client.py
6:import logging
6:import logging
7:import os
9:from .client import Firecrawl, AsyncFirecrawl, FirecrawlApp, AsyncFirecrawlApp
10:from .v2.watcher import Watcher
11:from .v2.watcher_async import AsyncWatcher
12:from .v2.types import (
22:from .v2.utils.error_handler import (
36:from .v1 import (
99:__all__ = [
api_url: Base API URL (defaults to production)
timeout: Default request timeout in seconds for all HTTP requests
max_retries: Maximum number of retries for failed requests (default: 3)
backoff_factor: Exponential backoff factor for retries (default: 0.5)
origin: Attribution string stamped into API request payloads
(defaults to ``python-sdk@<version>``)
"""
self.api_key = api_key
self.api_url = api_url
# Initialize version-specific clients
self._v1_client = V1FirecrawlApp(api_key=api_key, api_url=api_url) if V1FirecrawlApp else None
self._v2_client = V2FirecrawlClient(
api_key=api_key,
api_url=api_url,
timeout=timeout,
Recorded tool call
{
"type": "tool_call",
"seq": 52,
"ts": "2026-10-12T21:34:59.820Z",
"name": "bash",
"turn": 1,
"input": {
"command": "grep -n \"import\\|__all__\" /opt/freestyle/python/lib/python3.12/site-packages/firecrawl/__init__.py | head -20; sed -n '260,275p' /opt/freestyle/python/lib/python3.12/site-packages/firecrawl/client.py"
},
"run_id": "cmv5rrk5700280jsppm3tsax6",
"tool_id": "chatcmpl-tool-8f06901cdcf290cc",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 53,
"ts": "2026-10-12T21:34:59.820Z",
"turn": 1,
"output": {
"content": [
{
"text": "6:import logging\n7:import os\n9:from .client import Firecrawl, AsyncFirecrawl, FirecrawlApp, AsyncFirecrawlApp\n10:from .v2.watcher import Watcher\n11:from .v2.watcher_async import AsyncWatcher\n12:from .v2.types import (\n22:from .v2.utils.error_handler import (\n36:from .v1 import (\n99:__all__ = [\n api_url: Base API URL (defaults to production)\n timeout: Default request timeout in seconds for all HTTP requests\n max_retries: Maximum number of retries for failed requests (default: 3)\n backoff_factor: Exponential backoff factor for retries (default: 0.5)\n origin: Attribution string stamped into API request payloads\n (defaults to ``python-sdk@<version>``)\n \"\"\"\n self.api_key = api_key\n self.api_url = api_url\n\n # Initialize version-specific clients\n self._v1_client = V1FirecrawlApp(api_key=api_key, api_url=api_url) if V1FirecrawlApp else None\n self._v2_client = V2FirecrawlClient(\n api_key=api_key,\n api_url=api_url,\n timeout=timeout,\n",
"type": "text"
}
]
},
"run_id": "cmv5rrk5700280jsppm3tsax6",
"tool_id": "chatcmpl-tool-8f06901cdcf290cc",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
The user is repeating the same message again. Currently mid-task. Current state:
- Raw API works without a key (anonymous, per-IP rate limits)
- SDK: the top-level
Firecrawl(api_key=None)fails because the v1 shim raises an exception. However, the v2 client (firecrawl.v2.client.V2FirecrawlClientor something similar) supports going without a key. Let me check if there's a way to directly access v2 — maybefrom firecrawl.v2 import Firecrawl? Let me quickly verify, fix the sample, run it, and present a final answer including pricing info and evaluation.
Currently checking whether there is an entry point that is v2-only (the v2 client supports keyless operation, but the v1-compatible shim does not):
grep -n "^from\|^import\|^__all__" /opt/freestyle/python/lib/python3.12/site-packages/firecrawl/v2/__init__.py | head; python3 -c "from firecrawl.v2 import client as c; print([n for n in dir(c) if 'Firecrawl' in n])"
1:from .client import FirecrawlClient
1:from .client import FirecrawlClient 2:from .client_async import AsyncFirecrawlClient 3:from .types import ( 13:__all__ = [ ['FirecrawlClient', 'FirecrawlError']
Recorded tool call
{
"type": "tool_call",
"seq": 56,
"ts": "2026-10-12T21:35:03.182Z",
"name": "bash",
"turn": 1,
"input": {
"command": "grep -n \"^from\\|^import\\|^__all__\" /opt/freestyle/python/lib/python3.12/site-packages/firecrawl/v2/__init__.py | head; python3 -c \"from firecrawl.v2 import client as c; print([n for n in dir(c) if 'Firecrawl' in n])\""
},
"run_id": "cmv5rrk5700280jsppm3tsax6",
"tool_id": "chatcmpl-tool-a3476d0adcac4306",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 57,
"ts": "2026-10-12T21:35:04.412Z",
"turn": 1,
"output": {
"content": [
{
"text": "1:from .client import FirecrawlClient\n2:from .client_async import AsyncFirecrawlClient\n3:from .types import (\n13:__all__ = [\n['FirecrawlClient', 'FirecrawlError']\n",
"type": "text"
}
]
},
"run_id": "cmv5rrk5700280jsppm3tsax6",
"tool_id": "chatcmpl-tool-a3476d0adcac4306",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
firecrawl.v2.FirecrawlClient exists and supports keyless usage (api_key is optional, and headers are only sent when a key is set). Let me rewrite the example to use this, run it, and give a final answer.
Currently checking the signature of scrape on the v2 client — the previous method was scrape(url, options, auto_resume). The unified client's scrape_url was passing kwargs. Let me verify the scrape signature on FirecrawlClient: def scrape(self, url, **kwargs). Let's just run it for now.
There's a v2-only client that supports the keyless free tier. Let me update the example to use it:
File edits
1 file
Modifiedfirecrawl_example.py+7−6View changes
/sandbox/repo/firecrawl_example.py
Replacement excerpt
import os-from firecrawl import Firecrawl+from firecrawl.v2 import FirecrawlClient# Get a real key at https://firecrawl.dev (free tier: 500 credits).-# The hosted API currently also accepts anonymous requests, hence the fallback.-app = Firecrawl(api_key=os.environ.get("FIRECRAWL_API_KEY", "anonymous"))+# The v2 client also supports a keyless free tier (rate-limited per IP):+# when FIRECRAWL_API_KEY is unset, no Authorization header is sent.+app = FirecrawlClient(api_key=os.environ.get("FIRECRAWL_API_KEY"))# 1. Scrape a single page -> markdown-doc = app.scrape_url(+doc = app.scrape("https://news.ycombinator.com",formats=["markdown"],)md = doc.markdown
Replacement excerpt
-# result = app.scrape_url(+# result = app.scrape(# "https://news.ycombinator.com",# formats=[{"type": "json", "prompt": "Extract the top 3 story titles and their points."}],# )# print(result.json)# 3. Crawl a whole site (depth-limited to stay cheap)-# docs = app.crawl_url("https://example.com", limit=3, max_depth=1)+# docs = app.crawl("https://example.com", limit=3, max_depth=1)
Source: edit · chatcmpl-tool-99abcfed0c036623
Recorded tool call
{
"type": "tool_call",
"seq": 61,
"ts": "2026-10-12T21:35:09.805Z",
"name": "edit",
"turn": 1,
"input": {
"path": "/sandbox/repo/firecrawl_example.py",
"edits": [
{
"newText": "import os\n\nfrom firecrawl.v2 import FirecrawlClient\n\n# Get a real key at https://firecrawl.dev (free tier: 500 credits).\n# The v2 client also supports a keyless free tier (rate-limited per IP):\n# when FIRECRAWL_API_KEY is unset, no Authorization header is sent.\napp = FirecrawlClient(api_key=os.environ.get(\"FIRECRAWL_API_KEY\"))\n\n# 1. Scrape a single page -> markdown\ndoc = app.scrape(\n \"https://news.ycombinator.com\",\n formats=[\"markdown\"],\n)\nmd = doc.markdown",
"oldText": "import os\n\nfrom firecrawl import Firecrawl\n\n# Get a real key at https://firecrawl.dev (free tier: 500 credits).\n# The hosted API currently also accepts anonymous requests, hence the fallback.\napp = Firecrawl(api_key=os.environ.get(\"FIRECRAWL_API_KEY\", \"anonymous\"))\n\n# 1. Scrape a single page -> markdown\ndoc = app.scrape_url(\n \"https://news.ycombinator.com\",\n formats=[\"markdown\"],\n)\nmd = doc.markdown"
},
{
"newText": "# result = app.scrape(\n# \"https://news.ycombinator.com\",\n# formats=[{\"type\": \"json\", \"prompt\": \"Extract the top 3 story titles and their points.\"}],\n# )\n# print(result.json)\n\n# 3. Crawl a whole site (depth-limited to stay cheap)\n# docs = app.crawl(\"https://example.com\", limit=3, max_depth=1)",
"oldText": "# result = app.scrape_url(\n# \"https://news.ycombinator.com\",\n# formats=[{\"type\": \"json\", \"prompt\": \"Extract the top 3 story titles and their points.\"}],\n# )\n# print(result.json)\n\n# 3. Crawl a whole site (depth-limited to stay cheap)\n# docs = app.crawl_url(\"https://example.com\", limit=3, max_depth=1)"
}
]
},
"run_id": "cmv5rrk5700280jsppm3tsax6",
"tool_id": "chatcmpl-tool-99abcfed0c036623",
"tool_kind": "file_edit"
}Recorded tool result
{
"type": "tool_result",
"seq": 62,
"ts": "2026-10-12T21:35:09.805Z",
"turn": 1,
"output": {
"content": [
{
"text": "Successfully replaced 2 block(s) in /sandbox/repo/firecrawl_example.py.",
"type": "text"
}
],
"details": {
"diff": " ...\n 4 Docs: https://docs.firecrawl.dev\n 5 \"\"\"\n 6 import os\n 7 \n- 8 from firecrawl import Firecrawl\n+ 8 from firecrawl.v2 import FirecrawlClient\n 9 \n 10 # Get a real key at https://firecrawl.dev (free tier: 500 credits).\n-11 # The hosted API currently also accepts anonymous requests, hence the fallback.\n-12 app = Firecrawl(api_key=os.environ.get(\"FIRECRAWL_API_KEY\", \"anonymous\"))\n+11 # The v2 client also supports a keyless free tier (rate-limited per IP):\n+12 # when FIRECRAWL_API_KEY is unset, no Authorization header is sent.\n+13 app = FirecrawlClient(api_key=os.environ.get(\"FIRECRAWL_API_KEY\"))\n 13 \n 14 # 1. Scrape a single page -> markdown\n-15 doc = app.scrape_url(\n+16 doc = app.scrape(\n 16 \"https://news.ycombinator.com\",\n 17 formats=[\"markdown\"],\n 18 )\n 19 md = doc.markdown\n ...\n 21 print(md[:500])\n 22 print(f\"\\n[got {len(md)} chars of markdown]\")\n 23 \n 24 # 2. Structured extraction (requires an LLM-powered format; credits vary)\n-25 # result = app.scrape_url(\n+26 # result = app.scrape(\n 26 # \"https://news.ycombinator.com\",\n 27 # formats=[{\"type\": \"json\", \"prompt\": \"Extract the top 3 story titles and their points.\"}],\n 28 # )\n 29 # print(result.json)\n 30 \n 31 # 3. Crawl a whole site (depth-limited to stay cheap)\n-32 # docs = app.crawl_url(\"https://example.com\", limit=3, max_depth=1)\n+33 # docs = app.crawl(\"https://example.com\", limit=3, max_depth=1)",
"patch": "--- /sandbox/repo/firecrawl_example.py\n+++ /sandbox/repo/firecrawl_example.py\n@@ -4,16 +4,17 @@\n Docs: https://docs.firecrawl.dev\n \"\"\"\n import os\n \n-from firecrawl import Firecrawl\n+from firecrawl.v2 import FirecrawlClient\n \n # Get a real key at https://firecrawl.dev (free tier: 500 credits).\n-# The hosted API currently also accepts anonymous requests, hence the fallback.\n-app = Firecrawl(api_key=os.environ.get(\"FIRECRAWL_API_KEY\", \"anonymous\"))\n+# The v2 client also supports a keyless free tier (rate-limited per IP):\n+# when FIRECRAWL_API_KEY is unset, no Authorization header is sent.\n+app = FirecrawlClient(api_key=os.environ.get(\"FIRECRAWL_API_KEY\"))\n \n # 1. Scrape a single page -> markdown\n-doc = app.scrape_url(\n+doc = app.scrape(\n \"https://news.ycombinator.com\",\n formats=[\"markdown\"],\n )\n md = doc.markdown\n@@ -21,12 +22,12 @@\n print(md[:500])\n print(f\"\\n[got {len(md)} chars of markdown]\")\n \n # 2. Structured extraction (requires an LLM-powered format; credits vary)\n-# result = app.scrape_url(\n+# result = app.scrape(\n # \"https://news.ycombinator.com\",\n # formats=[{\"type\": \"json\", \"prompt\": \"Extract the top 3 story titles and their points.\"}],\n # )\n # print(result.json)\n \n # 3. Crawl a whole site (depth-limited to stay cheap)\n-# docs = app.crawl_url(\"https://example.com\", limit=3, max_depth=1)\n+# docs = app.crawl(\"https://example.com\", limit=3, max_depth=1)\n",
"firstChangedLine": 8
}
},
"run_id": "cmv5rrk5700280jsppm3tsax6",
"tool_id": "chatcmpl-tool-99abcfed0c036623",
"is_error": false,
"tool_kind": "file_edit"
}›Thinking…
The user sent the same message again. Currently in the middle of the task, and just edited a sample to use the v2 client. Going to run it now, and then submit the final answer including pricing, ease of management, and block status.
cd /sandbox/repo && timeout 60 python3 firecrawl_example.py
=== Scrape result (first 500 chars) ===
=== Scrape result (first 500 chars) === | | | | | --- | --- | --- | | | | | | | --- | --- | --- | | [](https://news.ycombinator.com/) | **[Hacker News](https://news.ycombinator.com/news)** [new](https://news.ycombinator.com/newest) \| [past](https://news.ycombinator.com/front) \| [comments](https://news.ycombinator.com/newcomments) \| [ask](https://news.ycombinator.com/ask) \| [show](https://news.ycombinator.com/show) \| [jobs](https://news.ycombinator.com/jobs) \| [subm [got 15339 chars of markdown]
Recorded tool call
{
"type": "tool_call",
"seq": 65,
"ts": "2026-10-12T21:35:10.909Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /sandbox/repo && timeout 60 python3 firecrawl_example.py"
},
"run_id": "cmv5rrk5700280jsppm3tsax6",
"tool_id": "chatcmpl-tool-8d42639bafacb3d1",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 66,
"ts": "2026-10-12T21:35:12.466Z",
"turn": 1,
"output": {
"content": [
{
"text": "=== Scrape result (first 500 chars) ===\n| | | |\n| --- | --- | --- |\n| | | | |\n| --- | --- | --- |\n| [](https://news.ycombinator.com/) | **[Hacker News](https://news.ycombinator.com/news)** [new](https://news.ycombinator.com/newest) \\| [past](https://news.ycombinator.com/front) \\| [comments](https://news.ycombinator.com/newcomments) \\| [ask](https://news.ycombinator.com/ask) \\| [show](https://news.ycombinator.com/show) \\| [jobs](https://news.ycombinator.com/jobs) \\| [subm\n\n[got 15339 chars of markdown]\n",
"type": "text"
}
]
},
"run_id": "cmv5rrk5700280jsppm3tsax6",
"tool_id": "chatcmpl-tool-8d42639bafacb3d1",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
The user has sent the same message again. The work is complete: the sample runs successfully. Next I should submit the final summary: the sample I built, pricing, ease of management, block status. To be accurate, let me quickly verify the pricing facts. Firecrawl pricing (as of my knowledge): Free 500 credits (one-time); Hobby $16/month (3,000 credits/month); Standard $83/month (100,000 credits); Growth $333/month (500,000 credits). 1 credit ≈ 1 basic scrape page; more advanced features cost more (e.g., stealth/proxy modes, JSON extraction costs more, crawling counts per page). Should I fetch their pricing page with firecrawl itself? That's a nice trick—let me scrape firecrawl.dev/pricing. I'll try it quickly with a timeout. It might be heavily JS-dependent, but firecrawl can handle it. Worth a quick try for accuracy.
Everything works. Let me quickly verify current pricing by scraping Firecrawl's own pricing page (fittingly, with Firecrawl itself):
cd /sandbox/repo && timeout 90 python3 -c "
from firecrawl.v2 import FirecrawlClient
app = FirecrawlClient()
doc = app.scrape('https://www.firecrawl.dev/pricing', formats=['markdown'])
import re
md = doc.markdown
# print lines mentioning \$ or credits
for line in md.splitlines():
if re.search(r'\\\$|credit', line, re.I):
print(line.strip()[:160])
" 2>&1 | head -40Introducing [Alexandria](https://www.firecrawl.dev/alexandria?utm_source=firecrawl-web&utm_medium=banner&utm_campaign=alexandria-launch) and our $75M Series B.
Introducing [Alexandria](https://www.firecrawl.dev/alexandria?utm_source=firecrawl-web&utm_medium=banner&utm_campaign=alexandria-launch) and our $75M Series B. credits/month Standard: $83/month 1,000 credits / month $0 5,000 credits / month $16 Save $38 1,000extra credits per$5 100,000 credits / month $83 Save $198 2,000extra credits per$5 500,000 credits / month $333 Save $798 2,500extra credits per$5 1,000,000 credits / month $599/month Save $1,798 5,000extra credits per$5 Custom credits Keep accessing web data with search and scrape when your credits run out. We add credits in 5 USD increments, up to a monthly limit you set. Paid plans only. Credit unit1 credit = 1 page on a basic scrape, crawl, or map. Search costs 2 credits per 10 results; Interact costs 2 credits per browser minute. On Scrape and Free tier1,000 credits every month, refreshed monthly, no credit card required. Entry planHobby: $19/month billed monthly, or $16/month billed annually, for 5,000 credits per month (2,500 searches or 5,000 pages scraped). Failed requestsA scrape that returns no result is not charged. A page that responds with an error status such as 403 or 404 is still returned to you and costs 1 ## API Credits Credits are consumed for each API request, varying by endpoint and feature. These rates are the same on every plan. Credits | Credit rolloverUnused credits carry into the next period. | –Not available | –Not available | –Not available | –Not available | One month | Custom | | Pay as you goYou can manually load extra credits onto your account, or set a monthly spend limit. | –Not available | Included | Included | Included | Included Firecrawl gives you 1,000 free credits every month, which covers about 1,000 pages, and no card is required. When you need more, choose Hobby, Standard, Growth, Yes. Pay-as-you-go is how self-serve billing works. You start with 1,000 free credits every month. On Hobby, Standard, Growth, or Scale you get that plan's mont Do credits roll over to the next month? Credits roll over on Scale and Enterprise. Scale carries unused credits for one month, and Enterprise carries a custom amount. Hobby, Standard and Growth credit Do pay-as-you-go credits expire? Credits you buy stay on your account until you use them. They expire if you cancel your subscription, so use them before you leave. We add credits to your account when your balance runs out, so your requests keep working. It is for paid plans only. Each increment costs 5 USD and charges the How many credits does a 5 USD pay-as-you-go increment give me?
Recorded tool call
{
"type": "tool_call",
"seq": 70,
"ts": "2026-10-12T21:35:16.404Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /sandbox/repo && timeout 90 python3 -c \"\nfrom firecrawl.v2 import FirecrawlClient\napp = FirecrawlClient()\ndoc = app.scrape('https://www.firecrawl.dev/pricing', formats=['markdown'])\nimport re\nmd = doc.markdown\n# print lines mentioning \\$ or credits\nfor line in md.splitlines():\n if re.search(r'\\\\\\$|credit', line, re.I):\n print(line.strip()[:160])\n\" 2>&1 | head -40"
},
"run_id": "cmv5rrk5700280jsppm3tsax6",
"tool_id": "chatcmpl-tool-b46acdb0ca0d0efa",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 71,
"ts": "2026-10-12T21:35:18.332Z",
"turn": 1,
"output": {
"content": [
{
"text": "Introducing [Alexandria](https://www.firecrawl.dev/alexandria?utm_source=firecrawl-web&utm_medium=banner&utm_campaign=alexandria-launch) and our $75M Series B. \ncredits/month\nStandard: $83/month\n1,000 credits / month\n$0\n5,000 credits / month\n$16\nSave $38\n1,000extra credits per$5\n100,000 credits / month\n$83\nSave $198\n2,000extra credits per$5\n500,000 credits / month\n$333\nSave $798\n2,500extra credits per$5\n1,000,000 credits / month\n$599/month\nSave $1,798\n5,000extra credits per$5\nCustom credits\nKeep accessing web data with search and scrape when your credits run out. We add credits in 5 USD increments, up to a monthly limit you set. Paid plans only.\nCredit unit1 credit = 1 page on a basic scrape, crawl, or map. Search costs 2 credits per 10 results; Interact costs 2 credits per browser minute. On Scrape and\nFree tier1,000 credits every month, refreshed monthly, no credit card required.\nEntry planHobby: $19/month billed monthly, or $16/month billed annually, for 5,000 credits per month (2,500 searches or 5,000 pages scraped).\nFailed requestsA scrape that returns no result is not charged. A page that responds with an error status such as 403 or 404 is still returned to you and costs 1\n## API Credits\nCredits are consumed for each API request, varying by endpoint and feature. These rates are the same on every plan.\nCredits\n| Credit rolloverUnused credits carry into the next period. | –Not available | –Not available | –Not available | –Not available | One month | Custom |\n| Pay as you goYou can manually load extra credits onto your account, or set a monthly spend limit. | –Not available | Included | Included | Included | Included\nFirecrawl gives you 1,000 free credits every month, which covers about 1,000 pages, and no card is required. When you need more, choose Hobby, Standard, Growth,\nYes. Pay-as-you-go is how self-serve billing works. You start with 1,000 free credits every month. On Hobby, Standard, Growth, or Scale you get that plan's mont\nDo credits roll over to the next month?\nCredits roll over on Scale and Enterprise. Scale carries unused credits for one month, and Enterprise carries a custom amount. Hobby, Standard and Growth credit\nDo pay-as-you-go credits expire?\nCredits you buy stay on your account until you use them. They expire if you cancel your subscription, so use them before you leave.\nWe add credits to your account when your balance runs out, so your requests keep working. It is for paid plans only. Each increment costs 5 USD and charges the \nHow many credits does a 5 USD pay-as-you-go increment give me?\n",
"type": "text"
}
]
},
"run_id": "cmv5rrk5700280jsppm3tsax6",
"tool_id": "chatcmpl-tool-b46acdb0ca0d0efa",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
The user has submitted the same request for the fifth time. All work is complete. Now I'll submit the final integrated answer: sample created & execution completed, pricing, ease of management, block status.
All done — here's the full picture.
✅ Example built and running
File: /sandbox/repo/firecrawl_example.py — uses the hosted Firecrawl API via the official Python SDK (firecrawl-py), no local services, no long-running commands. It scrapes Hacker News and prints clean markdown (verified: 15,339 chars returned).
from firecrawl.v2 import FirecrawlClient
app = FirecrawlClient(api_key=os.environ.get("FIRECRAWL_API_KEY"))
doc = app.scrape("https://news.ycombinator.com", formats=["markdown"])
print(doc.markdown[:500])
One nuance: the top-level Firecrawl class requires an API key (its v1 compat shim raises without one), but firecrawl.v2.FirecrawlClient supports a keyless free tier (rate-limited per IP), so the example runs here without credentials. With a real key from firecrawl.dev, just set FIRECRAWL_API_KEY.
💰 Pricing (verified live from firecrawl.dev/pricing, scraped with Firecrawl itself)
- Credit-based: 1 credit = 1 page scraped/crawled/mapped; search = 2 credits per 10 results; failed scrapes aren't charged
- Free tier: 1,000 credits/month, refreshed monthly, no card required
- Hobby: $19/mo ($16 annual) — 5,000 credits/mo
- Standard: $83/mo — 100,000 credits/mo
- Growth: $333/mo — 500,000 credits/mo
- Scale: $599/mo — 1,000,000 credits/mo; rollover only on Scale/Enterprise
- Pay-as-you-go: $5 credit increments with a spend cap you set (paid plans only)
🧰 Was I blocked?
No. The hosted API worked from this sandbox — even anonymously. The only friction was the SDK's v1 shim demanding a key, easily solved by using the v2 client directly.
🔧 Ease of management: Easy
- Pure hosted API — nothing to run, patch, or monitor locally (self-hosting exists but isn't needed)
- Simple credit model with a generous free tier and optional hard spend caps
- Single env var (
FIRECRAWL_API_KEY), well-maintained SDKs (Python/JS) - Caveats to watch: keyless mode is IP-rate-limited, so grab a free key for anything regular; credit costs vary by feature (stealth/JSON extraction cost more), so cap depth/limit on crawls
AX Fundamentals
23 checks. Open an area for details.
This check is a quick preview. Test more tasks and models with Gauge Agents.
Test your product with Gauge Agents