Case study · Applied AI systems · Hiring tech

Job hunting is exhausting.
So we built an agent to handle it.

Opshire turns a raw daily feed of job listings into a short list of scored, explainable matches — then helps the candidate act on them with constrained resume tailoring, generated interview prep and salary intelligence. Built for mid–senior professionals in India, with the candidate holding the only decision that matters: whether to apply.

Sector · Careers & hiring marketplaceType · Autonomous AI job-search agentAudience · Mid–senior talent in IndiaStack · Embeddings · LLM workflows · scoring engine
7
AI capabilities across 6 operational layers
6
Weighted signals behind every match score
75
Score threshold a role must clear to be shown
0
Applications sent without the candidate’s click
01Executive Summary

The whole job search, running itself

India’s hiring market produces thousands of new roles a day. The candidate’s loop is brutal and entirely manual: filter the noise, judge fit from a job description, rewrite a resume for each posting, guess the salary band, and assemble interview prep from scattered blog posts — usually after a full day of work. Doing that well doesn’t scale past a handful of applications a week.

Opshire is an always-on agent that owns everything up to the decision. It ingests and de-duplicates listings nightly, extracts normalised skills from each job description, embeds roles and profiles to find fit beyond keyword overlap, scores every candidate role across six weighted signals, and suppresses everything under threshold. What surfaces arrives with a human-readable reason. Everything generative — tailoring, prep, mocks, coaching — waits for explicit intent from the candidate and runs inside entitlement limits.

The core mandate: turn a firehose of listings into a handful of roles worth applying to — scored, explained and act-on-able — without ever applying on the candidate’s behalf, inventing a fact on their resume, or sending a notification that isn’t worth opening.
02The Challenge

Why a better job board wouldn’t do

Every existing tool optimises for listing volume. The candidate’s actual problem is the opposite — too many options, no trustworthy ranking, and all the real work sitting after the click.

🔍

Keyword filters miss the fit

An SRE never sees the Platform Engineer role that suits them perfectly, because the title doesn’t contain their search term. Exact-match filtering fails precisely where a career change matters most.

📄

Tailoring doesn’t scale by hand

Rewriting a resume per job description costs 30–45 minutes. Do it properly and you apply to four roles a week; skip it and the ATS filters you out before a human ever reads it.

🎯

Prep arrives after it’s needed

Candidates walk into interviews with no company context and no salary range, having pulled together generic advice from five browser tabs the night before.

🔔

Alerts train you to ignore them

Daily digests full of near-misses are worse than nothing. Once a candidate learns the emails aren’t worth opening, the good match arrives and goes unread.

🧩

Nothing closed the loop

Discovery, applications, interviews and negotiation lived in different places — so follow-ups were forgotten and no system knew an interview was two days away.

⚖️

A score nobody believes is worthless

One “94% match” on a role paying 30% under the candidate’s floor poisons trust in every number that follows. Explainability isn’t polish here — it’s the product working at all.

The design consequence: the interesting engineering isn’t recommendation, it’ssuppression and explanation — deciding that most of what was found should never be shown, and being able to defend every row that survives.
03The Architecture

Eight stages, routed by cost class

Stages never call each other. Each one reads rows in a given state, writes its output and advances a status column — so every stage is independently re-runnable, inspectable and resumable. Pick a stage to see its contract, where it spends money, and how it fails.

Why the split matters: overlap counts, salary arithmetic and seniority distance are things a language model will misreport with total confidence. Fit across a title change is something no lookup table can answer. Routing each question to the layer that can actually answer it is most of the architecture.
04The Pipeline

Three thousand listings in, nine roles out

One nightly run, per profile, unattended. Volumes below are representative of a single candidate’s scan — the shape is the point: aggressive reduction at every stage, and the last stage’s job is to say nothing at all when nothing qualifies.

~3,000Listings ingested
Pulled from every connected source, de-duplicated by content hash, previously seen roles re-validated and dead ones expired.
~300Job descriptions parsed fresh
Normalised skills, required years and seniority extracted per JD. Extraction iscached by JD hash, so the other ~2,700 cost nothing to re-read on a nightly run.
240Candidate set after semantic retrieval
Roles and profile embedded into one space; nearest neighbours retrieved. This is what lets an SRE see Platform Engineer roles — and it exists to shrink the set before anything expensive runs.
9Surfaced above the 75 gate
Six weighted signals scored per role, hard blockers evaluated separately.231 suppressed or watchlisted — each with its contributions retained so the decision stays auditable.
1Notification sent
One email and push naming the actual roles, deep-linked. If a scan clears nothing,the candidate hears nothing — silence is a valid output.
96%of the candidate set suppressed
9roles a candidate can actually read
100%shipped with a reason string
0applications sent by the agent

Bar widths are scaled for legibility, not drawn to proportion — the counts beside them are the real reduction.

The scoring contract

Weights are versioned configuration, not code, and they sum to 100. Every surfaced match keeps its per-signal contributions, so the reason string is derived from the same numbers the gate used — it can’t contradict the score it explains.

SignalWeightComputed fromCost class
Role fit30Embedding similarity across title and JD summaryEmbeddings
Skills overlap25Normalised extracted skills ∩ profile skillsCode + LLM
Experience fit15Years against the posted band, penalised both directionsCode
Salary alignment12Expectation against the posted or inferred bandCode
Location & mode10City plus remote / hybrid / onsite preferenceCode
Company fit8Stage, size and sector signals against stated preferenceCode
Hard blockersvetoSalary floor, work authorisation, mode conflict, seniority gapCode
The trap we designed around: a single blended number hides hard blockers. An 80 built from a brilliant skills match and a salary 20% below the candidate’s floor is not an 80 — it’s a wasted application. Blockers are evaluated outside the weighted sum so they can veto, which is why the reason string always names one when it exists.
05Delivery Surfaces

Three surfaces, none of them a firehose

Every surface below is threshold-gated. If a scan produces nothing above the bar, the candidate hears nothing — because the fastest way to make a good agent useless is to make its alerts skippable.

Live

Dashboard

The full picture: surfaced matches with scores and reasons, the watchlist behind a click, the application tracker, and every preference control that feeds scoring.

  • Score plus the two signals that earned it
  • Blocker named explicitly when present
  • Stale rows labelled, never shown as current
Live

Email & push

New-match notifications that name the actual roles and deep-link straight to them. Sent only when the scan clears the threshold, so an empty night sends nothing at all.

  • Real role titles in the subject, not “new matches”
  • Deep links into the specific match
  • Interview reminders driven by tracker stage
Entitlement-gated

Assisted flows

WhatsApp-assisted journeys and in-product chat for candidates who’d rather message than navigate — carrying the same scores and the same reasons, never a different answer.

  • Coaching grounded in the product’s own data
  • Rate limited and capped per tier
  • No company salary figures stated as fact
06The Differentiator

A tailored resume that is still, entirely, true

An unconstrained rewrite will happily promote a side project to an employer or add the exact skill the JD asked for. For a document a recruiter will cross-examine, that is the worst failure the product can produce — so tailoring is built as a constrained transform, not a generation task.

🔒

Facts are immutable inputs

Employers, titles, dates, degrees and skills are passed as fixed context. The model chooses emphasis, ordering and phrasing — it is never the source of content.

Output is validated against the source

The rewrite is checked back against the profile. New employers, dates, degrees or skills mean the tailoring is rejected and the untouched resume is used instead.

🧾

Gaps are named, not filled

Skills the JD wants that the candidate doesn’t have are surfaced as a gap to decide about — not quietly written into the document.

💬

Explanations come from the numbers

Reason strings are templated over score components rather than generated beside them, so the sentence and the percentage can never disagree.

What this bought: trust survives contact with reality. A candidate who checks the tailored resume against their own history finds nothing invented — and a candidate who trusts the resume trusts the score that led them to it.
07Human in Command

Autonomous up to the decision, never past it

Opshire is a decision-support agent, not an auto-applier. Everything before the click is automated; the click itself is deliberately, permanently manual.

🙋 The candidate applies

No blind submissions on anyone’s behalf. Tailoring runs on apply intent for one specific job description, and the application is sent by the person whose name is on it.

🎚️ Preferences are the control surface

Title, skills, salary floor, work mode and company preferences are the scoring inputs. Changing one is how a candidate re-tunes the agent — no support ticket required.

👁️ Suppression is visible

The watchlist exists so “we hid things from you” is inspectable. Anything scored between 60 and the threshold is one click away rather than silently discarded.

♻️ Edits invalidate stale scores

Raising a salary expectation marks that candidate’s scored rows stale. The next scan re-scores them and the dashboard labels anything not yet re-scored instead of showing an old number.

08Engineering Rigor

The hard part was making autonomy safe and affordable

Nothing here runs while somebody is watching. Every guardrail below exists because of a failure mode we hit, and each one is now a bound rather than a hope.

Blockers outside the blend

Hard constraints are evaluated separately from the weighted score so they can veto a high number instead of being averaged away inside it.

prevents · a great score the candidate can’t act on

Entitlements before the API call

Per-intent generation has no natural ceiling — a motivated candidate can request fifty tailorings in an evening. Limits are enforced before the request, not in the UI.

prevents · fifty tailorings billed in one evening

Extraction cached by JD hash

The same posting is never parsed twice, and nightly re-scans reuse it. Matching for a free user is effectively a rounding error because almost nothing repeats an LLM call.

prevents · paying twice to read the same posting

Async generation, progressive UI

A full prep pack takes long enough to die behind a gateway timeout, and it’s requested when the candidate is anxious. Sections stream in as they complete and each one is cached.

prevents · a prep pack dying behind a gateway timeout

Recency is a first-class field

Listings are re-validated and expired; salary intel carries its vintage and degrades to a wider range as it ages rather than presenting one confident figure.

prevents · one confident figure drawn from stale data

Every stage re-runnable

Coordination through row state rather than function calls means any stage can be re-run for one candidate, one night, or one job — which is what makes debugging possible at all.

prevents · a bug with no entry point to reproduce it
Text embeddingsANN retrievalStructured LLM extractionConstrained rewrite + validationWeighted scoring engineVersioned weight configAsync job queueStatus-column coordinationEntitlement middlewareEmail + push delivery
09Delivery

Shipped in disciplined milestones

The pivotal release wasn’t the one that added a capability. It was the one that gave the scoring stage authority to hide roles the candidate would otherwise have seen.

v0
Filters over a daily ingest

Prove the loop end to end

Preference filters and keyword matching over ingested listings. It worked — and it recreated the exact problem candidates already had: a wall of near-misses with no ranking anyone believed.

v0.5
Semantic candidate set

Fix the adjacent-title blindness

Embedding roles and profiles meant an SRE finally saw Platform Engineer postings. Recall improved sharply; precision didn’t, because everything retrieved still reached the candidate.

v1
Six signals, one threshold

Make relevance measurable

Six weighted sub-scores, weights in versioned config, and a gate at 75. The first release that suppressed anything — and the first candidates called genuinely useful, because the list got short.

v1.1
Reason strings from components

Make the score defensible

Explanations templated from the same numbers the gate used, plus hard blockers pulled outside the blend. Trust in the percentage turned out to depend entirely on the sentence beside it.

v1.5
Constrained tailoring

Make acting on a match cheap

Resume tailoring per job description, bounded by profile facts, with output validated against the source. The validation pass was added after the first fabricated employer, not before it.

v2
Prep, mocks, salary framing

Close the loop after the click

Reaching interview stage in the tracker became a trigger: async prep generation with progressive UI, graded mock sessions, and salary intel paired with negotiation language.

v2.1
Entitlements as a product boundary

Make autonomy affordable

Free and Pro drawn along the line the cost model exposed: cached and arithmetic work is free, per-intent generation is gated — checked before the API call rather than in the interface.

10Where It Landed

A production agent, not a prototype

Nine, not three thousand

A nightly scan ends with a short list the candidate can actually read, each row carrying the two signals that earned it and any blocker that didn’t.

~3,000 scanned · 240 retrieved · 9 surfaced
Minutes, not an hour

Tailoring a resume to a specific job description stops being the reason someone applies to four roles a week instead of fifteen.

Constrained rewrite · profile facts immutable
Prep before the call

Company context, likely questions, questions to ask and salary framing arrive when an interview enters the tracker — not the night before, assembled by hand.

Tracker stage is the trigger · async, cached per section
Explainable by construction

Every surfaced match ships with a reason derived from its own score components, so the product can defend any number it shows.

Six weighted signals · gate at 75
Sustainable unit economics

Nightly matching is cached and arithmetic; only per-intent generation costs real money, and that is exactly where the entitlement boundary sits.

Extraction cached by JD hash · entitlement before the API call
Candidate still in command

Zero applications sent autonomously, zero facts invented on a resume, and suppression visible rather than silent.

0 applications sent by the agent · watchlist inspectable
The strategic outcome: the candidate’s job search stops depending on their evenings. The agent handles discovery, ranking, explanation and preparation; the person keeps judgment and consent — which is the only division of labour a hiring product can honestly offer.
On the numbers in this case study: figures here describe architecture, configuration and enforced guardrails — signal weights, the 75 threshold, scan cadence, tiers and caps. Volumes and timings are representative of a single profile’s run rather than aggregate platform statistics. Candidate outcome metrics such as reply rates, interviews and offers should be read from live product analytics rather than quoted from a document.

Idea in your head? Let’s
bring it to life.

Got a project? A wild idea? Or just want to say hey?
We're here for all of it — reach out anytime.

I’m looking for a help with:

I’m hoping to stay around of (in USD):