The Known Good API
One endpoint, no key required to start. It answers with the sites whose agent-facing capabilities we have actually probed, and tells you when we probed them.
Search
GET https://knowngood.sh/api/find?q=buy%20perfume&country=SA&limit=3
Returns JSON. No authentication is needed; a key only raises your rate limit.
Parameters
| Name | Type | Meaning |
|---|---|---|
q | string, optional with a filter | The query. Natural language works; so do keywords. Truncated at 300 characters. Leave it out and set at least one filter to list every matching site instead — see Filter only below. |
country | ISO 3166-1 alpha-2 | Upper-cased for you. GB, SA, DE. |
entity_type | string | The kind of site, as graded. See /type/ for the values in use. |
transactional | true | false | Whether the site exposes something an agent can act on. |
has_mcp | true | Only sites where our strict MCP re-grade ran and passed — the server behind the card answered tools/list when we tested it. This is narrower than “has a working MCP server”: sites we have not yet re-graded are excluded even if their server would answer. The response’s filter_completeness field states how many that is. |
has_md | true | Only sites whose markdown negotiation passed the strict re-grade — the body is markdown and differs from the HTML — or that ship a markdown twin. |
has_endpoint | true | Only sites with a working MCP server or an API catalogue. |
has_webmcp | true | Only sites whose in-page WebMCP tools were runtime-verified: the page was executed and registered at least one tool. Code that registers nothing does not count. |
limit | 1–25, default 10 | How many results to return. |
Filter only
GET https://knowngood.sh/api/find?has_mcp=true&country=GB
Leave out q and give at least one filter, and the response lists every listed site matching the filters rather than the most relevant ones: mode is filter, total is how many match, and the order is fixed — platform subdomains last (as the flag policy publishes), then readiness score (checks passed, of 5) descending, then most recent probe first, then hostname. The same URL returns the same page.
Response
Each result carries what we observed, separated from what the site claims. The
verified block is probe evidence; apparent_actions is read from page
content and is explicitly unverified. Cite the report URL and the date.
Two name fields. name is the stored value exactly as
crawled — what the site calls itself, which is often a full meta title.
display_name is the same value with a trailing tagline removed and a length
cap applied; it is what our own pages render. Both appear on
/api/find, get_site_report and the MCP tool output.
{
"query": "buy perfume",
"count": 2,
"data_as_of": "2026-08-28",
"top_similarity": 0.70,
"results": [
{
"host": "example.com",
"url": "https://example.com/",
"name": "Example - The best widgets online",
"display_name": "Example",
"report": "https://knowngood.sh/site/example.com",
"verification_tier": 2,
"tier_label": "probe-verified",
"transactional": true,
"apparent_actions": [ { "action": "buy", "evidence": "…" } ],
"verified": {
"verified_at": "2026-08-23",
"markdown_negotiation": true,
"llms_txt": true,
"api_catalog": false,
"mcp_server": false,
"mcp_tools": []
}
}
]
}When nothing genuinely matches, the API returns an empty results array and an
empty_note saying so. It does not pad the answer with weak nearest neighbours.
In a search, has_md and has_endpoint are applied to the
results rather than inside the search, so they search deeper until the page
is full. If it still is not, the response carries page_not_filled with what was
asked for, what came back, how many candidates were searched and why it stopped:
population_exhausted, depth_limit or
could_not_search_deeper. A short page is never presented as complete.
A search without those two filters drops weak matches: a row scoring below
floor_fraction of the best match is left out. When that shortens the page, the
response carries page_not_filled with reason: below_relative_floor and
candidates_cut. The best match is never dropped, so this never turns a result into
an empty one.
When a filter leaves nothing, the empty_note says which filter applied and how
many listed sites have no known value for it, so an empty answer under a filter is never
presented as the whole index having nothing.
Errors
Every error is the same JSON object: a stable code, a class
naming the family, human error and hint prose, and
see. code and class are the contract
and do not change; error and hint are prose and may be reworded,
so do not match on them.
| class | Status | What it means |
|---|---|---|
bad_request | 400 | You asked something we cannot answer — a missing q, or a filter value outside its vocabulary. valid lists the choices where there is a closed set. Your fix. |
rate_limited | 429 | You asked too often. Retry-After says when the window resets; a free key raises the limit and nothing else. |
unavailable | 5xx | We could not answer. Nothing whatever is implied about the index: this is never an empty result set, never count: 0, and never an empty_note. A search that could not run is not a search that found nothing, and we will not report it as one. |
A no-match is not an error. When nothing matches, the API returns
200 with an empty results array and an empty_note.
That is a measurement. An unavailable is the absence of one, and the two are
never conflated in either direction.
Filter completeness
A filter is a promise about completeness, and ours cannot always keep it. Any response
with a filter applied carries filter_completeness, keyed by filter name, saying
what that filter could not speak for. It is absent when nothing was excluded.
“Unknown” is reported separately from “no”, and that distinction is the point. A site excluded because we know it does not match is a measurement. A site excluded because nobody has asked is not — it may well match. An API that sums the two tells you a filter returned few results without telling you whether the index is empty or merely unmeasured.
| Field | Meaning |
|---|---|
eligible | Listed sites whose value is KNOWN and satisfies this filter. These are the rows the filter could return. |
excluded_no | Listed sites whose value is KNOWN and does not satisfy it. A real negative — we asked, and the answer was no. |
excluded_unknown | Listed sites where the value is UNKNOWN, so they cannot match any value of this filter. Not a negative: we did not ask, or could not. These sites may well satisfy it. |
of_listed | Every listed site, the denominator for the three above. |
The three populations reconcile against the fourth, so you can always check them:
eligible + excluded_no + excluded_unknown = of_listed. Each entry also carries
a note stating precisely what the filter tests — has_mcp=true,
for instance, means our strict re-grade ran and passed, which is narrower than
“has a working MCP server”.
Rate limits
Counted per caller per UTC day.
| Caller | Requests per day |
|---|---|
| Keyless (by IP) | 500 |
| With a key | 10,000 |
Getting a key
Keys are issued instantly and anonymously to agents — no personal data, no approval
step. The flow is documented for machines at /auth.md. In short:
POST https://knowngood.sh/agent/auth, then send the credential as
Authorization: Bearer <credential>. Registration is capped at
3 per caller per day; reuse the key you were given. A key raises your rate
limit and nothing else — it never affects results, ranking or placement.
MCP
The same index is available over the Model Context Protocol at
https://knowngood.sh/mcp, exposing a find tool with the same filters. The
server card is at /.well-known/mcp/server-card.json.
In your browser
You can add Known Good to your browser's search bar: visit the site once, then pick Known Good in your browser's search-engine settings and query the index straight from the address bar.
Fair use
Please cite the report URL and its date when you use our findings — the date is the point. The terms set out what is and is not allowed; in particular, do not bulk-extract the index or register keys to evade the limits.
Changelog
Every row is a release of this API and its MCP server, labelled with its software version and dated the day it shipped. Changes to the index and the rubric are logged on /benchmark.