Web search tools for agno agents with ContextFetch
agno's built-in search toolkits work with ContextFetch unchanged. Give TavilyTools an api_base_url of https://api.contextfetch.dev/tavily, or FirecrawlTools an api_url of https://api.contextfetch.dev, and pass your ContextFetch key. ExaTools needs one extra line after construction. All three draw from the same balance, and with search_depth set to basic each search costs $1 per 1,000, page Markdown included.
1. Claim a free key
Send one POST request to get a key holding $2.50, which covers 2,500 searches at the default rate. No account is needed and each network gets one free key. Put it in CONTEXTFETCH_API_KEY for the program below.
curl -X POST https://api.contextfetch.dev/v1/keys/free# {"apiKey": "cf_live_...", "balance_usd": 2.5, "searches": 2500}export CONTEXTFETCH_API_KEY="cf_live_..."2. Install agno and the provider clients
agno's toolkits import each provider's own SDK. Install the ones you will use: tavily-python for TavilyTools, firecrawl-py for FirecrawlTools and exa-py for ExaTools.
pip install agno tavily-python firecrawl-py exa-py3. TavilyTools
TavilyTools accepts api_base_url, or reads TAVILY_API_BASE_URL from the environment. Set search_depth="basic": the toolkit defaults to advanced, which ContextFetch runs on its deep tier at $3 per 1,000 instead of $1. The toolkit also asks for an answer by default; ContextFetch's answer is a passage quoted from a source, not generated text.
import osfrom agno.tools.tavily import TavilyTools tavily = TavilyTools( api_key=os.environ["CONTEXTFETCH_API_KEY"], api_base_url="https://api.contextfetch.dev/tavily", search_depth="basic",)4. FirecrawlTools and ExaTools
FirecrawlTools passes api_url to the Firecrawl client, which then calls ContextFetch's Firecrawl-compatible /v2/search. Turn on enable_search=True; scraping is on by default and search is not.
ExaTools has no base URL argument, but its Exa client reads base_url on every request, so set tools.exa.base_url right after you create it. The program below does all three.
from agno.tools.exa import ExaToolsfrom agno.tools.firecrawl import FirecrawlTools firecrawl = FirecrawlTools( api_key=os.environ["CONTEXTFETCH_API_KEY"], api_url="https://api.contextfetch.dev", enable_search=True,) exa = ExaTools(api_key=os.environ["CONTEXTFETCH_API_KEY"])exa.exa.base_url = "https://api.contextfetch.dev"5. Attach a toolkit to an agent
Toolkits go in the agent's tools list. When the model decides to search, agno calls the toolkit function, such as web_search_using_tavily or search_web, with the model's query. The program calls those functions directly, so it needs no model key and shows what the agent receives. TavilyTools also stops adding results once its output passes max_tokens (6,000 characters by default), which is why it can print fewer results than max_results.
from agno.agent import Agent agent = Agent(tools=[tavily], markdown=True)agent.print_response("What are Postgres logical replication slots?")6. What it costs
One toolkit call is one search. Tavily basic, Exa auto and Firecrawl search all run the default tier at $1 per 1,000. Tavily advanced and Exa's deep types cost $3 per 1,000. Up to 25 results are in that price. When a balance runs out, Tavily-style routes answer HTTP 432 as Tavily does, so the clients raise the error they already raise today.
The complete code
Python, runnable as is with CONTEXTFETCH_API_KEY set. Install first: agno, tavily-python, firecrawl-py, exa-py. We ran it against the live API; the output below is what it printed.
import jsonimport os from agno.tools.firecrawl import FirecrawlToolsfrom agno.tools.tavily import TavilyTools KEY = os.environ["CONTEXTFETCH_API_KEY"] # Tavily-shaped search. search_depth="basic" keeps it at the default $1 per 1,000 rate.tavily = TavilyTools( api_key=KEY, api_base_url="https://api.contextfetch.dev/tavily", search_depth="basic", format="json",)res = json.loads(tavily.web_search_using_tavily("postgres logical replication slots", max_results=3))print("TavilyTools:")print(" answer:", (res.get("answer") or "none")[:160].replace("\n", " "))for r in res["results"]: print(" ", r["title"], "|", r["url"]) # Firecrawl-shaped search, through the same key.firecrawl = FirecrawlTools( api_key=KEY, api_url="https://api.contextfetch.dev", enable_scrape=False, enable_search=True, limit=3,)data = json.loads(firecrawl.search_web("postgres logical replication slots"))print("FirecrawlTools:")for r in data["web"]: print(" ", r["title"], "|", r["url"]) # Exa-shaped search: ExaTools takes no base URL, so point its client after construction.from agno.tools.exa import ExaTools exa = ExaTools(api_key=KEY, num_results=3)exa.exa.base_url = "https://api.contextfetch.dev"print("ExaTools:")for r in json.loads(exa.search_exa("postgres logical replication slots")): print(" ", r["title"], "|", r["url"]) TavilyTools:
answer: # PostgreSQL logical replication slots | Selectel Documentation To continuously replicate data from one database to another, you can configure logical replicati
Postgres Logical Replication Limits and Setup | Adapters | https://adapters.io/blog/postgres-logical-replication
Failover Replication Slots with Postgres 17 | https://www.decodable.co/blog/failover-replication-slots-with-postgres-17
FirecrawlTools:
Postgres Logical Replication Limits and Setup | Adapters | https://adapters.io/blog/postgres-logical-replication
Failover Replication Slots with Postgres 17 | https://www.decodable.co/blog/failover-replication-slots-with-postgres-17
PostgreSQL logical replication slots | Selectel Documentation | https://docs.selectel.ru/en/managed-databases/postgresql/replication-slots/
ExaTools:
Postgres Logical Replication Limits and Setup | Adapters | https://adapters.io/blog/postgres-logical-replication
Failover Replication Slots with Postgres 17 | https://www.decodable.co/blog/failover-replication-slots-with-postgres-17
PostgreSQL logical replication slots | Selectel Documentation | https://docs.selectel.ru/en/managed-databases/postgresql/replication-slots/The same call with curl
The raw HTTP request, and the response it returned (long text fields and lists shortened).
curl -s https://api.contextfetch.dev/v2/search \ -H "Authorization: Bearer $CONTEXTFETCH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"query": "postgres logical replication slots", "limit": 3, "scrapeOptions": {"formats": ["markdown"]}}'{
"success": true,
"data": {
"web": [
{
"url": "https://adapters.io/blog/postgres-logical-replication",
"title": "Postgres Logical Replication Limits and Setup | Adapters",
"description": "# Postgres logical replication slots, limitations, setup, and logical vs physical replication\n\n10 min read Databases The Adapters team\n\nField mapping auto-plugged · tap a port to rewire\n\nPlug a source port into\n\nTransform on this cable\n\nJSON in\n\nJSON out\n\n5 sample records ready\n\n…",
"position": 1,
"markdown": "# Postgres logical replication slots, limitations, setup, and logical vs physical replication\n\n10 min read Databases The Adapters team\n\nField mapping auto-plugged · tap a port to rewire\n\nPlug a source port into\n\nTransform on this cable\n\nJSON in\n\nJSON out\n\n5 sample records ready\n\n…",
"metadata": {
"title": "Postgres Logical Replication Limits and Setup | Adapters",
"sourceURL": "https://adapters.io/blog/postgres-logical-replication",
"url": "https://adapters.io/blog/postgres-logical-replication",
"statusCode": 200,
"publishedTime": "2026-08-16"
}
},
{
"url": "https://www.decodable.co/blog/failover-replication-slots-with-postgres-17",
"title": "Failover Replication Slots with Postgres 17",
"description": "Logical replication slots—which keep track of how far a specific subscriber has consumed the database’s change event stream—could only be created on the primary node of a Postgres cluster. This meant that after a failover from primary to replica you’d have to create a new replica…",
"position": 2,
"markdown": "Postgres read replicas are commonly used not only to distribute query load amongst multiple nodes, but also to ensure high availability (HA) of the database. If the primary node of a Postgres cluster fails, a read replica can be promoted to be the new primary, processing write (a…",
"metadata": {
"title": "Failover Replication Slots with Postgres 17",
"sourceURL": "https://www.decodable.co/blog/failover-replication-slots-with-postgres-17",
"url": "https://www.decodable.co/blog/failover-replication-slots-with-postgres-17",
"statusCode": 200,
"publishedTime": "2024-12-03"
}
},
{
"url": "https://docs.selectel.ru/en/managed-databases/postgresql/replication-slots/",
"title": "PostgreSQL logical replication slots | Selectel Documentation",
"description": "# PostgreSQL logical replication slots | Selectel Documentation\n\nTo continuously replicate data from one database to another, you can configure logical replication using a logical replication slot.\n\nReplication slots must always have a consumer. If there is no consumer, the volum…",
"position": 3,
"markdown": "# PostgreSQL logical replication slots | Selectel Documentation\n\nTo continuously replicate data from one database to another, you can configure logical replication using a logical replication slot.\n\nReplication slots must always have a consumer. If there is no consumer, the volum…",
"metadata": {
"title": "PostgreSQL logical replication slots | Selectel Documentation",
"sourceURL": "https://docs.selectel.ru/en/managed-databases/postgresql/replication-slots/",
"url": "https://docs.selectel.ru/en/managed-databases/postgresql/replication-slots/",
"statusCode": 200
}
}
]
},
"id": "79f8b2c0cf27434287c1d51d9b7d920b"
}