Free beta
API
Give Nerdulator something. It tells you what it could be, in recognition order, and what derives from it. The same answer every time, with no model anywhere in the path.
No key, no account, no rate card during beta. Version 1, behaviour 4b93e0722875.
Or install it
These call this same API, so anything it recognises, they recognise.
Try it
This calls the live endpoint. Click a relation to follow the graph.
Thirty seconds
curl https://nerdulator.com/api/v1/inspect \
-H "Content-Type: application/json" \
-d '{"input":"1066"}'{
"input": "1066",
"deterministic": true,
"readings": [
{ "rank": 1, "reading": "integer", "name": "Number", ... },
{ "rank": 2, "reading": "year", "name": "Year", ... },
{ "rank": 3, "reading": "colour", "name": "Colour", ... }
],
"api_version": "1",
"behaviour_version": "4b93e0722875"
}Prefer a typed client? /api/v1/openapi.json describes all of this, and every generator and API client reads it.
You never choose an interpretation. That is the point: 1066 honestly is a number, a year and a colour, and which one you meant is not something a parser can know, so all three come back and you pick.
Endpoints
POST /api/v1/inspect
Every reading of an input, in recognition order, each with its facts and relations. Body: {"input": "…"}, or {"input": {"type": "text", "value": "…"}}.
POST /api/v1/inspect/{reading}
One reading in full, once you know which you want. An input that is not that kind of thing returns an empty list, not an error.
GET /api/v1/objects/{reading}/{canonical}
A node you already know the identity of, which is what a relation hands you. The two segments are the halves of an object id: integer:1066 is /objects/integer/1066. A GET rather than a POST, because a POST cannot be cached by anything in between and traversal should be cheap.
GET /api/v1/readings
The vocabulary: every reading id and every typed relation name.
The machine-readable description. Point Postman, a client generator or Swagger UI at it. Generated from the same constants the code validates against, so it cannot drift: the reading ids in the spec are the reading ids the API accepts, because they are the same array.
GET /api/v1/health
Runs a real inspection rather than reporting that the process is up.
The response shape
Identical for every reading, whatever the engine underneath. A UUID, a chess square and a balanced chemical equation all come back the same way, so you write the loop once:
for (const reading of result.readings) {
reading.reading // stable id, e.g. "integer"
reading.id // "integer:1066", a stable object identity
reading.rank // recognition order, NOT a probability
reading.match // "exact" | "normalised" | "fallback"
reading.canonical // "1066", the object's name
for (const fact of reading.facts) {
fact.label // human-readable, may be reworded
fact.value // always a string
fact.value_type // integer | decimal | quantity | date | boolean | string
fact.value_number // present when the value IS a number
fact.value_unit // present for a quantity: "g/mol"
fact.approximate // true when it is not exact
}
for (const rel of reading.relations) {
rel.relation // typed name, or null
rel.target.id // "integer:1067"
GET("/v1/objects/" + rel.target.reading + "/" + rel.target.canonical)
}
}rank is recognition order, not a probability. There is no confidence score here and there never will be: recognition is regular expressions and lookup tables, so a percentage would be inventing a number nothing computed. Rank 1 is the recogniser with the strongest claim under a fixed rule, and the same input always gives the same order. Where you need something sharper, match says how the reading was reached: exact parsed the input as given, normalised matched only after rewriting or deslugging it, and fallback is the catch-all text reading.
What is contractual: id, reading, canonical, match, value_type, relation names and the envelope. What is not: fact label and note text, which is written for people and gets reworded when a page reads better for it. Key off ids, not prose.
What deterministic means here
Within one behaviour version, the same input returns byte-identical output. The version is the boundary, and it is the thing to pin: it covers the engines and the response shape together, so any change a byte comparison would notice moves it. That is checked rather than promised: 55 inputs are run through the real engines and hashed, and the digest is the behaviour_version on every response and in the X-Nerdulator-Behaviour-Version header. A date would not do: five deploys in one day can all be “2026.09” while the answers move underneath you.
The cost is that clock-dependent facts are excluded. A year page says “960 years ago” to a reader, and that cannot be in a reproducible response, so it is dropped and deterministic: true says so. Add ?volatile=true if you want them, and the flag flips to false.
Responses carry an ETag. Because the output is deterministic, a conditional request costs nothing until the behaviour version moves; worth honouring if you are inspecting the same values repeatedly.
Not knowing is an answer
{ "input": "flibbertigibbet", "readings": [ { "reading": "text", "fallback": true, … } ] }Nothing recognised it, so the only reading is the catch-all text analysis, marked fallback: true. It is returned rather than hidden, because a character count is a real answer, and flagged rather than presented plainly, because it is not recognition. Filter on the flag if you only want confident readings. Either way it is a 200: “we looked and we do not know” is not an error.
Limits
Per caller, no key needed. Every response carries X-RateLimit-Limit, -Remaining and -Reset, plus X-Quota-* for the hourly window; a refusal is a 429 with Retry-After.
A revalidated request is refunded. If you send an If-None-Match and get a 304, nothing was computed and the token goes back. Caching therefore costs you nothing against the limit, which is the point of counting here rather than at the proxy, since a proxy cannot tell the two apart.
Errors
{ "error": { "code": "invalid_request", "message": "…", "request_id": "req_…" } }- 400 invalid_request: malformed body or missing input
- 404 unsupported_reading: unknown reading id
- 405 method_not_allowed: inspect is POST
- 413 input_too_large: over 10 KB
- 429 rate_limited: too fast, or hourly quota spent
- 504 timeout: exceeded the per-request budget
- 500 internal_error: our fault
Beta terms
The API is free while in beta, with no key required. Responses are deterministic and strongly cacheable; clients should honour the ETag and cache successful responses where appropriate, which also costs nothing against the rate limit.
Results are deterministic and reproducible, produced by the same engines that serve nerdulator.com. No AI is used to generate any of them.
If usage-based pricing is introduced later, or the free allowance is materially reduced, there will be reasonable notice first. Keys and quotas will arrive if and when the traffic warrants them; nothing you build against version 1 will break without a version 2.
74 readings
Stable ids. Engine names are provenance, not contract; an engine is an internal grouping and may be reorganised; these will not be renamed under you.
37 typed relations
An edge with a fixed meaning gets a name. Every other edge is still returned and still traversable, with relation: null and its label; an honest null beats an invented name you might act on.
Built on the same engines as the site itself. Nothing is calculated twice, and nothing here is API-specific.