Grounder › Docs
Tools & API reference
Grounder is three tools that ground any LLM in the live web - and nothing else. It returns evidence; your model writes the answer. This page documents every tool, every parameter, what it returns, and what it costs. Call them over MCP (any MCP client) or REST (any language).
Your key
Enter your email and we email you a key. No card, and no confirmation step - the key itself is the email, so receiving it is what proves the address is yours.
Two ways to call
Base URL https://grounder.dev · Auth every request carries
Authorization: Bearer gnd_live_….
1 · MCP (recommended for LLM clients)
A thin stdio server (grounder-mcp) that exposes the three tools to LM Studio, Ollama,
Claude Desktop, Cursor, Continue.dev, Aider, or any MCP runtime. It just relays each call to
grounder.dev with your key - the fetching and billing happen server-side, so it runs fine on
a laptop. Add one block to your client's MCP config:
{
"mcpServers": {
"Grounder": {
"command": "uvx",
"args": ["grounder-mcp"],
"env": { "GROUNDER_API_KEY": "gnd_live_your_key_here" }
}
}
}
Your model then calls web_search, fetch, and deep_search like any
other tool. The MCP surface is intentionally minimal (the few parameters a model should reason about); the
REST endpoints accept a couple more (noted per tool).
2 · REST (any language)
curl -X POST https://grounder.dev/v1/search \
-H "Authorization: Bearer gnd_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{"query": "latest stable Node.js LTS version", "region": "us"}'
Endpoints: POST /v1/search (web_search), POST /v1/fetch,
POST /v1/deep_search. Every response includes a request_id for tracing, and the
billed calls return X-Credits-Charged / X-Credits-Remaining headers - the
count is in pages.
How pages work
- A page is any single thing Grounder pulls for you - one
web_searchis one page, one page read byfetchis one page. deep_searchreads several pages to build one pack and counts 1 page per page it actually reads (up tomax_fetches, default 6) - the search step itself is free. A typical call is ~3-6 pages.- You only pay for pages we deliver. A
fetchthat comes back blocked/empty is free; a non-blockingstate:"fetching"reply is free until the content actually returns. - A recently-fetched page is served from cache and returns instantly - look for
X-Cache: HIT. A cache hit counts as one page, the same as any other. - Your monthly pages reset on the 1st and never accumulate. Full numbers on the
pricing page; the live, authoritative limits are at
/v1/plans.
web_search
Search Google and return the organic blue-link results - title, URL, snippet, domain - for a query, up to
10 results. Use it when you need current sources or URLs to read further (then call fetch).
For a quick fact a snippet already answers, this alone is enough; to read a whole page, follow up with
fetch.
Parameters
| Param | Type | Default | Notes |
|---|---|---|---|
| query | string | - | Required. 1-2048 chars. Natural language; quoted phrases work as on google.com. |
| region | string | "us" | ISO-3166-1 alpha-2 lowercase (Google's gl) - e.g. us vs gb return different sites. Note: de/fr have DMA-driven citation-pool restrictions. |
| source | "google" | "ddg" | "google" | REST only. ddg = an anonymized search mode: the query is sent privately and never touches our servers, so it can't be tied back to you. Results can differ slightly from google. |
| page | integer | 1 | REST only. 1-10, deeper into the SERP. |
Example & response
curl -X POST https://grounder.dev/v1/search \
-H "Authorization: Bearer gnd_live_…" -H "Content-Type: application/json" \
-d '{"query": "best wireless headphones 2026", "region": "us"}'
{
"query": "best wireless headphones 2026",
"results": [
{ "position": 1,
"title": "The Best Wireless Headphones for 2026",
"url": "https://www.rtings.com/headphones/…",
"domain": "www.rtings.com",
"snippet": "Our top pick is the Sony WH-1000XM6…" }
],
"results_count": 10,
"meta": { "captcha": { "fired": false, "cost_usd": 0.0 }, "cookie_persistence": true },
"request_id": "req_…"
}
If the search fails after retries, results comes back empty with a top-level
unavailable_reason (captcha_failed | wait_timeout |
transient_failure) instead of an exception - check results_count.
Using it well
- Treat it as the URL-discovery step: search, pick the right result, then
fetchthat page for the actual text. (AfetchURL must come from a recent search - see below.) - Use
regionto get a specific country's sources (local pricing, local vendors). - Use
source:"ddg"(REST) when you want the query sent anonymously, not from our servers.
fetch
Fetch the full, clean content of ONE web page as markdown - the actual current page (exact pricing, full docs, the complete article), fetched live. Handles most sites, including hard, protected pages that other tools return empty on. Use it to READ a result when the snippet isn't enough.
url must be on a domain a recent web_search
(or deep_search) by this key returned. fetch is for reading the sources you found,
not arbitrary URLs. Out-of-scope → 403 url_not_in_scope.
(Need to fetch any URL directly? That's the separate scraper API.){ "state": "fetching", "retry_after_seconds": 15 }; call fetch again with the
same url until content returns. A fetching reply is free, and most pages
return on the first call.Parameters
| Param | Type | Default | Notes |
|---|---|---|---|
| url | string | - | Required. 1-2048 chars. Must be a URL a recent search returned (scope rule). |
| clean | boolean | true | true → clean markdown in content. false → raw HTML in html. |
| query | string | null | Optional, ≤2048 chars. If set, the response ALSO returns passages: the few verbatim slices most relevant to this query (bge-small rerank), each with a score. content is still returned - passages is additive. |
| max_tokens | integer | null | With query only: cap passages to ~this many tokens (est. ~4 chars/token) so they fit your window. 100-8000. No effect without query. |
Example & response
curl -X POST https://grounder.dev/v1/fetch \
-H "Authorization: Bearer gnd_live_…" -H "Content-Type: application/json" \
-d '{"url": "https://www.rtings.com/headphones/…",
"query": "best noise-cancelling pick", "max_tokens": 600}'
{
"success": true, "status": 200, "tier": 1,
"content": "# The Best Wireless Headphones for 2026\n\nOur top pick is…",
"content_chars": 6213,
"passages": [ { "text": "For noise cancelling, the Sony WH-1000XM6…", "score": 0.82 } ],
"cached": false, "elapsed_ms": 180, "request_id": "req_…"
}
Tiers. tier is the cost class that succeeded, from 1 (an easy page) up to
3 (a hard, protected page). Your plan caps which tiers you can reach; a Free key never spends a
tier-3 fetch - a hard page returns 402 upgrade_required instead, so you're never silently
charged for one.
Using it well
- Pass
query+max_tokensto get back only the relevant passages capped to your context window - essential for a small local model, where dumping a whole page overflows an 8k window and returns nothing (see the benchmark). - Use
clean:falsewhen you need the raw HTML (tables, structured markup) rather than prose. - You can fetch any page on a domain a search surfaced - follow sub-pages and links of your grounded sources, not just the exact result URL.
deep_search
Answer a question in ONE call that needs reading and comparing MULTIPLE pages. It runs a search, fetches
several of the top result pages, ranks passages across all of them, decides internally when it has read
enough, and returns the few verbatim slices most relevant to your question. It returns
evidence, not a written answer: your model reads the passages and composes the answer - and decides
whether they actually answer the question, because that depends on what you are really asking, not on us.
Don't use it for a quick fact a single snippet answers (use web_search) or to read one known URL
(use fetch).
Parameters
| Param | Type | Default | Notes |
|---|---|---|---|
| query | string | - | Required. 1-2048 chars. The question, in natural language. |
| max_fetches | integer | 6 | Hard cap on pages read (cost + latency bound). 1-12. Counts as 1 page per page actually read, up to this. |
| max_tokens | integer | 600 | Cap the returned pack to ~this many tokens so it fits your window (~4 chars/token). 100-4000. |
| source | "google" | "ddg" | "google" | REST only. Same anonymized tier as web_search. |
Example & response
curl -X POST https://grounder.dev/v1/deep_search \
-H "Authorization: Bearer gnd_live_…" -H "Content-Type: application/json" \
-d '{"query": "which production databases use the Raft consensus algorithm?",
"max_fetches": 6, "max_tokens": 600}'
{
"query": "which production databases use the Raft consensus algorithm?",
"passages": [
{ "text": "YugabyteDB uses Raft for leader election and data replication…",
"score": 0.84, "url": "https://www.yugabyte.com/…" }
],
"pages_fetched": 2, // pages that returned real content (billed)
"pages_attempted": 3, // incl. pages we couldn't fetch, skipped (free)
"retrieval_relevance": 0.84, // best passage's similarity to the query - a coverage hint
"stop_reason": "answered", // why the fan-out stopped: answered | page cap reached | search results exhausted
"request_id": "req_…"
}
Each passage carries its source url so your model can cite it. Grounder does not return
a SUPPORTED / NOT_FOUND verdict: whether the evidence answers a question depends on how you phrased it, which
only your model knows, so it reads the passages and decides. retrieval_relevance and
stop_reason are coverage hints, not a verdict. If the passages don't contain the answer, have
your model say so rather than guess.
Using it well
- Reach for it on comparison / synthesis questions ("how do X and Y differ", "what do reviewers
say about Z") instead of orchestrating
web_search+ manyfetchcalls yourself. - Set
max_tokensto your model's budget - the pack is guaranteed to fit, so a context-starved local model gets only the answer-bearing evidence. - Instruct your model to answer only from the passages and to say it couldn't verify the answer when they don't contain it - that is what keeps a small model from inventing.
- Lower
max_fetchesto cap cost/latency on cheap questions; raise it (up to 12) for hard ones.
Errors
Errors return { "error": { "code", "message", "request_id" } } with an HTTP status. The
code is stable - branch on it.
| Code | HTTP | Meaning & what to do |
|---|---|---|
| invalid_request | 400 / 422 | Bad/missing parameter (e.g. query > 2048 chars). Fix the input. |
| invalid_api_key | 401 | Missing or unknown key. Check Authorization: Bearer gnd_live_…. |
| quota_exceeded | 402 | Out of your monthly pages. Wait for the reset or upgrade. |
| upgrade_required | 402 | The call is valid, but your plan can't fetch this one - a hard, protected page needs a paid plan. Upgrade. |
| url_not_in_scope | 403 | fetch only: the URL's domain wasn't returned by a recent search by this key. Search first. |
| rate_limited | 429 | Per-key rate limit hit. Back off and retry (the bucket refills at your plan's per-minute rate). |
| fetch_failed | 422 | The page couldn't be fetched (blocked or unreachable). Not charged. If the domain has refused every tier on recent attempts we stop buying attempts and say so - it is re-tried automatically once a day, so a retry loop won't help but coming back tomorrow will. |
| internal_error | 500 / 502 / 503 | Our backend was briefly unavailable. Retry with backoff. This is the only family where a 5xx means us - a page we simply could not read is a 422, not a 502. |
| google_rate_limited · captcha_failed · timeout | - | Transient search-side conditions on web_search/deep_search. Retry with backoff. |
Getting a written answer (optional)
deep_search returns evidence, not prose: ranked verbatim passages, each with its source
URL, so you compose the answer and can always check it. Pass include_answer: true and the response
also carries answer - a short answer written from those same passages by a 70B model.
It is additive and free: passages come back either way, so the answer never replaces the evidence
it was built from, and you can check one against the other. If the passages don't support an answer the field is
simply omitted rather than filled with a guess. Costs no extra pages.
curl -s https://grounder.dev/v1/deep_search \
-H "Authorization: Bearer gnd_live_your_key_here" -H "Content-Type: application/json" \
-d '{"query": "what does R2 charge for class A operations", "include_answer": true}'
Rate limits & plans
Each plan sets a monthly page budget, per-tool rate limits (a token bucket - a burst capacity plus a
sustained per-minute refill, sized so an agent's fan-out completes instead of 429-ing halfway), and caps on
the harder fetches. Hard-fetch is a paid-plan feature; deep_search is free to try
(rate-limited on Free) and runs at full speed on paid plans. See the full
pricing page, and query /v1/plans for the exact
limits the gate is enforcing right now.
Recipes
Ground a local model, capped to its window
import requests
KEY = "gnd_live_…"
pack = requests.post("https://grounder.dev/v1/deep_search",
headers={"Authorization": f"Bearer {KEY}"},
json={"query": "latest stable Node.js LTS version", "max_tokens": 600},
timeout=180).json()
evidence = "\n\n".join(f'[{p["url"]}] {p["text"]}' for p in pack["passages"])
# hand `evidence` to your local model (it fits an 8k window) with an instruction like:
# "Answer only from the sources below; if they don't contain it, say you couldn't verify it."
Read one page for an exact fact
hits = requests.post("https://grounder.dev/v1/search",
headers={"Authorization": f"Bearer {KEY}"},
json={"query": "OpenAI API pricing"}).json()["results"]
page = requests.post("https://grounder.dev/v1/fetch",
headers={"Authorization": f"Bearer {KEY}"},
json={"url": hits[0]["url"], "query": "price per million input tokens",
"max_tokens": 400}).json()
print(page["passages"]) # only the pricing lines, not the whole page
Search without touching our infrastructure
requests.post("https://grounder.dev/v1/search",
headers={"Authorization": f"Bearer {KEY}"},
json={"query": "site:reddit.com best budget mechanical keyboard",
"source": "ddg"}) # sent anonymously, not from our servers
Get a key and try it
Free tier: 1,500 calls a month, email only, no card. Drop it into your MCP config and your model can search and read the live web.
Get a free key