Skip to content

Web search tool for the Vercel AI SDK with ContextFetch

Install @contextfetch/ai-sdk and add two tools: contextfetchSearch, which searches the web and returns each result with the parts of the page that answer the query, and contextfetchContents, which reads pages as Markdown. Both drop into generateText or streamText and read your key from CONTEXTFETCH_API_KEY. A search costs $1 per 1,000 with the page content included. Already on Tavily's or Exa's AI SDK tools? Those can point at ContextFetch too.

Verified TypeScriptVercel AI SDK/v1/search/v1/contents

1. Get an API key

POST to the free-key endpoint and you get a key worth $2.50, or 2,500 default searches. It takes no signup; each network can claim one. Set it as CONTEXTFETCH_API_KEY in .env.local or your shell.

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

@contextfetch/ai-sdk works with AI SDK 5, 6 and 7 and needs zod beside it.

npm install @contextfetch/ai-sdk ai zod

3. Add the tools

contextfetchSearch() returns up to numResults results (1 to 25, default 5). Each one has a title, URL, published date and page content. content picks what that content is: prunedContext (the default, only the parts of the page relevant to the query), text (the whole page as Markdown) or highlights (a few key sentences).

The model can narrow a search with includeDomains, excludeDomains and startPublishedDate. contextfetchContents() reads up to 10 URLs per call, with an optional query to keep only the relevant parts. Both take apiKey; by default they read CONTEXTFETCH_API_KEY.

import { contextfetchContents, contextfetchSearch } from "@contextfetch/ai-sdk"; export const tools = {  webSearch: contextfetchSearch({ numResults: 5 }),  readPages: contextfetchContents(),};

4. Use it in generateText

Register the tools under any names and let the model call them; stopWhen gives it room to search, read and then answer. The AI SDK runs each tool's execute(input, options), which the program below calls directly so it runs without a model key.

import { generateText, isStepCount } from "ai";import { tools } from "./tools"; const { text } = await generateText({  model: "openai/gpt-5-mini",  tools,  stopWhen: isStepCount(5),  prompt: "What changed in Next.js App Router caching? Cite sources.",});console.log(text);

5. Already using Tavily's or Exa's tools?

tavilySearch from @tavily/ai-sdk passes apiBaseURL to its client, so tavilySearch({ apiKey, apiBaseURL: "https://api.contextfetch.dev/tavily" }) runs on ContextFetch. It declares a peer dependency on AI SDK 5 or 6, so it will not install beside version 7.

@exalabs/ai-sdk always calls https://api.exa.ai/search and cannot be redirected, but exa-js takes the base URL as its second argument: new Exa(key, "https://api.contextfetch.dev"), wrapped in your own tool().

6. Costs and rate limits

A call at the default depth is one search: $1 per 1,000, results up to 25, page text included. advanced depth or Exa's deep type costs $3 per 1,000. Free keys are limited to 100 requests a minute and paid keys to 300, which matters if many users trigger tool calls at once.

The complete code

TypeScript, runnable as is with CONTEXTFETCH_API_KEY set. Install first: @contextfetch/ai-sdk, ai, zod. We ran it against the live API; the output below is what it printed.

import { contextfetchContents, contextfetchSearch } from "@contextfetch/ai-sdk"; // The AI SDK calls execute(input, options) when the model uses a tool; here we call it directly.const options = { toolCallId: "demo", messages: [] };const apiKey = process.env.CONTEXTFETCH_API_KEY; // the tools also read it on their own const search = await contextfetchSearch({ apiKey, numResults: 3 }).execute!({ query: "nextjs app router caching" }, options);for (const r of search.results) console.log(`${r.title}\n  ${r.url}`); const pages = await contextfetchContents({ apiKey }).execute!(  { urls: ["https://nextjs.org/docs/app/guides/caching"], query: "revalidate" },  options,);console.log("\nread:", pages.results[0]?.title); 
OutputRecorded
Next.js App Router in 2026 — Caching Sanity, Server Actions, and What Settled
  https://blog.rajpoot.dev/posts/typescript/nextjs-app-router-2026/
Next.js 15 App Router Caching: Why Self-Hosted Apps Need Redis (And How to Implement It) - DEV Community
  https://dev.to/technnik/nextjs-15-app-router-caching-why-self-hosted-apps-need-redis-and-how-to-implement-it-23op
Building a Multi-Layer Caching Strategy in Next.js App Router: From Static to Real-Time - DEV Community
  https://dev.to/maurya-sachin/building-a-multi-layer-caching-strategy-in-nextjs-app-router-from-static-to-real-time-4che

read: Caching and Revalidating (Previous Model)

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/v1/search \  -H "Authorization: Bearer $CONTEXTFETCH_API_KEY" \  -H "Content-Type: application/json" \  -d '{"query": "nextjs app router caching", "numResults": 3, "contents": {"highlights": true}}'
ResponseRecorded
{
  "requestId": "3c497eb5cca349bb8ce889a723c7558e",
  "results": [
    {
      "id": "f16aee57-e905-5d2b-93fc-a350cc15575a",
      "url": "https://blog.rajpoot.dev/posts/typescript/nextjs-app-router-2026/",
      "title": "Next.js App Router in 2026 — Caching Sanity, Server Actions, and What Settled",
      "score": 0.9999999949499682,
      "publishedDate": null,
      "text": null,
      "highlights": [
        "Suggest an edit\nTable of Contents\nWhat’s better\nThe cache redesign\nServer Components\nClient Components\nServer Actions\nDynamic IO\nCaching tags + revalidation\nStreaming with Suspense\nLoading and error states\nMiddleware\nPerformance\nDeployment\nMigration Pages Router → App Router\nComm…",
        "#\n\nFor new Next.js apps:\nApp Router\n(RSC).\nDrizzle / Prisma\nfor DB.\nServer Actions\nfor mutations.\nSuspense + streaming\nwhere relevant.\nTagged caching\nwith explicit revalidation.\nVercel\nif ergonomic > cost;\nself-host\notherwise.\nBiome\nfor lint/format.\nRead this next\n\n#\n\nReact Serve…",
        "#\n\nCache redesign\n(v15+): explicit opt-in vs implicit caching everywhere.\nServer Actions\nstable; the patterns are clear.\nRSC\nmature; less foot-gun.\nStreaming\nimproved; better SSR.\nTurbopack\nstable for dev; faster.\nThe cache redesign\n\n#\n\nOld App Router cached\nfetch()\naggressively …"
      ],
      "highlightScores": [
        0.9981608296919939,
        0.9977034750171458,
        0.9974280324879887
      ],
      "prunedContext": "Next.js App Router in 2026 — Caching Sanity, Server Actions, and What Settled\nNext.js App Router in 2026 — caching after the redesign, Server Actions, dynamic IO, and the patterns that work in production.\nMay 4, 2026\n·\n4 min\n·\n800 words\n·\nManvendra Rajpoot\n|\n\nSuggest an edit\nTabl…",
      "textSource": "page",
      "language": "en"
    },
    {
      "id": "bd304133-81ac-5a33-89fd-4abd8c235c57",
      "url": "https://dev.to/technnik/nextjs-15-app-router-caching-why-self-hosted-apps-need-redis-and-how-to-implement-it-23op",
      "title": "Next.js 15 App Router Caching: Why Self-Hosted Apps Need Redis (And How to Implement It) - DEV Community",
      "score": 0.9999999933456212,
      "publishedDate": "2025-05-23",
      "text": null,
      "highlights": [
        "## Monitoring and Debugging\n\nThe package includes a `DEBUG_CACHE_HANDLER` environment variable that can be set to `true` to enable debug logging of the caching handler operations.\n\n## Performance Considerations\n\nRedis cache handlers can significantly improve application performan…",
        "The key is understanding the trade-offs between performance optimizations and consistency guarantees, then configuring your cache handler to match your application's specific requirements. Modern implementations like `@trieb.work/nextjs-turbo-redis-cache` are purpose-built for Ne…",
        "### Consistency Levels\n\nThe documentation clearly explains the consistency guarantees:\n\n**Strong Consistency**: Available only with single Redis node setup and request deduplication disabled.\n\n**Eventual Consistency**: When using request deduplication or distributed Redis setups,…"
      ],
      "highlightScores": [
        0.9996315138326064,
        0.9996101852955023,
        0.9992430998883752
      ],
      "prunedContext": "When you deploy Next.js 15 applications with the App Router outside of Vercel's managed platform, you quickly encounter a fundamental challenge: the default filesystem-based cache doesn't work effectively in distributed or containerized environments. This limitation becomes appar…",
      "textSource": "page",
      "language": "en"
    },
    {
      "id": "c8cb26d8-dc56-577c-a3fb-a2d8e1dc1249",
      "url": "https://dev.to/maurya-sachin/building-a-multi-layer-caching-strategy-in-nextjs-app-router-from-static-to-real-time-4che",
      "title": "Building a Multi-Layer Caching Strategy in Next.js App Router: From Static to Real-Time - DEV Community",
      "score": 0.9999999782215643,
      "publishedDate": "2026-01-01",
      "text": null,
      "highlights": [
        "1. **Full Route Cache** – Entire route responses (HTML) cached at build or request time\n2. **Data Cache** – Results of `fetch` requests persisted across requests\n3. **Client Cache** – Caching done in the browser (via React Query, Zustand, etc.)\n4. **CDN/Edge Cache** – Caching at …",
        "## Final Thoughts\n\nCaching in the App Router is powerful but requires intentional design. There’s no one-size-fits-all solution. By building a **multi-layer caching strategy**, you can optimize for both performance and freshness.\n\nIf you’re working on a Next.js app and thinking a…",
        "## Layer 3: Personalized Dashboards – No Server Cache\n\nFor authenticated user dashboards (`/dashboard`, `/settings`), we disabled server caching entirely.  \n\n```\n// app/dashboard/page.js\nexport const dynamic = 'force-dynamic'\n\nexport default async function DashboardPage() {\n  // …"
      ],
      "highlightScores": [
        0.9981854826952677,
        0.9979658035863868,
        0.9979521157063125
      ],
      "prunedContext": "Caching is one of those make-or-break aspects of modern web development. Get it right, and your app is fast, resilient, and scalable. Get it wrong, and you’re left with stale data, poor performance, or worse — broken user experiences.\n\nAt my previous role, I led the migration of …",
      "textSource": "page",
      "language": "en"
    }
  ],
  "costDollars": {
    "total": 0.001
  },
  "searchTimeMs": 2,
  "searchTime": 2,
  "contextfetch": {
    "tokens": {
      "raw": 7358,
      "pruned": 3125,
      "savedPct": 57.52921989671107
    },
    "sources": {
      "local": 3
    },
    "cache": "hit",
    "timingsMs": {
      "recall": 0,
      "fetch": 0,
      "rerank": 0,
      "total": 2
    }
  }
}

Questions

How much cheaper is it than Tavily or Exa?

Tavily lists a basic search at one credit, $0.008 pay-as-you-go, which is $8 per 1,000; Exa lists auto search at $7 per 1,000. ContextFetch charges $1 per 1,000 for a default search, with the page content in the price.

Which content option should I use?

Keep the default, `prunedContext`, for most agents: the model gets the passages that answer the query without whole pages filling its context. Use `text` when the agent needs the complete page, and `highlights` when you want the fewest tokens.

Can I use it before claiming a key?

Yes. With no key set, requests use the keyless tier: 25 per IP address per day. Claim the free key for more.

Does it work with streamText?

Yes. The tools are standard AI SDK tools, so they work in generateText, streamText and agents alike.

Run it with your own key

A free key has 2,500 searches and needs no signup. Agents can claim one themselves with the POST below.

curl -X POST https://api.contextfetch.dev/v1/keys/free
Get a free key