Back to blog

Best Exa Alternatives For AI Agents

Share article:

Exa is a neural search API for AI agents. It matches queries to pages by meaning, using embeddings, and returns their contents. It belongs to the retrieval layer, next to search APIs and web data APIs that also crawl and extract. Teams compare Exa alternatives when an agent needs every page of a documentation site or pages behind bot protection.

Icon of a magnifying glass inside a rounded square, with a wavy line extending from the square’s lower-left corner.

TL;DR

  • Shortlist. Decodo's Web Data API, Firecrawl, Tavily, Bright Data, Linkup, Perplexity, and Brave Search API.
  • Know the difference. Search APIs return ranked links with snippets or page text. Web data APIs also map, crawl, and scrape full pages.
  • Test access early. An index-based API can only serve what it stored earlier, so for protected and JavaScript-heavy pages it may silently return empty or partial content.
  • Default choice. For live answers alone, a search API or a model's built-in web search is enough. For whole sites or protected pages, Decodo's Web Data API is the pick, and it also searches. Our Exa-compatible client lets you switch with one import change and a Decodo key.

What Exa does and where it stops

Exa's strengths and limits show whether you need an alternative.

What Exa is built for

Exa works best for natural-language agent prompts and research-style discovery, where keyword search misses useful pages. Keyword search still works better for exact terms, such as product names, error codes, and SKUs.

Exa's API includes Search, Deep Search, Deep-Reasoning Search, Contents, Monitors, and an Agent API. Exa's pricing page lists $7 per 1K searches with the default auto type and $12 to $15 per 1K deep searches. Contents cost $1 per 1K pages for each content type, and monitors cost $15 per 1K requests.

Where teams need more than Exa

  • No map or crawl endpoint. Exa's Contents endpoint can crawl up to 100 subpages of a page, but Exa has no single call that lists every URL on a site. For retrieval-augmented generation (RAG), full-site ingestion still needs a crawler you build and run. Our guide to crawling vs. scraping explains the difference.
  • Cached page contents. Exa finds new pages quickly, but by default, its Contents endpoint can return a cached copy. For prices and other fast-changing pages, that copy can be hours old unless you set maxAgeHours to 0.
  • Access matters more than search quality. Better semantic matching can't help when bot protection blocks the page.
  • Cost structure. Exa charges separately for searches, for each type of content it returns per page, and for summaries. Retries in an agent loop multiply these charges.

How to evaluate an Exa alternative

Built-in web search in Claude, OpenAI, and Gemini is often enough for a chat answer. A separate API helps when the agent needs to crawl a site, reach protected pages, or work across model vendors.

  • Endpoints your agent calls. Search ranks results for a query, map lists a site's URLs, crawl returns a site's pages, and scrape returns one page. A support bot needs search alone to answer from live results, and a RAG pipeline needs map plus crawl to ingest a documentation site. Rebuilding map and crawl with search plus a custom crawler is often the hardest part of migrating.
  • Index-based vs. live fetch. Index-based APIs answer from pages they stored earlier, so results are only as complete and current as their index. Live-fetch APIs load the page when you call them, unless a cache returns an older copy. To test this, compare a fetched price page with the live one.
  • Hard-target access. Ask what happens on sites with bot protection or geo-restrictions. Vendors can add endpoints quickly, but not a large proxy network, so weigh access above endpoint count.
  • Output format and token cost. Raw HTML wastes tokens on tags and scripts. Markdown or structured JSON avoids most of that cost.
  • Latency. Interactive agents depend on p95 latency, not on the average, and on generous concurrency limits.
  • Agent integrations. A Model Context Protocol (MCP) server works with LangChain, LlamaIndex, the OpenAI Agents SDK, and most coding agents, so you integrate once for many frameworks. See also our roundup of AI data collection tools and our list of MCP servers for AI workflows.
  • Compare pricing models, not list prices. Credits, per-result charges, and monthly minimums make list prices hard to compare, so compare the cost per usable page.

Rate each API on what your agent does at runtime, not on features. Exclude any that lack an endpoint your agent calls, then compare access and cost on your hardest URLs.

The best Exa alternatives in 2026

*Prices are from each vendor's pricing page as of October 2026.

1. Decodo Web Data API

  • What it is. Decodo's Web Data API offers search, scrape, map, and crawl with one API key and routes requests through residential and datacenter proxies. /search returns live Google results, /map lists a site's URLs in one call, and /crawl collects the pages as markdown, with optional JavaScript rendering. The same key works with the Decodo MCP server, and the Playground's Copy agent setup prompt adds Decodo's CLI and agent skill to coding agents.
Decodo Web Data API dashboard
  • How it compares to Exa. Decodo handles discovery, crawling, and scraping with one key, and requests can target a specific country. Exa is a search API with content retrieval added. With Decodo's proxies, more protected pages load, as the benchmark shows:

Benchmark (same pages, 2 runs)

Decodo Web Data API

Exa

Page content returned (50 pages, 100 fetches)

93%

72%

Page content returned, bot-protected pages (20 pages, 40 fetches)

85%

55%

Correct local price on Steam, 3 countries (30 checks)

30 of 30

No country option

Both APIs fetched the same pages at the same time, with Exa on its default settings, and only real page content counted.

  • When to choose it over Exa. Choose it when the agent ingests whole sites, when many targets are behind bot protection, or when it can replace your custom crawler. A callback_url notifies you when a crawl finishes. The pricing page lists current rates, and the security and compliance page covers certifications and how residential IPs are sourced.

Four verbs, one API

Web Data API searches, scrapes, crawls, and maps sites, then hands your agent clean Markdown.

2. Firecrawl

  • What it is. Firecrawl is a web data API with scrape, map, crawl, search, and schema-based extraction endpoints. It has an open-source core and framework integrations.
  • How it compares to Exa. Firecrawl is built to ingest sites you already know, and Exa is built to find sources you don't know yet.
  • When to choose it over Exa. Choose it for documentation ingestion, building knowledge bases, and workflows where the URL list is already known. Firecrawl's pricing includes 1K free credits a month, and the Hobby plan costs $19 a month.
  • What to check. By default, Firecrawl can return a cached copy that's up to 2 days old. For prices and stock levels, set maxAge to 0.

3. Tavily

  • What it is. Tavily is a search API built for agents. It returns short, ranked, LLM-ready results with low latency, and has extract, crawl, and map endpoints.
  • How it compares to Exa. Tavily optimizes for fast, clean answers to each query, while Exa optimizes for finding relevant pages across a large index.
  • When to choose it over Exa. Choose it for interactive agents where response time matters most and cost per query needs to be predictable. Tavily's pricing is $0.008 per credit pay-as-you-go, with a $30 monthly plan and a free plan of 1K credits a month.
  • What to check. In a test on 20 bot-protected pages, Tavily Extract returned page content for 6.

4. Bright Data

  • What it is. Bright Data is a large proxy and data platform. Its AI-focused products include a search results (SERP) API, an Unlocker API, a Crawl API that maps and crawls sites, datasets, and an MCP server.
  • How it compares to Exa. Bright Data has a broader scope and built-in unblocking. More products can mean more setup for small agent projects.
  • When to choose it over Exa. Choose it for enterprise data programs where datasets and compliance tools matter more than a simple API. Bright Data's pricing lists its SERP API at $1.50 per 1K requests pay-as-you-go, with 5K free a month.

5. Linkup

  • What it is. Linkup is a search API with standard and deep search modes, structured output, and a fetch endpoint.
  • How it compares to Exa. Like Exa, it supports company research and search across premium publisher sources, and it costs less than Exa's default search. Linkup's pricing lists standard search at $5 per 1K and deep search at $50 to $55 per 1K.
  • When to choose it over Exa. Choose it for company research, where you need enriched data rather than page content.

6. Perplexity Search API and Agent API

  • What it is. Perplexity's Agent API, which replaced Sonar Chat Completions, returns an answer with citations. The Search API returns ranked results with page text.
  • How it compares to Exa. Exa gives the agent sources to analyze, and Perplexity's Agent API gives it a conclusion. That saves a reasoning step, but you control less of what the agent reads.
  • When to choose it over Exa. Choose it for assistant features where a cited answer is the final output. Perplexity's pricing lists the Search API at $5 per 1K requests.
  • What to check. Perplexity has no map or crawl endpoint.

7. Brave Search API

  • What it is. Brave Search API uses its own index, with an LLM Context endpoint for page chunks.
  • How it compares to Exa. Brave ranks results mainly by keywords, Exa ranks them by meaning, and Brave costs less per request than Exa's default search.
  • When to choose it over Exa. Choose it for high-volume, cost-sensitive lookups where standard ranked results are enough. Brave's pricing is $5 per 1K requests, with $5 in free credits a month.

Exa alternatives compared

Shortlist by the endpoints your agent calls, then by hard-target access.

Tool

Endpoints

Retrieval model

Hard-target access

Pricing model

Best for

Exa

Search, Contents, Monitors, Agent

Neural index

55% in our benchmark

Per search and content type

Semantic discovery

Decodo Web Data API

Search, map, crawl, scrape

Live fetch

Residential and datacenter proxies, 85% in our benchmark

Per 1K requests, one API key

Full sites and protected targets

Firecrawl

Scrape, map, crawl, search, extract

Live fetch, cached by default

Built-in, proxy modes

Credits, by plan

Ingesting known sites

Tavily

Search, Extract, Crawl, Map

Index plus live Extract

Partly, through Extract

Credits, by plan

Low-latency agent search

Bright Data

SERP, Unlocker, Crawl, datasets

Live fetch

Built in

Per 1K requests, by product

Enterprise data collection

Linkup

Search, deep search, fetch

Index plus enrichment

Partly, through fetch

Per 1K searches

Company research

Perplexity

Search API, Agent API

Index plus answers

Not built in

Per 1K requests, or tokens

Cited answers

Brave Search API

Search, LLM Context

Independent index

Not built in

Per 1K requests

Low-cost lookups

Migrating off Exa without rewriting your agent

  • Map each Exa call to an endpoint. Replace Exa Search with a search endpoint and Exa Contents with scrape. Exa has no map or full-site crawl, so a replacement with both lets you delete the crawler you built around it. Keep retrieval behind one internal interface, so a vendor change affects only one adapter.
  • Keep your Exa code. The full code at the end includes decodo_exa.py, a client that matches the search, contents, and agent tool methods of Exa's Python SDK. The calls below keep working after you change one import and add a key from the free plan:
from decodo_exa import Exa # was: from exa_py import Exa
exa = Exa() # reads DECODO_AUTH_TOKEN
results = exa.search("tutorial first steps", num_results=5,
include_domains=["fastapi.tiangolo.com"], contents={"text": True})
for r in results.results:
print(r.title, r.url, len(r.text or ""))
page = exa.get_contents(["https://books.toscrape.com/"], text=True).results[0]
print(page.url, len(page.text), "characters of markdown")
urls = exa.map("https://docs.docker.com/") # A site's URLs in one call
print(len(urls), "URLs found by map()")

The client's search() returns live Google results, and get_contents() fetches each page live. Agent loops using the exa.anthropic or exa.openai tool helpers keep working too. You can switch in stages, starting with the crawler.

  • Compare the cost, not the price. Calculate what last month's traffic would cost under each pricing model, counting searches, pages, content types, and retries. Ask each vendor whether a failed fetch is billed.
  • Run a retrieval evaluation before you switch. Run 50 production queries through both APIs with the same model and prompt, and measure answer accuracy and pages loaded. Measure coverage separately from relevance, because fewer complete pages often work better than more partial ones. Let search return snippets, and fetch only pages the agent picks, since 10 full pages can overload the model's context.

Our guide to building a production RAG pipeline with LlamaIndex covers the pipeline after retrieval. Our guide to setting up an MCP server shows how to connect the API to an agent.

Which Exa alternative fits your project

Find your use case below.

RAG over a documentation or knowledge site. You need map plus crawl, not better search. Choose Decodo's Web Data API or Firecrawl, depending on how protected the targets are. On 4 large documentation sites, one Decodo /map call found 1.7 to 5 times as many sitemap pages as 10 Exa searches.

On Docker's documentation, /map found all 2,150 sitemap pages, while Exa found 422. On 4 small sites, both found nearly every page, so the gap grows with site size.

Live search inside an IDE or agent. Latency and cost per query matter most. Decodo's MCP server adds live search with the same key, so there's no second integration. Tavily is another option.

Research over regulator, court, or archive sites. Map the document set with Decodo's Web Data API, then crawl only the pages you need.

Price and competitive intelligence. Schedule Decodo's map plus crawl over catalog pages, since each blocked page is a gap in your dataset. For a custom crawler, use Decodo's residential proxies. Our guide to n8n and Decodo MCP orchestration shows a scheduled version.

Decodo's Web Data API also has targets for ChatGPT, Perplexity, Gemini, and Google AI Mode, so you can track what they say about your brand.

Full code for the migration example

Both files need Python 3.10 or later and a Web Data API key, which the free plan includes. The Web Data API documentation lists every parameter. Install the dependency and set your key:

pip install httpx
export DECODO_AUTH_TOKEN="your-api-key" # Windows PowerShell: $env:DECODO_AUTH_TOKEN="your-api-key"

Save the Exa-compatible client as decodo_exa.py:

"""An Exa-compatible client backed by Decodo's Web Data API.
Change one import, from `from exa_py import Exa` to `from decodo_exa import Exa`, and calls to search(),
get_contents(), and the agent tool helpers in exa.anthropic and exa.openai keep working. map() and crawl()
add site discovery and crawling.
"""
import json
import os
import re
import time
import warnings
from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass, field
from datetime import date
import httpx
BLOCK = re.compile(r"(?i)captcha|access denied|verify you are human|enable javascript|unusual traffic")
LINK_TARGETS = re.compile(r"!\[[^\]]*\]\([^)]*\)|(?<=\])\([^)]*\)") # Images and link URLs, which aren't readable text
H1 = re.compile(r"(?m)^# \S")
MIN_PAGE_CHARS = 2_000 # Below this, the site served a snippet, an error page, or a page that needs rendering
MAX_TOOL_CHARS = 10_000 # Page text per result sent to the model, the same default as exa_py's tools
RETRY_STATUS = {429, 500, 502, 503, 504}
SEARCH_SCHEMA = {"type": "object", "required": ["query"], "properties": {
"query": {"type": "string", "description": "Natural language description of the pages to find."}}}
CONTENTS_SCHEMA = {"type": "object", "required": ["urls"], "properties": {
"urls": {"type": "array", "items": {"type": "string"}, "description": "Absolute URLs of the pages to read."}}}
@dataclass
class Result:
url: str
id: str = ""
title: str = ""
text: str | None = None
highlights: list[str] = field(default_factory=list)
published_date: str | None = None
@dataclass
class ContentStatus:
id: str
status: str # "success", or "error" when no complete page was returned
@dataclass
class SearchResponse:
results: list[Result]
statuses: list[ContentStatus] = field(default_factory=list)
def page_is_complete(text: str) -> bool:
# Count readable text only, because a page shell full of menu links can be long and still have no content
return not BLOCK.search(text[:5_000]) and len(LINK_TARGETS.sub("", text)) >= MIN_PAGE_CHARS
def main_content(text: str | None) -> str | None:
# Many docs and article pages put the site menu before the H1 heading, so start the text at the first H1
match = H1.search(text or "")
return text[match.start():] if match else text
def _warn_ignored(options: dict) -> None:
if options:
warnings.warn(f"decodo_exa doesn't use these options: {', '.join(sorted(options))}", stacklevel=3)
def _get(obj, name: str, default=None):
# Read a field from an SDK response object or from a plain dict
return obj.get(name, default) if isinstance(obj, dict) else getattr(obj, name, default)
def _format(response: SearchResponse) -> str:
blocks = []
for r in response.results:
if r.text:
body = f"Text: {r.text[:MAX_TOOL_CHARS]}"
else:
body = "Highlights:\n" + "\n".join(r.highlights) if r.highlights else "Text: N/A"
blocks.append(f"Title: {r.title or 'N/A'}\nURL: {r.url}\n{body}")
return "\n\n---\n\n".join(blocks) or "No results found."
class AgentTools:
"""Tool definitions and tool-call handling for agent loops, with the method names of exa_py's helpers."""
def __init__(self, exa: "Exa", style: str, handlers: dict | None = None):
self.exa, self.style = exa, style
self.handlers = {} if handlers is None else handlers
@property
def responses(self) -> "AgentTools":
"""Tools in the format of OpenAI's Responses API, as in exa.openai.responses."""
return AgentTools(self.exa, "responses", self.handlers)
def web_search(self, name: str = "web_search", num_results: int = 10, **_) -> dict:
return self._tool(name, "Search the web for up-to-date, relevant information.", SEARCH_SCHEMA,
lambda args: self.exa.search(args["query"], num_results, contents={"highlights": True}))
def get_contents(self, name: str = "get_contents", **_) -> dict:
return self._tool(name, "Read the full contents of web pages you already have URLs for.", CONTENTS_SCHEMA,
lambda args: self.exa.get_contents(args["urls"]))
def handle_tool_use(self, message) -> list[dict]:
"""Anthropic Messages API: answer each tool_use block with a tool_result block."""
return [{"type": "tool_result", "tool_use_id": _get(b, "id"),
"content": self.run(_get(b, "name"), _get(b, "input"))}
for b in _get(message, "content", []) if _get(b, "type") == "tool_use"]
def handle_tool_calls(self, message) -> list[dict]:
"""OpenAI: answer each tool call, from a Chat Completions message or a Responses API response."""
items = message if isinstance(message, list) else _get(message, "output")
if items is not None:
return [{"type": "function_call_output", "call_id": _get(i, "call_id"),
"output": self.run(_get(i, "name"), _get(i, "arguments"))}
for i in items if _get(i, "type") == "function_call"]
calls = [(c, _get(c, "function")) for c in _get(message, "tool_calls") or []]
return [{"role": "tool", "tool_call_id": _get(c, "id"),
"content": self.run(_get(f, "name"), _get(f, "arguments"))} for c, f in calls]
def run(self, name: str, args) -> str:
if name not in self.handlers:
return f'Error: unknown tool "{name}"'
try:
return _format(self.handlers[name](json.loads(args) if isinstance(args, str) else args))
except Exception as exc: # Send the error to the model as text, so the agent loop continues
return f"Error: {exc}"
def _tool(self, name: str, description: str, schema: dict, handler) -> dict:
self.handlers[name] = handler
if self.style == "anthropic":
return {"name": name, "description": description, "input_schema": schema}
if self.style == "responses":
return {"type": "function", "name": name, "description": description, "parameters": schema, "strict": False}
return {"type": "function", "function": {"name": name, "description": description, "parameters": schema}}
class Exa:
"""Same method names as exa_py.Exa for search(), get_contents(), and the agent tool helpers."""
def __init__(self, api_key: str | None = None, geo: str = "United States", timeout: float = 150.0):
key = api_key or os.environ.get("DECODO_AUTH_TOKEN")
if not key:
raise RuntimeError("Set DECODO_AUTH_TOKEN to your Web Data API key. The free plan includes one: "
"https://decodo.com/web-data-api")
self.geo = geo
self.http = httpx.Client(base_url="https://data.decodo.com/v1", timeout=timeout,
headers={"Authorization": f"Bearer {key}"})
self.anthropic, self.openai = AgentTools(self, "anthropic"), AgentTools(self, "openai")
def _post(self, path: str, body: dict, attempts: int = 3) -> dict:
return self._request("POST", path, body, attempts)
def _request(self, method: str, path: str, body: dict | None = None, attempts: int = 3) -> dict:
for attempt in range(1, attempts + 1):
try:
resp = self.http.request(method, path, json=body)
resp.raise_for_status()
data = resp.json()
if data.get("status") in ("failed", "faulted"):
raise RuntimeError(f"{path} failed: {data.get('message')}")
return data
except (httpx.TransportError, httpx.HTTPStatusError, RuntimeError) as exc:
if isinstance(exc, httpx.HTTPStatusError) and exc.response.status_code == 401:
raise PermissionError("The Web Data API rejected the key. Check DECODO_AUTH_TOKEN.") from exc
if isinstance(exc, httpx.HTTPStatusError) and exc.response.status_code not in RETRY_STATUS:
raise # Don't retry a bad request or a bad key
if attempt == attempts:
raise
time.sleep(2 ** attempt) # Wait 2 s, then 4 s
def search(self, query: str, num_results: int = 10, include_domains: list[str] | None = None,
exclude_domains: list[str] | None = None, start_published_date: str | None = None,
end_published_date: str | None = None, contents: dict | None = None, **options) -> SearchResponse:
_warn_ignored(options)
if include_domains:
query += " (" + " OR ".join(f"site:{d}" for d in include_domains) + ")"
query += "".join(f" -site:{d}" for d in exclude_domains or [])
body = {"target": "google_search", "query": query, "parse": True, "geo": self.geo, "google_nfpr": True}
tbs = ["cdr:1"] # Google's custom date range
for edge, value in (("min", start_published_date), ("max", end_published_date)):
if value:
d = date.fromisoformat(value[:10])
tbs.append(f"cd_{edge}:{d.month}/{d.day}/{d.year}")
if len(tbs) > 1:
body["google_tbs"] = ",".join(tbs)
results, page = [], 1
while len(results) < num_results and page <= 10:
data = self._post("/scrape", {**body, "page_from": page})
organic = data["results"][0]["content"]["results"]["results"].get("organic", [])
if not organic:
break
results += [Result(url=o["url"], id=o["url"], title=o.get("title", ""),
highlights=[o.get("desc", "").removesuffix("Read more")]) for o in organic]
page += 1
results = results[:num_results]
if contents is None or (contents and contents.get("text")): # Like exa_py, omitting contents returns text
for r, (page_text, _) in zip(results, self._fetch_many([r.url for r in results])):
r.text = page_text
return SearchResponse(results)
def get_contents(self, urls: list[str] | str, text: bool = True, **options) -> SearchResponse:
_warn_ignored(options)
urls = [urls] if isinstance(urls, str) else urls
pages = self._fetch_many(urls)
return SearchResponse([Result(url=u, id=u, text=t) for u, (t, _) in zip(urls, pages)],
[ContentStatus(u, "success" if ok else "error") for u, (_, ok) in zip(urls, pages)])
def map(self, url: str, **params) -> list[str]:
"""A site's URLs from its sitemap, in one call."""
data = self._post("/map", {"url": url, **params})
return [link["url"] for link in data["results"][0]["content"].get("links", [])]
def crawl(self, url: str, limit: int = 100, poll_seconds: int = 5, timeout_seconds: int = 1800,
**params) -> list[Result]:
"""Crawl a site and return each page as markdown. Add callback_url to params to get notified instead."""
job = self._post("/crawl", {"url": url, "limit": limit, "output": "markdown", "JS_retry": True, **params})
deadline = time.monotonic() + timeout_seconds
while self._request("GET", f"/crawl/{job['crawl_id']}/status").get("status") != "done":
if time.monotonic() > deadline:
raise TimeoutError(f"crawl {job['crawl_id']} is still running after {timeout_seconds} s")
time.sleep(poll_seconds)
pages, url = [], f"/crawl/{job['crawl_id']}?type=content"
while url: # Results arrive in batches, each with a link to the next one
batch = self._request("GET", url)
pages += batch.get("url", [])
url = batch.get("next")
return [Result(url=p["url"], id=p["url"], text=main_content(p.get("content")))
for p in pages if p.get("status") == "done"]
def _fetch_many(self, urls: list[str]) -> list[tuple[str, bool]]:
with ThreadPoolExecutor(5) as pool:
return list(pool.map(self._fetch, urls))
def _fetch(self, url: str) -> tuple[str, bool]:
# Try the lowest-cost settings first, and add JavaScript rendering only for pages that need it
steps = [{"proxy_pool": "standard"}, {"proxy_pool": "premium"}, {"proxy_pool": "premium", "headless": "html"}]
text = ""
for i, options in enumerate(steps):
body = {"url": url, "markdown": True, "geo": self.geo, **options}
try:
data = self._post("/scrape", body, attempts=3 if i == len(steps) - 1 else 1)
except httpx.HTTPStatusError as exc:
if exc.response.status_code != 400:
continue
body.pop("geo")
try:
data = self._post("/scrape", body, attempts=1)
except (httpx.HTTPError, RuntimeError):
continue
except (httpx.TransportError, RuntimeError):
continue
text = data["results"][0].get("content") or ""
if page_is_complete(text):
return main_content(text), True
return main_content(text), False

Save the example as demo.py and run python demo.py. It searches, fetches a test page and a protected retail page, maps a site, and crawls 5 pages:

"""Same calls as with exa_py, handled by Decodo's Web Data API."""
from decodo_exa import Exa # was: from exa_py import Exa
exa = Exa()
results = exa.search("tutorial first steps", num_results=5,
include_domains=["fastapi.tiangolo.com"], contents={"text": True})
for r in results.results:
print(r.title, r.url, len(r.text or ""), sep=" | ")
pages = exa.get_contents(["https://books.toscrape.com/", "https://www.walmart.com/search?q=laptop"], text=True)
for p in pages.results:
print(p.url, len(p.text or ""), "characters")
urls = exa.map("https://docs.docker.com/")
print(len(urls), "URLs found by map()")
for page in exa.crawl("https://www.mkdocs.org/", limit=5):
print(page.url, len(page.text or ""))

One run printed this:

First Steps | https://fastapi.tiangolo.com/tutorial/first-steps/ | 15192
Tutorial - User Guide | https://fastapi.tiangolo.com/tutorial/ | 7661
Security - First Steps | https://fastapi.tiangolo.com/tutorial/security/first-steps/ | 10655
Dependencies | https://fastapi.tiangolo.com/tutorial/dependencies/ | 13389
Request Body | https://fastapi.tiangolo.com/tutorial/body/ | 8841
https://books.toscrape.com/ 7478 characters
https://www.walmart.com/search?q=laptop 35717 characters
2153 URLs found by map()
https://www.mkdocs.org/ 2270
https://www.mkdocs.org/getting-started 7232
https://www.mkdocs.org/about/contributing 8673
https://www.mkdocs.org/about/license 2165
https://www.mkdocs.org/about/release-notes 157598

Final thoughts

Exa is a strong semantic discovery layer, but discovery is rarely what fails. What fails is ingesting a whole site or reading protected pages. The choice depends on which endpoints your agent calls and how well protected your targets are. Decodo's Web Data API handles both with one API key. Run our Exa-compatible client against your hardest URLs on the free plan.

Agents hate getting blocked

Web Data API handles proxies and anti-bot bypass, so your agent just gets the page.

Share article:

About the author

Justinas Tamasevicius

Director of Engineering

Justinas Tamaševičius is Director of Engineering with over two decades of expertise in software development. What started as a self-taught passion during his school years has evolved into a distinguished career spanning backend engineering, system architecture, and infrastructure development.

Connect with Justinas via LinkedIn.

All information on Decodo Blog is provided on an as is basis and for informational purposes only. We make no representation and disclaim all liability with respect to your use of any information contained on Decodo Blog or any third-party websites that may belinked therein.

Frequently asked questions

What is the best alternative to Exa?

For agents that ingest whole sites or read protected pages, choose Decodo's Web Data API as your Exa alternative. It combines search, map, crawl, and scrape with one API key. Firecrawl works for known sites, Tavily for fast search, and Perplexity for cited answers.

Is Exa or Firecrawl better for RAG?

For RAG over a known site, Firecrawl works better than Exa because it maps and crawls the site into Markdown. Exa is better for finding sources you don't know yet, but it has no full-site crawl. Decodo's Web Data API maps and crawls too, with built-in proxies for protected pages.

Does Exa crawl entire websites?

No. Exa returns indexed pages that match a query, and its Contents endpoint can crawl up to 100 subpages of a page. It has no map or full-site crawl endpoint. For full-site ingestion, use Decodo's Web Data API, Firecrawl, or Bright Data's Crawl API.

How much does Exa cost compared to other search APIs?

Exa's pricing page lists $7 per 1K searches by default, plus free credits a month. Contents cost $1 per 1K pages for each content type. Brave and Linkup start at $5 per 1K searches, and Decodo's pricing page lists its per-1K rates. Compare the cost per usable page.

Can I use an Exa alternative with LangChain or an MCP server?

Yes. Decodo, Firecrawl, Tavily, Linkup, and Bright Data run MCP servers. An MCP server works with Claude, Cursor, LangChain, and most agent SDKs, so you integrate once for many agents. Decodo's MCP server uses the same API key as the Web Data API.

Which Exa alternative works on sites with bot protection?

Decodo's Web Data API, with residential proxies, reached more protected pages than Exa. Our benchmark fetched 20 bot-protected pages in 2 runs. Decodo returned page content for 85% of those fetches, and Exa, on its default settings, returned it for 55%. Test your hardest URLs on the free plan.

Web Crawling vs Web Scraping: What’s the Difference?

When it comes to gathering online data, two terms often create confusion: web crawling and web scraping. Although both involve extracting information from websites, they serve different purposes and employ distinct methods. In this article, we’ll break down these concepts, show you how they work, and help you decide which one suits your data extraction needs.

Central AI icon glowing, flanked by code snippets and AI Parser panel on a dark dotted gradient background

How to Build Production-Ready RAG with LlamaIndex and Web Scraping (2026 Guide)

Production RAG fails when it relies on static knowledge that goes stale. This guide shows you how to build RAG systems that scrape live web data, integrate with LlamaIndex, and actually survive production. You'll learn to architect resilient scraping pipelines, optimize vector storage for millions of documents, and deploy systems that deliver real-time intelligence at scale.

10 Best MCP Servers for AI Workflows in 2026

Choosing the best MCP servers matters even more now that MCP has shifted from niche adoption to widespread use, with OpenAI, Microsoft, and Google supporting it natively. This guide covers what MCP is, why it matters for real-world AI agents, which 10 MCP servers are currently most useful, and how to safely choose and combine MCPs for your setup.

© 2018-2026 decodo.com (formerly smartproxy.com). All Rights Reserved