Tavily vs Firecrawl vs Exa, and what a gateway changes

Three products solve different halves of web data for agents. Where a gateway helps, and where going direct is clearly better.

These three aren’t really competitors. Tavily is search-first and built for agent consumption, Firecrawl is a crawl-and-extract engine, and Exa is embeddings-based retrieval over the web. Infrai’s POST /v1/web/search and POST /v1/web/scrape are Tavily-backed — stated plainly, because the useful question isn’t which index is better but what you gain and lose by reaching it through a gateway.

What you gain is one credential. What you lose is the vendor’s full surface.

What the gateway exposes

curl -sS -X POST "https://api.infrai.cc/v1/web/search" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{"query": "what is a capability gateway api", "max_results": 3, "include_domains": ["f5.com"]}'
{
  "ok": true,
  "data": {
    "query": "what is a capability gateway api",
    "results": [
      {"title": "What Is an API Gateway? A Quick Learn Guide | F5",
       "url": "https://www.f5.com/glossary/api-gateway",
       "snippet": "An API gateway accepts API requests from a client, processes them based on defined policies..."}
    ],
    "raw_vendor_payload": null
  }
}

Four request fields for search — query, max_results, include_domains, exclude_domains — and two for scrape: url and format. That’s the whole surface, deliberately.

The comparison that matters

NeedTavilyFirecrawlExaInfrai web
Agent-shaped search resultsits purposevia searchsemanticyes, Tavily-backed
Crawl a whole sitenoits purposenono
Sitemap discovery, depth limitsnoyesnono
Embeddings-based semantic retrievalnonoits purposeno
Answer synthesis from resultsyesnoyesnot exposed
Single page to clean markdownyesyesyesyes
Same key as the model calling itnononoyes
Full vendor parameter surfaceyes, directyes, directyes, directno — trimmed

Rows two to five are the honest reasons to go direct. If you need to crawl a documentation site nightly, Firecrawl does that and this doesn’t. If your retrieval problem is semantic rather than keyword — “find me pages arguing the opposite of this” — Exa’s index is built for it and a keyword search won’t substitute.

When the gateway is the right call

One case, and it’s common: your agent occasionally needs to look something up, and you’d rather not add a vendor for it.

The model call and the tool are the same credential, so there’s one key in the process, one bill, and one usage view that attributes both the inference and the lookups to the same tenant. For an agent that searches a few times per conversation, that’s the whole value proposition — and it’s not a small one, because the alternative is a second account whose rate limits, status page and invoice you now own.

import json
import os

import requests

API = "https://api.infrai.cc"
KEY = os.environ["INFRAI_API_KEY"]
SESSION = requests.Session()
SESSION.headers.update({"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"})

TOOLS = [
    {"type": "function", "function": {
        "name": "web_search",
        "description": "Find current information on the public web. Prefer official sources.",
        "parameters": {"type": "object", "properties": {
            "query": {"type": "string"},
            "include_domains": {"type": "array", "items": {"type": "string"}}},
            "required": ["query"]}}},
    {"type": "function", "function": {
        "name": "web_read",
        "description": "Fetch one page as markdown when a snippet is not enough.",
        "parameters": {"type": "object", "properties": {"url": {"type": "string"}},
                       "required": ["url"]}}},
]


def web_search(query: str, include_domains: list[str] | None = None) -> dict:
    body = {"query": query, "max_results": 3}
    if include_domains:
        body["include_domains"] = include_domains
    r = SESSION.post(f"{API}/v1/web/search", json=body, timeout=40).json()
    return r.get("data", {"results": []})


def web_read(url: str) -> dict:
    r = SESSION.post(f"{API}/v1/web/scrape", json={"url": url, "format": "markdown"},
                     timeout=60).json()
    return r.get("data", {"content": ""})


HANDLERS = {"web_search": web_search, "web_read": web_read}


def answer(question: str, model: str = "auto") -> str:
    """Search to find, read to confirm — two tools, one credential, no second
    vendor in the request path."""
    messages = [{"role": "user", "content": question}]
    for _ in range(5):
        resp = SESSION.post(f"{API}/v1/chat/completions",
                            json={"model": model, "messages": messages, "tools": TOOLS},
                            timeout=120)
        resp.raise_for_status()
        message = resp.json()["choices"][0]["message"]
        messages.append(message)
        calls = message.get("tool_calls") or []
        if not calls:
            return message.get("content", "")
        for call in calls:
            name = call["function"]["name"]
            args = json.loads(call["function"]["arguments"] or "{}")
            messages.append({"role": "tool", "tool_call_id": call["id"],
                             "content": json.dumps(HANDLERS[name](**args))})
    return "tool budget exhausted"


if __name__ == "__main__":
    print(answer("What does an API gateway actually do?"))

Cost, without a comparison table you’d have to trust

Search and scrape are each billed per call here, with the live figures in GET /v1/discovery/web.search and GET /v1/discovery/web.scrape — read from your own account, verified 2026-09-21. Tavily, Firecrawl and Exa all publish credit- or page-based pricing that moves, and a table of their rates in a vendor’s own guide is exactly the kind of thing that stops being true.

The structural difference worth knowing: per-call pricing here means no monthly plan and no credit bundle to size in advance, so an agent that searches rarely costs almost nothing rather than a plan minimum. If your volume is high and steady, a vendor plan may well be cheaper per call — work it out with your own numbers, and remember that platform rates here drift downward as vendor contracts improve.

How to choose

Ask what fraction of your product web data is. If it’s the substance — you’re building a research tool, a monitoring service, a scraper — buy the specialist that matches your shape and accept the extra account. If it’s a capability your agent reaches for occasionally, the gateway removes an integration and the trimmed surface won’t limit you.

The limitation to accept either way: this is two endpoints, not a web-data platform. No crawl, no scheduling, no semantic index, no answer synthesis — and if you find yourself wishing for those, that wish is the signal to go direct.

References

Browse more web developer guides