# VentaKit — Guía para agentes / Agent guide

> **Canonical URL:** https://ventakit.com/agents.md  
> **Human product:** https://ventakit.com · **Editor:** https://ventakit.com/app  
> **OpenAPI (draft):** https://ventakit.com/openapi.json  
> **Skill:** https://ventakit.com/skills/ventakit/SKILL.md  
> **Index:** https://ventakit.com/llms.txt

VentaKit genera carruseles editables para Instagram y LinkedIn (texto + diseño con IDs del sistema de diseño). No publica ni programa en redes. Este documento es para agentes de navegador, asistentes con herramientas HTTP y automatización — **sin sustituir** la UI humana.

---

## Español

### Qué puede hacer un agente hoy

| Capacidad | Cómo |
|-----------|------|
| Descubrir el producto | `llms.txt`, esta guía, `/para-agentes` |
| Crear carrusel (copy + JSON de diseño) | Sesión en navegador + compositor, o `POST /api/generate` con cookie/JWT de sesión |
| Colocar imágenes/video en slides | `window.vkMedia` en `/app` (documentado abajo) |
| Exportar PNG/ZIP/MP4/PDF (píxeles) | Clic en export en el navegador (`data-testid=export-btn`); cuota en `/api/export/claim` |
| Conectar Claude, ChatGPT o Cursor | https://ventakit.com/mcp (sesión de Google) |
| Exportar por agente (HTTP) | `POST /api/agent/v1/export` con la sesión de Google y header `x-vk-export-key`. Cuesta 1 crédito y el cupo de la cuenta. Devuelve un enlace corto a un ZIP con un PNG por slide: sin sesión, un solo uso, el token de la query es la única credencial. El editor y `/api/export/claim` no cobran ese crédito. |
| Consultar plan y créditos | `GET /api/me` con sesión |

### Qué no hace (límites honestos)

- El agente entra con la sesión de Google de la cuenta. No hay claves de agente. Guía: https://ventakit.com/mcp
- **No hay** `list_projects` ni guardado de decks en servidor: el proyecto = JSON en `localStorage`/IndexedDB del navegador.
- `POST /api/agent/v1/export` y `ventakit_export` cuestan 1 crédito y entregan un ZIP con un PNG por slide. Con el Worker de render configurado la captura es el mismo `exportSlides` del editor (modern-screenshot, escala 2). Sin él la respuesta trae `degraded: true` y un aviso: el ZIP es la placa de reserva, no el editor. El enlace caduca a los 15 minutos, es de un solo uso y no pide sesión: basta el token de la query (`curl` directo). Reenviar la misma `x-vk-export-key` emite un enlace nuevo sin segundo cobro. El ticket del navegador es de un solo uso (`GET /api/agent/v1/export/frame`, noindex).
- **No hay** generación de imágenes por IA en servidor (el agente aporta archivos o URLs).
- Editar el deck en el navegador: [`/vkagent.md`](https://ventakit.com/vkagent.md) (`window.vkAgent`). Imágenes: [`/vkmedia.md`](https://ventakit.com/vkmedia.md).
- El servidor aplica los límites de la cuenta en generación, créditos y exportación. El botón del editor sigue en `/api/export/claim` (sin débito de crédito). Un cliente HTTP sin navegador usa `POST /api/agent/v1/export`: 1 crédito, ZIP de PNG. Escribir, editar texto o colocar una imagen no cuesta.

### Autenticación

1. **Browser agent:** el humano inicia sesión (Google SSO en `/login`); el agente reutiliza la misma pestaña.
2. **HTTP:** la misma sesión de Google (cookie o token OAuth de ese inicio de sesión).
3. Sin sesión, las rutas protegidas responden `401`, **salvo** `GET /api/me`, que responde `200` con `authenticated: false` y los límites de cuenta sin sesión.
4. `robots.txt` no permite rastrear `/app/`. Un agente con sesión actúa en nombre de esa cuenta, no como rastreador.

Los agentes son principalmente para cuentas Pro. La sesión de Google identifica la cuenta y usa sus mismos límites. Con plan de pago, el acceso de esa cuenta; con cuenta de prueba, los mismos límites que una persona en esa cuenta. Si la generación no sigue, la API responde `403` con `code` (por ejemplo `free_credits_exhausted` o `free_carousel_limit_reached`), `paywall: true` y `upgrade_url`. Igual para una persona y para un agente.

```bash
Abre https://ventakit.com/mcp e inicia sesión con Google.
```

### Flujo browser-agent recomendado

1. Abrir `https://ventakit.com/app` (sesión activa).
2. `[data-testid=composer-input]` — prompt o instrucciones (máx. 4000 en UI; API admite más).
3. `[data-testid=composer-generate]` — generar; esperar `data-vk-state=done` en `<body>` (o `error`).
4. Opcional: `await window.vkMedia.ready` → `vkMedia.place({ slide, tipo, src })`.
5. `[data-testid=export-btn]` — export; observar overlay `[data-testid=export-overlay]` y `[data-testid=export-saved]`.

Adjuntos PDF/txt/md/docx: `[data-testid=composer-file-input]` (input oculto, `setInputFiles`).

### Mapa `data-testid` (editor `/app`)

| data-testid | Elemento (`id`) |
|-------------|-----------------|
| `composer-input` | `aiComposerInput` |
| `composer-generate` | `aiComposerGenerate` |
| `composer-attach` | `aiComposerAttachBtn` |
| `composer-status` | `aiComposerStatus` |
| `composer-file-input` | `aiComposerFileInput` |
| `new-carousel` | `newCarouselBtn` |
| `export-btn` | `exportBtn` |
| `slides-container` | `slidesContainer` |
| `export-overlay` | `vkExportOverlay` |
| `export-title` | `vkExportTitle` |
| `export-step` | `vkExportStep` |
| `export-progress` | `vkExportSlosh` |
| `export-saved` | `vkExportSavedState` |
| `paywall-overlay` | `vkPaywallOverlay` |
| `paywall-modal` | diálogo dentro del paywall |
| `paywall-later` | `vkPaywallLater` |
| `badge-logo-input` | `badgeLogoInput` |

Estado agregado: `document.body.dataset.vkState` → `idle` \| `generating` \| `done` \| `error` \| `paywall`.

Instalación: `next/public/app/modules/vk-agent-a11y.js` (solo atributos en runtime).

### `window.vkMedia` (resumen)

API estable en el editor. **JSON in, JSON out; no lanza.**

```js
await window.vkMedia.ready;
vkMedia.getDeck(); // slides, safe zone 1080×1350
await vkMedia.place({ slide: 1, tipo: 'completa', src: fileOrUrl });
await vkMedia.place({ slide: 1, tipo: 'recuadro', src: url, aboveText: true, radiusPx: 24 }); // por encima del texto
await vkMedia.describe(1); // text boxes, free rects, recommended placement
```

Tipos: `fondo` \| `recuadro` \| `completa` \| `conector`. Contrato: https://ventakit.com/vkmedia.md

### `window.vkAgent`

Contrato: https://ventakit.com/vkagent.md

```js
await window.vkAgent.getDeck();
window.vkAgent.setText(1, 'body', 'Texto');
await window.vkAgent.setImage({ slide: 1, tipo: 'completa', src: fileOrUrl });
window.vkAgent.setColor('text', '#111111');
window.vkAgent.setTheme('flat-cobalt');
window.vkAgent.setFont('body', 'inter');
window.vkAgent.addSlide();
window.vkAgent.reorder(2, 3);
window.vkAgent.deleteSlide(2);
window.vkAgent.exportDeck();
```

Selectores: `theme-picker`, `body-font`, `text-ink`, `subtitle-ink`, `add-slide`, `undo`, `redo`. Éxito de export: `document.body.dataset.vkExport === 'saved'`.

JSON in, JSON out. No lanza. Una llamada, un paso de deshacer. `exportDeck` es el mismo clic de export (mismo reclamo de cupo).

URL one-shot: `/app?media=<base64url(json)>` (se aplica una vez al cargar).

### HTTP API (sesión requerida)

Ver **openapi.json**. Resumen:

| Método | Ruta | Notas |
|--------|------|--------|
| GET | `/api/me` | Plan, créditos, `canExport`, límites |
| POST | `/api/generate` | Carrusel completo; header **`x-vk-generate-key`** idempotencia |
| GET | `/api/generate/status?jobId=` | Polling tras `202` |
| POST | `/api/generate/slide` | Regenerar una slide (1 crédito) |
| GET/POST | `/api/export/claim` | Elegibilidad / consumo de export del editor |
| POST | `/api/agent/v1/export` | 1 crédito por agente, además del cupo. Header **`x-vk-export-key`**. Alcance `export`. Respuesta: enlace a un ZIP de PNG (15 min, un solo uso, sin sesión). |
| GET | `/api/media-fetch` | Proxy SSRF-safe para imágenes/video |
| POST | `/api/brand` | Brand kit (multipart; nube Pro) |

**Idempotencia:** `x-vk-generate-key` — mismo usuario + misma clave + mismo payload → mismo job, sin doble débito. Payload distinto → `409` `idempotency_payload_mismatch`. Generación en curso → `409` `generate_in_flight` (+ `jobId`).

**Export HTTP:** `x-vk-export-key` en `POST /api/agent/v1/export` — misma cuenta + misma clave + mismo cuerpo → la misma respuesta, sin otro crédito ni otro cupo. Cuerpo distinto → `409` `idempotency_payload_mismatch`. Sin créditos o con el cupo agotado: `403`, `paywall: true` y `upgrade_url`.

**Async:** respuesta `202` + `{ jobId }` → poll hasta `done` \| `failed`. Campo `retryAfterSec` en status/rate limit.

**Rate limit:** por cuenta. Respuesta `429` con `retryAfterSec`. El número no forma parte del contrato.

**Errores (ejemplos `code` y HTTP):**

| `code` | HTTP | Cuándo |
|--------|------|--------|
| `free_credits_exhausted` | 403 | Cuenta de prueba sin créditos de prueba (`error`: `paywall_required`) |
| `free_carousel_limit_reached` | 403 | Límite de carrusel en cuenta de prueba (`paywall_required`) |
| `free_trial_network` | 403 | Red/cuenta de prueba no elegible (`paywall_required`) |
| `export_denied` | 403 | Cupo de export agotado (`/api/export/claim` o `POST /api/agent/v1/export`). En la ruta de agente: `paywall: true` y `upgrade_url` |
| `credits_unavailable` | 403 | Sin créditos de generación disponibles |
| `pro_limit_reached` | 403 | Tope del plan de pago en el período |
| `iterate_digest_required` | 400 | Falta el carrusel actual para iterar |
| `idempotency_payload_mismatch` | 409 | Misma clave idempotencia, distinto payload |
| `generate_in_flight` | 409 | Generación en curso (reusar `jobId`) |
| `invalid_api_key` | 401 | Clave ausente, inválida o revocada |
### Planes y límites (documentación)

- **Plan de pago:** hasta **365** generaciones por período de facturación, exportación HD, uso comercial — https://ventakit.com/#pricing
- **Cuenta de prueba:** **1** crédito de generación y **2** exportaciones. El saldo vivo de esa cuenta está en `GET /api/agent/v1/me` (o `GET /api/me` con sesión).
- Conexión desde Claude, ChatGPT o Cursor: https://ventakit.com/mcp. Servidor: `https://ventakit.com/api/mcp`. Ficha: `https://ventakit.com/.well-known/mcp`. El acceso es el inicio de sesión con Google. Cada generación o exportación por agente cuesta 1 crédito, también en `POST /api/agent/v1/export`. Escribir o editar texto no cuesta. El editor y `/api/export/claim` no cambian.

### JSON del carrusel

La API devuelve `carousel: { global, slides[] }` con IDs del catálogo (`lib/design-system/catalog.ts`). La IA no envía HEX ni px; el layout lo calcula el cliente. Esquema: `CarouselSchema` en `contract.ts`. `ventakit_place_image` añade `carousel.media` (imágenes, posiciones, panorama y fade) sin tocar ese esquema. Hay que reenviar ese objeto en `ventakit_edit_text`, `ventakit_set_look` y `ventakit_export`.

`ventakit_place_image` acepta `aboveText: true` (alias `encimaTexto`; igual por imagen en `ventakit_create_from_json` → `imagenes[{src, tipo, ancla, margen, bleed, aboveText}]`): la imagen pinta por encima de los widgets de texto en el editor, así el glass del texto no la difumina. Con `aboveText` la imagen **nunca tapa la copia**: se ancla en el hueco libre bajo el cuerpo del texto (encoge si el alto pedido no cabe) y, si no hay hueco, falla con `no_gap` en vez de pintar sobre el texto. El hueco se mide como el editor (aire entre oraciones, columna del glass, pack del stack), `ancla` solo se respeta si su caja no toca el texto con margen (si toca, aviso `above_text_relocated` y hueco), y la captura del export re-coloca contra el DOM real con las fuentes cargadas: la geometría guardada de un post viejo nunca tapa glifos. Por defecto va detrás (comportamiento actual). `fondo` ignora `aboveText`. El mismo input acepta `radiusPx` (radio del borde).

`ventakit_remove_image` quita una entrada de `carousel.media.images` sin recrear el carrusel. Selector: `imageId` y/o `slide`+`tipo`. Con `postId` persiste el cambio en el post guardado (`state_json` conserva `vkAgentCarousel`); sin `postId`, actualiza el objeto `carousel` que se le pase y devuelve el JSON. Si la imagen era `fondo`, también retira el fade de esa slide. No cuesta créditos.

### Perfil de marca, layout creativo y énfasis (create_from_json)

**Perfil de marca persistente** (tipografía + paleta de 3 colores, máx. 6 perfiles por cuenta, en `kit.settings.deckProfiles`):

```json
ventakit_set_brand { "perfil": "Hombre Club", "tituloPx": 120, "subtituloPx": 60, "cuerpoPx": 40, "peso": 800, "paleta": ["#c8a24b", "#111111", "#f5f0e6"] }
```

Luego cada carrusel solo pide `marca: { "perfil": "Hombre Club", "variar": true }`: el servidor rellena `tituloPx`, `subtituloPx` (piso 56px / 42% del título), `cuerpoPx`, `peso`, `paleta` y `badgeName`. Lo explícito en `marca` gana. `paleta[0]` es el acento (palabras `{entre llaves}`, barras, CTA) si no hay `acento`. `ventakit_set_look { postId, perfil }` re-aplica el perfil a un post. `ventakit_get_brand` lo lee.

**Por slide:** `layout` (`standard-stack` | `centered-hero` | `split-left` | `card-callout`), `alineacion` (`left` | `center` | `right`), `puntos: [...]` (≤5 → widget bullets), `widgets: [{tipo, ...}]` (≤3; `bullets`/`lista`/`stack` con `items`, `textbox`/`cita`/`globo` con `contenido`, `metrica` con `valor`+`etiqueta`+`contexto`; `y` 8–92, `icono` check|star|arrow, `glass`), `resaltados: [{frase, estilo}]` (`accent-color`, `text-bold`, `text-color-*`, `underline-draw`…). `marca.variar: true` rota layout y alineación en slides de contenido sin `layout` explícito; sin `variar`, el resultado es idéntico al de antes.

**Hombre Club fondos:** solo `abyss-trench`, `onyx-gold`, `flat-crimson`, `ink-black`, `midnight-dark`, `tokyo-night` (Bosque/`flat-forest` ya no está permitido; se descarta al guardar).

**Portada:** el `subtitulo` de la portada es **opcional**. Una portada solo con `titulo` es válida y no genera warning.

**CTA final:** nunca se trunca en silencio. Hasta 28 caracteres cabe a tamaño normal; de 29 a 40 se entrega completo con warning `cta_too_long` y la píldora achica la tipografía; más de 40 se rechaza (`too_long`). `ctaId` / `apply_cta` usan el texto completo del CTA de marca.

**Hombre Club (nov 2026):** objetivo único = valor y gusto del lector. Hook = escena + curiosidad + beneficio + contraste. Estructura: hook → escena → consecuencia → guion textual usable esa noche → escena resuelta → CTA con momento de uso («Repásalo antes de verla», «Elige el sitio antes»; «Guárdalo para el chat» = `cta_generico`). Mostrar > explicar (máx 1 frase de psicología); sin aforismos. La slide de cita sola es **opcional** (`cita_ausente` ya no existe; `cita_unica` si hay más de una). Densidad única: contenido ≥2 frases y ≥20 palabras (`body_too_short`, rechazo; ideal 20–30), >70 palabras `body_too_long` (warning); portada, cierre y cita exentos. Auto-énfasis (negrita/cursiva/color) activo por defecto, también en HC, sin pisar los resaltados explícitos; `autoEnfasis:false` (create_from_json y edit_text) lo apaga, también al rehidratar en /ver y export. Explícitos HC: `text-color-yellow`/`text-color-orange` + `text-bold`/`text-italic`. **v5:** estructura 7 slides + final (S1 creencia rota, S2-3 escena, S4 absolución, S5-6 idea, S7 acción); CTA HC ≤24 (frase de marca o acción, sin `cta_sin_razon` ni momento de uso; sin CTA = tesis válida; puente «El libro está en la bio ↙» aceptado). Gate duro: `deck.preMcp` («PRE-MCP: N/N» con todos los checks pasados (cualquier versión: 25/25 v5, 21/21 v5.1…; o 1✔ … N✔ consecutivos, sin ✘) + línea «ÁNGULO: Cxx-An V-xx-## o V-###» (ej. «ÁNGULO: C03-A2 V-MA-34» o «V-014», tilde opcional) o «PRINCIPIO: P## o P###» (ej. P87). Se acepta dentro de deck.preMcp o en el nivel superior de los args. El theme pedido (marca.fondo/theme, deck.theme o theme top-level) siempre se respeta; si la rotación o la whitelist lo prohíben → rechazo hc_rotation_theme / hc_theme_not_allowed, nunca cambio silencioso. Portada sin números/horas/cifras (hc_cover_numbers); warning hc_body_echoes_title si un cuerpo repite su titular.) o `hc_gate_rejected` / `hc_premcp_missing`. Warnings nuevos: `tono_juez` («fallaste», «tu error»), `recuadro_repite_parrafo` (recuadro ≥60% igual al cuerpo). `claim_sin_fuente` ignora horas (21:30, 9 pm). `ventakit_edit_text`: un edit solo con `resaltados` (sin `text`) REEMPLAZA los del slide (sin duplicados); con `text` se suman. Cada edit acepta `resaltados:[{frase, estilo}]` (frase literal del slide tras el cambio; HC solo `text-color-yellow`/`text-color-orange`/`text-bold`; errores `bad_highlight_phrase`/`bad_highlight_style`); si el texto nuevo deja resaltados sin frase literal se eliminan y vuelve `warnings:[{code:"resaltado_huerfano", slide, frases}]`. «charla» no es modismo.

**Heurísticas de copy (warnings):** `thesis_repeated` (dos slides con la misma tesis), `abstraction_without_scene` (idea abstracta sin escena), `weak_action` («intenta», «considera», «deberías»…), `multiple_ideas` (más de una idea en el título).

**Énfasis inline** en titulo/subtitulo/cuerpo/puntos: `{palabra}` = color de acento, `**negrita**`, `_cursiva_`.

### Roles, sinCierre y gate de calidad (create_from_json)

- **Rol respetado:** `rol` enviado manda. Solo una última slide **sin** `rol` se vuelve `final`. `sinCierre: true` (en el deck, en `marca` o en el perfil de `set_brand`) no inyecta cierre nunca. El ZIP nombra cada PNG por su rol real: `0-portada.png`, `N-contenido.png`, `N-cierre.png` / `N-cta.png` solo si la slide es `final`.
- **`calidad`** (opcional): `{"rubrica":"v1","ejes":{"tono":9,"persuasion":8,"valor":9,"coherencia":9,"simpleza":9,"legibilidad":9,"noRedundancia":9,"hook":9,"escena":8,"factual":10}}` (0–10). Media < 8.5 o un eje < 7 → error `quality_gate_failed` (no crea). Sin `calidad` → warning `quality_missing` (crea igual).
- **warnings[]** (no bloquean): `body_too_long` (cuerpo > 60 palabras; Hombre Club > 70), `cover_too_long` (portada Hombre Club > 8 palabras), `claim_sin_fuente` (cifra o % sin fuente/«ilustrativo»), `forbidden_word` (`marca.prohibidas` o perfil), `owner_name_present` (`marca.dueno`, por defecto «Ignacio», aparece en Hombre Club).

### Copy excelente por defecto (cualquier marca)

Guía completa con ejemplos: [ventakit.com/mcp#calidad](https://ventakit.com/mcp#calidad). Vale para cualquier marca; lo explícito del usuario (prompt o JSON) siempre manda.

**Guía de sistema para el agente:** 1) pide o lee el brief de marca y mándalo en `deck.brief`; 2) redacta con las reglas de voz; 3) puntúa los 10 ejes con evidencia y entrega solo con media ≥ 8,5 y ningún eje < 7 (máx. 2 reescrituras); 4) manda `deck.calidad` y respeta `rol` / `sinCierre`.

**Reglas de voz universales:** idioma neutro (ES, EN u otro) sin modismos regionales, sin voseo y sin jerga; frases cortas (6–14 palabras); una idea por slide; escena antes que concepto; cero cifras, estudios, «la mayoría» o testimonios inventados (si es ilustrativo, dilo); nada sobre lo que piensa o siente un tercero.

**Rúbrica v1 (ejes 1–10):** `tono`, `persuasion`, `valor`, `coherencia`, `simpleza`, `legibilidad`, `noRedundancia`, `hook`, `escena`, `factual`. Umbral: media ≥ 8,5 y ningún eje < 7.

**`brief`** (recomendado): `{"nicho":"…","icp":"…","voz":"…","oferta":"…","claimsProhibidos":["…"],"cierre":"sin|suave|cta","idioma":"es"}`. Warnings (no bloquean): `brief_missing` (sin brief), `brief_incomplete` (falta nicho, icp, voz, oferta o idioma), `forbidden_claim` (un claim prohibido aparece en el texto), `brief_cierre_conflict` (`cierre:"sin"` pero una slide tiene `rol` final; usa `sinCierre:true`). `brief.cierre` no cambia roles: el cierre lo controlan `rol` y `sinCierre`.

**Ejemplos (evita → mejor):**
- Alcance: «5 claves para transformar tu productividad» → «Cierras el portátil a las 18:00 y la lista sigue llena».
- Autoridad: «El 87% de los expertos coincide…» → «Probé tres formas de responder ese correo. Esta fue la única que tuvo respuesta».
- Venta: «¡Compra ya y cambia tu vida!» → «Si hoy armas tus posts a mano, la plantilla te ahorra ese paso. El enlace está en el perfil».

**Hoja de ruta:** hoy `quality_missing` y `brief_missing` son warnings. En Pro, falta de `calidad` o `brief` responderá error (`quality_gate_failed` / `brief_missing`) y no creará. Fase 2 (próximamente): `ventakit_score_copy` (puntuación automática) y generación con rúbrica.

---

## English

### What an agent can do today

Same table as above: discovery via `llms.txt` and this file; generation via browser composer or `POST /api/generate`; media via `window.vkMedia` or `ventakit_place_image` (written into `carousel.media`; optional `aboveText: true` paints an image above the text widgets in the editor without ever covering the copy — it anchors in the free gap below the body text and fails with `no_gap` when nothing fits; `radiusPx` sets its corner radius); removing one image via `ventakit_remove_image` (by `imageId` or `slide`+`tipo`, optionally persisted with `postId`); pixel export in the browser; agent export via `POST /api/agent/v1/export` (1 credit, ZIP of PNGs, 15-minute account link); account via `GET /api/me`. The editor and `/api/export/claim` do not debit that credit. Writing text, placing an image, or removing one does not cost a credit.

### Excellent copy by default (any brand)

Guide + examples: [ventakit.com/mcp#calidad](https://ventakit.com/mcp#calidad). Send `deck.brief` (`nicho`, `icp`, `voz`, `oferta`, `claimsProhibidos`, `cierre` sin|suave|cta, `idioma`) and `deck.calidad` (10 axes, mean ≥ 8.5, no axis < 7). Neutral language (no regional slang), short sentences, one idea per slide, scene before concept, no invented numbers or testimonials; explicit user instructions override. Warnings: `brief_missing`, `brief_incomplete`, `forbidden_claim`, `brief_cierre_conflict`, `quality_missing`. Pro will enforce them; `ventakit_score_copy` is coming (phase 2).

### What it does not do

- The server applies the account’s limits on generation, credits, and export. The editor button still uses `/api/export/claim` (no credit debit). `POST /api/agent/v1/export` costs 1 credit. With the `BROWSER` binding the ZIP is the editor’s own `exportSlides` capture. Without it the body has `degraded: true` and a warning: the ZIP is the fallback plate, not the editor. The download link lasts 15 minutes, works with plain curl (the query token is the only credential) and is single use; replaying the export key reissues a fresh link without a second charge. The headless page uses a one-time token (`GET /api/agent/v1/export/frame`, noindex) and does not spend the editor claim.

### Authentication

Browser agents reuse a signed-in tab. HTTP accepts that same Google session (cookie or OAuth token). Without a session, protected routes return `401`, except `GET /api/me` (`200`, `authenticated: false`). Connect: https://ventakit.com/mcp

`robots.txt` disallows crawling `/app/`. A signed-in agent acts for that user.

Agents are mainly for **Pro** accounts. The Google session identifies the account. Paid accounts and trial accounts keep the same limits as a person on that account. When generation is blocked the API returns `403` and a stable `code` (for example `free_credits_exhausted`, `free_carousel_limit_reached`) plus `paywall: true` and `upgrade_url`, for a person and for an agent.

### `data-testid` map

Same selectors as the Spanish table. State: `document.body.dataset.vkState`.

### HTTP API

See **openapi.json**. Generate idempotency header: **`x-vk-generate-key`**. Export idempotency header: **`x-vk-export-key`** on `POST /api/agent/v1/export`. Poll `GET /api/generate/status` after `202`.

Paywall-related codes (`free_credits_exhausted`, `free_carousel_limit_reached`, `export_denied`, …) return **HTTP 403** with JSON `code` (not 402). See the Spanish error table above.

### Plans

Paid: up to **365** generations per billing period — https://ventakit.com/en#pricing. Trial: **1** generation credit and **2** exports. Live balance: `GET /api/agent/v1/me`.

---

## Changelog

- 2026-10-29: `ventakit_remove_image` quita una imagen de `carousel.media` (por `imageId` o `slide`+`tipo`; con `postId` persiste el post). `ventakit_place_image` y `ventakit_create_from_json` aceptan `aboveText`/`encimaTexto` (z-order sobre el texto en el editor) y `radiusPx` documentado en place. Ninguno debita créditos.
- 2026-10-29: `aboveText` ya no tapa la copia: se ancla en el hueco libre bajo el cuerpo (encoge si hace falta) o falla con `no_gap`; `ancla` se respeta solo si no toca el texto (`above_text_relocated` si reubicada). Misma regla en editor, captura del export y JSON.
- 2026-10-29 (v2): el hueco se mide como el editor renderiza (aire entre oraciones, columna del glass-card, pack top/center), la captura del export espera las fuentes antes de re-colocar y rehúsa geometría guardada que taparía glifos; `no_gap` en slides densos es un resultado válido (baja `size`, cambia a `fondo` o acorta la copia).
- 2026-10-28: `/mcp` connects Claude, ChatGPT, or Cursor with the account Google session. Agent export (`ventakit_export` and `POST /api/agent/v1/export`) costs 1 credit and returns a ZIP of PNGs. Placing an image writes `carousel.media`. The editor and `/api/export/claim` do not debit that credit. No agent keys.
- 2026-10-27: Guía de agentes (agents.md, openapi, llms). La sesión es la de Google.
