AI Agents Marketing

A Skill do Claude Que Escreve Meus Anúncios do Google Ads

Alejandro Rioja
Alejandro Rioja
9 min de leitura
TL;DR

Construí uma skill do Claude que consulta a API do Google Ads diretamente com GAQL, identifica anúncios de pesquisa responsivos com CTR fraco ou CPA alto, e reescreve títulos e descrições dentro dos limites exatos de caracteres do Google (30 e 90). Ela cria o novo anúncio em status PAUSADO — eu revisor e ativo. A parte que realmente importa: os limites de caracteres do Google são a restrição real, não o texto. Um modelo que não conta caracteres corretamente produz anúncios que o Google corta ou rejeita silenciosamente.

Newsletter gratuita

Toda quarta-feira. 28.400+ operadores. Zero enrolação.

[Leitura de operador] Já tenho uma skill do Claude que gerencia meus anúncios do Facebook — essa construção está aqui. O Google Ads foi a continuação mais difícil, não porque a API seja pior, mas porque anúncios de pesquisa responsivos têm limites de caracteres rígidos que o Facebook não tem: 30 caracteres por título, 90 por descrição. Uma reescrita que lê bem mas é rejeitada ou cortada na hora de servir não é uma reescrita, é um bug. A maior parte do trabalho nessa skill foi forçar o modelo a respeitar essa restrição em vez de confiar que ele faria isso.

Table of contents

Open Table of contents

Por que o Google Ads precisou de uma construção diferente da do Facebook

A skill do Facebook lê dados de desempenho, reescreve o texto e publica um novo conjunto de anúncios. O Google Ads faz os mesmos três trabalhos, mas duas coisas mudam:

  1. A linguagem de consulta é GAQL, não filtros REST. A API do Google Ads v18 roda em Google Ads Query Language — no formato SQL, somente leitura, construída exatamente para esse tipo de extração de relatórios.
  2. Anúncios de pesquisa responsivos têm limites rígidos. Até 15 títulos (30 caracteres cada) e 4 descrições (90 caracteres cada) por anúncio. O Google não avisa com delicadeza — ou rejeita o ativo no envio ou, pior, exibe uma versão cortada que fica errada para um cliente real.

O ponto 2 é o que quebra implementações ingênuas. Peça a um modelo para “escrever um título mais impactante” e ele devolverá alegremente 34 caracteres. O Google Ads rejeita esse ativo ou renderiza cortado no meio da palavra. Eu só descubro quando estou vendo um anúncio ao vivo que termina em “Agende seu horári—”.

Conseguindo acesso à API (a parte que todo mundo subestima)

Três coisas, e a terceira é a que custa uma semana se você não planejar:

  1. Um projeto no Google Cloud com credenciais OAuth2 — client ID, client secret e um refresh token com o escopo https://www.googleapis.com/auth/adwords. Gere o refresh token uma vez, via o OAuth playground ou um script local curto; depois disso é uma credencial armazenada, não um fluxo de login.
  2. Um developer token, solicitado pela sua conta gerenciadora (MCC) do Google Ads, em Administração → Centro de API.
  3. O nível de acesso. Todo developer token novo começa com acesso de Conta de teste — só funciona contra contas de teste do Google Ads, não contra sua conta real. Você solicita o Basic Access (até 15.000 operações/dia, suficiente para isso) por um formulário que o Google revisa manualmente. Reserve alguns dias úteis para a aprovação antes de planejar ir ao vivo numa conta real.

Guarde GOOGLE_ADS_CLIENT_ID, GOOGLE_ADS_CLIENT_SECRET, GOOGLE_ADS_REFRESH_TOKEN, GOOGLE_ADS_DEVELOPER_TOKEN e GOOGLE_ADS_CUSTOMER_ID (o ID da conta de 10 dígitos, sem hifens) no seu .env. Se você gerencia a conta por um MCC, também precisa de GOOGLE_ADS_LOGIN_CUSTOMER_ID com o ID da conta gerenciadora — sem isso, as consultas são rejeitadas com erro de permissão, e a mensagem de erro não diz o motivo.

A estrutura de arquivos da skill

code
.claude/skills/google-ads/
  SKILL.md          ← instruções que o Claude lê
  index.ts          ← a implementação real da ferramenta
  limits.ts         ← validação dos limites de caracteres

SKILL.md:

markdown
# Google Ads Manager Skill

Use this skill when the user says "check my Google Ads", "run ads
report", or "rewrite underperforming RSAs". Never run this without
explicit user instruction — it touches live ad spend.

## What it can do
- Pull campaign and ad group performance via GAQL (last 7 or 30 days)
- Flag responsive search ads with CTR < 3% or CPA above the stored
  target as underperformers
- Rewrite headlines (≤30 chars) and descriptions (≤90 chars) for
  flagged ads in Ale's voice
- Create new ad variations as PAUSED — never enabled automatically

## What it will NOT do
- Change budgets or bids without explicit confirmation
- Enable a paused ad
- Submit any headline or description that fails the character check

A última linha de “What it will NOT do” é a que adicionei depois que a skill publicou um título de 32 caracteres na primeira semana. A própria contagem do modelo estava errada por dois caracteres — perto o suficiente para passar num olhar rápido, longe o suficiente para o ativo ser rejeitado. Desde então, tudo passa por um validador rígido, não pelo autorrelato do modelo.

O código TypeScript principal

typescript
// .claude/skills/google-ads/index.ts
import { GoogleAdsApi } from "google-ads-api";
import Anthropic from "@anthropic-ai/sdk";

const client = new GoogleAdsApi({
  client_id: process.env.GOOGLE_ADS_CLIENT_ID!,
  client_secret: process.env.GOOGLE_ADS_CLIENT_SECRET!,
  developer_token: process.env.GOOGLE_ADS_DEVELOPER_TOKEN!,
});

const customer = client.Customer({
  customer_id: process.env.GOOGLE_ADS_CUSTOMER_ID!,
  login_customer_id: process.env.GOOGLE_ADS_LOGIN_CUSTOMER_ID,
  refresh_token: process.env.GOOGLE_ADS_REFRESH_TOKEN!,
});

interface AdPerformance {
  adGroupAdId: string;
  campaignName: string;
  headlines: string[];
  descriptions: string[];
  impressions: number;
  clicks: number;
  costMicros: number;
  conversions: number;
  ctr: number;
  cpa: number;
}

async function getAdPerformance(days = 7): Promise<AdPerformance[]> {
  const rows = await customer.query(`
    SELECT
      ad_group_ad.ad.id,
      campaign.name,
      ad_group_ad.ad.responsive_search_ad.headlines,
      ad_group_ad.ad.responsive_search_ad.descriptions,
      metrics.impressions,
      metrics.clicks,
      metrics.cost_micros,
      metrics.conversions
    FROM ad_group_ad
    WHERE segments.date DURING LAST_${days === 30 ? "30" : "7"}_DAYS
      AND ad_group_ad.status = 'ENABLED'
  `);

  return rows.map((r: any) => {
    const impressions = Number(r.metrics.impressions ?? 0);
    const clicks = Number(r.metrics.clicks ?? 0);
    const cost = Number(r.metrics.cost_micros ?? 0) / 1_000_000;
    const conversions = Number(r.metrics.conversions ?? 0);
    return {
      adGroupAdId: r.ad_group_ad.ad.id,
      campaignName: r.campaign.name,
      headlines: (r.ad_group_ad.ad.responsive_search_ad?.headlines ?? []).map((h: any) => h.text),
      descriptions: (r.ad_group_ad.ad.responsive_search_ad?.descriptions ?? []).map((d: any) => d.text),
      impressions,
      clicks,
      costMicros: Number(r.metrics.cost_micros ?? 0),
      conversions,
      ctr: impressions > 0 ? (clicks / impressions) * 100 : 0,
      cpa: conversions > 0 ? cost / conversions : Infinity,
    };
  });
}

const HEADLINE_MAX = 30;
const DESCRIPTION_MAX = 90;

function validateAsset(text: string, max: number): boolean {
  return text.length <= max;
}

async function rewriteRsa(
  ad: AdPerformance,
  targetCpa: number,
): Promise<{ headlines: string[]; descriptions: string[] }> {
  const anthropic = new Anthropic();
  const msg = await anthropic.messages.create({
    model: "claude-sonnet-4-6",
    max_tokens: 1024,
    messages: [{
      role: "user",
      content: `Rewrite this Google Ads responsive search ad in Alejandro Rioja's voice: direct, operator-focused, specific. It's underperforming — CTR ${ad.ctr.toFixed(2)}%, CPA $${ad.cpa.toFixed(2)} vs a $${targetCpa} target.

Current headlines: ${JSON.stringify(ad.headlines)}
Current descriptions: ${JSON.stringify(ad.descriptions)}

Rules, non-negotiable:
- Each headline: 30 characters MAXIMUM, including spaces and punctuation
- Each description: 90 characters MAXIMUM, including spaces and punctuation
- Return 5 headlines and 4 descriptions
- Return JSON only: {"headlines": ["...", ...], "descriptions": ["...", ...]}`,
    }],
  });
  const text = (msg.content[0] as any).text.replace(/```json\n?/, "").replace(/```/, "").trim();
  const parsed = JSON.parse(text);

  const headlines = parsed.headlines.filter((h: string) => validateAsset(h, HEADLINE_MAX));
  const descriptions = parsed.descriptions.filter((d: string) => validateAsset(d, DESCRIPTION_MAX));

  if (headlines.length < 3 || descriptions.length < 2) {
    throw new Error("Model returned too many over-limit assets — rerun or revise manually.");
  }

  return { headlines, descriptions };
}

Cada título e descrição é verificado contra o limite no código, depois da geração, antes de qualquer coisa tocar a API. A reescrita do modelo é um rascunho. O validador é o filtro.

Como eu uso no dia a dia

code
> verifique meu Google Ads dos últimos 7 dias

O Claude executa a consulta GAQL, marca qualquer coisa abaixo de 3% de CTR ou acima do meu CPA alvo salvo, e pergunta antes de reescrever. Eu confirmo. Ele gera novos títulos e descrições, passa pelo validador de caracteres, descarta o que falha, e cria um novo anúncio em status PAUSED pela operação mutate da API do Google Ads. Comparo o texto novo com o antigo dentro do Google Ads, ativo os que eu realmente usaria, e deixo o resto pausado.

Mesmo teto da versão do Facebook: cerca de 20 minutos em vez de uma tarde inteira na interface, porque a parte mecânica — extrair os dados, redigir dentro de uma restrição rígida, preparar a mudança — é exatamente o que um modelo deveria estar fazendo no meu lugar.

O que o validador não detecta

Contagem de caracteres é mecânica. Relevância não é. Um título pode ter 28 caracteres, passar todas as verificações, e ainda ser a mensagem errada para o termo de busca ao qual está vinculado. A skill não pode saber se um grupo de anúncios com baixo desempenho tem uma oferta ruim, um desalinhamento com a landing page, ou um texto genuinamente fraco — ela só pode me dizer que o texto é fraco em relação aos anúncios à sua volta. Se todos os anúncios de uma campanha têm CTR baixo, isso é um problema de segmentação ou de landing page, e dar à skill mais ciclos de reescrita sobre uma intenção de busca mal combinada só produz textos mais obedientes e igualmente ignorados.

Eu mesmo leio a saída do GAQL antes de aprovar um lote de reescritas, assim como faço do lado do Facebook. A skill elimina a digitação e a contagem de caracteres. Ela não elimina o julgamento sobre o que está realmente quebrado.

As ferramentas que uso para isso

Claude é a base de ambas as skills de anúncios — a do Facebook e esta — compartilham o mesmo padrão de reescrever-e-validar, só contra uma API diferente e um orçamento de caracteres mais apertado.

Google Ads API (pacote npm google-ads-api) — o wrapper em TypeScript sobre a API bruta gRPC/REST. Ele cuida do refresh do OAuth e da construção das consultas GAQL para você não precisar montar protobuf na mão.

Perguntas frequentes

Preciso de Basic Access, ou o acesso de Conta de teste funciona?

O acesso de Conta de teste só funciona contra contas de teste do Google Ads — útil para desenvolvimento, inútil para uma conta real com gasto real. Você precisa do Basic Access (solicitado no Centro de API da sua conta gerenciadora) antes que essa skill possa tocar uma campanha ao vivo. A aprovação é revisão manual, não instantânea, então solicite antes de precisar, não na semana em que quer lançar.

Por que 5 títulos e 4 descrições em vez dos máximos de 15 e 4?

O Google permite até 15 títulos, mas tanto o Ad Strength quanto testes reais sugerem retornos decrescentes depois de 8–10 ângulos genuinamente distintos — a maioria das contas termina com vários títulos quase duplicados só inflando a contagem. Eu limito a saída do modelo a 5 novos títulos por lote de reescrita para revisar opções distintas, não filtrar nove variações da mesma frase.

O que acontece se a reescrita do modelo falhar na verificação de caracteres em toda tentativa?

O validador lança um erro em vez de truncar ou enviar silenciosamente um ativo fora do limite. Na prática isso significa que eu rodo o prompt de novo uma vez, e se falhar duas vezes eu escrevo aquele título específico à mão. Acontece mais com descrições — 90 caracteres é um espaço mais apertado para uma frase completa do que parece.

Relacionado: para relatórios somente leitura em GA4, Search Console, Bing, Ahrefs e Core Web Vitals num único conector do Claude em vez de construir uma skill específica por plataforma, eu uso o SiteWide MCP — a mesma ideia de conectar o Claude a dados reais de marketing, só com uma superfície mais ampla que uma única plataforma de anúncios.

O veredito do operador

Se você está escrevendo à mão variações de anúncios de pesquisa responsivos e contando caracteres sozinho, isso é exatamente o tipo de tarefa mecânica e baseada em regras que um modelo deveria assumir. A parte que exige julgamento real — se é um problema de texto ou de segmentação — continua sendo sua. Construa a skill, deixe-a redigir e validar contra os limites do Google, e guarde o diagnóstico para você.

Publicidade
Continue lendo

Posts relacionados

Continue lendo

Receba o manual de IA na sua caixa de entrada

Toda quarta-feira. 28.400+ operadores. Zero enrolação.

↵ ↵ ver todos os resultados esc esc para fechar