Home/Blog

Back to blog

Blog

This is where I will publish my articles, notes, and ideas about the things I am building and learning.

/
English
Engineering journalJune 21, 20268 min read

Real-Time Resume Optimization: How I Built a Telegram Bot Using LLMs and Puppeteer

An analysis of event-driven architecture, separation of concerns, and API cost optimization using DeepSeek-v4-Flash and OpenRouter.

AIBackendNodeJSPuppeteer

If you are actively searching for an internship or job in tech, you have definitely run into the feared Applicant Tracking System (ATS). These automated recruitment systems filter resumes using algorithms for keyword matching and semantic alignment with job descriptions. As computer engineering students approaching the job market, we face a dual problem: the ATS filters require tailoring our resume for every single application to highlight relevant experiences, but doing this manually is an exhausting, time-consuming task prone to formatting errors.

To automate this customization pipeline in a practical way, I built a real-time, event-driven Telegram bot. The core architectural idea is to treat the resume as a structured JSON document (the data layer), the job description as unstructured text (the query layer), and the output PDF as a compiled view (the presentation layer). By separating these layers, we create a reproducible, modular compiler that performs semantic translation via AI and layout compilation via Puppeteer.

Following clean software architecture principles, the TypeScript and Node.js project is structured with a clear separation of concerns in the src/ directory. The configurations layer (src/config.ts) handles the initialization of API clients like Telegraf for the Telegram bot and the integration with the OpenRouter gateway, as well as managing environment variables. The service layer is split into src/services/ai.ts, which interfaces with the inference API and manages the system instructions and schemas, and src/services/pdf.ts, which compiles the HTML/CSS templates into PDFs and manages the headless browser lifecycle.

In addition to services, we have a utility layer (src/utils/skills.ts) that takes raw, unstructured tags generated by the LLM and maps them against pre-defined engineering domains such as Languages, Databases, Backend, and DevOps. For data security, personal information is separated into myResume.json and a baseResume.json template. The personal resume details are kept gitignored, protecting Personally Identifiable Information (PII) from entering version control while keeping the application code modular.

Since Telegram is a stateless interface, the bot manages session progress using a temporary in-memory map (userSessions). When the user sends the /adapt [job description] command, the bot saves the description and prompts the user with inline keyboard buttons to choose the target language. This state machine keeps the flow interactive and lightweight without needing a database connection for short-lived sessions.

Once the language callback is triggered, the AI Service builds a prompt containing the user's raw resume data and the job description, instructing the model to behave as an elite recruiter. The model runs a keyword density analysis on the job description and reformulates professional achievements using the STAR method (Situation, Task, Action, Result), injecting metrics and strong action verbs (such as 'optimized database queries... reducing latency by 50%'). Critically, the AI only outputs structured JSON matching the original schema, ensuring we can parse it reliably.

The JSON returned by the AI is then merged back with the base contact details—which the LLM is not allowed to change in order to prevent hallucinations—and sent to the PDF Service. The template is written in HTML/CSS using strict styling rules. To solve the CPU and memory bottleneck of spawning a new browser process for every document, we implemented the Singleton connection pool pattern in pdf.ts. A single Puppeteer browser is lazily instantiated on demand, and each request opens lightweight tabs (browser.newPage()) that close immediately after PDF compilation. We also utilize CSS directives like @page and page-break-inside: avoid to enforce A4 bounds and prevent awkward layout splits.

A crucial aspect of developing AI projects under a student budget is the API consumption cost. Robust proprietary models like OpenAI's gpt-4o ($2.50/M input, $10.00/M output tokens) or Anthropic's Claude 3.5 Sonnet ($3.00/M input, $15.00/M output tokens) get expensive at scale. For this reason, we integrated with the OpenRouter API gateway to evaluate various models, eventually selecting DeepSeek-v4-Flash. It offers excellent semantic writing quality and structure adherence for a fraction of the cost, pricing at just $0.07 per 1M input tokens and $0.21 per 1M output tokens.

To put the financial difference in perspective, we can perform a quantitative cost analysis of a single execution. The process requires about 2,500 tokens for the baseline JSON resume and prompt parameters, 1,000 tokens for the job description, and 500 tokens for system instructions, totaling roughly 4,000 input tokens. The adapted output JSON resume accounts for about 1,500 tokens. Running this on gpt-4o costs 1.0 cent for input (4,000 $2.50/1M) and 1.5 cents for output (1,500 $10.00/1M), totaling 2.5 cents per run. On DeepSeek-v4-Flash, the input costs $0.00028 (4,000 $0.07/1M) and the output costs $0.000315 (1,500 $0.21/1M), totaling $0.000595.

This represents an incredible savings of approximately 97.6% per execution. For a student applying to 100 job listings and optimizing their resume for each, using gpt-4o would cost $2.50, whereas DeepSeek-v4-Flash would cost a mere $0.06. This economic difference makes the project highly sustainable and scalable for job hunters on a budget.

The bot's architecture also brought key learnings about engineering trade-offs. Choosing in-memory sessions avoided the latency and hosting costs of database engines (like MongoDB or PostgreSQL) for sessions that only last a few seconds, accepting the minor risk that an active session is lost if the server restarts. Similarly, selecting Puppeteer with HTML/CSS templates over native PDF libraries like PDFKit increased the container deployment size by 150MB to include Chromium, but saved us from manually placing text coordinates, allowing us to build dynamic, responsive resumes with stable layouts.

Building this bot proved that modern development with artificial intelligence requires much more than knowing how to write prompts. Designing decoupled, event-driven architectures, applying software design patterns to reuse heavy resources, and performing detailed cost analyses are the true foundations for building sustainable, production-grade solutions. You can check out the full source code and implementation details of this project on GitHub under the MIT license!

Real-Time Resume Optimization: How I Built a Telegram Bot Using LLMs and Puppeteer | Renan Costa