Introdução
Uma das coisas que me fez me apaixonar pela engenharia de software é a capacidade de construir robôs que cuidam de tarefas tediosas por mim.
Procurar emprego definitivamente é uma dessas tarefas. É repetitivo, consome tempo e frequentemente frustrante. Então eu automatizei.
Motivação
Ver dezenas de pop-ups de Easy Apply por dia é entorpecente. Eu queria um script capaz de encontrar vagas que correspondessem aos meus filtros, usar IA para decidir se a descrição da vaga realmente se encaixa no meu perfil, e preencher automaticamente cada formulário com respostas humanizadas e personalizadas.
O resultado: linkedin-jobs-applier.
Stack Tecnológico
| Camada | Escolha | Por quê |
|---|---|---|
| Runtime | Bun | Inicialização rápida, suporte nativo a TypeScript, bun install sem configuração |
| Automação de browser | Puppeteer | API Chrome madura, excelente debugging com headless: false |
| Linguagem | TypeScript | Tipagem forte para handles de DOM e validação rigorosa de prompts |
| Camada de IA | OpenAI ChatGPT | API de completion confiável com múltiplas opções de modelo |
| Armazenamento de prompts | about-me.txt | Mantém dados pessoais e CV separados do código-fonte |
Estrutura do Projeto
src/
├── index.ts # Bootstrap & configuração global
├── functions.ts # Utilitários auxiliares (sleep, getText, getTextFromElement)
├── helpers/
│ └── ChatGptHelper.ts # Wrapper da API OpenAI
└── services/
├── PuppeteerService.ts # Gerenciamento do ciclo de vida do browser/página
├── LinkedInService.ts # Busca, paginação & lógica de login
├── JobCardService.ts # Processamento individual de vagas & verificação de fit com IA
└── ApplyService.ts # Navegação em formulários & preenchimento automatizado
about-me.txt # Seu perfil, currículo & preferências
Fluxo de Ponta a Ponta
1. Fase de Bootstrap
index.ts carrega a URL de busca de vagas, lê seu perfil de about-me.txt e inicializa o browser.
2. Autenticação
LinkedInService.login() abre a página de login do LinkedIn e aguarda a conclusão manual do login, detectada pelo aparecimento do avatar.
3. Descoberta de Vagas
searchJobs() navega até a URL de busca, rola pelos cards de vagas e implementa paginação recursiva com offsets &start= (25 vagas por página).
4. Filtragem com IA
Para cada card de vaga, JobCardService extrai o título e a descrição completa, e então pergunta ao ChatGPT se a vaga se encaixa no seu perfil (SIM/NÃO). Apenas respostas SIM avançam.
5. Preenchimento Inteligente de Formulários
Quando uma vaga passa pelo filtro de IA, ApplyService abre o modal Easy Apply, itera por cada etapa do formulário, extrai o texto do label de cada pergunta, envia um prompt contextual ao ChatGPT e mapeia a resposta ao tipo de input adequado: texto, select, radio, upload de arquivo, etc.
6. Conclusão Graciosa
Após preencher todos os campos, o bot envia a candidatura ou a descarta se ocorrerem erros. O processo continua até atingir o limite diário do LinkedIn, então aguarda uma hora.
Análise Detalhada por Componente
1. index.ts: Hub de Configuração
const puppeteer = require("puppeteer");
import { PuppeteerService } from "./services/PuppeteerService";
import { LinkedInService } from "./services/LinkedInService";
import fs from 'fs';
export const DEFINES = {
JOB_LINK: `https://www.linkedin.com/jobs/search/?currentJobId=4234489284&distance=25&f_AL=true&f_WT=2&geoId=103644278&keywords=senior%20software%20engineer&origin=JOBS_HOME_SEARCH_CARDS`,
ABOUT_ME: fs.readFileSync(__dirname + '/../about-me.txt', 'utf8')
}
const main = async () => {
const linkedInService = new LinkedInService(await PuppeteerService.init());
await linkedInService.login();
await linkedInService.searchJobs();
}
main();
O Bun executa TypeScript diretamente com bun run, sem ts-node, sem Babel, sem dores de cabeça de configuração.
2. PuppeteerService.ts: Gerenciamento do Browser
import puppeteer, { Browser, Page } from "puppeteer";
export class PuppeteerService {
static async init(){
const browser = await puppeteer.launch({
headless: false,
defaultViewport: null,
args: ["--start-maximized"],
userDataDir: "./.puppeteer-data",
});
const page = await browser.newPage();
return new PuppeteerService(browser, page);
}
constructor(public browser: Browser, public page: Page) {}
async goto(url: string){
await this.page.goto(url);
}
}
Executar com headless: false torna o debugging de seletores CSS trivial: você pode assistir exatamente o que o bot está fazendo.
3. LinkedInService.ts: Navegação Inteligente
async login(){
await this.puppeteerService.goto("https://www.linkedin.com/login");
let loggedIn = false;
while (!loggedIn) {
try{
await this.puppeteerService.page.waitForSelector(".global-nav__me-photo", { timeout: 1000 });
loggedIn = true;
}catch(e){
console.log("Not logged in, waiting 5 seconds");
await sleep(5000);
}
}
console.log("Logged in");
}
scrollDown() usa page.mouse.wheel() para carregar mais cards de vagas de forma lazy. A paginação recursiva mantém o uso de memória baixo, e a conclusão do login é detectada pela presença do avatar.
4. JobCardService.ts: Filtragem de Vagas com IA
const answer = await ChatGptHelper.sendText(
'gpt-4.1-nano',
`PROFILE: ${DEFINES.ABOUT_ME}\n\nBased on my profile, answer if this job is a good fit for me. Return only the "YES" or "NO", without any other text.`
);
if(answer?.toLocaleLowerCase().includes('yes')){
console.log('🟢 This job is a good fit');
await easyApplyButton.click();
// ... proceed with application
}else{
console.log('🔴 This job is not a good fit');
}
Prompts em dois estágios para o ChatGPT lidam com a filtragem rápida SIM/NÃO seguida de respostas detalhadas às perguntas. O serviço também monitora mensagens de "limite de Easy Apply atingido" e aguarda 1 hora quando os limites são alcançados.
5. ApplyService.ts: Automação Inteligente de Formulários
// Extract question text
const labelHandle = await q.$('label');
const question = (await (await labelHandle.getProperty('innerText')).jsonValue()).trim();
// Get AI-powered answer
let answer = await ChatGptHelper.sendText(
'gpt-4.1-nano',
`PROFILE: ${DEFINES.ABOUT_ME}\n\n` +
`ROLE DESCRIPTION: ${this.jobCardService.about}\n\n` +
`Based on my profile, answer the following question: ${question}\n\n` +
`Return only the answer, without any other text.`
);
// Handle different input types
const textInput = await q.$('input[type="text"], input[type="email"], input[type="tel"]');
if (textInput) {
await textInput.click({ clickCount: 3 });
await textInput.type(answer, { delay: 50 });
}
O serviço lida com texto, e-mail, telefone, data, uploads de arquivo, selects, radios e checkboxes. Campos numéricos removem automaticamente não-dígitos. Erros de validação acionam um descarte gracioso, e a navegação em múltiplas etapas gerencia os botões Próximo/Revisar.
6. ChatGptHelper.ts: Wrapper Limpo da API
import OpenAI from 'openai';
const openai = new OpenAI();
export default class ChatGptHelper {
static async sendText(model: OpenAI.Chat.ChatModel = 'gpt-4o', content: string) {
const completion = await openai.chat.completions.create({
model,
messages: [{ role: 'user', content }],
});
console.log(`🤖 Response from ChatGPT: ${completion.choices[0].message.content}`);
return completion.choices[0].message.content;
}
}
Alterne entre modelos (gpt-4o, gpt-4-turbo, gpt-3.5-turbo) alterando o parâmetro.
Estratégia de Engenharia de Prompts
Avaliação Rápida de Fit
PROFILE:
Based on my profile, answer if this job is a good fit for me.
Return only the "YES" or "NO", without any other text.
Resposta Detalhada a Perguntas
PROFILE:
ROLE DESCRIPTION:
Based on my profile, answer the following question:
If you don't know the exact answer, return what you think is the best answer for this role.
Return only the answer, without any other text.
Prompts determinísticos mantêm os custos de tokens baixos. Consultas de propósito único melhoram a precisão. Passar tanto o perfil quanto a descrição da vaga fornece ao modelo contexto suficiente para responder perguntas que não são diretamente respondíveis pelo currículo.
Primeiros Passos
Instalação e Configuração
# Clone the repository
git clone https://github.com/samuelfaj/linkedin-jobs-applier
cd linkedin-jobs-applier
# Install dependencies
bun install # or npm install
# Set up environment
export OPENAI_API_KEY=sk-your-openai-key-here
export RESUME_PATH=/path/to/your/resume.pdf # Optional
# Create your profile
echo "Your professional background, skills, and preferences..." > about-me.txt
# Run the bot
bun run src/index.ts
Primeira Execução
- O Chrome abre. Conclua o login no LinkedIn manualmente.
- Acompanhe o console para atualizações em tempo real.
- O bot assume o controle a partir daí.
Opções de Customização
| Tarefa | Como Customizar |
|---|---|
| Alterar critérios de busca de vagas | Edite DEFINES.JOB_LINK com sua URL de busca do LinkedIn |
| Ajustar tom das respostas | Modifique os prompts em ApplyService.ts |
| Adicionar uploads de carta de apresentação | Estenda o bloco de tratamento de upload de arquivo |
| Mudar para modo headless | Altere headless: true em PuppeteerService.ts |
| Modificar modelo de IA | Atualize o parâmetro de modelo nas chamadas de ChatGptHelper.ts |
Considerações Importantes
Ética e Conformidade
- O LinkedIn detecta ativamente automações. Use VPNs, delays realistas e volume moderado.
- Sempre revise as candidaturas finais. A IA pode ocasionalmente alucinar expectativas salariais.
- Mantenha o
about-me.txtatualizado com suas habilidades e projetos mais recentes.
Limitações Técnicas
- Limites de taxa se aplicam. O LinkedIn restringe envios de Easy Apply por dia.
- Mudanças no site significam que seletores CSS podem precisar de atualizações conforme o LinkedIn evolui.
- Revise as respostas geradas por IA quanto à adequação antes da primeira execução.
Melhorias Futuras
- Integração com IA local (ex.: Ollama + Llama 3) para reduzir custos de API
- Integração com a API do Gmail para rastrear respostas de recrutadores
- Dashboard de analytics mostrando taxas de sucesso das candidaturas
- Testes A/B para otimização de prompts
Conclusão
Este projeto mostra o que acontece quando você combina Bun, Puppeteer e ChatGPT: um script em background que lida com mais de 4 horas de trabalho manual de candidatura. A ideia central é simples: a IA lida com julgamentos (essa vaga é uma boa opção? como respondo essa pergunta?), enquanto o Puppeteer lida com as interações mecânicas do browser. Nenhum dos dois é suficiente sozinho.
Mantenha seus serviços pequenos e focados, separe preocupações transversais e use TypeScript para que o compilador capture erros antes do runtime. É isso.
Use com responsabilidade, respeite os termos de serviço da plataforma e revise suas candidaturas antes de enviá-las.
Recursos: