Currently at IBM·Open to remote roles

Ajay Gangarde

Senior AI Full Stack Developer | 11+ Years | Full Stack | GenAI & Agentic AI | AWS | React | Python | Node JS | Claude

I build Applied AI systems that are evaluated by evidence, protected by safety controls, and engineered for reliability. My development harness is Claude Code, which I use to plan, write, test and review production code.Builds end to end: React/Angular frontends, Python and Node.js services, and AI features with RAG, agentic workflows and structured LLM evaluation. Works independently in distributed teams, owning features end to end with clear communication.

Certifications · 2026 · Verified on Credly

Selected AI builds.

01 / Autonomous outreach

Multi-Channel Autonomous Candidate Outreach

Agents reach job seekers on WhatsApp, voice and email, run a structured screening conversation, and write typed results back into the pipeline.

85%
Voice delivery
69%from 40%
WhatsApp delivery
1
LLM call / conversation
NodePlivoWhatsApp APIWhispergpt-4o-miniSQSPostgres
GitHub ↗
02 / Explainable ranking

Candidate Intelligence · Evidence-Backed Ranking

A deterministic engine that scores and ranks resumes against a job, with every match backed by a cited line from the candidate's work history.

0
LLM calls in request path
~1,200
Canonical skills (ESCO)
100%
Reproducible rankings
PythonFastAPIPydantic v2RapidFuzzESCO
GitHub ↗
03 / LLM observability

LLM Cost Intelligence

Daily AI spend and cost bursts were invisible until the monthly invoice. Every call is now priced and tagged by feature, user and model, and reported every day.

19
AI features tagged
7
Methods, one choke point
$0→$0.19
Image cost blind spot closed
TypeScriptGeminiOpenAIPostgreSQLAWS SESCloudWatch
GitHub ↗
04 / Agentic workflow

Agency Intelligence Agent

Tells a recruitment agency what to act on today: which candidate to call back, which job has gone stale, which recruiter needs coaching. Ranked and backed by evidence.

16
Deterministic indicators
4
Parallel tool nodes
1
LLM-authored lane, evaluated first
LangGraphAmazon BedrockNova ProPydantic v2Python
GitHub ↗

A decade across IBM, Quest, Synechron, EDR & Infodreamz.

Notes I publish on Medium.

© 2026 · Built with Claude Design & care
Ajay Gangarde
01 / Autonomous outreach

Multi-Channel Autonomous Candidate Outreach

Autonomous agents that reach job seekers on WhatsApp or by phone, run a structured screening conversation, and write typed results back into the recruitment pipeline — with a human involved only on explicit escalation.

01Problem

Before a recruiter talks to a candidate, someone has to collect five basics: current salary, expected salary, notice period, location, and work status. Doing this by hand doesn't scale, and a plain AI bot breaks in three ways.

One channel isn't enoughSome candidates never answer calls; others never reply to texts.
Conversations aren't dataA great call still leaves a sentence in a notes field, not a filterable record.
Partial replies get lost3 of 6 answers then silence looks the same as never replying.
02Architecture
T + 0
WhatsApp
Consent → questions → wrap-up
T + 2h, no reply
Voice call
Intro → questions → wrap-up
Still no response
Email form
Candidate fills the 5 fields directly
Recruiter UIcampaign · application create
→
SQS outreach queueone queue, all channels
→
Outreach dispatcherlong poll 20s · routes on channel
↓
Lane 1WhatsApp
whatsapp.handlerconcurrent sends
→
Meta Graph API
→
WhatsApp agentconsent → question → wrapup
↔
PostgreSQLsession state · row locks
Lane 2Voice
voice.handler5s gap per candidate
→
Plivo
→
Voice agentintro → question → wrapup
↔
Cartesia TTS + Whisper STTin-process state
FallbackEmail
email.handler
→
Email with form linkcandidate submits 5 fields
↓
LLM extraction1 call / conversation · gpt-4o-mini, gemini-2.5-flash fallback
→
ATS tablesstructured write-back
Abandonment worker18h nudge · 24h finalize
Dead-letter queuemaxReceiveCount 3
ECS · PM2 processes run the dispatcher, channel workers and abandonment worker
03Approach & decisions
  1. Same playbook, three channelsWhatsApp, voice and email escalate in sequence and share one design with different scripts. A new channel reuses the playbook.
  2. Queue-driven outbound, webhook-driven inboundCampaign sends and call initiation run from queue workers. Each inbound message or call turn is handled as its webhook arrives, with a simple decision at each step and no agent framework.
  3. State that fits the channelA voice call is a live ~30-second conversation held in an in-process map, so a restart or a second instance drops live calls. WhatsApp replies can arrive hours later, so sessions persist in Postgres with row locking.
  4. Keywords first, AI secondWord matching, including Hindi-English phrases like haan, nahi, kitna, handles most replies instantly and for free. AI only steps in for the tricky ones.
  5. One AI call per conversationThe LLM reads the full conversation once at the end and writes salary, notice period and location straight into the ATS.
  6. No dead air on callsEvery question is pre-recorded, and the next one is prepared while the candidate is still answering.
  7. Throttled dispatchA 5-second gap between sends keeps the campaign inside provider rate limits. This lifted WhatsApp delivery from 40% to 69%.
  8. Nothing gets lostDuplicates are ignored, failed sends retry, a dead-letter queue catches repeat failures, and quiet candidates get a nudge before the session closes.
04Evaluation
Metric Before After Δ
WhatsApp deliveryafter 5s throttle 40% 69% +29 pts
Voice deliverycalls connected — 85% —
Campaign dispatch time100 contacts — ~8 min —
LLM calls per conversationby design 1 per turn 1 total —
Pilot campaigns · up to 100 numbers each
05Results
  • End-to-end autonomyMessage or dial → converse → classify → extract → update the pipeline. A human steps in only on explicit escalation.
  • Structured data, not transcriptsFive fields populated automatically on every completed conversation.
  • Durable by constructionConversations survive restarts, duplicate messages and candidate abandonment without data loss.
  • Cost and latency boundedOne AI call per conversation, and no waiting on speech generation during a live call.
06What's next
  • Move voice session state out of process memory for horizontal scaling.
  • Real-time streaming voice so candidates can interrupt mid-prompt.
  • Screening questions per job from a database table, without a deploy.
  • Per-candidate WhatsApp delivery status in the campaign screen.
07Source
github.com/ajaygangarde/candidate-outreach-ai-agentOpen repo ↗
02 / Explainable ranking

Candidate Intelligence · Evidence-Backed Ranking

A FastAPI service that deterministically scores and ranks resumes against job requirements. Every score comes from a clear rule, and every matched skill is backed by cited text from the candidate's work history.

01Problem

Recruiters process hundreds of applications per role. Keyword search, manual review and generic LLM workflows each fail in a way that costs good candidates or can't be defended.

Brittle keyword searchReact vs. ReactJS, k8s vs. Kubernetes. Qualified candidates drop out silently.
Unbacked skill listsA skill in a sidebar doesn't prove hands-on use in the work history.
No auditable defenceAn unexplained score or rejection can't stand up to a hiring-bias audit.

Raw LLMs add hallucinated justifications, rankings that change between runs, and per-candidate API cost.

02Architecture
POST /candidate-intelligence/rankjob_dna + candidates[]
→
Pydantic validationmalformed payloads stop here
↓
Per candidateSkill matching
Canonical vocabularyESCO ~1,200 + hand list
→
Fuzzy fallbackon miss · rapidfuzz ≥ 90
→
Matched / missing skills
↓
Evidence lookupscan work-history responsibilities
Experience fitbelow / in / above range
↓
Weighted scoreskills 0.7 + experience 0.3
→
Label + confidencethreshold bands
→
Below minimum?yes → dropped
→
Ranked responsesummary + per-candidate evidence
Every stage is deterministic Python · no model call anywhere in the request path
03Approach & decisions
  1. No LLM in the request pathRanking is a scoring problem with a defined rubric. Keeping it deterministic gives identical output for identical input, scores that decompose into named parts, millisecond latency, and zero inference cost.run 1 Asha Rao → 82 · Recommended
    run 2 Asha Rao → 82 · Recommended
  2. Canonical vocabulary, not string equalityAbout 1,200 ESCO digital skills, with a hand-maintained layer on top that covers ESCO's gaps (React, Docker, Kubernetes, FastAPI, LangGraph, RAG) and abbreviations like js, ts, k8s.
    │  Canonical vocabulary
       │
       ├── react.js ──┐
       ├── reactjs ───┼──► React
       ├── React ─────┘
       │
       ├── k8s ───────┐
       ├── K8s ───────┼──► Kubernetes
       └── kubernetes ─┘
  3. Fuzzy matching only as a fallbackAt a low threshold, "Java" becomes "JavaScript". RapidFuzz runs only after a canonical miss, at a cutoff of 90, so it rescues near-misses without inventing matches."postgre sql" → postgresql ✓ rescued
    "Flask" → FastAPI ✗ below 90
  4. Evidence over assertionEach matched skill is tied to a responsibility line with company, role and duration. Skills that are listed but never demonstrated carry no evidence block.FastAPI → Acme Corp · Backend Engineer · 5 yrs
    "Built FastAPI services deployed on Docker"
    Python → skills list only, no evidence
  5. Tunable without a deployAll seven weights and thresholds live in config and can be overridden through environment variables, so an agency can retune ranking without changing code.# .env
    rank_skills_weight=0.6
    rank_minimum_score=0 → return everyone, for audits
  6. Cut off, don't padCandidates below the minimum score are dropped before ranks are assigned, so the shortlist stays numbered 1, 2, 3 with no gaps.Asha Rao 82 → rank 1
    Vikram Shah 38 → below minimum, dropped
04Evaluation
Candidate Skills Experience Overall Outcome
Asha Rao4.5 yrs · Python, FastAPI, Docker 753 of 4 100in 3–6 range 82 Recommended
Vikram Shah2 yrs · Python, Flask 251 of 4 67below range 38 Dropped
Sample job: Backend Engineer · Python, FastAPI, AWS, Docker · 3–6 yrs · 75 × 0.7 + 100 × 0.3 = 82
05Results
  • The score decomposesA recruiter challenging a rank can be shown exactly which term moved it.
  • Matches carry proofFastAPI resolves to a specific line at a specific employer for a stated duration.
  • Gaps are statedAWS shows up as missing, with a severity and a reason, and again in the plain-language explanation.
  • The high cutoff heldFlask did not fuzzy-match to FastAPI, which is why the contrast case scored 38 and was filtered out.
06Source
github.com/ajaygangarde/candidate-intelligence-ai-agentOpen repo ↗
03 / LLM observability

LLM Cost Intelligence

Instrumentation inside every AI request that shows the day's cost and pinpoints cost bursts, attributing every call to the feature, user and model behind it.

01Problem

The real gap was knowing what AI cost each day and catching a cost burst when it happened. Spend showed up as one number per month, per API key, so a spike was only visible weeks later on the invoice, with no way to tell which feature, user or model caused it.

Billing units don't matchTokens, audio minutes, images and input-only embeddings. No common unit to rank features.
Two billing accountsThe social agent runs on a separate OpenAI account, so even the total arrived as two invoices.
A silent blind spotImage generation, the most expensive call, wasn't in the price table. Every image logged as $0.

The feature, user and employer behind a call only exist while the call is in flight. This had to be solved on the critical path, without ever being the reason a request fails.

02Architecture
Callers19 features
analysis.serviceresume parse · reports
screening / role-DNAsourcing · embeddings
social agentposts · images
↓
Wiring seamindex.ts
aiServicemain billing account
socialAiServicesocial billing account
↓
Corezero host imports
AiService · 7 methodschat · JSON · schema · embed · transcribe · image · vision
→
Gemini 2.5 Flashattempt 0 · primary
⇢
OpenAIattempt 1 · same requestId
↓
Normalize usageusageFromOpenAI / Gemini
→
calculateCosttoken · per-minute · per-image
→
record()fail-open
→
llm_api_callscost frozen at call time
↓
llm-cost-report-workerdaily 19:00 IST · advisory lock
Handlebars → AWS SESdaily email to product owner · INR + USD
PostgresObserver errors go to CloudWatch · tracking failures never touch the user's request
03Approach & decisions
  1. One choke pointEvery provider call goes through AiService. An optional wrapper gets forgotten, which is how image costs went untracked. Routing everything through one service makes instrumentation structural.aiService.chat({ feature: 'candidate_report', ... }) ✓
    aiService.chat({ ... }) ✗ type error: feature required
  2. Price at call time, freeze the dollar figureProvider prices change. A report-time join against a price table would silently rewrite history, so cost is computed once in USD and never recalculated.cost_usd 0.000412 ← stored at call time, survives a price change
  3. One calculateCost over every billing shapeSeparate functions per modality give numbers you can't compare. One function returning USD makes ranking all 19 features a single ORDER BY.tokens · audio minutes · images → USD
    ORDER BY SUM(cost_usd) DESC
  4. One row per provider attemptA failed Gemini attempt and a successful OpenAI fallback are two events with two costs. Collapsing them would destroy the fallback-rate metric.req_7f2 · gemini · error · $0.000031
    req_7f2 · openai · ok · $0.000220 fallback_from: gemini
  5. Fail-open at two layersA Postgres outage must degrade reporting, never the product. The observer call and the database write each have their own try/catch, because they fail for different reasons.DB down → user gets the response, error logged to CloudWatch
  6. Cost levers the data justifiedGemini-first on structured generation, gpt-4o-mini as chat default, low-detail vision, and caching parsed results so a cache hit costs zero LLM calls.Gemini vs OpenAI input → ~5× cheaper
    gpt-4o-mini vs gpt-4o input → ~17× cheaper
  7. Every row carries feature, user and modelEach call is logged with its feature tag, the logged-in user's identity and the exact model used. Since providers are picked per use case, this allows analysis by any of those, and makes a sudden spike on a given day easy to trace.feature: resume_parse · user: u_2041 · model: gemini-2.5-flash · $0.000180
  8. Daily evaluation, sent to the product ownerA scheduled worker runs every evening and emails total spend for the day in INR and USD, broken down by feature, so spend can be evaluated without opening a dashboard.19:00 IST Today ₹ / $ · vs yesterday · month to date
    by feature · by model · errors + fallbacks
04Evaluation
Question Before After
Cost of one candidate report Unknown One SQL query
Spend ranked across 19 features No common unit USD per feature
Cost to serve one employer Unknown Per-employer total
Image generation cost $0 (unpriced) $0.19 / image
Gemini → OpenAI fallback rate Invisible GROUP BY request_id
Attribution resolution Per key, per month $0.000001 per call
Daily spend and spikes Seen on the monthly invoice Daily email, INR + USD
No savings percentage is quoted: the "before" state had no measurement at all.
05Results
  • Attribution by feature, user and employerCost per employer turns subscription pricing from a guess into a calculation.
  • One comparable numberTokens, audio minutes and images all resolve to USD, so optimisation effort goes where spend is.
  • Reliability metrics for freeThe same ledger answers what a call cost and how often the cheap primary fails.
  • Daily report to the product ownerTotal spend in INR and USD with feature-wise breakdown and day-over-day deltas, so a sudden spike is caught the same day. Multi-instance safe; the core is ~800 lines with zero host dependencies.
06Future upgrade
  • Connect to the providers' real cost and token-usage APIs to price calls against live rates, replacing the hardcoded price table that needs manual updates.
07Source
github.com/ajaygangarde/ai-cost-intelligence-agentOpen repo ↗
04 / Agentic workflow

Agency Intelligence Agent

An agentic system on LangGraph and Amazon Bedrock that gives a recruitment agency a daily, prioritized, explained list of what to act on. Computed deterministically wherever possible and backed by evidence it can point to.

01Problem

An agency's operational data is spread across jobs, applications, call transcripts and recruiter assignments. The signals that matter cut across all of them and don't surface on their own, so they get caught only by whoever looks at the right dashboard on the right day.

Stale jobsActive 41 days, no activity for 12. The client relationship quietly decays.
Unseen drop-offsA candidate ends a screening call negatively and nobody follows up.
Misread performanceA recruiter converting at 0.28 vs. team median looks like a market problem.

Pointing an LLM at the database makes it worse: confident recommendations citing partly invented figures, with no way to tell a real finding from a fabricated one.

02Architecture
Client request text
→
Input guardrailBedrock · blocked → HTTP 400
→
Planner agentLLM tool-use · toolChoice: any
↓
Fan-out4 tool nodes
Job tool10 indicators
Recruiter performance4 indicators
Workload2 indicators
Candidate riskcall sentiment
↓
Evidence buildertyped, capped object
→
Evaluator agentcandidate risk only
→
Reasoning agentmerge + grounded polish
↓
Numeric grounding checkper insight field
→
Not groundeddiscard rewrite, keep template
→
Ranked recommendationsevidence + action chips
LangGraph StateGraph · Bedrock Converse API · Nova Pro (swappable for Claude)
03Approach & decisions
  1. Numeric indicators with zero LLM surface16 indicators across 4 modules fetch facts with plain SQL, check sample size, compute a verdict against a peer median or threshold, and render a fixed-template sentence. No model sees this data on the way to a verdict.recruiter conversion 0.28 vs team median → below peers
    sample size sufficient ✓ · template sentence
  2. One LLM-authored lane, evaluated before it's trustedCall sentiment is qualitative, so candidate risk goes through an Evaluator that scores confidence, gaps and coverage before a narration model writes the recommendation.candidate risk → Evaluator: confidence · gaps · coverage → narration
  3. Action chips are human-authoredEach indicator ships a fixed list of manual next steps. The owner reads a chip and acts; there is no click-through, no routing and no backend mutation.workload imbalance → [ Reassign candidates ]
    client acceptance low → [ Schedule an account review ]
  4. Numeric grounding on every rewriteThe reasoning step polishes insight text for fluency, but any rewrite that introduces a number not in the evidence is discarded and the original template sentence is kept.rewrite cites "45 days" · evidence says 41 → discarded, template kept
  5. Guardrail before any queryThe raw request text is screened by a Bedrock guardrail at the planner, before it can reach a tool or the database.blocked request → HTTP 400 · no tool runs
  6. Typed contracts between every stageEvery hand-off is a validated Pydantic model, and LangGraph fans out to 4 tool nodes in parallel with reducers for concurrent state writes.ToolPlan → NormalizedEvidence → EvidenceEvaluation → RecommendationList
04Evaluation
Lane Who authors it How it's trusted
Numeric indicators16 across 4 modules SQL + template Sample-size sufficiency per entity
Candidate riskcall sentiment LLM narration Evaluator: confidence, gaps, coverage
Insight wordingfluency polish LLM rewrite Numeric grounding, else discarded
Action chipsnext steps Human-authored Fixed per indicator
Each lane is held to the standard that fits it. No stage is asked to do two jobs.
05Results
  • A daily, ranked to-do listStale jobs, drop-offs, coaching needs, overload and drifting clients, ranked by priority.
  • No invented figuresNumbers come from SQL; any LLM rewrite that adds a number is thrown away.
  • Evidence behind every itemEach recommendation links to the evidence that produced it.
  • Swappable modelNova Pro by default, switchable to Claude in config. Thresholds are tunable without touching query logic.
06Source
github.com/ajaygangarde/agency-intelligent-agentOpen repo ↗