Seltz
Guide

Guide · 8 min read · 2026-06-04

Build an AI recruiting agent with Seltz.

Describe the person you want to hire, and a minute or two later a shortlist of real people comes back, with links and evidence. The planning and ranking is just a language model. The part that finds actual humans out on the live web is Seltz.

By William EspegrenSeltz / Recruiting

A recruiting tool has two hard problems hiding inside it. One is judgment: given a person, deciding whether they are actually worth a recruiter’s time for this role. A good model is excellent at that. The other is finding people in the first place, and a model on its own is useless at it. It cannot see anyone who was not in its training data, and the people you want to hire are out on the open web right now, not sitting in a snapshot that froze months ago.

This post walks through a demo that splits those two jobs cleanly. The judgment runs on a language model, and you can pick which one. The finding runs on Seltz, a web knowledge API that returns clean, source-backed documents. The Seltz call itself is tiny, almost boring. What makes the demo interesting is how many times that call runs and what wraps it, and that is the whole point.

01.What the demo does

The demo is a chat page. You type a hiring request in plain language, something like “machine learning engineer who medaled in olympiads” or “CEO of a startup who raised in 2026,” and a minute or two later a shortlist of real people comes back. Each one has a name, a link to their profile, a short reason they fit, and evidence pulled from the open web. If the request is too thin to act on, the agent asks a clarifying question first instead of guessing.

From there it behaves like a working tool. You pick the candidates you like and skip the ones you do not, and the list re-ranks around your taste. Here is the thing to hold onto before we look at any code: the model never browses the web. It decides what to look for and which results are any good. Every actual person on the screen came out of a Seltz search.

02.The architecture: who finds, who decides

The instinct now that some models ship with web search built in is to let one model do this end to end. This demo keeps two jobs apart on purpose, because they want different things.

The line between them is strict. The model emits queries; it never reaches the web itself. It can ask for a search, but only the server runs one. That boundary holds whether you are firing off a single search or, as here, a whole fan of them at once.

03.The Seltz call

Here is the hero, the reason any of this works. When the agent needs to find people, this is what answers. It is short, because Seltz does the hard part.

typescript
// app/api/recruiting/search/route.ts  (server)
import { Seltz } from "seltz";

const seltz = new Seltz({ apiKey: process.env.SELTZ_API_KEY });

async function searchPeople(queries: string[]) {
  const results = await Promise.all(
    queries.map(async (query) => {
      // the "people" scope returns real profiles, not raw web pages
      const result = await seltz.search({ query, maxResults: 5, scope: "people" });

      // each doc is clean content with the source attached, the exact
      // shape a model can read and judge
      return result.documents.map((doc) => ({
        name: extractName(doc.content ?? "", doc.url ?? ""),
        url: doc.url ?? "",
        headline: extractHeadline(doc.content ?? ""),
        location: extractLocation(doc.content ?? ""),
        profile: doc.content ?? "",
      }));
    })
  );

  return results.flat();
}

The three lines that matter are the import, the client, and the call: new Seltz(...) with your server-side key, then seltz.search({ query, maxResults: 5, scope: "people" }). That scope is the interesting bit. Instead of a pile of generic web pages, you get back profiles of actual humans, already shaped as readable content with a url attached. There is a "news" scope too, used to pull funding rounds, press, and company signals that help explain why a person fits. The route then trims each document into a small candidate object the model can reason over.

What is not here is just as important. No scraper, no crawler, no LinkedIn integration to keep alive, no freshness pipeline. You ask Seltz for people and it hands back people. For the full set of parameters the call accepts, the Seltz docs cover the search API.

And if it looks almost too simple, that is the point. This single seltz.search() is the entire engine, and the next section is really just about running it more than once.

04.The same call, fanned out

Here is where a single search stops being enough. A hiring request reads like one question, but it is rarely one search. “Product manager with fintech experience” is really a dozen searches hiding in a sentence: exact title matches, adjacent backgrounds, specific companies, people who turned up in recent fintech news. So the agent plans several search lanes, each with its own angle and its own short list of queries, and runs them at the same time.

Each lane is handled by a small sub-agent: a model that can ask for searches and do nothing else. It calls the same Seltz-backed functions, reads what comes back, and keeps only the candidates worth passing on.

typescript
// each lane gets a worker that can *ask* for searches, but only the
// server runs them
const worker = new ToolLoopAgent({
  model,
  instructions: WORKER_RULES,
  tools: {
    searchPeople: tool({
      description: "Search Seltz people profiles for this lane.",
      inputSchema: peopleQueries,
      execute: ({ queries }) => searchPeople(queries),
    }),
    searchNews: tool({
      description: "Search Seltz news for funding, press, and company signals.",
      inputSchema: newsQueries,
      execute: ({ queries }) => searchNews(queries),
    }),
  },
});

// the planner produces a handful of lanes; we run them all at once
const workerOutputs = await Promise.all(
  plan.lanes.map((lane) => runSearchLane(lane, model))
);

A final pass reviews everyone the workers kept and produces the ranked shortlist. The shape is plan, search in parallel, filter, rank, and you can make that loop as clever as you like. But strip the orchestration away and the only thing reaching the outside world is still seltz.search(), now called many times across two scopes instead of once. The model supplies the queries and the judgment; Seltz supplies the people.

05.Why it runs as a background job

One practical wrinkle is worth a paragraph, and it is worth being precise about where the time actually goes. Seltz is not the slow part: each search comes back in milliseconds. What takes a minute or two is the model — reading every batch of profiles, judging them lane by lane, and ranking the survivors. That reasoning is too long to hold a browser request open and wait on. So the browser does not wait. It posts the search, gets back a run id, and then follows a stream of events: brief ready, lanes planned, this worker found eight profiles, ranking shortlisted nine.

typescript
// app/api/recruiting/search/runs/route.ts  (server)
export async function POST(request: Request) {
  const body = await request.json();
  const runId = crypto.randomUUID();
  const origin = new URL(request.url).origin;

  await createRecruitingRun(runId);

  // a full search can run for a minute or two, so we kick it off in
  // the background and hand the browser an id to follow
  after(async () => {
    await consumeSearchRun(runId, origin, body);
  });

  return Response.json({ runId });
}

The work runs in the background, and the events are durable, so you can reload the page in the middle of a search and reconnect to the same run instead of starting over. From the user’s side it just feels like watching the agent think out loud. What is being streamed, underneath, is the progress of a pile of Seltz searches.

06.Where the keys live

Step back and look at where the secrets sit, because for anything you would put in front of real users this is the part that matters most. There are a few keys in play: the Seltz key, the model provider key, and a couple more for enrichment like contact lookup and company research. Every one of them stays on the server, read from environment variables, and none is ever sent to the browser.

The browser only ever talks to your own /api/recruiting routes. Those routes hold the secrets and make the real calls. The capability that reaches the live web sits behind your endpoint, where you can rate-limit it, log it, and decide what the model is allowed to act on. The model asks; the server decides whether and how to honor it.

07.Conclusion

The fun machinery here is the agent loop: the brief, the parallel lanes, the ranking. That is the part you get to be clever about, and it is genuinely satisfying to build. But none of it matters if the people on the screen are not real. What makes them real is one call, seltz.search({ query, maxResults, scope: "people" }), run as many times as the search needs — and because each call comes back in milliseconds, you can fan out a whole lane of them in parallel without the search ever feeling slow.

That is the takeaway worth keeping. One Seltz call is enough to turn a model from a confident guesser into something you can trust, and that same call scales: wrap it in sub-agents, fan it across lanes and scopes, and it goes from answering a question to staffing a search. You can try the live demo at demo.seltz.ai/recruiting, and when you want to build your own, grab a key from the Seltz console and point a search call at whoever you need to find.