# Example: Brave Search

**Official plugin** · `web.search` provider · one secret setting · egress `api.search.brave.com`

Brave Search lets an account run every agent's `web_search` tool on the Brave Search API with **its own** Brave
key. It is the canonical example of calling a vendor from a plugin: a secret setting, a declared host, and
`host.fetch`.

## The code

```ts
// plugins/brave-search/index.ts
globalThis.manifest = {
  egress: ["api.search.brave.com"],
  settings: [
    {
      key: "apiKey",
      title: "Brave API key",
      description: "From https://api-dashboard.search.brave.com — the free tier works.",
      type: "secret",
      required: true,
    },
  ],
};

globalThis.web.search = async (input) => {
  const apiKey = await host.settings.getSecret("apiKey");
  const count = Math.min(Math.max(input.maxResults, 1), 20); // Brave caps count at 20
  const response = await host.fetch({
    url: `https://api.search.brave.com/res/v1/web/search?q=${encodeURIComponent(input.query)}&count=${count}`,
    headers: { "X-Subscription-Token": apiKey, Accept: "application/json" },
  });
  if (response.status !== 200) throw new Error(`Brave search API returned ${response.status}`);
  const parsed = JSON.parse(response.body) as { web?: { results?: { title?: string; url?: string; description?: string }[] } };
  const results = (parsed.web?.results ?? [])
    .filter((result) => result.title && result.url)
    .slice(0, input.maxResults)
    .map((result) => ({ title: result.title!, url: result.url!, snippet: result.description ?? "" }));
  return { results };
};
```

## Walkthrough

**The manifest.**

- `egress: ["api.search.brave.com"]` — the only host `host.fetch` may reach, on the default HTTPS port. Without it
  the publish fails, because the code calls `host.fetch`. The installer sees this host under **What it can reach**.
- `settings` declares one `secret` field. In the console it's a write-only password field; the value is encrypted
  and never shown again. `required: true` means the plugin isn't usable until it's filled in. Secrets can't have a
  `default`.

**The handler.**

1. `await host.settings.getSecret("apiKey")` — the key is fetched into the sandbox only now, only for this call.
2. `input.maxResults` is what the agent's `web_search` call asked for. Brave caps `count` at 20, so the plugin
   clamps it. (The platform caps results at 10 on the way back anyway.)
3. `host.fetch` makes the request. It resolves for any HTTP status, so the plugin checks `status` and **throws** on
   anything but 200 — the agent then sees a clear "provider failed" error. The platform won't fall back to its own
   engine behind the user's back.
4. It maps Brave's shape into `{ results: [{ title, url, snippet }] }` and drops rows without a title or URL.

**Permissions.** None. `host.fetch` needs an egress entry, not a permission, and `host.settings` is always there.

## Using it

1. Install **Brave Search**, open its page, paste the Brave API key.
2. **Settings → Engines → Web search** → choose *Brave Search*.

Every agent's `web_search` now goes through the user's own Brave account and quota.

## Going further

- **Add `web.fetch`** in the same plugin; it becomes a second, separately selectable capability. **Wiki Web** does
  both: Wikipedia's opensearch for search, a reader service for fetch — see
  [Web providers](plugin-web-providers.md#webfetch).
- **Surface bad keys clearly:** throw `new Error("Brave rejected the API key (401) — check the key in the plugin's settings")`
  on a 401; that sentence is what the model (and then the user) reads.
