Docs
Everything a client needs: the base URL is https://api.grounder.net, and every HTTP request carries a search key as Authorization: Bearer $GROUNDER_KEY. New keys are no longer made: agents connect over MCP and sign in, and the keys already made keep working.
Endpoints
Search
POST /v1/search takes a query and returns one ranked list. Unless you pin providers, Grounder routes it to the provider, or pair, most likely to answer it well.
curl https://api.grounder.net/v1/search \
-H "Authorization: Bearer $GROUNDER_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "what is the half-life of caffeine", "top_k": 5}'
Request fields
| Field | Type | Meaning |
|---|---|---|
query | string | Required, up to 2,000 characters. |
top_k | integer | Results after merging, 1 to 100. Default 10. |
filters | object | domains, recency (rolling days or a date range), language; Ozone also takes publisher_ids, article_types, and temporal_sensitivity. A filter a provider can’t apply returns 400 unsupported_filter. |
include_text | boolean | Passage text with each result. Default true. |
requested_provider | string | Pin one provider for this query: ozone, tavily, exa, brave, perplexity, you, firecrawl, octen, parallel, or valyu. |
providers | string[] | Fan out to exactly these providers and merge. |
realtime | boolean | Latency-bound callers: route to a single fast provider. |
routing_settings | object | Your account’s routing settings for this search only, over the saved ones, never saved: weights (accuracy, speed, results, recency, cost), allowed_only, licensed_only, top_providers, providers, fallback, specialized and caps.search. See Routing. |
depth | string | How much the user wants back, from what they asked for: answer (a direct answer), extensive (a broad set of results or sources) or report (a multi-part research report). Leave it out when unsure. |
context.user_location | string | Where the user is (“Austin, Texas”), only when the user said so. Never a guess. |
context.user_time_zone | string | The user’s time zone as an IANA name (“America/Chicago”), only when the user said so. Never a guess. |
Response
A response to what is the half-life of caffeine, recorded on 7 October 2026, before Licensed sources only was on by default (its routing policy label is today’s): two of its five results shown, passage text shortened. found_by lists every provider that returned the URL; licensed is true for Ozone’s licensed publisher content.
{
"query": "what is the half-life of caffeine",
"results": [
{
"rank": 1,
"url": "https://www.ncbi.nlm.nih.gov/books/NBK223808/",
"domain": "ncbi.nlm.nih.gov",
"title": "Pharmacology of Caffeine - Caffeine for the Sustainment of Mental Task Performance - NCBI Bookshelf",
"text": "Pharmacology of Caffeine - Caffeine for the Sustainment of Mental Task Performance - NCBI…",
"score": 1,
"provenance": {
"provider": "exa",
"found_by": ["perplexity", "exa"],
"licensed": false,
"promoted": "none"
}
},
{
"rank": 2,
"url": "https://www.efsa.europa.eu/sites/default/files/corporate_publications/files/efsaexplainscaffeine150527.pdf",
"domain": "efsa.europa.eu",
"title": "Caffeine - EFSA",
"text": "In adults the half-life of caffeine – the time it takes for the body to eliminate\n50% of t…",
"published_date": "2026-08-22",
"score": 1,
"provenance": {
"provider": "perplexity",
"found_by": ["perplexity"],
"licensed": false,
"promoted": "none"
}
}
],
"routing": {
"policy": "ranked",
"calls": [
{ "provider": "perplexity", "status": "ok", "latency_ms": 194, "result_count": 3 },
{ "provider": "exa", "status": "ok", "latency_ms": 2158, "result_count": 3 }
]
},
"usage": {
"latency_ms": 2537,
"cached": false,
"licensed_result_count": 0
},
"request_id": "2f696c9c-c80a-4dd6-b03f-465455b35c3a"
}
With Allowed sources only on (see Routing), the response also carries allowed, an array with one entry for each provider that answered, sorted by provider: { "provider": "exa", "allowed_pct": 100, "checked": 4 }. Of that provider’s distinct results that Grounder could check, allowed_pct is the share whose site’s robots.txt allows the provider and does not opt out of search or AI answers, to one decimal, or null when none could be checked, and checked is how many it checked. A result that could not be checked is left out of both, and a provider that does not crawl the open web, such as Ozone, is always 100. Every result returned then has provenance.allowed set to true. With the setting off, neither field is in the response. allowed reports; it is not part of any score.
MCP
Agents connect to https://api.grounder.net/mcp over streamable HTTP. Add that address to your client; when it opens Grounder’s screen, sign in with Google, GitHub or Apple and allow it. There’s no key to copy. The server exposes search, list_providers, record_user_response, and user_info.
# Customize, Connectors, + Add,
# Add custom connector
Name grounder
URL https://api.grounder.net/mcp
# Add, then Connect and sign in.
# No key needed.
# So Claude searches with Grounder:
# Customize, Skills, +, Create skill,
# Upload a skill
Skill grounder-web-search.zip
# chatgpt.com/plugins, +,
# Add custom MCP server
Name grounder
URL https://api.grounder.net/mcp
Auth OAuth
# Create and sign in, then type
# @grounder in a new chat.
claude mcp add --transport http \
grounder \
https://api.grounder.net/mcp
# then /mcp, grounder, Authenticate
codex mcp add grounder \
--url https://api.grounder.net/mcp
codex mcp login grounder
copilot mcp add --transport http \
grounder \
https://api.grounder.net/mcp
# then start copilot and sign in
gemini mcp add -s user -t http \
grounder \
https://api.grounder.net/mcp
# then /mcp auth grounder in a session
{
"mcp": {
"grounder": {
"type": "remote",
"url": "https://api.grounder.net/mcp"
}
}
}
// then opencode mcp auth grounder
openclaw mcp add grounder \
--url https://api.grounder.net/mcp \
--transport streamable-http \
--auth oauth
openclaw mcp login grounder
hermes mcp add grounder \
--url https://api.grounder.net/mcp \
--auth oauth --connect-timeout 300
# sign in as it adds
pi mcp add grounder \
--url https://api.grounder.net/mcp \
--exposure direct
pi mcp login grounder
extensions:
grounder:
enabled: true
type: streamable_http
name: grounder
uri: https://api.grounder.net/mcp
timeout: 300
# sign in when a session starts
{
"servers": {
"grounder": {
"type": "http",
"url": "https://api.grounder.net/mcp"
}
}
}
// sign in when VS Code starts it
{
"mcpServers": {
"grounder": {
"url": "https://api.grounder.net/mcp"
}
}
}
// then Cursor Settings, MCP, grounder
{
"context_servers": {
"grounder": {
"url": "https://api.grounder.net/mcp"
}
}
}
// then Authenticate, beside grounder
// Settings, MCP, Add, then paste:
{
"grounder": {
"url": "https://api.grounder.net/mcp"
}
}
// sign in when it starts
{
"mcpServers": {
"grounder": {
"type": "http",
"url": "https://api.grounder.net/mcp"
}
}
}
{
"mcpServers": {
"grounder": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://api.grounder.net/mcp"
]
}
}
}
// for clients without remote sign-in
A search key made earlier works here too: send it as Authorization: Bearer <key> instead of signing in.
Routing
Routing per query is the default: Grounder sends each query to the provider, or pair of providers, most likely to answer it well, and keeps learning which that is.
{
"query": "what is the half-life of caffeine",
"requested_provider": "tavily"
}
{
"routing": {
"policy": "ranked",
"calls": [
{ "provider": "perplexity", "status": "ok", "latency_ms": 194, "result_count": 3 },
{ "provider": "exa", "status": "ok", "latency_ms": 2158, "result_count": 3 }
]
}
}
The response’s routing.calls shows where the query went: here the router sent the query to Perplexity and Exa together. To decide yourself, pin one provider with requested_provider or fan out with providers. A search that names no provider reports ranked in routing.policy; one that names providers reports fanout. A key carries no routing policy of its own.
Your account’s own choices apply to every search it makes, over the API and MCP alike. Under Routing on your account page, set the mode of every search, turn each provider On, Always or Off and choose its modes, add your own key for a provider so its searches run on your key, turn each specialized API On, Always or Off or add your key for it, choose how many of the best providers each search goes to at once (their top sources are returned together), and say how much accuracy, speed, results returned, recent sources and low cost matter. A provider set to Off is never called, even when a search names it; one set to Always gets every search that names no provider.
Allowed sources only, in the same place, is off until you turn it on. On, Grounder searches as before, but returns a result only from a site whose robots.txt allows the provider that found it and does not opt out of search or AI answers. A result a site disallows, for that provider or by opting out, is replaced, by the same page from another provider that is allowed, else by Ozone’s publisher content from the same site when Ozone licenses it, else by a result from another provider’s search; a result that could not be checked in time is left out, so a search can return fewer results than it would otherwise. Ozone’s publisher content and the specialized APIs are not crawls of the open web, so they are always allowed. The response then reports allowed, described under Endpoints.
Licensed sources only, beside it, is on until you turn it off, and applies to every search your account makes. On, Grounder searches as before, also asks Ozone beside the first provider when the search names none, and returns only licensed results: Ozone’s publisher content and pages on sites Ozone licenses, each with provenance.licensed set to true. A search returns what is licensed, so it can return fewer results than it asked for, and is never filled with unlicensed ones. The specialized APIs’ values are data feeds and are always returned. A search that names its providers is filtered the same way and makes no added call. When both settings are on, a result must pass both. Turn it off on your account page, or send {"licensed_only": false} to PUT /v1/me/routing, to get every source again.
Caps, in the same place, are off until you set them. Each search can be held to a time, which stops the providers it calls; to a number of tokens, counted at about 4 characters a token, the lowest results cut and then dropped to fit; to a number of provider calls; and to a number of sources. A monthly cap on searches, tokens or search time refuses your searches with cap_reached (HTTP 429) once the month’s count reaches it, until the 1st, UTC. How you used Grounder, at the top of the same page, shows the figures of the home page’s results table from your own searches: the share graded correct, tokens, search latency, provider calls and sources per search, and the share of results from licensed publishers, beside the average provider on its SimpleQA and SealQA tables.
Your agent can change any of these for one search. Send routing_settings with the search, in the shape the account page saves: each part replaces your saved choice for that search alone, and nothing is saved. Weights take 0 (ignore), 0.5, 1 (the default), 2 or 3 (most). A search’s caps only lower yours, never raise them, and the monthly caps are set on the account page only. The MCP search tool takes the same field; user_info, or GET /v1/account/routing with your key, reads your saved settings and what each can be set to.
{
"query": "is the 6:40 train to Milwaukee on time",
"routing_settings": {
"weights": { "speed": 3, "accuracy": 2 },
"allowed_only": true,
"caps": { "search": { "time_ms": 3000 } }
}
}
Providers
GET /v1/providers lists each live provider, 10 today, with the filters it supports. Ozone is real-time licensed publisher content; the other nine search the open web.
| Provider | Domain | Date | Publisher | Full text | Similar | Index |
|---|---|---|---|---|---|---|
| Ozone | Licensed publisher content | |||||
| Tavily | Tuned for LLMs, open web | |||||
| Exa | Embeddings, open web | |||||
| Brave | Independent index | |||||
| Perplexity | Real-time web index | |||||
| You.com | Web and news index | |||||
| Firecrawl | Live web and news | |||||
| Octen | Live web index | |||||
| Parallel | Agent search index | |||||
| Valyu | Web and specialist sources |
Specialized APIs
For a question about right now, such as a market’s odds, a score, a price, a flight, a wait or an outage, Grounder also asks the specialized API that holds the value, beside the web search, and serves its answer first with the time it was read. routing.live_calls lists the ones a search asked. Your account page sets each to On, Always or Off and takes your own key for one.
| Specialized API | Answers |
|---|---|
| Polymarket | Prediction market prices |
| Kalshi | Prediction market prices |
| The Odds API | Sportsbook lines across books |
| ESPN | Scores, game state and lines |
| MLB | Game state, pitch by pitch |
| OpenF1 | Formula 1 positions and gaps |
| YouTube | A channel’s latest videos and their views |
| Bluesky | Recent posts |
| Truth Social | Recent posts |
| Coinbase | Crypto prices |
| Yahoo Finance | Stock, fund and currency quotes |
| FlightAware | Flight status, delays and gates |
| Transitland | Next departures at a stop |
| Queue-Times.com | Theme park wait times |
| USGS | Earthquakes now |
| National Weather Service | US weather alerts and conditions |
| Federal Reserve | Federal Reserve announcements |
| Status pages | Whether a service is down |
Drop-in endpoints
Already on Tavily or Brave? Swap the base URL and key; keep your code. These endpoints accept each provider’s request format and return its response shape, routed and deduplicated by Grounder.
# Tavily
- https://api.tavily.com/search
+ https://api.grounder.net/compat/tavily/search
# Brave
- https://api.search.brave.com/res/v1/web/search
+ https://api.grounder.net/compat/brave/res/v1/web/search
Code examples
import os
import httpx
KEY = os.environ["GROUNDER_KEY"]
r = httpx.post(
"https://api.grounder.net/v1/search",
headers={"Authorization": f"Bearer {KEY}"},
json={"query": "offshore wind capacity 2026", "top_k": 5},
timeout=30,
).json()
for hit in r["results"]:
source = "Licensed" if hit["provenance"]["licensed"] else "Open web"
print(hit["rank"], hit["domain"], source, hit["provenance"]["found_by"])
const res = await fetch("https://api.grounder.net/v1/search", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.GROUNDER_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
query: "offshore wind capacity 2026",
top_k: 5,
filters: { recency: { mode: "rolling", days: 30 } },
}),
})
const { results, routing } = await res.json()
Data use
What Grounder stores when you search, who else sees it, how long it is kept, how it trains routing, and how to opt out.
What a search logs
- The search query, or the prompt your agent passed along in
context.prompt_content; through MCP, the user’s message as typed (user_prompt), so follow-ups in the same chat can be counted. - Your account, the key’s ID, the time, the client’s user agent, and any agent name, model, or metadata the client sent.
- How the query was routed, and for each provider its results’ rank, score, URL, title, date, and the first 500 characters of the snippet; then the merged list with
found_by. - Not stored: your IP address, your key itself (only its SHA-256), other request headers, or full result text. Credentials that appear in a prompt are redacted.
- Not logged: queries typed into the live demo on the home page. They run through the same routing and providers, are capped per visitor and per day, and leave no record.
Who else sees it
To route a search, its prompt and filters go to TypeSafe, which runs Grounder’s routing model, and its query and filters go to each provider that runs it, on Grounder’s own provider keys or on yours where you added one. Grading a finished search once a day sends its query to Claude to write a reference answer, and its query and top results to OpenRouter’s hosts to answer and grade it, never account or chat identifiers. Each of those parties keeps data under its own terms.
How long it is kept
Search records are kept indefinitely, and so is a nightly export of the whole database. Only rate-limit counters and sign-in tokens expire, within 30 days at most.
How it trains routing
No model is trained on your data. Once a day, each finished search is answered from the results it got and graded right or wrong against a reference answer. The grades update shared statistics for each kind of query, and the router reads only those statistics when it picks a provider.
Opting out
Turn it off on your account page, or use your key:
curl -X PUT https://api.grounder.net/v1/account/training \
-H "Authorization: Bearer $GROUNDER_KEY" \
-H "Content-Type: application/json" \
-d '{"opt_out": true}'
# {"training_opt_out": true, "updated_at": "…"}
GET /v1/account/training shows the current setting and {"opt_out": false} reverses it. Searches keep working and are still logged for rate limiting and debugging. None of your searches, past or future, are graded or count toward the statistics; the next nightly recalculation leaves out what routing learned from them. Opting out deletes nothing.