AI Agents Marketing

La Skill di Claude Che Scrive i Miei Annunci Google Ads

Alejandro Rioja
Alejandro Rioja
8 min di lettura
TL;DR

Ho costruito una skill Claude che interroga direttamente l'API di Google Ads con GAQL, individua gli annunci di ricerca responsivi con CTR debole o CPA alto, e riscrive titoli e descrizioni entro i limiti esatti di caratteri di Google (30 e 90). Crea il nuovo annuncio in stato IN PAUSA — io controllo e attivo di persona. La parte che conta davvero: i limiti di caratteri di Google sono il vincolo reale, non il copy. Un modello che non conta correttamente i caratteri produce annunci che Google tronca o rifiuta in silenzio.

Newsletter gratuita

Ogni mercoledì. 28.400+ operatori. Zero riempitivo.

[Lettura da operatore] Ho già una skill Claude che gestisce i miei annunci Facebook — quella build è qui. Google Ads è stato il seguito più difficile, non perché l’API sia peggiore, ma perché gli annunci di ricerca responsivi hanno limiti di caratteri rigidi che Facebook non ha: 30 caratteri per titolo, 90 per descrizione. Una riscrittura che si legge bene ma viene rifiutata o troncata alla pubblicazione non è una riscrittura, è un bug. Gran parte del lavoro su questa skill è stato forzare il modello a rispettare quel vincolo invece di fidarsi che lo facesse.

Table of contents

Open Table of contents

Perché Google Ads richiedeva una build diversa da Facebook

La skill Facebook legge i dati di performance, riscrive il copy e pubblica un nuovo gruppo di annunci. Google Ads fa gli stessi tre compiti, ma due cose cambiano:

  1. Il linguaggio di query è GAQL, non filtri REST. L’API di Google Ads v18 funziona con Google Ads Query Language — a forma di SQL, in sola lettura, costruito esattamente per questo tipo di estrazione di report.
  2. Gli annunci di ricerca responsivi hanno limiti rigidi. Fino a 15 titoli (30 caratteri ciascuno) e 4 descrizioni (90 caratteri ciascuna) per annuncio. Google non avvisa con delicatezza — o rifiuta l’elemento all’invio o, peggio, serve una versione troncata che si legge male a un cliente reale.

Il punto 2 è quello che rompe le implementazioni ingenue. Chiedi a un modello di “scrivere un titolo più incisivo” e ti restituirà volentieri 34 caratteri. Google Ads rifiuta quell’elemento o lo rende tagliato a metà parola. Io me ne accorgo solo quando vedo un annuncio live che finisce con “Prenota il tuo appuntame—”.

Ottenere l’accesso all’API (la parte che tutti sottovalutano)

Tre cose, e la terza è quella che ti costa una settimana se non la pianifichi:

  1. Un progetto Google Cloud con credenziali OAuth2 — client ID, client secret e un refresh token con lo scope https://www.googleapis.com/auth/adwords. Genera il refresh token una volta, via l’OAuth playground o un breve script locale; dopo di che è una credenziale salvata, non un flusso di login.
  2. Un developer token, richiesto dal tuo account manager (MCC) Google Ads, sotto Amministrazione → Centro API.
  3. Il livello di accesso. Ogni nuovo developer token parte con accesso Account di test — funziona solo contro account di test Google Ads, non contro il tuo account reale. Richiedi il Basic Access (fino a 15.000 operazioni/giorno, sufficiente per questo) tramite un modulo che Google esamina manualmente. Prevedi alcuni giorni lavorativi per l’approvazione prima di voler passare a un account reale.

Salva GOOGLE_ADS_CLIENT_ID, GOOGLE_ADS_CLIENT_SECRET, GOOGLE_ADS_REFRESH_TOKEN, GOOGLE_ADS_DEVELOPER_TOKEN e GOOGLE_ADS_CUSTOMER_ID (l’ID account a 10 cifre, senza trattini) nel tuo .env. Se gestisci l’account tramite un MCC, ti serve anche GOOGLE_ADS_LOGIN_CUSTOMER_ID impostato sull’ID dell’account manager — senza questo le query vengono rifiutate con un errore di permessi, e il messaggio di errore non dice perché.

La struttura dei file della skill

code
.claude/skills/google-ads/
  SKILL.md          ← istruzioni che Claude legge
  index.ts          ← l'implementazione effettiva dello strumento
  limits.ts         ← validazione dei limiti di caratteri

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

L’ultima riga di “What it will NOT do” è quella che ho aggiunto dopo che la skill ha pubblicato un titolo di 32 caratteri nella prima settimana. Il conteggio del modello stesso era sbagliato di due caratteri — abbastanza vicino da passare un’occhiata veloce, abbastanza lontano da far rifiutare l’elemento. Da allora tutto passa attraverso un validatore rigido, non l’autodichiarazione del modello.

Il codice TypeScript centrale

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 };
}

Ogni titolo e descrizione viene verificato rispetto al limite nel codice, dopo la generazione, prima che qualcosa tocchi l’API. La riscrittura del modello è una bozza. Il validatore è il filtro.

Come la uso ogni giorno

code
> controlla il mio Google Ads degli ultimi 7 giorni

Claude esegue la query GAQL, segnala tutto ciò che è sotto il 3% di CTR o sopra il mio CPA target salvato, e chiede conferma prima di riscrivere. Confermo. Genera nuovi titoli e descrizioni, li passa nel validatore di caratteri, scarta ciò che non passa, e crea un nuovo annuncio in stato PAUSED tramite l’operazione mutate dell’API Google Ads. Confronto il nuovo copy con quello vecchio direttamente in Google Ads, attivo quelli che userei davvero, e lascio il resto in pausa.

Stesso tetto della versione Facebook: circa 20 minuti invece di un intero pomeriggio nell’interfaccia, perché la parte meccanica — estrarre i dati, scrivere dentro un vincolo rigido, preparare la modifica — è esattamente ciò che un modello dovrebbe fare al posto mio.

Cosa non rileva il validatore

Il conteggio dei caratteri è meccanico. La pertinenza no. Un titolo può avere 28 caratteri, superare ogni controllo, ed essere comunque il messaggio sbagliato per il termine di ricerca a cui è associato. La skill non può sapere se un gruppo di annunci poco performante ha un’offerta scadente, un disallineamento con la landing page, o un copy genuinamente debole — può solo dirmi che il copy è debole rispetto agli annunci che lo circondano. Se tutti gli annunci di una campagna hanno un CTR basso, è un problema di targeting o di landing page, e dare alla skill più cicli di riscrittura su un’intenzione di ricerca mal associata produce solo copy più conforme e ugualmente ignorato.

Leggo io stesso l’output GAQL prima di approvare un lotto di riscritture, come faccio dal lato Facebook. La skill elimina la digitazione e il conteggio dei caratteri. Non elimina il giudizio su cosa sia realmente rotto.

Gli strumenti che uso per questo

Claude è ciò su cui girano entrambe le skill pubblicitarie — quella Facebook e questa — condividono lo stesso schema riscrivi-e-valida, solo contro un’API diversa e un budget di caratteri più stretto.

Google Ads API (pacchetto npm google-ads-api) — il wrapper TypeScript attorno all’API grezza gRPC/REST. Gestisce il refresh OAuth e la costruzione delle query GAQL così non devi armeggiare con protobuf a mano.

FAQ

Mi serve il Basic Access, o basta l’accesso Account di test?

L’accesso Account di test funziona solo contro account di test Google Ads — utile per lo sviluppo, inutile per un account reale con spesa reale. Serve il Basic Access (da richiedere nel Centro API del tuo account manager) prima che questa skill possa toccare una campagna live. L’approvazione è una revisione manuale, non istantanea, quindi richiedila prima di averne bisogno, non la settimana in cui vuoi lanciare.

Perché 5 titoli e 4 descrizioni invece dei massimi di 15 e 4?

Google permette fino a 15 titoli, ma sia l’Ad Strength che i test reali suggeriscono rendimenti decrescenti oltre 8-10 angoli davvero distinti — la maggior parte degli account finisce con diversi titoli quasi duplicati che gonfiano il conteggio. Limito l’output del modello a 5 nuovi titoli per lotto di riscrittura così esamino opzioni distinte, non filtro nove variazioni della stessa frase.

Cosa succede se la riscrittura del modello fallisce il controllo dei caratteri a ogni tentativo?

Il validatore lancia un errore invece di troncare o inviare in silenzio un elemento fuori limite. In pratica significa che rilancio il prompt una volta, e se fallisce due volte scrivo quel titolo specifico a mano. Succede più spesso con le descrizioni — 90 caratteri sono uno spazio più stretto per una frase completa di quanto sembri.

Da leggere anche: per report in sola lettura su GA4, Search Console, Bing, Ahrefs e Core Web Vitals in un unico connettore Claude invece di costruire una skill su misura per ogni piattaforma, uso SiteWide MCP — la stessa idea di collegare Claude a dati di marketing reali, ma con una superficie più ampia di una singola piattaforma pubblicitaria.

Il verdetto dell’operatore

Se stai scrivendo a mano le varianti di annunci di ricerca responsivi e contando tu stesso i caratteri, è esattamente il tipo di compito meccanico e basato su regole che un modello dovrebbe gestire. La parte che richiede vero giudizio — se è un problema di copy o di targeting — resta tua. Costruisci la skill, lasciala scrivere e validare contro i limiti di Google, e tieni la diagnosi per te.

Pubblicità
Continua a leggere

Articoli correlati

Continua a leggere

Ricevi il manuale dell'IA nella tua casella di posta

Ogni mercoledì. 28.400+ operatori. Zero riempitivo.

↵ ↵ per tutti i risultati esc esc per chiudere