Errors
View as MarkdownOne error shape for every endpoint, a closed set of types, and which of them are worth retrying. What gets refunded when a call fails.
Every failure on every endpoint has the same shape, and the type is from a closed set. That is deliberate: a client writing a retry policy needs to know which failures are worth retrying, and free text cannot answer that.
The shape
{
"error": {
"type": "insufficient_credits",
"message": "This search reserves 10 credits and your balance is 3. Top up at /pricing.",
"balance": 3
}
}Branch on type. Print message: it is written for the person reading their
own terminal, and it says what to change rather than apologising.
The types
| Type | Status | Retry? |
|---|---|---|
unauthorized | 401 | No. No key, a malformed one, or one that has been revoked. |
invalid_request | 400 | No. The body or a field is wrong, and the message names which. |
insufficient_credits | 402 | After topping up. Carries balance. |
rate_limited | 429 | After retryAfter seconds. |
upstream_error | 502 | Once. A source I read failed; any charge is refunded. |
server_error | 500 | With backoff. My fault. |
forbidden | 403 | No. A valid key, but not for this. |
Retrying without making it worse
async function withRetry<T>(call: () => Promise<Response>): Promise<T> {
const retryable = ["server_error", "upstream_error", "rate_limited"];
for (let attempt = 0; ; attempt++) {
const res = await call();
if (res.ok) return res.json();
const { error } = await res.json();
if (!retryable.includes(error.type) || attempt >= 3) {
throw new Error(`${error.type}: ${error.message}`);
}
const wait = error.retryAfter ?? 2 ** attempt;
await new Promise((r) => setTimeout(r, wait * 1000));
}
}Retry server_error and upstream_error with exponential backoff. Retry
rate_limited after the time it asked for, and not before: the limit is a
token bucket, so an early retry spends the budget it was waiting for. Never
retry unauthorized or invalid_request in a loop, because nothing about
either will change on its own.
What a broken run tells you, and what it does not
A failed search reports one sentence and no internals. That is on purpose: the underlying message names the model stack and the services behind it, which are not yours to debug, and an upstream "API key is invalid" read literally sends somebody off to revoke and re-mint a key of their own that was working perfectly. The real message goes to the error tracker, where the person who can act on it will see it.
Errors inside a stream
Once a streamed search has started, the HTTP status is already 200 and there
is no way to take it back. A failure after that point arrives as an
{ "type": "error", "message": "..." } event on the stream.
Everything that can refuse a search - no credits, too many at once, a malformed body - is decided before the first byte is written, so those stay ordinary status codes.
Try it on the search you came here for
Stop building boolean strings. Just describe the person.
Type one sentence. I plan the search, read GitHub, public LinkedIn and Crunchbase records, and the open web as it is right now, and hand back a ranked list with the reason next to every name.
01Describe them
One plain sentence. Role, city, stack, stage, whatever matters to you.
02I read the web live
GitHub, public LinkedIn and Crunchbase records, the open web. Not a database that went stale last quarter.
03You read the shortlist
Ranked, with the reasoning under every name. Open a profile, ask a follow-up, narrow it down.
- Staff backend engineers in NYC who shipped Rust in production
- Series A fintechs in SF under 50 people, growing headcount this year
- Maintainers of fast-growing Rust web frameworks on GitHub
- No boolean, no filters, no seat to buy. One box.
- Read at search time, so a profile updated yesterday counts today.
- Every step visible as it runs, every name with its reason.
500 free credits on sign-up. No card, no demo call. See real searches.