AI Agents Marketing

La Skill Claude Qui Écrit Mes Annonces Google Ads

Alejandro Rioja
Alejandro Rioja
9 min de lecture
TL;DR

J'ai construit une skill Claude qui interroge directement l'API Google Ads avec GAQL, repère les annonces responsives sur le Réseau de Recherche avec un CTR faible ou un CPA élevé, et réécrit les titres et descriptions dans les limites exactes de caractères de Google (30 et 90). Elle crée la nouvelle annonce en statut PAUSE — je relis et j'active moi-même. La partie qui compte vraiment : les limites de caractères de Google sont la vraie contrainte, pas le texte. Un modèle qui ne compte pas correctement les caractères produit des annonces que Google tronque ou rejette silencieusement.

Newsletter gratuite

Chaque mercredi. 28 400+ opérateurs. Zéro superflu.

[Lecture d’opérateur] J’ai déjà une skill Claude qui gère mes annonces Facebook — cette construction est ici. Google Ads a été la suite la plus difficile, pas parce que l’API est pire, mais parce que les annonces responsives sur le Réseau de Recherche ont des limites de caractères strictes que Facebook n’a pas : 30 caractères par titre, 90 par description. Une réécriture qui se lit bien mais qui est rejetée ou tronquée à la diffusion n’est pas une réécriture, c’est un bug. La majeure partie du travail sur cette skill a consisté à forcer le modèle à respecter cette contrainte plutôt que de lui faire confiance.

Table of contents

Open Table of contents

Pourquoi Google Ads demandait une construction différente de Facebook

La skill Facebook lit les données de performance, réécrit le texte et publie un nouvel ensemble de publicités. Google Ads fait les trois mêmes tâches, mais deux choses changent :

  1. Le langage de requête est GAQL, pas des filtres REST. L’API Google Ads v18 fonctionne avec Google Ads Query Language — en forme de SQL, en lecture seule, construit exactement pour ce type d’extraction de rapports.
  2. Les annonces responsives ont des limites strictes. Jusqu’à 15 titres (30 caractères chacun) et 4 descriptions (90 caractères chacune) par annonce. Google ne prévient pas en douceur — il rejette l’élément à la soumission ou, pire, diffuse une version tronquée qui se lit mal pour un vrai client.

Le point 2 est celui qui casse les implémentations naïves. Demandez à un modèle d’« écrire un titre plus percutant » et il vous rendra gentiment 34 caractères. Google Ads rejette cet élément ou l’affiche coupé en plein mot. Je ne le découvre qu’en regardant une annonce en direct qui se termine par « Réservez votre rende— ».

Obtenir l’accès à l’API (la partie que tout le monde sous-estime)

Trois choses, et la troisième est celle qui vous coûte une semaine si vous ne la planifiez pas :

  1. Un projet Google Cloud avec des identifiants OAuth2 — client ID, client secret et un refresh token avec le scope https://www.googleapis.com/auth/adwords. Générez le refresh token une fois, via l’OAuth playground ou un court script local ; après ça c’est une identifiant stocké, pas un flux de connexion.
  2. Un developer token, demandé depuis votre compte gestionnaire (MCC) Google Ads, sous Administration → Centre d’API.
  3. Le niveau d’accès. Chaque nouveau developer token démarre avec un accès Compte de test — il ne fonctionne que contre des comptes de test Google Ads, pas contre votre vrai compte. Vous demandez le Basic Access (jusqu’à 15 000 opérations/jour, suffisant pour ça) via un formulaire que Google examine manuellement. Prévoyez quelques jours ouvrés pour l’approbation avant de vouloir passer sur un compte réel.

Stockez GOOGLE_ADS_CLIENT_ID, GOOGLE_ADS_CLIENT_SECRET, GOOGLE_ADS_REFRESH_TOKEN, GOOGLE_ADS_DEVELOPER_TOKEN et GOOGLE_ADS_CUSTOMER_ID (l’ID de compte à 10 chiffres, sans tirets) dans votre .env. Si vous gérez le compte via un MCC, vous avez aussi besoin de GOOGLE_ADS_LOGIN_CUSTOMER_ID réglé sur l’ID du compte gestionnaire — les requêtes sont rejetées avec une erreur de permission sans ça, et le message d’erreur ne dit pas pourquoi.

La structure de fichiers de la skill

code
.claude/skills/google-ads/
  SKILL.md          ← instructions que Claude lit
  index.ts          ← l'implémentation réelle de l'outil
  limits.ts         ← validation des limites de caractères

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

La dernière ligne de « What it will NOT do » est celle que j’ai ajoutée après que la skill a publié un titre de 32 caractères dès la première semaine. Le comptage du modèle lui-même était faux de deux caractères — assez proche pour passer un coup d’œil, assez loin pour que l’élément soit rejeté. Depuis, tout passe par un validateur strict, pas par l’auto-déclaration du modèle.

Le code TypeScript central

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

Chaque titre et description est vérifié par rapport à la limite dans le code, après génération, avant de toucher l’API. La réécriture du modèle est un brouillon. Le validateur est le filtre.

Comment je l’utilise au quotidien

code
> vérifie mon Google Ads des 7 derniers jours

Claude lance la requête GAQL, signale tout ce qui est sous 3 % de CTR ou au-dessus de mon objectif de CPA enregistré, et demande avant de réécrire. Je confirme. Il génère de nouveaux titres et descriptions, les passe par le validateur de caractères, écarte ce qui échoue, et crée une nouvelle annonce en statut PAUSED via l’opération mutate de l’API Google Ads. Je compare le nouveau texte à l’ancien dans Google Ads, j’active ceux que j’utiliserais vraiment, et je laisse le reste en pause.

Même plafond que la version Facebook : environ 20 minutes au lieu d’un après-midi entier dans l’interface, parce que la partie mécanique — extraire les données, rédiger dans une contrainte stricte, préparer le changement — est exactement ce qu’un modèle devrait faire à ma place.

Ce que le validateur ne détecte pas

Le comptage de caractères est mécanique. La pertinence ne l’est pas. Un titre peut faire 28 caractères, passer toutes les vérifications, et rester quand même le mauvais message pour le terme de recherche auquel il est associé. La skill ne peut pas savoir si un groupe d’annonces peu performant a une mauvaise offre, un décalage avec la landing page, ou un texte réellement faible — elle peut seulement me dire que le texte est faible par rapport aux annonces qui l’entourent. Si toutes les annonces d’une campagne ont un CTR faible, c’est un problème de ciblage ou de landing page, et donner à la skill plus de cycles de réécriture sur une intention de recherche mal associée ne produit qu’un texte plus conforme et toujours aussi ignoré.

Je relis moi-même la sortie GAQL avant d’approuver un lot de réécritures, comme je le fais côté Facebook. La skill supprime la frappe et le comptage de caractères. Elle ne supprime pas le jugement sur ce qui est réellement cassé.

Les outils que j’utilise pour ça

Claude est ce sur quoi tournent les deux skills publicitaires — celle de Facebook et celle-ci — partagent le même schéma réécrire-puis-valider, juste contre une API différente et un budget de caractères plus serré.

Google Ads API (paquet npm google-ads-api) — le wrapper TypeScript autour de l’API brute gRPC/REST. Il gère le rafraîchissement OAuth et la construction des requêtes GAQL pour éviter de bricoler du protobuf à la main.

FAQ

Ai-je besoin du Basic Access, ou l’accès Compte de test suffit-il ?

L’accès Compte de test ne fonctionne que contre des comptes de test Google Ads — utile pour le développement, inutile pour un vrai compte avec de vraies dépenses. Il faut le Basic Access (à demander dans le Centre d’API de votre compte gestionnaire) avant que cette skill puisse toucher une campagne en direct. L’approbation est un examen manuel, pas instantané, donc demandez-la avant d’en avoir besoin, pas la semaine où vous voulez lancer.

Pourquoi 5 titres et 4 descriptions au lieu des maximums de 15 et 4 ?

Google autorise jusqu’à 15 titres, mais l’Ad Strength comme les tests réels suggèrent des rendements décroissants au-delà de 8 à 10 angles vraiment distincts — la plupart des comptes finissent avec plusieurs titres quasi-identiques qui gonflent le compte. Je limite la sortie du modèle à 5 nouveaux titres par lot de réécriture pour examiner des options distinctes, pas filtrer neuf variations de la même phrase.

Que se passe-t-il si la réécriture du modèle échoue à la vérification de caractères à chaque tentative ?

Le validateur lève une erreur plutôt que de tronquer ou soumettre silencieusement un élément hors limite. En pratique, cela veut dire que je relance le prompt une fois, et si ça échoue deux fois j’écris ce titre précis à la main. Ça arrive plus souvent sur les descriptions — 90 caractères, c’est un espace plus serré pour une phrase complète qu’il n’y paraît.

À lire aussi : pour des rapports en lecture seule sur GA4, Search Console, Bing, Ahrefs et Core Web Vitals dans un seul connecteur Claude plutôt que de construire une skill sur mesure par plateforme, j’utilise SiteWide MCP — la même idée de connecter Claude à de vraies données marketing, avec une surface plus large qu’une seule plateforme publicitaire.

Le verdict de l’opérateur

Si vous écrivez à la main des variations d’annonces responsives et comptez vous-même les caractères, c’est exactement le genre de tâche mécanique et basée sur des règles qu’un modèle devrait prendre en charge. La partie qui demande du vrai jugement — un problème de texte ou de ciblage — reste la vôtre. Construisez la skill, laissez-la rédiger et valider par rapport aux limites de Google, et gardez le diagnostic pour vous.

Publicité
Continuer à lire

Articles liés

Continuer à lire

Recevez le guide IA dans votre boîte mail

Chaque mercredi. 28 400+ opérateurs. Zéro superflu.

↵ ↵ pour voir tous les résultats esc esc pour fermer