npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

websxa

v0.1.2

Published

Unified web search provider for agents and CLI.

Readme

webxa

npm version npm downloads license

One API for Brave, Exa, Tavily, SerpAPI, and SearXNG. Write your search logic once, swap the provider string, done.

If you're building an AI agent or a CLI tool that needs web search, you don't want to hardcode a single provider's API. They all return roughly the same thing — a list of URLs with titles and snippets — but the auth, endpoints, and response shapes are all different. Exa uses POST with x-api-key, Brave uses GET with X-Subscription-Token, Tavily puts the key in the request body. And so on.

webxa normalizes all of that behind a single interface. It also ships an AI SDK tool and a CLI.

Install

pnpm add websxa

For the AI SDK tool (websxa/ai subpath), you also need ai and zod as peer dependencies:

pnpm add ai zod

Usage

Set your API key as an environment variable and create a provider:

import { create } from 'websxa'

// Reads EXA_API_KEY from process.env
const exa = create('exa')

const results = await exa.search('typescript runtime benchmarks', { maxResults: 5 })

for (const result of results) {
  console.log(result.title, result.url)
}

Swap the provider string, same code:

const brave = create('brave')   // reads BRAVE_API_KEY
const tavily = create('tavily') // reads TAVILY_API_KEY

You can also pass the key explicitly:

const exa = create('exa', { apiKey: 'your-key-here' })

Search all providers

Query all available providers in parallel and get deduplicated results:

import { searchAll } from 'websxa'

// Detects providers from env vars, queries them in parallel
const results = await searchAll('latest node.js release')

for (const result of results) {
  console.log(`[${result.provider}]`, result.title, result.url)
}

searchAll uses Promise.allSettled internally — if one provider fails, the others still return. Results are deduplicated by URL (normalized, UTM params stripped). When duplicates exist, the result with the higher score wins.

You can also specify which providers to query:

const results = await searchAll('query', {
  providers: ['exa', 'brave'],
  maxResults: 5,
})

AI SDK tool

The webxa/ai subpath exports a ready-made tool compatible with Vercel AI SDK:

import { generateText } from 'ai'
import { searchTool } from 'websxa/ai'

const { text } = await generateText({
  model: yourModel,
  tools: { webSearch: searchTool },
  prompt: 'Find the latest TypeScript release notes',
})

The tool accepts an optional provider parameter. Set it to "all" to query all available providers in parallel:

// The AI can choose: a specific provider, or "all" for parallel search
tools: { webSearch: searchTool }
// Input schema: { query: string, provider?: "brave" | "exa" | ... | "all", maxResults?: number }

When no provider is specified, the tool auto-detects the first available one from environment variables.

CLI

websxa search "your query" --provider brave --max-results 5
websxa search "your query" --json
websxa providers

| Command | Description | |---------|-------------| | websxa search <query> | Search the web using a provider | | websxa providers | List built-in providers |

| Flag | Description | |------|-------------| | --provider <name> | Provider to use (default: exa) | | --max-results <n> | Maximum results to return (default: 10) | | --json | Output as JSON |

Providers

| Provider | Env var | Auth | Free tier | |----------|---------|------|-----------| | Brave | BRAVE_API_KEY | Header | 2k queries/mo | | Exa | EXA_API_KEY | Header | 1k queries/mo | | SearXNG | — | None | Self-hosted | | SerpAPI | SERPAPI_API_KEY | Query param | 100 queries/mo | | Tavily | TAVILY_API_KEY | Body | 1k queries/mo |

SearXNG requires no API key — it's a self-hosted metasearch engine. By default webxa connects to http://localhost:8080. Override with baseURL:

const searx = create('searxng', { baseURL: 'https://searx.example.com' })

Errors

All providers throw the same error types:

import { AuthError, RateLimitError, HTTPError, UnknownProviderError } from 'websxa'

try {
  const results = await provider.search('query')
} catch (err) {
  if (err instanceof AuthError) {
    // Missing or invalid API key
  }
  if (err instanceof RateLimitError) {
    console.log(`Retry after ${err.retryAfter}s`)
  }
  if (err instanceof UnknownProviderError) {
    // Provider name not recognized
  }
}

A 401 from Exa and a 401 from Brave both become AuthError. A 429 from any provider becomes RateLimitError with a retryAfter value. Everything else is HTTPError or the base WebxaError.

Data model

Every provider returns the same normalized type:

interface SearchResult {
  url: string
  title: string
  snippet: string
  score?: number
  publishedDate?: string
  author?: string
  image?: string
  favicon?: string
  text?: string
  highlights?: string[]
  summary?: string
  metadata?: Record<string, unknown>
}

Optional fields depend on what the provider returns. Exa provides score, text, and highlights. Brave provides favicon. Not all providers populate all fields.

Development

pnpm install
pnpm typecheck   # tsc --noEmit
pnpm build       # obuild
pnpm test        # vitest (watch mode)
pnpm test:run    # vitest --run

License

MIT