Skip to content
goutham.dechineni

Flagship project

Tellability

Problem
Hiring screens filter on keywords in a CV, so people who can do the job but describe it differently often never reach a recruiter.
Approach
Job seekers build their profile in a text chat with Tess, an LLM agent; recruiters publish a job and a three-stage engine ranks candidates: vector search, a structured score, then a budgeted LLM review.
Result
Live at tellability.com with 20 early users. In a small early sample, 87.5% of prompt tokens were served from Gemini's context cache, for roughly 76% lower LLM spend (excluding cache storage fees).
My role
Founder and sole engineer (Aug 2025 – present). Designed and built the product, backend, matching engine and deployment alone.
Stack
  • Next.js 16
  • React 19
  • TypeScript
  • Vercel
  • Cloudflare Workers
  • FastAPI
  • Cloud Run
  • async SQLAlchemy
  • Alembic
  • Postgres + pgvector (HNSW)
  • Supabase
  • Redis (Upstash)
  • Gemini Flash-Lite (tool calling, context caching)
  • OpenAI embeddings
  • Vertex AI Batch
  • Resend
  • GitHub Actions

01

Architecture

Step 1 of 7: A job seeker chats with Tess

  1. 1A job seeker chats with Tess

    Instead of uploading a CV and stopping, the seeker has a text conversation of about 16 turns with Tess. Tess calls tools such as save_memory, update_skill and save_claim as it goes, and mark_profile_ready when the profile is complete.

  2. 2The context cache keeps each turn cheap

    The system prompt and conversation so far live in a Gemini context cache, so each turn sends only the new message on top of the cached prefix. The cache name is kept in Redis and Postgres, a 30-second Redis lock stops two requests creating duplicates, and the cache is rebuilt after the history is summarised at about 10k tokens.

  3. 3The finished profile is embedded

    Once the profile is ready, it is embedded with OpenAI text-embedding-3-small at 512 dimensions and stored as halfvec(512) in pgvector, with an HNSW cosine index.

  4. 4A recruiter publishes a job: Stage 1, similarity

    Recruiters also create jobs through a chat flow. Publishing runs a pgvector search over profiles that are ready, searchable and active, keeping up to 100 candidates with cosine similarity of at least 0.55.

  5. 5Stage 2: a structured score

    Each candidate gets a composite score: 0.60 semantic + 0.20 preference + 0.10 technical + 0.10 verification. Preference applies penalties for salary, location, notice period and experience; verification looks at claims, GitHub authenticity and red flags. Data gaps and soft conflicts are flagged, not hidden.

  6. 6Stage 3: budgeted LLM verdicts

    Only the top of the list goes to an LLM: vacancies × 5 candidates, at least 3, at most 30, and never more than 60% of the pool, with 5 calls in parallel. It writes a shortlist, consider or pass verdict with strengths, concerns and data gaps next to the score.

  7. 7Top matches are emailed

    The best-ranked candidates are notified by email through Resend, so a published job reaches people without anyone searching by hand.

02

Key decisions

  • Context cache with a Redis lock, instead of re-sending the whole history

    Re-sending the full conversation on every turn is simple but the cost grows with each message. A per-conversation Gemini context cache makes repeated tokens about 10× cheaper to read, at the price of lifecycle code: a 30-second Redis lock against duplicate creation, a 1-hour TTL, a rebuild after summarisation, invalidation when the profile changes and deletion on disconnect so storage is not billed for idle caches.

  • A cheap vector stage before any LLM call

    Asking an LLM to read every profile for every job does not scale. Stage 1 narrows the field with an indexed pgvector query (similarity ≥ 0.55, top 100) and Stage 2 is plain Python, so LLM cost depends on the number of vacancies, not the number of users. The trade-off is that someone the embedding misses is never seen by the later stages.

  • The LLM annotates the ranking but never changes the score

    The Stage 3 verdict is stored in separate columns (summary, strengths, concerns, recommendation) and the Stage 2 score stays as computed. That keeps the ranking reproducible and explainable, and the LLM is told it may never output "reject": rejecting is the recruiter's decision. The cost is that a clear LLM concern does not move a candidate down by itself.

  • Zero-traffic deploys with a separate migration job

    Production deploys are a manual workflow: the new Cloud Run revision starts with no traffic, an Alembic migration runs as a Cloud Run Job, a smoke test checks the tagged revision, and only then does traffic shift. It is slower than deploying on every merge, but a failed migration never reaches users.

03

Results

  • Live at tellability.com with 20 early users and no paying customers yet.
  • Early, small sample (production, 11 Feb – 19 May 2026, 468 LLM calls): 87.5% of prompt tokens served from Gemini's context cache, and 92.5% within Tess conversations.
  • Estimated LLM spend roughly 76% lower than without caching (excluding cache storage fees, which were not recorded): about $0.049 actual against about $0.20.
  • Spend guards in place: a global daily spend counter, per-user token and request-per-minute limits, and tier quotas.
  • CI runs ruff and pytest against real Postgres and Redis service containers on every change.

04

What I'd do next

  • Move background work to a durable job queue and WebSocket state out of process memory, so the API can run on more than one Cloud Run instance.
  • Record LLM latency per call; today only token counts and cost are tracked.
  • Build a gold set of labelled job–candidate pairs and measure match quality against it.
  • Ship the v2 matcher already in progress: LLM requirement extraction, a deterministic must-have check, and a cited LLM judge for the top candidates, with scores capped when a must-have is missing.