Abres la pestaña Network de DevTools mientras termina de cargar una página basada en GraphQL. Revisas los cuerpos de todas las peticiones buscando el query { ... } que te interesa, pero no aparece ni una línea. Solo ves un operationName, un hash de sesenta y cuatro caracteres y un conjunto de variables.
No se te ha escapado nada. Estás ante una operación persistida: el cliente ya no envía la consulta en texto plano, sino un hash registrado previamente. El servidor busca ese hash en su registro, recupera la consulta real y la ejecuta. La captura de paquetes deja de servir justo ahí. Puedes ver qué operación se llamó y con qué variables, pero no los campos que selecciona ni la estructura de la respuesta.
La reacción habitual, además, suele ser equivocada: intentar «romper» el hash. Un hash es unidireccional, no se puede descifrar y tampoco hace falta. El problema real es clasificar el caso. Primero determina en qué situación estás y, después, decide cómo obtener lo que necesitas. Entre ambas vías hay un orden de magnitud de diferencia en coste; elegir mal implica tirar el esfuerzo.
Por qué una operación persistida no muestra la consulta en la captura
Antes de decidir cómo abordarlo, conviene entender por qué existe este mecanismo.
Con GraphQL en texto plano el problema es directo: las consultas pueden ser largas, reenviar todo el árbol de campos en cada petición es ineficiente y el servidor debe aceptar consultas arbitrarias, lo que expone toda la superficie del esquema. Las operaciones persistidas resuelven ambos frentes. Durante el build se extraen, hashean y registran en el servidor todas las consultas que utilizará el cliente, a modo de lista blanca. En ejecución, el cliente envía únicamente el hash y las variables; el servidor solo acepta hashes registrados y rechaza cualquier consulta fuera de esa lista. Es una decisión de rendimiento real, no una medida anti-scraping deliberada. La documentación de Apollo sobre Automatic Persisted Queries lo plantea como práctica recomendada: se usa el SHA-256 de la consulta en lugar del texto plano, y que la captura no muestre la consulta es solo una consecuencia.
En ingeniería inversa, esa consecuencia es muy concreta: el «qué pedir» desaparece de la petición. Te quedas con tres piezas: un identificador de operación —un hash o un operationName legible—, un conjunto de variables y una respuesta. La capa intermedia, es decir, «qué campos seleccionó esta operación», no viaja por la red.
En proyectos reales encontrarás ambos extremos. En uno, la plataforma no ha adoptado nada persistido y la consulta en texto plano sigue apareciendo sin disimulo en el cuerpo de la petición. En el otro, la petición se reduce a un sobre opaco del que ni siquiera puedes leer un nombre de campo. Cada extremo exige una estrategia distinta, y las veremos por separado.
Dos caminos, con un orden de magnitud de diferencia en coste
La primera vía consiste en localizar el texto plano o una tabla de mapeo dentro del build del cliente. La segunda renuncia a perseguir el texto plano: trata la operación completa como una caja negra y la reproduce tal cual.
La primera parece más exhaustiva, así que mucha gente se lanza por defecto a ella. Ahí empieza buena parte del trabajo perdido. Solo es barata si el texto plano realmente se distribuyó al cliente, y esa premisa a menudo no se cumple.
Vía uno: encontrar el texto plano o el mapeo en el build del cliente
El caso más sencillo es aquel en el que el texto plano nunca se ocultó.
El ranking de tendencias de una plataforma china de vídeos cortos funciona así: un único endpoint /graphql, un cuerpo de petición con la terna estándar {operationName, variables, query}, el campo query contiene la consulta GraphQL completa en texto plano y operationName es un nombre legible como hotRankQuery. Aquí no hay nada que «obtener»: una sola captura lo deja todo a la vista. Ni siquiera ha adoptado operaciones persistidas, y representa el extremo más fácil del espectro.
Requiere algo más de trabajo el caso en que sí se usan operaciones persistidas, pero el cliente conserva el mapeo. Para enviar un hash, el cliente necesita saber qué operación corresponde a cada hash, y esa tabla operationName-hash —a veces acompañada de la consulta en texto plano— suele estar integrada en el bundle del frontend. Las herramientas de build suelen generarla como archivo de manifiesto o insertarla en algún módulo. Si la encuentras, obtienes a la vez el texto plano y el hash, con libertad para añadir campos o modificar después el conjunto de selección.
La dificultad no está en buscar como tal, sino en que el build tiene decenas de miles de líneas, está minificado y ofuscado, y el mapeo puede estar fragmentado e insertado en línea. Ahí es donde el modelo aporta valor en este artículo, como veremos más adelante. Es recuperación por fragmentos, no razonamiento.
La prueba para la vía uno es simple: si el texto plano o el mapeo pueden estar en cualquier parte del cliente, dedica primero diez minutos a buscarlos. Si aparecen, es la forma más potente de obtenerlo y tendrás control total sobre el endpoint.
Vía dos: no persigas el texto plano, reproduce la operación como caja negra
El problema es que, muchas veces, el texto plano no está en el cliente.
Una operación persistida bien implementada deja al cliente únicamente el hash; la consulta en texto plano vive solo en el registro del servidor. Puedes destrozar el bundle a búsquedas y no encontrarla, porque nunca se distribuyó. Insistir con la vía uno en este escenario es perseguir algo que no existe.
La vía dos es la solución más infravalorada: no necesitas el texto plano. Lo que buscas es la respuesta, no el árbol de campos de la consulta. Registra el identificador de la operación —hash u operationName— y su sobre de variables, envíalos sin alterarlos y cambia solo los parámetros de entrada que te interesen. Nunca sabrás qué campos selecciona, pero el servidor te los seguirá devolviendo. Para la inmensa mayoría de tareas de extracción de datos y monitorización, basta con eso.
YouTube innertube es la forma canónica de esta estrategia. Ni siquiera es GraphQL: utiliza un conjunto fijo de endpoints autoexplicativos, youtubei/v1/{player,search,next}, y un cuerpo de petición formado por un sobre context —tipo y versión del cliente— junto con un conjunto de parámetros. Nadie intenta «reconstruir» el grafo interno de consultas de YouTube: ni es posible ni merece la pena. Lo útil es leer una vez la versión y el contexto del cliente desde los recursos actuales de la página, conservar ese sobre intacto en cada petición y cambiar solo entradas como videoId o un término de búsqueda, enviándolas a ese endpoint fijo. La semántica de la operación permanece como una caja negra de principio a fin. Este caso, en el que un valor clave no está en el código estático y hay que extraerlo de recursos en tiempo de ejecución, pertenece a otra clase de problema de ingeniería inversa, tratada por separado.
La ventaja de la vía dos es que no depende de que el texto plano esté en el cliente. Da igual que tengas un hash o un sobre opaco: no intentas entenderlo, solo lo reproduces fielmente. A cambio, quedas limitado a las peticiones que el cliente ya realiza. Si necesitas un campo que el cliente nunca solicita, la reproducción de caja negra no puede dártelo.
Hay un caso más que rompe la reproducción: cuando el sobre incluye un campo de firma calculado de nuevo para cada petición y que caduca. Ahí termina la caja negra: tendrás que resolver ese campo por separado. Reconocer a qué familia de algoritmos pertenece la firma es el trabajo de otro artículo.
Dónde encaja cada plataforma y por qué
Al poner ambos casos reales en una misma tabla, se ve con claridad dónde cae cada uno y por qué:
Caso de plataforma | Forma de la petición | ¿Texto plano en el cliente? | Vía natural | Motivo |
|---|---|---|---|---|
GraphQL de ranking de tendencias de vídeos cortos |
| Sí, texto plano directamente en el cuerpo de la petición | Vía uno (coste casi nulo) | No hay operaciones persistidas, el operationName es legible y la consulta está en texto plano; una captura lo contiene todo |
YouTube innertube | Endpoints fijos + un sobre | No hay una consulta en texto plano propiamente dicha | Vía dos (reproducción de caja negra) | No es GraphQL, no existe una consulta que reconstruir; basta con leer una vez el sobre de contexto y conservarlo sin cambios |
El contraste entre ambos extremos deja una idea clara: la vía no la eliges tú, la determina el diseño de la API de la plataforma. La primera concentra su protección en otros mecanismos —dejar el texto plano visible no es un problema—, así que lo recoges casi de pasada. La segunda convierte el «qué pedir» en un sobre opaco; no hay texto plano que perseguir y solo queda reproducirlo.
La amplia zona intermedia, GraphQL persistido de verdad, es donde se pone a prueba el criterio. El texto plano puede estar en el cliente —el mapeo integrado en el bundle, vía uno— o solo en el servidor —el cliente conserva únicamente el hash, vía dos—. Antes de empezar, debes clasificarlo en uno de los dos extremos.
Equivocarte cuesta tiempo: primero decide si necesitas el texto plano
Toda la diferencia de coste de un orden de magnitud depende de una única decisión.
Si una plataforma solo entrega el hash y guarda el texto plano en el servidor, pero insistes en recuperar la consulta mediante la vía uno, puedes pasar días excavando en el bundle para descubrir al final que aquello que buscabas jamás se distribuyó. No es un problema de dificultad, sino de dirección: ningún esfuerzo te dará un resultado.
A la inversa, si necesitas modificar la consulta —por ejemplo, pedir un campo que el cliente nunca solicita—, la reproducción de caja negra de la vía dos no sirve. Tendrás que volver a la vía uno para obtener el texto plano, y quedarás bloqueado si no puedes conseguirlo.
Por tanto, el orden correcto no es «¿cómo obtengo la consulta?», sino empezar con una pregunta: ¿de verdad necesito el texto plano?
Solo quieres reproducir una petición que el cliente ya realiza y leer su respuesta: vía dos, reproducción de caja negra. Es la más barata, la más ignorada y funciona tanto si el texto plano existe como si no. Debería ser tu opción por defecto.
Necesitas cambiar el conjunto de selección o construir una consulta que el cliente nunca envía: necesitas la vía uno, recuperar el texto plano. Que sea barata o no depende de si el cliente distribuyó el mapeo. Si no lo hizo, aparece el salto de coste de un orden de magnitud: tendrás que asumirlo o replantearte si realmente necesitas modificar la consulta.
Plantear esta decisión al principio evita la mayor parte del desperdicio de «lanzarse por la vía uno y, tras tres días atascado, descubrir que debía haber elegido la vía dos». Este compromiso entre «reescribir o tolerar la caja negra» se aborda en el artículo sobre la escalera de purificación; aquí solo sirve para decidir qué vía usar.
Localizar el mapeo es recuperación por fragmentos, no razonamiento
El paso técnicamente exigente de la vía uno —encontrar la declaración de la operación y su mapeo entre decenas de miles de líneas del build frontend— es justo donde el modelo puede ahorrarte tiempo real. Pero primero hay que entender qué tipo de tarea es.
No es una tarea de razonamiento. No necesitas que el modelo comprenda qué calcula el código; necesitas que localice, dentro de un volumen enorme de texto, qué bloque declara el mapeo operationName-hash, qué módulo inserta la consulta en texto plano o dónde se construye el sobre de contexto. Eso es recuperación por fragmentos. Lo que importa es poder introducir suficiente contexto de una vez y señalar con precisión dentro de él, no debatir sobre el código.
Haz antes el paso mecánico de división: utiliza un script para separar el build en módulos, indexarlo y filtrar polyfills y módulos de negocio no relacionados. Entrega ese resultado al modelo y la tarea pasa a ser «encuentra la declaración en estos bloques», de forma limpia y directa.
En este flujo, las cuatro capas se separan con claridad por capacidad:
Paso | Capacidad necesaria | Elección | model id |
|---|---|---|---|
Localizar el mapeo o la declaración de la operación entre decenas de miles de líneas | Contexto largo, capaz de absorber una gran porción del build y señalar con precisión | Kimi K3 |
|
Decidir entre vía uno y vía dos, e inferir de unas pocas muestras qué campos del sobre varían | Razonamiento sólido, lectura de la estructura y evaluación del compromiso | Claude Opus 5 |
|
Etiquetar cientos de operaciones en lote, generar stubs de reproducción y completar tipos de variables | Barato y con alta concurrencia | Claude Sonnet 5 |
|
Cuando la reproducción no conecta, leer el diff para atribuir la causa (¿falta un campo de contexto? ¿cambió la versión del hash?) | Razonamiento intermedio, capaz de explicar a partir de la diferencia en la respuesta | GPT-5.6 Sol |
|
La primera capa es el terreno de este artículo. Cambiar de modelo durante el paso de localización modifica visiblemente el resultado porque el límite lo marca la ventana de contexto. El build tiene decenas de miles de líneas; un modelo de contexto corto no puede albergarlo y debe truncarlo. Un solo truncamiento puede dejar fuera el mapeo, y entonces su «no lo encuentro» no significa que no sepa buscar, sino que nunca lo vio.
No aceptes la diferencia sin comprobarla:
Captura una petición real y guarda su identificador de operación —operationName o hash— y sus variables.
Fragmenta el build frontend con un script, entrégaselo junto con ese identificador a
kimi-k3y pídele que localice «dónde se declara esta operación y qué bloque contiene la consulta en texto plano o el mapeo de hash correspondiente».Fíjate en una sola cosa: si va directamente a la línea correcta. Acierto, fallo o una ubicación cercana pero equivocada.
Como control, proporciona la misma entrada a un modelo de contexto corto y comprueba si falla porque no puede alojarla. La tasa de acierto es tu criterio de selección.
Una sola ronda basta para comprobar que, en este tipo de tarea, el contexto largo no es «un poco mejor»: marca la diferencia entre poder hacerlo y no poder hacerlo.
El verdadero freno es el coste de cambiar de modelo
Cuatro modelos de tres proveedores, tres SDK y tres sistemas de autenticación, además de tres formatos de error. Para usar un modelo distinto en recuperación, criterio, procesamiento masivo y atribución, el enfoque ingenuo es integrar los tres clientes. La mayoría hace cuentas, concluye que no compensa y termina usando un único modelo para todo el flujo: usa una capa que no puede contener el build en la recuperación de contexto largo y luego culpa al modelo porque «no lo encuentra».
AIReiter elimina esa capa de complejidad: una clave, una interfaz compatible con OpenAI, las cuatro capas detrás y el cambio de modelo reducido a modificar el campo model del cuerpo de la petición.
# Locate the mapping: the long-context tier, swallows a big chunked build at once
curl https://aireiter.com/api/v1/chat/completions \
-H "Authorization: Bearer $AIREITER_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "kimi-k3",
"messages": [{"role": "user", "content": "<chunked frontend build + the operation identifier to locate>"}]
}'
# Label operations / generate replay stubs in bulk: change the model field, leave the rest
# "model": "claude-sonnet-5"
# Replay attribution:
# "model": "gpt-5.6-sol"
Si ya utilizas el SDK de OpenAI, apunta base_url a https://aireiter.com/api/v1 y no cambies nada más. Con el SDK de Anthropic, usa POST /api/v1/messages con la misma clave.
En precio, los modelos Claude tienen un 30% de descuento sobre tarifa, los GPT cuestan la mitad y Kimi K3 se puede invocar con la misma clave. El coste de este flujo se concentra en dos puntos: la localización de la vía uno, que introduce todo el build fragmentado a unos cientos de miles de tokens por entrada mediante kimi-k3; y el etiquetado de cientos de operaciones junto con la generación masiva de stubs de reproducción, el paso con más llamadas, mediante claude-sonnet-5 con un 30% de descuento. El descuento se aplica precisamente al lote más intensivo en llamadas.
Pruébalo sin registrarte: primero introduce a mano un fragmento del build y comprueba si la capa de contexto largo localiza el mapeo en una pasada; después decide si quieres integrarla.
En resumen
Que una API GraphQL no tenga documentación no significa que no puedas integrarla. Una operación persistida solo ha sacado de la petición el «qué pedir» y lo ha llevado a uno de dos lugares: el build del cliente —encuéntralo, vía uno— o únicamente el servidor —no persigas el texto plano; reproduce la operación como caja negra, vía dos—.
El coste de ambas vías difiere en un orden de magnitud. Lo que decide cuál corresponde nunca es «cuál es más exhaustiva», sino dos preguntas previas: si el texto plano está en el cliente y si necesitas modificar la consulta. Respóndelas antes de empezar y evitarás la mayor parte del trabajo desperdiciado.
El papel del modelo aquí es específico: en la vía uno, el paso de «localizar la declaración entre decenas de miles de líneas» es recuperación pura. Una capa de contexto largo puede absorberlo en una pasada y señalar el lugar preciso, convirtiendo días de búsqueda en minutos. No decidirá por ti qué vía elegir; ese criterio deberías tenerlo tras leer este artículo. Solo se encarga del trabajo mecánico de encontrar el mapeo.