Loucura de Promo documento do desenvolvedor 20 ago 2026

Como mandar oferta para o site

O site saiu do Lovable e virou WordPress próprio. O coletor não fala com o banco: ele faz POST de uma lista de ofertas e o WordPress aplica as mesmas regras que aplica na coleta interna — preço em centavos, desconto conferido, categoria classificada, duplicata barrada. Nada disso precisa ser reescrito do lado do Python.

POST https://loucuradepromo.com.br/wp-json/ldp/v1/ofertas

O que já está de pé

Tudo isto está no ar em produção e não precisa ser refeito. A lista existe para evitar trabalho duplicado — a tentação de reimplementar dedupe e conversão de preço no coletor é grande, e é justamente o que faz as duas pontas divergirem.

Vitrine
filtros, busca, paginação, destaques
Link /go/
prévia no WhatsApp + clique contado
Faxina
oferta some em 48 h, automática
Fila WhatsApp
Z-API, ritmo e horário silencioso
Dedupe
índice único no banco
Categoria
classificador por título

O que não existe: geração de link curto de afiliado do Mercado Livre, e qualquer coleta que dependa de navegador. É esse o buraco que o serviço em Python preenche.

O contrato

Autenticação

Token no cabeçalho. Também é aceito Authorization: Bearer <token>, que é o padrão da maioria das bibliotecas HTTP.

cabeçalhoshttp
Content-Type: application/json
X-LDP-Token: SEU_TOKEN
Trate como senha

Quem tem esse token escreve no banco do site. Ele vai em cabeçalho, nunca em query string — query string entra em log de servidor, em histórico de proxy e no Referer.

Guarde em variável de ambiente ou cofre, fora do código e fora do git. Se vazar, é só gerar outro na tela de configurações: o antigo morre na hora.

Corpo

Aceita {"ofertas": [...]}, uma lista pura ou um objeto único. O teto é de 200 ofertas por lote.

requisiçãojson
{
  "ofertas": [
    {
      "marketplace":   "mercadolivre",
      "externo_id":    "MLB3901234567",
      "titulo":        "Fone Bluetooth JBL Tune 520BT Preto",
      "preco_atual":   199.90,
      "preco_antigo":  399.00,
      "desconto":      50,
      "url_produto":   "https://produto.mercadolivre.com.br/MLB-3901234567-...",
      "url_afiliado":  "https://mercadolivre.com/sec/1AbCdEf",
      "url_imagem":    "https://http2.mlstatic.com/D_NQ_NP_123456-MLB.jpg",
      "categoria":     "Eletrônicos",
      "avaliacao":     4.7,
      "tem_variacao":  true,
      "rotulo_variacao": "Cor",
      "loja":          "JBL Oficial",
      "expira_em":     "2026-08-21T23:59:00-03:00",
      "extras":        { "comissao": 5.5, "vendas": 1420 }
    }
  ]
}

Campos da oferta

CampoTipoObrig.Observação
marketplacetextosimmercadolivre, shopee, aliexpress, amazon, magalu. Variações são reconhecidas (meli, ML, Magazine Luiza); o que não for vira outro e continua aparecendo.
titulotextosimMáx. 400 caracteres. HTML é removido.
preco_atualnúmero/textosimEm reais. Aceita 199.9, "199,90" e "R$ 1.299,90" — a conversão para centavos é do servidor.
preco_antigonúmero/textonãoAusente ou menor que o atual? É recomposto a partir do desconto.
descontonúmeronãoAceita 35 e 0.35. Se não vier, é calculado dos dois preços.
url_produtoURLum dos doisLink cru do produto.
url_afiliadoURLum dos doisÉ este que o /go/ usa. Sem ele, cai no url_produto.
url_imagemURLnãoSem imagem o cartão fica sem foto e o disparo vai como texto puro.
externo_idtextorecomendadoID do item no marketplace. É o que impede duplicata.
categoriatextonãoSó entra como reserva — o título classifica primeiro.
avaliacaonúmeronão0 a 5.
tem_variacaobooleanonãoLiga o selo de variação no cartão.
rotulo_variacaotextonão"Cor", "Modelo", "A partir de"…
lojatextonãoNome do vendedor.
expira_emdatanãoISO 8601 ou Y-m-d H:i:s. Ao passar da data a oferta some da vitrine, mesmo antes da faxina.
capturado_emdatanãoPadrão: agora. Define a idade — e é a idade que faz expirar em 48 h.
extrasobjetonãoJSON livre: comissão, número de vendas, o que for.

O que o servidor faz com cada oferta

Não é só gravar. Toda oferta — venha do conector interno ou da rota de ingestão — passa por ldp_salvar_oferta(), nesta ordem:

  1. Título — tira HTML, corta em 400 caracteres. Vazio ⇒ recusa.
  2. Preço — converte para centavos aceitando as três notações. Zero ou negativo ⇒ recusa.
  3. Desconto — normaliza a escala (0,35 vira 35); se não veio, calcula pelos preços.
  4. Preço antigo — se for menor ou igual ao atual, recompõe pelo desconto. Evita "de R$ 0,00 por R$ 89,90".
  5. Corte — abaixo do desconto mínimo configurado ⇒ recusa com ldp_desconto_baixo.
  6. Categoria — classifica pelo título com dicionário de palavras-chave; a categoria enviada só entra se o título não resolver e se ela parecer uma categoria (curta, sem trilha >, sem "Ver mais").
  7. Digital sha1(marketplace + externo_id), ou o título normalizado quando não há id. É índice UNIQUE no banco.
  8. Grava ou atualiza — digital já existente ⇒ atualiza preço, imagem e prazo, sem mexer em capturado_em, em cliques nem no status de envio.
Consequência prática

Reenviar a mesma oferta é seguro. Ela atualiza o preço sem duplicar e sem rejuvenescer — por isso dá para rodar de 30 em 30 minutos sem controle de estado do lado do coletor.

Resposta e erros

Um item ruim não derruba o lote: ele volta em detalhes com o motivo, e o resto entra.

200 OKjson
{
  "recebidas": 50,
  "criadas": 37,
  "atualizadas": 11,
  "recusadas": 2,
  "detalhes": [
    { "indice": 12, "codigo": "ldp_desconto_baixo",
      "motivo": "Desconto de 6% abaixo do mínimo de 10%." }
  ]
}

Recusas por item

CódigoSignifica
ldp_sem_titulotítulo vazio
ldp_sem_precopreco_atual ausente, zero ou ilegível
ldp_sem_linknenhum dos dois links veio
ldp_desconto_baixoabaixo do mínimo configurado — comportamento esperado, não é erro
ldp_falha_inserterro do banco (raro; olhar o diário de execuções no painel)

Erros da requisição inteira

HTTPQuandoO que fazer
401token ausente ou erradoparar; não adianta repetir
403ingestão desligada nas configuraçõesparar e avisar
400corpo não é JSON, ou não é uma listacorrigir o payload
413lote acima de 200 ofertasdividir em partes
5xxo servidor tropeçoutentar de novo com espera crescente

Exemplos

teste rápidobash
curl -X POST https://loucuradepromo.com.br/wp-json/ldp/v1/ofertas \
  -H "Content-Type: application/json" \
  -H "X-LDP-Token: SEU_TOKEN" \
  -d '{"ofertas":[{"marketplace":"shopee","externo_id":"123456",
       "titulo":"Teste","preco_atual":49.9,"preco_antigo":99.9,
       "url_afiliado":"https://s.shopee.com.br/abc"}]}'
esqueleto do coletorpython
import os, time, requests

SITE  = "https://loucuradepromo.com.br"
TOKEN = os.environ["LDP_TOKEN"]      # nunca no código, nunca no git
LOTE  = 100                          # o teto do servidor é 200

sessao = requests.Session()
sessao.headers.update({
    "Content-Type": "application/json",
    "X-LDP-Token": TOKEN,
    "User-Agent": "coletor-ldp/1.0",
})

def enviar(ofertas):
    """Manda em lotes. Devolve o total criado e o total atualizado."""
    criadas = atualizadas = 0

    for inicio in range(0, len(ofertas), LOTE):
        fatia = ofertas[inicio:inicio + LOTE]

        for tentativa in range(3):
            try:
                r = sessao.post(f"{SITE}/wp-json/ldp/v1/ofertas",
                                json={"ofertas": fatia}, timeout=60)

                if r.status_code == 200:
                    dados = r.json()
                    criadas += dados["criadas"]
                    atualizadas += dados["atualizadas"]
                    for recusa in dados["detalhes"]:
                        # desconto baixo é rotina, não vale log
                        if recusa["codigo"] != "ldp_desconto_baixo":
                            print("recusada:", recusa)
                    break

                if r.status_code in (401, 403, 413):
                    raise SystemExit(f"parar: {r.status_code} {r.text[:200]}")

                time.sleep(2 ** tentativa)   # 5xx: tenta de novo

            except requests.RequestException:
                if tentativa == 2:
                    raise
                time.sleep(2 ** tentativa)

    return criadas, atualizadas

Ping e status

GET /wp-json/ldp/v1/ping responde {"ok": true} — serve para o serviço testar a credencial antes de coletar qualquer coisa.

GET /wp-json/ldp/v1/status devolve o espelho do estado, útil para decidir se precisa coletar mais e para descobrir de fora que a vitrine esvaziou:

GET /wp-json/ldp/v1/statusjson
{
  "total_ofertas": 613,
  "por_marketplace": [
    { "marketplace": "shopee", "total": 415, "ultima": "2026-08-19 22:14:03" }
  ],
  "fila_whatsapp": 27,
  "execucoes": [
    { "tarefa": "ingestao", "situacao": "ok", "encontradas": 50, "novas": 37 }
  ],
  "horas_de_vida": 48,
  "desconto_minimo": 10
}

Os dois exigem o mesmo token.

Banco de dados

Três tabelas no MySQL do próprio WordPress, prefixo wp_. Não é preciso escrever nelas — a rota faz isso —, mas ajuda a entender o que acontece com o que você manda.

TabelaO que guardaVida
wp_ldp_ofertasa oferta viva; preços em centavos (inteiro), impressao_hash UNIQUE, status de envio48 h
wp_ldp_cliquesum registro por clique no /go/; sem IP — só um hash com sal do site90 dias
wp_ldp_execucoesdiário de bordo de coleta, ingestão, disparo e faxina30 dias
Duas decisões que valem saber

Dinheiro é inteiro em centavos, do banco à tela. 0.1 + 0.2 não dá 0.3 em nenhuma linguagem com IEEE 754, e num somatório de comissão o erro aparece.

A duplicata é barrada pelo banco, não pela aplicação. Regra em PHP falha quando dois conectores gravam o mesmo item ao mesmo tempo; índice único não.

Ambiente

Host
Hostinger · LiteSpeed
WordPress
7.1
PHP
8.2
SSL
válido até 18/11/2026
WP-Cron
desligado, cron real */5
Ingestão
ligada

O agendador do WordPress está desligado no wp-config.php e quem acorda o site é um cron real do painel, a cada 5 minutos: /usr/bin/php /home/u695862969/public_html/wp-cron.php. Três tarefas rodam por ele — coleta (30 min), disparo (5 min) e faxina (1 h).

LiteSpeed

A rota /go/ manda três cabeçalhos de no-cache mais o aviso ao LiteSpeed. Sem isso o servidor guardaria a primeira resposta e serviria a mesma para todo mundo — o clique pararia de contar e a prévia sairia errada. Se mexer nessa rota, mantenha os cabeçalhos.

Onde mexer no código

Plugin ldp-ofertas, em wp-content/plugins/. O código-fonte completo vai no pacote junto com este documento.

ArquivoO quê
inc/ingestao.phpas rotas REST e a autorização
inc/oferta.phpldp_salvar_oferta() — todas as regras da seção anterior
inc/categorias.phpo dicionário de classificação por palavra-chave
inc/banco.phpo schema das três tabelas
inc/conectores.phpo registro de conectores e o contrato de um conector novo
inc/go.phpa rota /go/, prévia e contagem de clique
inc/whatsapp.phpfila e disparo pela Z-API
inc/agenda.phpos três agendamentos

Se preferir rodar dentro do WordPress

Dá para registrar um conector em PHP em vez de empurrar de fora — mesmo destino, sem rede no meio. O contrato é curto: uma função ativo e uma coletar que devolve a lista no mesmo formato desta página, ou um WP_Error. O conector não grava, não decide desconto mínimo, não classifica categoria e não checa duplicata — isso é tudo de ldp_salvar_oferta(), num lugar só.

O que falta

O site está no ar e a porta está aberta. O que ainda não existe é oferta entrando:

  • Mercado Livre — é o único dos três sem API de afiliado aberta. Há um conector interno que lê a lista pública, mas ele quebra quando o ML mexe no HTML, e o link curto oficial (mercadolivre.com/sec/…) não é gerável por HTTP simples. Quando o Python assumir a geração desse link, ele manda a oferta pronta pela rota e o conector interno pode ser desligado sem tocar em mais nada.
  • Shopee — Affiliate Open API (GraphQL). Falta AppId e Secret. Assinatura é sha256(AppId + timestamp + corpo + Secret), timestamp em segundos: "invalid signature" com credencial certa costuma ser relógio do servidor.
  • AliExpress — Open Platform. Falta app_key, app_secret e tracking_id. Sem o tracking_id o promotion_link volta vazio e a oferta vira link sem comissão.
  • Z-API — instância, token, Client-Token e o ID do grupo, para o disparo automático começar.
Recomendações para o coletor

Mande externo_id sempre que houver. Sem ele a duplicata é barrada pelo título normalizado, que erra quando o vendedor muda uma palavra.

Não filtre desconto do seu lado. Mande tudo; o corte é do servidor, num lugar só. Se a régua mudar, muda na tela e o coletor não precisa de deploy.

Trate 401 e 403 como parada, não como retentativa.

O token de ingestão fica no painel do WordPress, em Loucura de Promo → Ingestão externa. Ele não está escrito nesta página de propósito — peça ao responsável pelo site e guarde fora do código.