Refolk
DevelopersReference

One 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

TypeStatusRetry?
unauthorized401No. No key, a malformed one, or one that has been revoked.
invalid_request400No. The body or a field is wrong, and the message names which.
insufficient_credits402After topping up. Carries balance.
rate_limited429After retryAfter seconds.
upstream_error502Once. A source I read failed; any charge is refunded.
server_error500With backoff. My fault.
forbidden403No. 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.

  1. 01Describe them

    One plain sentence. Role, city, stack, stage, whatever matters to you.

  2. 02I read the web live

    GitHub, public LinkedIn and Crunchbase records, the open web. Not a database that went stale last quarter.

  3. 03You read the shortlist

    Ranked, with the reasoning under every name. Open a profile, ask a follow-up, narrow it down.

  • 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.