Dobby is freeeeeee — no key, no signup, no card. We don't read docs, we call the endpoint and publish what came back, including the 402s.
Free LLM API for agents: the /api/llm/up reference
Ask which free LLM endpoint answered recently, and get back the base URL, the model id, whether a key is needed and what a real call proved. No key, no signup. The list is ranked, so models[0] is the pick and the rest are your fallbacks.
Quick start
curl -s https://stillworks.supercapybara.com/api/llm/upAny client that speaks the OpenAI chat-completions shape can use what comes back. Or get the three environment variables directly:
curl -s https://stillworks.supercapybara.com/api/llm/up?format=envWalk the list until one answers
Keyless quotas are per IP, so the endpoint that answered us can refuse you. The pattern is a loop.
import json, urllib.request
from openai import OpenAI
shelf = json.load(urllib.request.urlopen(
"https://stillworks.supercapybara.com/api/llm/up"))
for endpoint in shelf["models"]: # already ranked, best first
client = OpenAI(base_url=endpoint["openai_base_url"],
api_key="not-needed")
try:
reply = client.chat.completions.create(
model=endpoint["model"],
messages=[{"role": "user", "content": "hello"}],
)
except Exception:
continue # your IP hit its quota — next
print(endpoint["model"], reply.choices[0].message.content)
breakimport OpenAI from "openai";
const shelf = await fetch("https://stillworks.supercapybara.com/api/llm/up")
.then((response) => response.json());
for (const endpoint of shelf.models) { // already ranked, best first
const client = new OpenAI({
baseURL: endpoint.openai_base_url,
apiKey: "not-needed",
});
try {
const reply = await client.chat.completions.create({
model: endpoint.model,
messages: [{ role: "user", content: "hello" }],
});
console.log(endpoint.model, reply.choices[0].message.content);
break;
} catch { continue; } // your IP hit its quota — next
}Parameters
| Parameter | Values | Effect |
|---|---|---|
key | none (default), key, any | none returns only endpoints that answered with no Authorization header. key and any add providers that need your own key, each row labelled. auth is the older spelling and still works. |
feature | tools json_schema json_object vision image_out | Repeat or comma-separate; all must hold. A feature is returned only where a real call proved it. A model never probed for it is not returned. |
format | env | Plain text: OPENAI_BASE_URL, OPENAI_API_KEY, OPENAI_MODEL, taken from the top OpenAI-compatible row. |
An unknown value is a 400 that names the valid ones, not an empty list: an empty list would read as "everything is down". At most 50 models are returned per call. Replies are Cache-Control: no-store and CORS is open, so a browser can call it too. GET /api/llm/<endpoint> returns one provider with its models and recent probes.
What each model carries
| Field | Meaning |
|---|---|
model, endpoint | The model id to send, and the provider slug. |
openai_base_url | What to give an OpenAI client as its base URL. Not always the provider's bare host. |
auth | none needs no Authorization header; bearer needs one of your own. |
answered, recent_attempts, recent_successes | How many of the last seven days' chat probes answered. |
last_ok, latency_ms | When it last answered, and how long that reply took. |
proved[] | Features a real call confirmed. |
claimed_unproved[] | Features the provider lists that we have not confirmed. Not a "no". |
failed[] | Features a call tried and did not get. |
capabilities | Each feature with the time it was checked and the error, where there was one. |
A real row, as returned a moment ago
{
"endpoint": "pollinations",
"base_url": "https://text.pollinations.ai",
"chat_path": "/openai",
"auth_mode": "none",
"model": "openai-fast",
"tier": "anonymous",
"chat_capable": true,
"openai_compatible": true,
"last_ok": "2026-10-12T07:46:10.272759Z",
"latency_ms": 309,
"capabilities": {
"json_object": {
"claimed": null,
"supported": true,
"checked_at": "2026-10-12T07:37:16.292697Z",
"last_ok": "2026-10-12T07:37:16.292697Z"
},
"json_schema": {
"claimed": null,
"supported": true,
"checked_at": "2026-10-12T07:36:45.926976Z",
"last_ok": "2026-10-12T07:36:45.926976Z"
},
"tools": {
"claimed": true,
"supported": false,
"checked_at": "2026-10-12T07:36:15.589728Z",
"last_ok": null,
"last_error": "answered without calling the tool"
},
"vision": {
"claimed": false,
"supported": null,
"checked_at": "2026-10-12T07:37:46.589406Z",
"last_ok": null,
"last_error": "no colour named in: The API key used for this request has reached its budget. Please [raise the key budget](https://enter.pollinations.ai/edit-key?id=89idjaTSg2hI4YwZDuO8ZME5Ma6GrFps\u0026ref=agent_key_budget), then try again."
}
},
"recent_attempts": 175,
"recent_successes": 175,
"openai_base_url": "https://text.pollinations.ai/openai",
"auth": "none",
"answered": "175/175",
"proved": [
"json_schema",
"json_object"
],
"claimed_unproved": [],
"failed": [
"tools"
]
}The sponsored field
The response carries one more top-level key, sponsored: null when the slot is open, otherwise a paid placement with "sponsored": true, a label, a notice, the sponsor's provider, pitch and a tracked url, and a probe that is our real probe row for the sponsor's endpoint when it is on the shelf and null when it is not. It is never inside models and it never changes their order, so models[0] is still the pick. Ignore the key if you only want the ranked list. Sponsor the slot.
Errors and guarantees
An unknown key or feature answers 400, with the valid values in the body. An unknown endpoint slug answers 404. There is no guarantee: these are other people's free services, and an endpoint can add a key requirement or vanish between two probes. The payload says so in disclaimer, on every call.