Em produção
Scrap Jobs
Um radar de vagas construído como cinco serviços coordenados, com um LLM classificando as vagas e um agente respondendo candidatos direto no WhatsApp.
O problema
Vagas relevantes estão espalhadas em dezenas de páginas de carreira, e conferir isso manualmente não escala.
O que construí
Cinco serviços — uma API em Go, um worker em Go, um agendador em Go, um agente em Python que conversa com candidatos no WhatsApp, e um frontend em React — coordenados por filas Redis.


Decisões técnicas
Dividi o sistema em cinco serviços publicados de forma independente — uma API, um worker, um agendador, um agente em Python para o WhatsApp e um frontend em React — principalmente para isolar domínios de falha, não para escalar cada um separadamente. O caso mais claro é o scraping com navegador: ele controla uma instância real do Chromium dentro do worker, então roda na própria fila com concorrência travada em exatamente um — uma aba travada pode atrasar esse scraping específico, mas nunca consegue travar o match, o e-mail ou o WhatsApp, porque cada um roda em fila e pool de concorrência separados.
Toda unidade de trabalho — raspar um site, casar um usuário com vagas novas, enviar um resumo, responder uma mensagem no WhatsApp — passa por filas no Redis (asynq), não por uma chamada direta de função. Isso me dá três coisas que uso de verdade: o agendador enfileira e retorna na hora, sem esperar o scraping ou o envio de e-mail terminar; tarefas que falham tentam de novo automaticamente com backoff em vez de simplesmente desaparecer; e travas de unicidade impedem que a mesma tarefa de match ou campanha seja enfileirada duas vezes enquanto uma já está rodando.
Uma vaga é identificada de forma única pelo site de origem mais o ID que o próprio site dá a ela, garantido por um índice único parcial em (site_id, requisition_id). Raspar de novo um site que ainda lista a mesma vaga não insere uma linha nova — cai num ON CONFLICT que só atualiza last_seen_at. Uma limpeza noturna apaga o que não foi visto na última janela de scraping, então a tabela só guarda vagas que ainda estão de fato no ar.
O passo caro — uma chamada de LLM — nunca roda no caminho em lote. Casar cada usuário ativo com cada vaga nova, o que acontece duas vezes por dia, é matching por palavra-chave e localização, sem nenhum modelo envolvido. O LLM só roda quando o usuário pede explicitamente para analisar uma vaga contra o currículo dele, e esse resultado fica em cache por usuário e vaga, então pedir duas vezes não custa duas vezes. Cada chamada também usa saída estruturada em JSON com um prompt de sistema detalhado em vez de depender de temperatura para manter consistência, e um livro-razão de idempotência garante que uma tentativa repetida devolve o resultado já pago em vez de cobrar a mesma análise de novo.
O agente do WhatsApp em si não guarda estado — é um serviço Python stateless que recebe o histórico da conversa a cada chamada e devolve texto. O estado de verdade fica no Go: os últimos 20 turnos e o progresso do onboarding ficam no Redis com TTL de 7 dias para o caminho quente, enquanto cada mensagem, cada execução do agente e cada chamada de ferramenta que ele fez são gravadas no Postgres como histórico durável e auditável — inclusive tokens e custo por execução, então uma conversa fora de controle aparece nos dados, não só na fatura no fim do mês.
Resultado
Em produção, entregando vagas classificadas por IA via e-mail e WhatsApp, com planos pagos liberados por pagamento online.
Stack
- Go
- Python
- React
- TypeScript
- PostgreSQL
- Redis
- Docker
- AWS S3