DECRYPTED_LOG[2026.07.24]

I Built a Recruiting Product That Scores Hiring Teams on Their Silence. Here's How.

Cover Image for I Built a Recruiting Product That Scores Hiring Teams on Their Silence. Here's How.

Nearly every candidate I've ever placed has a ghosting story. Applied, interviewed, got someone's hopes up, then nothing. The recruiter went quiet. The hiring manager "went back to the business." The role just sort of evaporated.

Everyone in hiring knows it happens. Nobody measures it. So I built something that does.

Kēhua turns hiring communication into a score. Did the candidate get closure? Was it timely? Was it appropriate to how much of their life they'd handed over? An instant auto-reject is fine. Silence after a final interview is the worst thing you can do to a person who took a day off work for you, and it should cost you something.

That's the whole product in one sentence: make the silence measurable, then make it uncomfortable.

The part worth writing about isn't the idea. It's how it got built.

Built by a pack of AI agents, supervised by one human who'd seen enough

I didn't hand-write most of this codebase. I orchestrated it.

The setup: an orchestrator model running the plan, coding subagents doing the actual implementation, and a second independent agent reviewing the first one's work before anything merged. Early on that was GPT-5.5 orchestrating Codex subagents. Later work leaned on Claude in its own harness. Different models, same shape. One agent writes a slice. Another one tears it apart. The findings become tickets. The fixes go through review again. The commit history is littered with "after independent review" and "codex P1" because that adversarial loop was the process, not an afterthought.

Each agent worked in its own git worktree, so parallel runs didn't stomp on each other. A wandering agent could be killed and relaunched without poisoning the tree everyone else was building on. That mattered, because the first coding run did exactly that.

If you've tried to get a single AI to build anything non-trivial, you know the failure mode. It wanders. It forgets what it decided an hour ago. It confidently rebuilds something it already built, differently.

Two things stopped that here.

The repo remembers

Every durable decision got written to a running memory file. Dated. Terse. Decision-oriented. Why candidates are a separate account type. Why I defer paid candidate features. Why public scores have to be aggregated and thresholded. When JobAdder access stalled and I pivoted the whole launch to candidate-first.

Any fresh agent session starts by reading that file. No re-litigating settled arguments. No rediscovering context. The project has a spine that survives between sessions, and it survives me forgetting things too.

There's even a rule governing what's worth remembering. Most of what an AI produces in a session is noise. The trick is knowing which 5% to keep.

Linear was the source of truth, not the chat window

The memory file holds decisions. Linear holds work.

Every fix, every review finding, every launch-gate item lived as a ticket. The flow was rigid on purpose: pull the issue, confirm scope if it wasn't trivial, implement locally, verify with tests and build, update the ticket with what changed and how it was checked, then open a PR. Nothing got marked Done until the PR was actually merged. When an independent agent flagged a P1, that became a Linear issue first, linked to the review thread, and got fixed through the same loop.

This is the boring bit that makes AI-built software not fall apart. The agents are fast and forgetful. Linear is slow and permanent. You want the permanent thing holding the plan.

Rules, not vibes

Before any code, I wrote the constitution. Test-driven: every scoring rule needs a case that passes, one that fails, one on the edge. Secure by default: never log a token, never log a candidate's raw message. And the one that shaped the entire architecture, privacy boundaries that can't be crossed.

Candidate data is split into four layers. Raw source data, normalised events, private scores, public aggregates. Data can move up toward public only through gates that strip identity. There are tests whose only job is to try to leak a candidate's name through a public endpoint and prove they can't.

You don't get that from telling an AI "be careful with privacy." You get it from tests that fail loudly when it isn't.

Under the hood

Nothing exotic in the stack, and that's deliberate. Next.js on the App Router. TypeScript. Prisma over Postgres, hosted on Supabase. Deployed on Vercel, functions pinned to the Singapore region so they sit next to the database instead of phoning across the planet on every query. PostHog for product analytics, Better Stack for logs that actually ship somewhere. Node's own test runner, no framework tax.

Auth is two separate worlds by design. Employers get org-scoped accounts through Auth.js. Candidates get their own account type entirely, with first-party magic links over Resend, because collapsing a candidate into an employer's org model would have quietly leaked one into the other's data. Refresh tokens are encrypted at rest with AES-256-GCM. Boring, explicit, testable. That was the rule.

The design pattern that repeats everywhere is the provider boundary. Anything from the outside world enters behind an interface that knows nothing about my storage or my UI, and returns raw, untrusted data that a single sanitiser has to clear before it reaches a human.

You can see it twice. The JobAdder integration is one provider behind a multi-ATS boundary, so when approval landed late I just built against synthetic webhook data shaped exactly like the real thing and slotted the real one in later. The dashboard never knew the difference. Company intel is the other: when a candidate hits "research this company" before an interview, that runs through the Parallel Task API behind the same kind of boundary, and a single sanitise step drops any fact that can't prove its source URL and timestamp. No source, no surface. I call it "worth knowing before the interview," never "real-time news," and there's a test that fails if that framing ever creeps back in.

The product loop closes itself

Here's the part I'm proudest of, and it isn't a feature.

Support runs on Chatwoot, with an AI agent-bot handling first-line replies. That bot reply is idempotent against a receipt table, because webhooks retry and a naive bot double-answers every retried delivery until your support channel is talking over itself. Assume the upstream is unreliable, dedupe hard, and the candidate hears back once. Fine. Table stakes.

The loop behind it is the interesting bit.

When a candidate leaves a suggestion or reports something broken, it doesn't sit in a queue waiting for me to notice. A Hermes agent picks up the Chatwoot triage, decides what it actually is, and writes it into Linear as a proper issue. From there it's the same loop the whole product was built on: Codex or Claude pulls the issue, implements the fix, and runs it back through CI. Typecheck, lint, tests, build. Same reviewer gate guarding the door.

Feedback goes in one end as a candidate's off-hand complaint. A tested, reviewed, merged change comes out the other. No human strictly has to stand in the middle, though I still read everything, because the day you stop reading is the day it ships something stupid with total confidence.

That's the actual product cycle. Not "I use AI to write code." A user's problem becomes a triage becomes a ticket becomes a fix becomes a deploy, mostly on its own, with the same tests and the same reviewer standing between a candidate's throwaway line and production.

What actually shipped, and what broke

It's live. Real deployment, real database, a genuine soft-launch gate with a pre-launch checklist and secret rotation and controlled single-recipient email tests before anything went near a real user. CI gates every merge on typecheck, lint, tests, and a production build.

Plenty broke on the way. The first coding agent wandered off and had to be killed and relaunched. A welcome email died in production because the macron in "Kēhua" choked an email header that wanted plain ASCII. I built an entire glassmorphism design system, then deprecated it and rebuilt the marketing site around a completely different look.

None of that is failure. That's just what building looks like when you're honest about the log.

The uncomfortable bit

A recruiter built a product that scores recruiters on the thing recruiters are worst at.

The careers page proves it. Live applicant counts, a visible stage funnel, response rate, all sitting there in public. If I ghost someone, my own product shows it.

Turns out the hard part was never the code. AI agents wrote most of that. The hard part was deciding what the thing should refuse to do. Not monetising candidate disappointment. Not becoming a public shaming board. Not exposing a single candidate's name to make a point.

The machines are getting very good at the how. The why is still on us.