~ / tutoriais /tracking-247-do-reativo-ao-agente-agendado $ _

Tracking 24/7: do agente que responde "quanto custa?" ao agente que avisa "baixou agora"

Lucas Souza Lucas Souza 5 min de leitura Tutoriais
Tracking 24/7: do agente que responde "quanto custa?" ao agente que avisa "baixou agora"

Tracking 24/7: do agente que responde "quanto custa?" ao agente que avisa "baixou agora"

Você montou o agente. Ele lê o catálogo, abre o Mercado Livre, devolve o preço. Bonito. O usuário pergunta uma vez, recebe a resposta, vai dormir.

Aí ele volta e pergunta de novo. E de novo. E quer saber se baixou. E quer ser avisado quando baixar. Pronto: agora você não tem mais um chatbot, você tem um problema de engenharia diferente. O agente precisa viver sozinho, acordar em horário marcado, decidir se algo importante mudou, e cutucar o usuário antes que ele cutuque você.

Neste post a gente sai do agente reativo (espera input, responde, dorme) e chega no agente agendado de produção: cron, webhook, notificação, idempotência, deduplicação de alerta e janelas de monitoramento que não estouram a fatura no fim do mês.

TL;DR

  • O que é: arquitetura para transformar um agente reativo em um agente que monitora 24/7 e dispara alertas quando algo muda.
  • Stack: Laravel scheduler + queue, Postgres com unique constraint para dedupe, Claude Agent SDK e/ou Routines para o ciclo de execução, webhook + push para a notificação.
  • Custo/Acesso: depende da cadência. Polling de minuto custa caro. Batch API da Anthropic corta 50% quando o trabalho não precisa ser real-time.
  • Link útil: Claude Code — Run prompts on a schedule.

O salto: de chat reativo para agente que vive sozinho

Agente reativo é fácil de raciocinar. Tem turno. Tem usuário do outro lado. Tem contexto vivo na conversa. Quando ele erra, o usuário corrige. Quando ele alucina, o estrago é local. Custo é previsível porque o ciclo é "pergunta → resposta".

Agente agendado é outra fera. Não tem ninguém olhando. O contexto é frio: você precisa carregar estado de algum lugar (DB, cache, vector store) toda vez que ele acorda. O custo escala com a cadência, não com o uso. E o erro propaga em silêncio: o agente pode disparar 800 push notifications às 3 da manhã e você só vai descobrir quando o time de suporte abrir o WhatsApp.

Três sabores de "agente que vive sozinho":

  1. Cron fixo — acorda em horário marcado. Simples, previsível, mas ignora o mundo entre as execuções.
  2. Polling adaptativo — o agente decide quando acordar de novo. Bom quando a frequência depende do que ele observou.
  3. Webhook-driven — alguém de fora avisa que algo mudou. Zero polling, custo só quando há sinal real.

Os três coexistem no mesmo produto. O tracker de preço é cron, o disparo de alerta é webhook, o follow-up no usuário é adaptativo.

As três janelas de monitoramento

Cron fixo

A maneira mais óbvia. No Laravel:

// app/Console/Kernel.php
protected function schedule(Schedule $schedule): void
{
    $schedule->job(new CheckPriceDrops)
        ->everyFiveMinutes()
        ->withoutOverlapping(10) // mata sobreposição se a anterior ainda rodando
        ->onOneServer()           // se rodar em cluster, só 1 worker pega
        ->runInBackground();
}

withoutOverlapping() é o tipo de coisa que parece detalhe e não é. A documentação do Laravel deixa claro: sem ele, scrape de 5min que demora 7min vira tempestade de jobs concorrentes batendo no mesmo endpoint, e o ML te bloqueia em 20 minutos.

Cron é bom quando o ritmo é estável. Ruim quando você quer rastrear 50 mil produtos com cadências diferentes. Aí cada produto vira uma Job na fila, com sua própria cadência calculada por categoria.

Polling adaptativo

Se o agente é quem decide quando voltar a olhar, você economiza chamadas em horas mortas. O Claude Code expõe isso com o /loop sem intervalo: o modelo escolhe um delay entre 1min e 1h baseado no que viu na iteração anterior. A doc oficial descreve assim:

"Curtos quando uma build está terminando ou um PR está ativo, mais longos quando nada está pendente."

Aplicado ao tracker de preço: se o produto oscilou três vezes na última hora, vale apertar a frequência. Se está estável há 12 horas, vale recuar. Quem decide é o agente, com base em sinal, não em cron-cego.

Cuidado: deixar o LLM decidir o intervalo significa que o LLM está no caminho crítico do custo. Coloque um teto no harness (mínimo 5min entre execuções, máximo 6h) e nunca confie no modelo para respeitar isso sozinho.

Webhook-driven

A melhor janela é "não ter janela". Se o vendor expõe webhook, você não roda nada — espera o evento chegar.

Problema: webhook é entregue várias vezes. Provedor faz retry. Rede cai. O mesmo evento price_changed pode bater no seu endpoint 4 vezes em 30 segundos. Sem dedupe, são 4 push notifications no celular do usuário.

Idempotência e dedupe: o problema que ninguém vê até tomar

A regra do Svix é simples: todo evento tem um event_id estável, e o receptor mantém uma tabela com unique constraint nesse campo. Se o INSERT falhar com violação de unicidade, é duplicata, descarta.

// database/migrations/xxxx_create_processed_events_table.php
Schema::create('processed_events', function (Blueprint $table) {
    $table->string('event_id')->primary();
    $table->string('source');
    $table->timestamp('created_at')->useCurrent();
    $table->index('created_at'); // pra job de limpeza
});
// app/Http/Controllers/PriceWebhookController.php
public function handle(Request $request)
{
    $eventId = $request->header('X-Event-Id');

    try {
        DB::table('processed_events')->insert([
            'event_id' => $eventId,
            'source' => 'mercado-livre',
        ]);
    } catch (QueryException $e) {
        // unique violation = duplicata, ack e segue a vida
        return response()->noContent();
    }

    ProcessPriceChange::dispatch($request->all());
    return response()->noContent();
}

TTL de 7 dias resolve 99% dos casos — a maioria dos provedores para de tentar antes disso. Job diário de limpeza:

$schedule->call(fn () => DB::table('processed_events')
    ->where('created_at', '<', now()->subDays(7))
    ->delete()
)->daily();

Volume alto? Troca a tabela por Redis com TTL nativo:

if (! Redis::set("event:{$eventId}", 1, 'EX', 604800, 'NX')) {
    return response()->noContent(); // duplicata
}

NX só seta se não existir. EX 604800 expira em 7 dias. Uma chamada, atômico, sem job de limpeza.

Dedupe de alerta é diferente de dedupe de evento

Aqui mora a confusão clássica. Você pode ter recebido um único webhook do ML — único event_id, sem duplicata — e ainda assim disparar 14 alertas para o mesmo usuário, porque seu agente roda de 5 em 5 minutos e cada execução vê o preço abaixo do limite e quer avisar.

Dedupe de alerta tem chave diferente: produto + usuário + janela de tempo. Algo como:

$alertKey = sprintf(
    'alert:%d:%d:%s',
    $userId,
    $productId,
    now()->format('Y-m-d') // 1 alerta por dia, no máximo
);

if (! Redis::set($alertKey, 1, 'EX', 86400, 'NX')) {
    return; // já avisei esse user hoje, sai
}

PushNotification::send($userId, $message);

Se o requisito for "1 alerta por queda significativa", a chave inclui o threshold cruzado: alert:{user}:{product}:below-100 e expira só quando o preço subir de novo acima do threshold (você apaga a chave manualmente nesse momento). É um state machine pequeno escondido numa chave de Redis.

Mão na massa: o tracker que não estoura custo

Passo 1: estado fora do prompt

A primeira tentação é mandar o histórico inteiro de preço para o modelo decidir se houve queda. Não faça isso. Cada execução vai ler 30 mil tokens de contexto à toa. O agente lê o last_known_price de uma tabela e decide com base em 2 números.

// app/Jobs/CheckPriceDrops.php
public function handle(): void
{
    Product::watching()->chunk(100, function ($products) {
        foreach ($products as $product) {
            $current = $this->scraper->fetch($product->external_id);

            if ($this->isSignificantDrop($product, $current)) {
                ProcessAlert::dispatch($product->id, $current);
            }

            $product->update(['last_known_price' => $current]);
        }
    });
}

private function isSignificantDrop(Product $p, int $current): bool
{
    if ($p->last_known_price === null) return false;
    $drop = ($p->last_known_price - $current) / $p->last_known_price;
    return $drop >= 0.10; // 10% de queda
}

Repare: a regra de "queda significativa" está em código PHP, não no prompt. Regra dura sai do LLM. O LLM entra só no que precisa de julgamento, como escrever a mensagem do alerta personalizada para o usuário, decidir se a queda parece glitch ou promoção real, ler a página e detectar mudança de modelo.

Passo 2: o agente na hora do alerta

Quando o pipeline detecta queda real, o agente entra em cena para enriquecer o alerta:

// app/Jobs/ProcessAlert.php
public function handle(ClaudeAgent $agent): void
{
    $product = Product::find($this->productId);
    $alertKey = "alert:{$product->user_id}:{$product->id}:below-{$this->threshold}";

    if (! Redis::set($alertKey, 1, 'EX', 86400 * 7, 'NX')) {
        return;
    }

    $message = $agent->run(
        prompt: "Preço caiu de R\$ {$product->last_known_price} para R\$ {$this->newPrice}. "
              . "Produto: {$product->title}. Escreva um alerta curto, sem hype.",
        tools: ['web_search'], // pra cruzar com histórico do produto
    );

    PushNotification::send($product->user_id, $message);
}

O Redis::set NX aqui dobra como dedupe e como cache de "já avisei". Sai disso só se o preço voltar pra cima do threshold (em outro job).

Passo 3: cron no harness — Claude Code, Routines ou Laravel?

Tem três caminhos, cada um com trade-off claro. A própria doc da Anthropic compara:

Cloud (Routines) Desktop /loop
Roda sem máquina ligada Sim Sim Não
Acesso a arquivos locais Não Sim Sim
Intervalo mínimo 1h 1min 1min
Persistente entre restarts Sim Sim Só com --resume

Para um tracker em produção, a resposta é nenhum dos três puros: o Laravel scheduler roda o ciclo (de 5 em 5 minutos), e o agente entra só nas decisões que precisam de raciocínio. Cron do Claude Code é ótimo para babysitar uma execução durante o dev, mas a sessão expira em 7 dias e morre quando você fecha o terminal. Routines da Anthropic foram lançadas em abril de 2026 e resolvem o "sem máquina ligada", mas o intervalo mínimo de 1h não serve para alertar oscilação de preço em near real-time.

A regra prática: cron do produto fica no seu harness (Laravel, Rails, n8n, o que for). Cron do Claude Code fica para automação interna do dev — babá de PR, revisão noturna, smoke test.

Passo 4: webhook como atalho para custo

Se o ML manda webhook quando o preço muda, você desliga o cron de scrape e só reage:

Route::post('/webhooks/ml/price', [PriceWebhookController::class, 'handle'])
    ->middleware('verify.ml.signature');

Pulou o polling, pulou o custo de N execuções por hora, pulou o problema de rate limit no scrape. O agente só entra no momento em que há sinal real, e a fatura escala com mudança real de preço, não com tempo decorrido.

Custo: como não acordar com fatura de R$ 4 mil

Polling de 1 minuto em 50 mil produtos é 72 milhões de execuções por dia. Mesmo com regra dura em PHP e LLM só no alerta, você precisa de teto. Quatro coisas para fazer no dia 1:

  1. Tool failure rate em janela curta. Recomendação consolidada: se o tool de scrape começar a falhar, retry amplifica custo de LLM (mais contexto, mais turnos). Alarme de 5 em 5 minutos quando a taxa de erro passa de 10%.
  2. Batch API quando real-time não importa. Análise de tendência semanal, relatório consolidado, classificação de catálogo: nada disso precisa de resposta agora. A Batch API custa metade e processa em até 24h. Tráfego batch costuma ser 20 a 40% do gasto total, vale a separação.
  3. Sampling de logs. Logar cada chamada do agente em produção alta vira fatura de observabilidade maior que a do LLM. Sample 10 a 20% pra trace detalhado, conta agregada para o resto.
  4. Cache de prompt do sistema. Prompt do sistema do agente é estável entre execuções. Prompt caching da Anthropic corta 90% do custo dos tokens cacheados. Em tracker que executa 10 mil vezes por hora, é a diferença entre R$ 50/dia e R$ 500/dia.

Limitações e pontos de atenção

  • /loop morre com a sessão. Não use para nada que precise rodar quando você fecha o laptop. A própria doc avisa: tasks só disparam enquanto o Claude Code estiver rodando e idle.
  • Routines têm 1h de intervalo mínimo. Se o produto exige cadência menor, é Laravel scheduler ou solução self-hosted, não cloud da Anthropic.
  • Webhook sem queue é bug esperando acontecer. Receba, valide assinatura, dedupe, dispare job assíncrono. Processar inline trava o endpoint e o vendor te marca como não-saudável.
  • LLM como decisor binário (alerta ou não) tem variância. Mesma entrada, decisões diferentes em runs distintos. Use o modelo para escrever o alerta e enriquecer o contexto. A decisão dispara/não-dispara fica em código determinístico.
  • Notificação repetida queima confiança. Usuário que recebe 3 alertas iguais marca o app como spam e desinstala. Deduplicação de alerta é parte do produto, não detalhe de infra.

Para o dev que quer ir mais fundo

Se você está montando um tracker, comparador de preço ou qualquer agente que precisa viver sozinho, dois conteúdos próximos do blog complementam este post:

E se você quer construir produtos de IA com engenharia de verdade, sem prompt mágico e sem stack hype, o Clã Beer & Code é o ambiente onde a galera está mexendo nesse tipo de problema todo dia. Entra na lista de espera — o foco é exatamente esse: dev PHP/Laravel construindo IA aplicada com produto real.

FAQ rápido

Por que /loop do Claude Code não serve para tracker em produção? Porque ele é session-scoped. A doc é explícita: tasks param de disparar quando o terminal fecha, e expiram em 7 dias mesmo com a sessão aberta. Para 24/7 real, vai de Routines (cloud) ou scheduler do seu framework.

Redis ou Postgres para dedupe? Volume baixo (até centenas de eventos por minuto) e Postgres já tem unique constraint é suficiente, sem dependência nova. Volume alto e latência apertada vai de Redis com SET NX EX. Os dois aceitam o mesmo padrão de chave estável + TTL.

Por que separar regra dura (queda 10%) do LLM? Porque LLM custa, varia entre runs e não tem garantia formal. Regra de negócio dura precisa ser auditável, testável e barata. O LLM entra onde julgamento humano agregaria valor: redação do alerta, detecção de promoção fake, classificação semântica de motivo.

Quanto custa um tracker desses por mês? Depende muito da cadência e do tamanho do catálogo. Estimativa rasa pra 1.000 produtos com checagem a cada 15min e LLM só no alerta (~50 alertas/dia, prompt cacheado): faixa de R$ 30 a R$ 100/mês em modelos Haiku/Sonnet. Se você joga LLM em cada checagem, multiplica por 100.

Conclusão

O agente reativo é o "olá mundo" da era de IA aplicada. Funciona, impressiona na demo, e morre na primeira vez que o usuário pede pra ser avisado em vez de perguntar. O salto pro agente que vive sozinho não é prompt melhor: é arquitetura, com cron, webhook, dedupe, idempotência e custo controlado.

O próximo passo dessa tecnologia já está aparecendo: agente que não só avisa "baixou agora", mas age — abre o checkout, separa o cupom, aciona o cartão, abre ticket de suporte. Aí entra um problema novo (autorização, reversibilidade, audit trail), mas a fundação é a mesma: harness com janela de monitoramento, dedupe e custo controlado.

Quem domina isso constrói produto de IA de verdade. O resto fica preso no chat.

Lucas Souza
Lucas Souza

{AI Engineer} — apaixonado por Laravel, arquitetura de software e construir produtos com impacto. Compartilho aqui tutoriais, descobertas e reflexões sobre o dia a dia de engenharia.

Você também pode gostar

VirguIA

beer & code assistant

conectando…

Não foi possível iniciar o chat agora.

tocando