Moon Flower Society > Bot de Monitoramento de Precos com Apify
Ideia de um bot autohospedavel para monitorar precos em marketplaces, normalizar resultados e enviar alertas de oportunidades.
Posted July 4, 2026. Markdown. Stage: sprout. Tags: apify, scraping, automacao, precos, bot.
Revised Oct 24, 2026 (expansion). "Deixa claro que o limite de preço vale para o preço com frete, com um exemplo, e onde fica a frequência de cada fonte."What changed | 1 revision
Projeto: bot pessoal/autohospedável para monitorar preços, encontrar oportunidades e alertar quando algo desejado aparecer abaixo de um limite.
Ideia central: usar a Apify para tudo que ela já cobre bem, porque seria burrice gastar semanas recriando scrapers que já existem. Para as fontes que não têm Actor confiável ou têm cobertura ruim, criamos scrapers próprios, pequenos, isolados e fáceis de substituir.
A ideia é construir um sistema chamado, provisoriamente, GarimpoBot.
Ele permite cadastrar coisas que você quer comprar, por exemplo:
O usuário cadastra:
O sistema consulta as fontes, normaliza os resultados, calcula preço total, remove duplicados, filtra porcaria, compara com o limite e envia alerta por Telegram, e-mail ou outro canal.
A V1 deve ser simples, feia e funcional. Nada de construir uma plataforma gigante antes de validar se os dados chegam direito.
A regra é:
Apify onde existir Actor bom. Scraper próprio onde não existir. API oficial apenas quando for claramente melhor.
Isso evita dois erros comuns:
A arquitetura precisa permitir trocar uma fonte sem quebrar o sistema inteiro.
Existe cobertura boa na Apify para Mercado Livre Brasil.
Exemplos encontrados:
Uso no projeto: usar Apify na V1, embora a API oficial do Mercado Livre também seja uma alternativa boa.
Motivo: começar rápido. Depois podemos substituir por API oficial se fizer sentido reduzir custo ou aumentar estabilidade.
Campos desejados:
Prioridade: alta.
A Apify tem vários Actors para OLX Brasil. Alguns aceitam keyword, categoria ou URL, e retornam título, preço, fotos, localização, vendedor, atributos e até dados úteis para monitoramento de preço.
Uso no projeto: Apify desde a V1.
Motivo: OLX é uma fonte muito importante para usados, mas scraping próprio tende a ser chato. Melhor validar com Actor pronto antes.
Campos desejados:
Prioridade: altíssima.
OLX é onde aparecem bons achados, mas também muito lixo. Precisa de filtro pesado.
A Apify tem diversos Actors para Amazon capazes de extrair produtos por keyword, ASIN ou URL, incluindo preço, avaliações, disponibilidade, vendedor, imagem e categoria.
Uso no projeto: Apify na V1 ou V2.
Motivo: a Amazon tem Product Advertising API, mas ela exige mais burocracia e regras. Para um bot pessoal, Apify pode acelerar muito.
Campos desejados:
Prioridade: alta.
Observação importante: Amazon pode dar falso positivo com marketplace, variações, produto internacional, preço sem imposto/frete e itens indisponíveis. O parser precisa ser conservador.
Existe Actor para Enjoei na Apify, com extração de listagens, preço, vendedor, imagens e detalhes.
Uso no projeto: testar na V2.
Motivo: existe cobertura pronta, mas os sinais de maturidade parecem menores que Mercado Livre/OLX/Amazon. Não dá para confiar cegamente sem teste.
Campos desejados:
Prioridade: média/alta.
Risco: Actor quebrar, ser pouco mantido ou não extrair exatamente o que queremos.
A Apify tem Actors para Google Shopping que extraem produtos por busca, com preço, loja, rating, reviews, URL e país/idioma.
Uso no projeto: Apify desde a V1 ou V2.
Motivo: scraping direto do Google é pedir CAPTCHA e dor de cabeça. Usar Apify ou SerpApi é mais sensato.
Campos desejados:
Prioridade: alta.
Uso ideal: encontrar lojas que não estavam no radar. O Google Shopping serve como radar amplo, não necessariamente como fonte perfeita.
A Apify também tem scraper para resultados gerais do Google.
Uso no projeto: opcional.
Motivo: útil para descobrir páginas de produtos, sebos obscuros e lojas pequenas. Mas para monitoramento recorrente de preço, Google Search geral é menos limpo que Google Shopping.
Prioridade: média.
Uso ideal: modo descoberta.
Exemplo:
"kindle paperwhite usado" site:.br
"bigme b751c" "R$"
"kobo libra" "venda"
Não encontrei uma solução Apify madura para Estante Virtual.
Existem alternativas parciais:
Decisão: criar scraper próprio.
Motivo: Estante Virtual é muito relevante para livros, e o scraping tende a ser possível. Melhor construir um módulo específico para busca de livros, preço, sebo, condição e frete.
Campos desejados:
Prioridade: alta para livros.
Para Buscapé, existe uma solução pronta fora da Apify: Bright Data tem um Buscapé Scraper dedicado, prometendo extrair nome, marca, categoria, preço, desconto, avaliação, SKU, descrição, disponibilidade, URL e imagens.
Decisão: duas opções.
Opção A: usar Bright Data para Buscapé.
Opção B: criar scraper próprio se o custo da Bright Data for alto demais.
Minha recomendação: testar Bright Data primeiro, mas não tornar o sistema dependente disso.
Campos desejados:
Prioridade: média/alta.
Buscapé/Zoom são bons para preço de produto novo, mas menos úteis para usados e raridades.
Aqui entra o scraper genérico configurável.
O usuário cadastra:
Exemplo de configuração:
site: "Sebo Exemplo"
source_type: "generic_css"
search_url: "https://exemplo.com.br/busca?q=kindle"
item_selector: ".product-card"
title_selector: ".product-title"
price_selector: ".price"
url_selector: "a"
image_selector: "img"
rendering: "http"
Prioridade: média.
Isso transforma o sistema em uma ferramenta flexível, sem exigir que cada lojinha tenha um scraper codado do zero.
+------------------------+
| Painel Web |
| Cadastro de desejos |
+-----------+------------+
|
v
+------------------------+
| Backend FastAPI |
| Regras, filtros, API |
+-----------+------------+
|
v
+------------------------+
| Scheduler / Worker |
| Roda buscas periódicas |
+-----------+------------+
|
v
+------------------------+
| Fontes |
| Apify + próprios |
+-----------+------------+
|
v
+------------------------+
| Normalizador |
| Padroniza resultados |
+-----------+------------+
|
v
+------------------------+
| PostgreSQL |
| Itens + histórico |
+-----------+------------+
|
v
+------------------------+
| Motor de alerta |
| Telegram/e-mail/etc. |
+------------------------+
Para V1:
Para V2:
Não faz sentido começar com frontend sofisticado. O gargalo é dado confiável, não botão bonito.
V1:
Depois:
garimpobot/
├── docker-compose.yml
├── .env.example
├── README.md
├── app/
│ ├── main.py
│ ├── config.py
│ ├── database.py
│ ├── models/
│ │ ├── watchlist.py
│ │ ├── source.py
│ │ ├── item.py
│ │ ├── price_history.py
│ │ └── alert.py
│ ├── api/
│ │ ├── watchlists.py
│ │ ├── items.py
│ │ ├── sources.py
│ │ └── alerts.py
│ ├── workers/
│ │ ├── scheduler.py
│ │ ├── run_watch.py
│ │ └── queue.py
│ ├── sources/
│ │ ├── base.py
│ │ ├── apify/
│ │ │ ├── mercadolivre.py
│ │ │ ├── olx.py
│ │ │ ├── amazon.py
│ │ │ ├── enjoei.py
│ │ │ ├── google_shopping.py
│ │ │ └── google_search.py
│ │ ├── custom/
│ │ │ ├── estante_virtual.py
│ │ │ ├── buscape.py
│ │ │ └── generic_css.py
│ │ └── brightdata/
│ │ └── buscape.py
│ ├── normalize/
│ │ ├── money.py
│ │ ├── text.py
│ │ ├── dedupe.py
│ │ └── scoring.py
│ ├── notifications/
│ │ ├── telegram.py
│ │ ├── email.py
│ │ └── webhook.py
│ └── templates/
│ ├── layout.html
│ ├── watchlists.html
│ ├── items.html
│ └── alerts.html
└── data/
├── screenshots/
└── html_cache/
Representa algo que você quer comprar.
CREATE TABLE watchlists (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
description TEXT,
max_price NUMERIC(12,2),
min_price NUMERIC(12,2),
max_shipping NUMERIC(12,2),
required_words TEXT[],
blocked_words TEXT[],
preferred_words TEXT[],
enabled BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT now(),
updated_at TIMESTAMP DEFAULT now()
);
Representa onde e como buscar aquele desejo.
CREATE TABLE watch_sources (
id SERIAL PRIMARY KEY,
watchlist_id INTEGER REFERENCES watchlists(id),
source_name TEXT NOT NULL,
source_type TEXT NOT NULL,
search_query TEXT,
search_url TEXT,
apify_actor_id TEXT,
config JSONB,
interval_minutes INTEGER DEFAULT 180,
enabled BOOLEAN DEFAULT TRUE,
last_checked_at TIMESTAMP,
created_at TIMESTAMP DEFAULT now()
);
Resultado normalizado vindo de qualquer fonte.
CREATE TABLE scraped_items (
id SERIAL PRIMARY KEY,
source_name TEXT NOT NULL,
source_item_id TEXT,
title TEXT NOT NULL,
normalized_title TEXT,
url TEXT NOT NULL,
image_url TEXT,
price NUMERIC(12,2),
shipping NUMERIC(12,2),
total_price NUMERIC(12,2),
currency TEXT DEFAULT 'BRL',
seller TEXT,
seller_rating TEXT,
location TEXT,
condition TEXT,
availability TEXT,
raw JSONB,
content_hash TEXT,
first_seen_at TIMESTAMP DEFAULT now(),
last_seen_at TIMESTAMP DEFAULT now()
);
Histórico de preço.
CREATE TABLE price_history (
id SERIAL PRIMARY KEY,
scraped_item_id INTEGER REFERENCES scraped_items(id),
price NUMERIC(12,2),
shipping NUMERIC(12,2),
total_price NUMERIC(12,2),
checked_at TIMESTAMP DEFAULT now()
);
Alertas enviados.
CREATE TABLE alerts (
id SERIAL PRIMARY KEY,
watchlist_id INTEGER REFERENCES watchlists(id),
scraped_item_id INTEGER REFERENCES scraped_items(id),
channel TEXT NOT NULL,
reason TEXT,
sent_at TIMESTAMP DEFAULT now()
);
Itens ignorados manualmente para evitar alerta repetido de lixo.
CREATE TABLE ignored_items (
id SERIAL PRIMARY KEY,
watchlist_id INTEGER REFERENCES watchlists(id),
item_url TEXT,
content_hash TEXT,
reason TEXT,
created_at TIMESTAMP DEFAULT now()
);
Todos os scrapers, Apify ou próprios, devem retornar o mesmo contrato.
{
"source_name": "olx",
"source_item_id": "abc123",
"title": "Kindle Paperwhite 11ª geração 16GB",
"url": "https://...",
"image_url": "https://...",
"price": 520.00,
"shipping": 0.00,
"total_price": 520.00,
"currency": "BRL",
"seller": "João",
"seller_rating": null,
"location": "Belo Horizonte, MG",
"condition": "used",
"availability": "available",
"raw": {}
}
Sem esse contrato, o projeto vira bagunça. Cada fonte pode ser caótica internamente, mas a saída precisa ser uniforme.
Exemplo real:
name: "Kindle Paperwhite"
description: "Quero um Kindle Paperwhite usado ou novo, mas não acessórios."
max_price: 650
min_price: 250
max_shipping: 80
required_words:
- kindle
blocked_words:
- capa
- case
- película
- caixa
- defeito
- quebrado
- peças
- assistência
preferred_words:
- paperwhite
- 11ª geração
- 16gb
sources:
- mercado_livre
- olx
- amazon
- google_shopping
- enjoei
Outro exemplo:
name: "Bigme B751C"
max_price: 1700
min_price: 900
required_words:
- bigme
blocked_words:
- capa
- caneta
- película
- case
preferred_words:
- b751c
- color
sources:
- mercado_livre
- olx
- google_shopping
- amazon
Exemplo para livros:
name: "O Castelo dos Cárpatos - Júlio Verne"
max_price: 80
required_words:
- castelo
- cárpatos
blocked_words:
- resumo
- pdf
- apostila
preferred_words:
- júlio verne
- estante virtual
sources:
- estante_virtual
- mercado_livre
- google_shopping
O motor precisa evitar alertas inúteis.
Um item só pode virar alerta se:
O sistema nunca deve alertar apenas pelo preço do produto.
preço_total = preço + frete
Se o frete não estiver disponível, o item pode ser marcado como:
shipping_unknown = true
Nesse caso, o alerta deve avisar:
Preço abaixo do limite, mas frete não identificado.
O mesmo vale para o limite: max_price é comparado com o preço total, não com o preço do anúncio. Um Kindle anunciado por R$ 600 com R$ 80 de frete custa R$ 680 e não passa num limite de R$ 650. O max_shipping serve para outra coisa: descartar anúncios baratos com frete desproporcional, mesmo quando o total ainda cabe no limite.
Para eletrônicos:
capa
case
película
carregador
cabo
caixa
manual
defeito
quebrado
peças
retirada de peças
bloqueado
não liga
assistência
sucata
Para livros:
pdf
resumo
apostila
digital
xerox
cópia
Para roupas/acessórios:
réplica
primeira linha
inspirado
similar
O mesmo produto pode aparecer várias vezes ou mudar levemente de título.
Deduplicação deve usar:
Exemplo:
Kindle Paperwhite 11 geração 16gb
Kindle Paperwhite 16 GB 11ª geração
Amazon Kindle Paperwhite 2021
Podem ser o mesmo item ou produtos equivalentes. O sistema não precisa resolver isso perfeitamente na V1, mas precisa evitar spam óbvio.
Cada resultado pode receber uma nota de 0 a 100.
Critérios:
Exemplo:
Score 92: forte candidato
Score 70: bom, mas verificar
Score 45: possível falso positivo
Score 20: lixo provável
O alerta só deve ser automático acima de certo score.
IA não deve ser usada para fazer scraping. Isso é caro, lento e desnecessário.
IA deve entrar em três pontos:
Usuário digita:
quero um e-reader colorido barato
IA sugere termos:
bigme b751c
bigme color
kobo colour
boox color
onyx boox color
kindle colorsoft
A IA recebe título, descrição e preço e responde:
{
"matches_intent": true,
"is_accessory": false,
"risk": "low",
"reason": "Parece ser o produto principal, não acessório, e está abaixo do limite."
}
Em vez de só mandar link, o bot envia:
Achei um Kindle Paperwhite por R$ 520 na OLX.
Parece bater com seu desejo porque o título menciona Paperwhite e não tem sinais de ser capa, caixa ou produto quebrado.
Atenção: frete não identificado. Verifique antes de comprar.
Importante: IA não deve inventar preço médio. Preço médio só pode vir do histórico real coletado.
Formato recomendado:
🚨 Achado encontrado
Kindle Paperwhite 11ª geração 16GB
Fonte: OLX
Preço: R$ 520,00
Frete: não identificado
Local: Belo Horizonte, MG
Limite configurado: R$ 650,00
Score: 88/100
Motivo:
Produto parece bater com sua busca e está abaixo do preço máximo.
Abrir:
https://...
Ações:
/ignorar abc123
/comprado abc123
/silenciar kindle-paperwhite 24h
Útil para resumo diário.
Possível, mas eu deixaria para depois. WhatsApp sempre adiciona complexidade desnecessária no início.
Boa opção simples e autohospedável.
Não seja burro de colocar tudo a cada 5 minutos. Isso custa dinheiro, gera bloqueio e polui alerta.
Sugestão inicial:
| Fonte | Frequência padrão |
|---|---|
| OLX | 30 a 120 minutos |
| Mercado Livre | 1 a 3 horas |
| Google Shopping | 3 a 6 horas |
| Amazon | 6 a 12 horas |
| Enjoei | 2 a 6 horas |
| Estante Virtual | 2 a 6 horas |
| Buscapé/Zoom | 6 a 12 horas |
| Sites pequenos | 12 a 24 horas |
Para itens muito desejados, permitir frequência especial.
Essa frequência mora em watch_sources.interval_minutes, com padrão de 180 minutos. Como cada desejo tem uma linha por fonte, o mesmo Kindle pode ser checado de hora em hora na OLX e duas vezes por dia na Amazon.
Fluxo:
1. Worker pega uma watch_source do tipo apify.
2. Monta input do Actor.
3. Executa Actor via Apify API.
4. Aguarda conclusão.
5. Lê dataset de saída.
6. Normaliza campos.
7. Salva no banco.
8. Roda filtros.
9. Envia alerta se necessário.
Exemplo conceitual em Python:
from apify_client import ApifyClient
client = ApifyClient("APIFY_TOKEN")
run = client.actor("ACTOR_ID").call(run_input={
"query": "kindle paperwhite",
"maxItems": 50,
"country": "BR"
})
items = list(client.dataset(run["defaultDatasetId"]).iterate_items())
Cada fonte Apify terá um adaptador, porque cada Actor retorna campos diferentes.
class BaseApifySource:
source_name: str
actor_id: str
def build_input(self, watch_source):
raise NotImplementedError
def normalize_item(self, raw_item):
raise NotImplementedError
class ApifyMercadoLivreSource(BaseApifySource):
source_name = "mercado_livre"
actor_id = "viralanalyzer/mercadolivre-scraper"
class ApifyOlxSource(BaseApifySource):
source_name = "olx"
actor_id = "haketa/olx-brasil-scraper"
class ApifyAmazonSource(BaseApifySource):
source_name = "amazon"
actor_id = "eccentric_layout/amazon-scraper"
class ApifyEnjoeiSource(BaseApifySource):
source_name = "enjoei"
actor_id = "ribtools/enjoei-scraper"
class ApifyGoogleShoppingSource(BaseApifySource):
source_name = "google_shopping"
actor_id = "automation-lab/google-shopping-scraper"
Observação: os Actor IDs devem ser configuráveis no banco ou .env, não fixos para sempre no código. Actor de marketplace pode sumir, mudar preço ou ficar ruim.
O scraper da Estante Virtual deve ter dois modos:
Fluxo:
1. Monta URL de busca.
2. Baixa HTML.
3. Tenta extrair dados estruturados embutidos, se existirem.
4. Se falhar, faz parse do HTML.
5. Se HTML vier incompleto, usa Playwright.
6. Normaliza livros encontrados.
7. Salva histórico.
Campos especiais:
Esse scraper precisa ser tratado como módulo importante, não gambiarra.
Primeiro caminho: Bright Data.
Segundo caminho: scraper próprio.
Fluxo scraper próprio:
1. Recebe termo ou URL de busca.
2. Extrai cards de produto.
3. Captura menor preço, loja e disponibilidade.
4. Entra na página de ofertas se necessário.
5. Normaliza resultado.
Risco: páginas podem ter JS, anti-bot ou estrutura variável.
Serve para lojas pequenas.
Entrada:
{
"url": "https://site.com/busca?q=kindle",
"item_selector": ".product-card",
"title_selector": ".title",
"price_selector": ".price",
"url_selector": "a",
"image_selector": "img",
"rendering": "http"
}
Isso não substitui scraper dedicado, mas permite adicionar fontes rapidamente.
A interface mínima precisa ter:
services:
app:
build: ./app
restart: unless-stopped
env_file: .env
ports:
- "8088:8000"
depends_on:
- postgres
- redis
worker:
build: ./app
restart: unless-stopped
command: python -m app.workers.run_worker
env_file: .env
depends_on:
- postgres
- redis
scheduler:
build: ./app
restart: unless-stopped
command: python -m app.workers.scheduler
env_file: .env
depends_on:
- postgres
- redis
postgres:
image: postgres:16
restart: unless-stopped
environment:
POSTGRES_DB: garimpobot
POSTGRES_USER: garimpobot
POSTGRES_PASSWORD: garimpobot
volumes:
- ./data/postgres:/var/lib/postgresql/data
redis:
image: redis:7
restart: unless-stopped
volumes:
- ./data/redis:/data
APP_URL=http://localhost:8088
DATABASE_URL=postgresql://garimpobot:garimpobot@postgres:5432/garimpobot
REDIS_URL=redis://redis:6379/0
APIFY_TOKEN=
BRIGHTDATA_TOKEN=
TELEGRAM_BOT_TOKEN=
TELEGRAM_CHAT_ID=
OPENAI_API_KEY=
ENABLE_AI_CLASSIFIER=false
Objetivo: provar que conseguimos buscar dados da Apify e enviar alerta.
Escopo:
Critério de sucesso:
Escopo:
Critério de sucesso:
Escopo:
Critério de sucesso:
Escopo:
Critério de sucesso:
Escopo:
Só faz sentido depois que o motor funcionar. Interface bonita em sistema burro é maquiagem em cadáver.
Mitigação:
Mitigação:
Mitigação:
Mitigação:
Mitigação:
Não fazer:
Se fizer isso, o projeto vira brinquedo quebrado.
Mercado Livre via Apify
OLX via Apify
Google Shopping via Apify
Telegram
Amazon via Apify
Enjoei via Apify
Estante Virtual próprio
Buscapé via Bright Data ou próprio
Zoom próprio ou via Buscapé
Sites pequenos via Generic CSS
🚨 GarimpoBot encontrou algo
Kindle Paperwhite 11ª geração 16GB
Fonte: OLX
Preço: R$ 520,00
Frete: não identificado
Local: Belo Horizonte, MG
Limite: R$ 650,00
Score: 88/100
Por que parece bom:
- contém Kindle e Paperwhite;
- está abaixo do limite;
- não parece capa, caixa ou acessório;
- preço está dentro de uma faixa plausível.
Atenção:
- frete não identificado;
- confirme estado do produto com o vendedor.
Abrir anúncio:
https://...
A ideia correta é construir o GarimpoBot como um agregador de fontes de preço, não como “um scraper gigante”.
O núcleo do produto não é scraping. O núcleo é:
Apify resolve boa parte do trabalho pesado em Mercado Livre, OLX, Amazon, Enjoei e Google Shopping. Para Estante Virtual e algumas fontes brasileiras específicas, criamos scrapers próprios. Para Buscapé, testamos Bright Data ou fazemos nosso módulo.
A melhor decisão técnica é começar pequeno, mas com arquitetura limpa:
Apify primeiro.
Scrapers próprios onde necessário.
Tudo normalizado no mesmo formato.
PostgreSQL para histórico.
Telegram para alerta.
Painel simples.
Sem firula.
Esse projeto é totalmente viável. O risco não é técnico. O risco é escopo. Se tentar nascer como “monitor universal de tudo”, vai morrer. Se nascer como “bot pessoal que monitora 5 fontes bem e alerta sem encher o saco”, tem grande chance de funcionar e evoluir.
View the original Markdown file
Ideas | What changed | RSS | About | Text version
Pablo Murad <pablomurad@pm.me>