Loading…
I Built a Portfolio Chatbot That Can Tell Recruiters to Pass on Me
post.meta
@dateMar 31, 2026
|
>
4 min
0%4m left

I Built a Portfolio Chatbot That Can Tell Recruiters to Pass on Me

I built an AI chatbot for my portfolio that's allowed to tell recruiters I'm not the right person for the job.

That's not a bug. It's the whole point.

The problem with portfolio bots

Most developer portfolio chatbots are marketing props in a trench coat. Ask them anything and they'll find a way to frame the author as the second coming of Dan Abramov. The LLM's natural sycophancy does most of the work. Feed it a résumé and a friendly system prompt, and it will happily inflate a Tailwind config into "deep CSS architecture experience."

I wanted the opposite. If a recruiter asks whether I've led a platform team of fifteen engineers, I want the bot to say "no, and here's what I have done." If my skills don't match a job description, I want the bot to recommend passing on me, before either of us wastes a recruiter call.

Turns out that takes more than a polite system prompt.

Honesty at the data layer

The first lesson: you can't bolt honesty onto a hype machine with prompting alone. The model will drift. You have to enforce honesty in the content model itself.

My content schema has a dedicated gaps type. Not a "growth areas" section framed as opportunity, but actual gaps, with structured metadata:

typescript
1type Gap = {
2  skill: string;
3  whyItsAGap: string;
4  roleTypesAffected: string[];
5  currentlyLearning?: boolean;
6};

Skills are tiered into strong, moderate, and gap, each with honest notes attached. My job history uses a whyLeft field. Projects carry a wouldDoDifferently field. None of this has to be invented at inference time. It's just there, retrievable, already written in a calibrated voice.

The benefit: the LLM can't over-index on ambient résumé vibes because the ground truth is explicit. When it's asked about a gap, it's reading data that already says "this is a gap."

A system prompt that commits

The prompt layer reinforces the content layer. The key line from lib/ai-prompts.ts:

Your honesty IS the value proposition — employers appreciate knowing fit upfront. If a skill is in the gaps list, say so plainly. Do not reframe gaps as strengths.

I also built a JD analyzer that scores a pasted job description against my profile and returns one of three verdicts: strong_fit, partial_fit, or not_a_fit. The prompt is instructed to be "BRUTALLY HONEST." The not_a_fit path isn't a safety rail I hope never fires. It's a first-class output with real UX attached. The bot will literally tell a recruiter "this role probably isn't a good match; here's why" and point them toward the kinds of roles that actually would fit.

This felt risky in writing. In practice, every recruiter I've talked to has appreciated it. Saying "no" upfront is cheaper than saying "yes" and disappointing someone in week three.

Two-tier model routing

Calibrated honesty has a cost, literally. Running Sonnet on every "hi" and "what's your name" adds up, and careful reasoning is wasted on a greeting. So the API route does a quick classification pass and routes:

typescript
1
2const model = isTrivialMessage(userInput)
3  ? 'claude-haiku-4-5'
4  : 'claude-sonnet-4-6';
5

Haiku handles small talk. Sonnet handles anything that touches the profile, the gaps data, or the JD analyzer. It's not a fancy optimization, but it's the kind of decision you start making when you're paying the bill, not just shipping a demo.

Rate limiting (the honest part)

The rate limiter is 20 requests per hour, keyed by a hashed IP, stored in memory. It works on a single serverless instance. It absolutely will not survive a scale-out or a fresh deploy.

I'm mentioning this because it fits the theme: I'd rather tell you the limitation than pretend the spike guard is production-grade. The next iteration moves it to Upstash. For v1, a leaky in-memory map is enough to stop casual abuse, and I'd rather ship with a known gap than block on the perfect solution.

What I'd tell another dev building this

Three things worth taking away:

  • Push honesty into the schema, not the prompt. Prompts drift. Data doesn't. If "gap" isn't a content type, the bot will eventually forget gaps exist.
  • Treat the disqualifying verdict as a feature. A bot that can say "pass on me" is more credible than one that can't, and credibility is the whole reason you're putting an AI in front of your portfolio in the first place.
  • Right-size the model per route. Sonnet for judgment, Haiku for pleasantries. You'll notice the difference on the invoice, not in the UX.

The weird part is that building a chatbot designed to occasionally reject the user on my behalf has done more for my pipeline than any version that just answered nicely. Turning honesty into infrastructure turns out to be a pretty good pitch.

---

Building something similar? I'm curious how other people are handling the sycophancy problem in portfolio AI. Reach out and tell me what you've tried.

@recent

©2026