
Como criei um rastreador de arbitragem de relógios em 2 semanas
TL;DR — Eu construí o Watch Arbitrage Tracker (Apify Store, GitHub): um ator Crawlee + Camoufox que coleta dados de 6 marketplaces de relógios de luxo em paralelo, calcula o preço médio entre plataformas para qualquer referência Patek/Rolex/AP e envia uma notificação no Telegram no momento em que uma listagem cai mais de X% abaixo do mercado. Menos de $1/mês para uso típico de revendedores. Também funciona como um servidor MCP para que Claude Desktop / Cursor / ChatGPT possam consultar o feed ao vivo em inglês simples.
A parte interessante não é a construção — são os 5 bugs que eu tive que corrigir em produção após o lançamento público, e a matemática da mediana entre plataformas que transforma "dados coletados" em um verdadeiro sinal de arbitragem.
O problema (real, validado, doloroso)
Revendedores de relógios profissionais — as pessoas que compram e vendem Patek 5711, Rolex Daytona, AP Royal Oak usados — passam mais de 3 horas por dia atualizando 6 marketplaces de revendedores em busca de inventário mal precificado. O trabalho é mecânico: abrir Chrono24, pesquisar 10 números de referência, comparar preços, mudar para WatchBox, repetir, mudar para Bobs Watches, repetir...
Existem ferramentas existentes (Watchcharts $79/mês, ChronoPulse $500/mês, Bezel Club), mas todas têm o mesmo defeito: ancoragem em uma única plataforma. Elas informam o preço médio no Chrono24, não no mercado. Isso é inútil para arbitragem — o objetivo é encontrar diferenças entre plataformas.
A matemática que realmente importa:
spread = cross_platform_median(refX) - listing_price(refX, platformY)
Se um Submariner 124060 está listado a $11,900 no WatchBox, mas a verdadeira mediana do mercado (calculada entre Chrono24 + WatchBox + Bobs) é $13,988 — isso representa um spread de 28,2%. Esse é o alerta que vale a pena acordar um revendedor às 3 da manhã.
Nenhuma ferramenta que eu consegui encontrar calcula uma verdadeira mediana entre plataformas. Então eu construí uma.
A pilha
Pilha padrão do Apify com uma reviravolta personalizada:
- Crawlee + Camoufox (fork stealthy do Firefox) para resiliência contra bots. Chrono24 + Bobs Watches estão atrás do Cloudflare; Camoufox + rotação de proxy do Apify lidam com eles de forma confiável.
- TypeScript em toda parte (modo estrito, Node 24).
-
Arquivos de crawler por plataforma (
src/crawlers/{chrono24,watchbox,bobs,...}.ts) — cada um com ~100 linhas, todos se conformam ao mesmo formato deListingpara que o agregador não se importe de qual plataforma uma listagem veio. -
Agregador (
src/aggregator.ts) — agrupa listagens por sub-referência extraída (mais sobre isso no Bug #5 abaixo), calcula uma mediana ajustada, detecta spreads. -
Dispatcher de alertas (
src/alerts.ts) — Telegram por oportunidade com deduplicação de 24h. - Modo duplo — o mesmo código-fonte roda como um crawler em lote (cron agendado) E como um servidor MCP em modo Standby do Apify, expondo 3 ferramentas HTTP para agentes de IA.
Total: ~2000 LOC em 25 arquivos. Repositório: github.com/DataKazKN/watch-arbitrage-mcp.
Os 5 bugs que tive que corrigir AO VIVO em produção
Eu lancei o ator como um ator público pago Pay-Per-Event do Apify depois que minha última execução de teste parecia limpa. Então eu executei o ator com meu próprio token de bot do Telegram + 3 referências no dia seguinte ao lançamento e imediatamente encontrei 5 bugs que teriam feito o ator parecer quebrado para usuários de primeira viagem.
Bug #1 — WatchBox redirecionou toda pesquisa para uma página de splash (0 listagens extraídas)
A URL do crawler era:
https://www.the1916company.com/search/pre-owned/?q=rolex+116500LN
Na nossa verificação de 2026-05-04, isso retornou uma grade de azulejos com 8 listagens. Dois dias depois: zero. Por quê?
A inspeção do DOM ao vivo via Playwright revelou: o WatchBox agora redireciona qualquer consulta contendo uma palavra-chave de marca (rolex, patek, audemars) para uma página de splash de sugestão de marca que não tem azulejos de produto. O padrão de URL anterior quebrou silenciosamente.
A correção foi pequena, mas só foi possível encontrar indo às mãos:
// ANTES — incluía prefixo da marca → redireciona para splash → 0 azulejos
return `https://www.the1916company.com/search/pre-owned/?q=${encodeURIComponent(`${brand} ${ref}`)}`;
// DEPOIS — referência nua → chega na página de resultados real /search/?q=
return `https://www.the1916company.com/search/?q=${encodeURIComponent(ref)}`;
Lição: nunca confie em padrões de URL documentados por mais de 30 dias para sites que você não controla. Programe execuções mensais de verificação do DOM, mesmo em plataformas estáveis.
Bug #2 — Bobs Watches ponto de pesquisa errado (0 produtos renderizados)
Mesma padrão, causa diferente. O formulário da página inicial do Bobs Watches:
<form action="/shop" method="get">
<input name="query" type="text">
</form>
O ponto de pesquisa real é /shop?query=124060 — mas eu estava roteando através de seus antigos URLs de catálogo /{brand}-{model}-{page}.html (que cobriam apenas as 7 principais coleções E inflacionavam o tamanho da amostra além da referência exata do usuário).
// ANTES — roteamento de URL de catálogo obsoleto
return `https://www.bobswatches.com/rolex-submariner-1.html`;
// DEPOIS — ponto de pesquisa real
return `https://www.bobswatches.com/shop?query=${encodeURIComponent(ref)}`;
Bônus: o intersticial "Um instante..." do Cloudflare leva ~8s para limpar com Camoufox. O timeout original de 30s do waitForSelector estava ocasionalmente muito apertado; aumentei para 45s. Menos execuções com zero falso.
B
O rastreador de arbitragem de relógios oferece uma solução inovadora para revendedores que buscam otimizar suas operações. Ao integrar dados de múltiplas plataformas, ele melhora a eficiência e a precisão na identificação de oportunidades de arbitragem, essencial para o mercado brasileiro.


