IA & Backend · MVP funcional
AdPilot
Foto + lenguaje natural a una campaña de Meta Ads que se publica sola, con dos cerebros LLM.
- Cloudflare Workers
- Meta Marketing API v23
- Claude/Gemini (vision + tool-use)
- D1
- Durable Objects
El dueño de una tienda DTC sube una foto de su producto y escribe, en español llano, qué quiere anunciar. Un LLM lee la imagen, diseña la campaña, y la deja creada en Meta — sin que esa persona toque Ads Manager nunca.
Stack: Cloudflare (Workers · Workflows · D1 · R2 · Durable Objects) · Meta Marketing API v23 · Claude/Gemini (tool-use + vision) · <parameter name="...">
Estado: MVP funcional (corre local, crea campañas reales en Meta) + spec de la arquitectura de producción
Rol: diseño y desarrollo end-to-end, solo
El problema
Lanzar una campaña de Meta decente exige decisiones que un no-marketer no sabe tomar: ¿objetivo de ventas o de tráfico? ¿el presupuesto va en la campaña o en el conjunto? ¿qué optimization_goal casa con ese objetivo? ¿qué promoted_object necesita el píxel? Equivócate en una y la campaña o no entrega o entrega basura. Ads Manager expone toda esa complejidad de golpe.
El cliente DTC que quiero atender no quiere aprender eso — quiere vender. La apuesta de AdPilot: que toda esa decisión la tome un modelo a partir de una foto y una frase, y que el dueño solo apruebe un preview. Es un producto pensado para revenderse como suscripción a varias tiendas, no una herramienta interna.
Qué construí
Un MVP que cierra el lazo completo, validado end-to-end contra Meta v23 sobre una cuenta de anuncios real:
- Subir foto + texto → campaña real. El modelo ve la imagen (vision), interpreta la intención, elige una de ~10 plantillas de estrategia y devuelve la campaña estructurada + el copy en español (primary text, headline, description, CTA). El backend la materializa en Meta:
adimages→ campaign → adset → ad → creative. - Catálogo de estrategias como plantillas curadas (ventas ABO/CBO, tráfico, mensajes a WhatsApp, interacción, reconocimiento, e impulsar una publicación existente). Cada plantilla codifica las combinaciones de parámetros de Meta que sí funcionan — eso es el activo, no el LLM.
- N conjuntos con audiencia distinta cada uno. El modelo devuelve un array
segments[]; cada entrada es un adset con su targeting propio (abierto/Advantage+ o segmentado por edad, género e intereses). - Resolución de intereses real: los intereses se pasan a Meta por ID, no por nombre, así que cada keyword se resuelve vía
GET /search?type=adinterestantes de armar el targeting. - Pautar una publicación existente por
object_story_id, listando los posts reales de la página (no se puede con el token equivocado — ver más abajo). - Editar una campaña por IA: instrucción en lenguaje natural (“súbele el presupuesto a 20”, “pausa la de verano”) → el modelo emite el diff → se aplica a Meta.
- Dashboard de métricas vía Insights API (gasto, CTR, CPC, CPM, compras, ROAS) a nivel cuenta y campaña.
El MVP es Node http plano, sin dependencias, a propósito: sirve para ver el producto y validar el flujo de Meta antes de invertir en la plataforma. No es la arquitectura final.
Decisiones técnicas interesantes
Dos cerebros, no uno. Separo el LLM en dos roles con responsabilidades distintas: un estratega (vision + tool-use) que traduce foto+intención a una estructura de campaña, y un creativo (en la spec) que reescribe el lenguaje natural a un prompt de generación de imagen y la produce. Separarlos importa porque tienen prompts, modelos y presupuestos de coste distintos — y porque el estratega es determinista-adyacente (su salida valida contra un esquema y alimenta llamadas a una API), mientras el creativo es abierto. Mezclarlos da un prompt frágil que hace ambas cosas mal.
El LLM propone; las plantillas mandan. El modelo elige y rellena, pero los parámetros que rompen la campaña si están mal (el optimization_goal, dónde va el presupuesto, el promoted_object, el CTA) los fija la plantilla, no el texto libre del modelo. Esto es lo que hace el sistema confiable en vez de una demo bonita: la creatividad va al copy y la audiencia; la mecánica de Meta está clavada.
Los gotchas de Meta API — esto solo se aprende lanzando. Lista parcial de lo que descubrí construyendo, no leyendo el doc:
- ABO vs CBO no es una etiqueta, es dónde vive el presupuesto. CBO:
daily_budget+bid_strategyvan en la campaña, el adset sin budget. ABO: van en el adset. Pones el budget en el nivel equivocado y Meta rechaza la jerarquía entera. - ABO además exige
is_adset_budget_sharing_enabledexplícito en la campaña (error 4834011 si falta), y el adset exige unbid_strategyexplícito o un bid (error 2490487). Meta no asume defaults razonables aquí. - Ventas requiere
promoted_object={pixel_id, custom_event_type: PURCHASE}+destination_type=WEBSITE. Elpromoted_objectes el contrato entre tu campaña y tu píxel; sin él la optimización a conversiones no existe. - Mensajes a WhatsApp:
destination_type=WHATSAPP+promoted_object={page_id}, y la página debe tener un número de WhatsApp conectado o Meta responde “objetivo de rendimiento no disponible” sin decirte por qué. - Impulsar un post de interacción necesita
destination_type=ON_POST. Sin ese parámetro, Meta pide un CTA/URL en bucle; conpromoted_objectrompe el goal;ON_AD/ON_PAGEno son compatibles.ON_POSTes la única llave que abre esa puerta, y no es obvia. - Targeting manual exige
targeting_automation.advantage_audience:0respeta tus intereses,1deja a Meta expandir. Sin el flag, error pidiendo “la marca de público Advantage”. - Para leer los posts de una página no sirve el system user token — hay que derivar el page access token (
GET /{page_id}?fields=access_token). Y el viejo truco de sacar el ID del post desde la URL ya no funciona: Facebook usa tokenspfbidque no son el ID numérico, así que elobject_story_id(pageid_postid) hay que sacarlo de/{page}/published_posts.
Crear-en-vivo vs crear-en-pausa como decisión de producto. El competidor crea las campañas en PAUSED y obliga al usuario a volver a Ads Manager a activarlas. Con un preview bueno, el preview es el approval gate — aprobar publica directo (status=ACTIVE). Nada gasta hasta el clic, pero el clic no te manda de vuelta a la herramienta que estás intentando evitar.
Full Cloudflare en la arquitectura de producción, por razones, no por moda. El cómputo y el loop de tools viven en un Worker (los fetch a Claude/Meta no queman CPU mientras esperan); la generación de imagen — que son jobs de ~2 min, durables y con reintentos — migra a Cloudflare Workflows en vez de meter un orquestador externo. R2 guarda las imágenes con egress gratis, que es justo lo que importa cuando se las sirves a Meta. Los tokens de Meta van cifrados en D1 y un Worker los resuelve; el aislamiento multi-tenant es en código (D1 es SQLite, no hay RLS). Un Durable Object hace de gate atómico para los topes de campañas y de créditos antes de gastar.
Arquitectura
Qué demuestra
- Integración con Meta Ads a nivel de quien la ha lanzado, no leído. La diferencia entre conocer la jerarquía campaña→adset→ad→creative y saber que ABO/CBO cambia el nivel del presupuesto, que
ON_POSTes el parámetro que destraba la interacción, o que el system user token no lee posts — eso no está en un tutorial, sale de iterar contra los códigos de error de Meta. - Criterio de dónde poner el límite del LLM. Dejar que el modelo decida copy y audiencia, pero clavar la mecánica que rompe la campaña, es la decisión que separa un producto confiable de una demo. El patrón de dos cerebros es la misma idea a otra escala.
- Pensamiento de producto, no solo de feature. El proyecto arranca de un benchmark concreto contra un competidor real (mismo idioma, mismo ICP), de ahí salen un modelo de negocio multi-tenant por suscripción y un metering por imagen generada con gate atómico server-side. La pregunta no fue “¿se puede construir?” sino “¿por qué me pagarían recurrente por esto?”.
- Elección de infraestructura defendible. Workers para lo síncrono, Workflows para los jobs largos, R2 por el egress gratis al servir a Meta, Durable Objects para el conteo atómico de créditos. Cada pieza está donde está por una restricción concreta, y hay un camino de migración limpio (D1→Postgres vía Hyperdrive) si la escala lo pide.
Estado y siguiente paso
Esto es un MVP funcional + una spec de producción, no un producto en el mercado. Lo que ya funciona end-to-end contra Meta real: las ~10 estrategias, los N conjuntos con audiencia propia, la resolución de intereses por ID, pautar posts existentes, editar por IA, y el dashboard de Insights — todo desde un servidor local sin dependencias.
Lo que falta para que sea producto: portar el backend a la arquitectura Cloudflare de la spec, cambiar el cerebro a Claude API, montar el flujo OAuth de Meta (Facebook Login for Business + App Review) para clientes ajenos en vez del system user token, y construir la capa de generación de imagen con su metering. La capa de ROAS real — cruzar el gasto de Meta con las órdenes reales de Shopify por UTM, sin CAPI, solo leyendo — está diseñada y diferida a una versión avanzada: es el diferencial que el benchmark no tiene.