Skip to content

Servidor MCP ​

Futuros incluye un servidor Model Context Protocol (MCP) para que cualquier agente externo: Claude Code, Claude Desktop, Cursor, o su propio cliente, pueda consultar el mismo corpus citado de desarrollo de LATAM que usa el chat de Futuros. Cada cifra que devuelve una herramienta lleva un citation_id; las herramientas solo leen datos precalculados y computan de forma determinista, sin escrituras, sin llamadas ocultas a un LLM.

  • Endpoint: POST https://futuros.xyz/api/mcp
  • Transporte: Streamable HTTP (JSON-RPC 2.0)
  • Versión de protocolo: 2025-06-18
  • Identidad del servidor: futuros-corpus v1.0.0: "Futuros — LATAM governed-data corpus"
  • Auth: cada POST exige una clave de API de cuenta (Authorization: Bearer ft_live_… o X-Api-Key), que se crea en /cuenta/claves. Sin clave → 401 {"error":"api_key_required"}; clave inválida, vencida o revocada → 401 {"error":"invalid_api_key"}; más de 600 solicitudes por minuto con la misma clave → 429. El modo prepago por créditos (ACCESS_ENABLED, /acceso) sigue en el código pero apagado; volver a usarlo como capa de medición sobre las claves de cuenta es trabajo futuro. Ver Claves de API. Un despliegue sin pila de cuentas (desarrollo local, autoalojado sin DATABASE_URL) sigue anónimo. Los datos abiertos estáticos (/api/v1) también exigen clave de cuenta, ver Autenticación.
  • Compuerta de despliegue: MCP_ENABLED debe valer "true". Falla cerrado: sin la variable, cada POST devuelve 403 {"error":"mcp_disabled"}: un despliegue nuevo o un .env vacío no expone el endpoint por accidente. GET ?health=1 sigue respondiendo y reporta el estado en su campo enabled (y el bloque access).
  • Estado: sin estado: cada petición es autocontenida
  • Métodos: initialize, ping, tools/list, tools/call
  • Tope de cuerpo: 256 KB, aplicado antes de parsear y antes de cobrar el límite de tasa; por encima, 413 con -32700. Un lote legítimo de MAX_BATCH mensajes son unos pocos KB.
  • Límite de tasa: 60 mensajes/minuto por IP de cliente (o por clave de cuenta si hay autenticación), cobrado una vez por mensaje JSON-RPC contenido, un lote de N cuesta N unidades, así que agrupar no multiplica el presupuesto. Entre instancias (respaldado por Upstash) cuando está configurado; si no, en memoria por instancia. Este endpoint cobra fail-closed: si el store compartido está configurado y no se puede consultar, la petición se rechaza con 503 en vez de degradar a un tope por instancia. Tanto el 429 (sobre cuota) como el 503 (limitador no disponible) traen Retry-After.

Semántica de transporte ​

El servidor implementa el mínimo de Streamable HTTP requerido para un servidor de herramientas sin estado:

Método en /api/mcpComportamiento
POSTJSON-RPC 2.0 — un solo mensaje o un lote (arreglo JSON). Devuelve el objeto de respuesta, o un arreglo para un lote.
POST (solo notificaciones)HTTP 202 Accepted, cuerpo vacío (sin id → sin respuesta).
OPTIONSPreflight CORS → 204.
GET ?health=1200 — { ok, service: "mcp", enabled, transport: "streamable-http", max_batch: 20 }, una sonda barata para monitores de uptime. ok/enabled reflejan MCP_ENABLED, así que la sonda distingue "caído" de "deshabilitado a propósito".
GET (a secas)405 — servidor sin estado, sin stream SSE iniciado por el servidor.

CORS está totalmente abierto (Access-Control-Allow-Origin: *, métodos POST, OPTIONS, cabeceras permitidas Content-Type, Mcp-Session-Id, Mcp-Protocol-Version), así que los agentes basados en navegador pueden llamarlo entre orígenes.

Códigos de error JSON-RPC usados: -32700 error de parseo, -32600 petición vacía/inválida (incluido un lote sobre el tope de 20 mensajes), -32601 método no encontrado / GET, -32602 herramienta desconocida, -32000 límite de tasa (HTTP 429) o limitador no disponible (HTTP 503), -32603 error interno , dentro de un lote, una excepción lanzada por la herramienta de un mensaje se vuelve un -32603 ligado al id de ese mensaje, con aislamiento por mensaje: los mensajes hermanos igual se responden, y el endpoint nunca devuelve un 500 que no sea JSON-RPC.

Ciclo de vida JSON-RPC ​

Lo que atraviesa un POST, en orden (api/mcp.ts → server/mcp/handler.ts → el ejecutor compartido de herramientas del corpus):

Clasificación de mensajes (handleMcpMessage):

Mensaje entranteTratamiento
No es un objeto JSON (cadena, número, null, arreglo anidado)-32600 con id: null
Tiene id pero jsonrpc !== "2.0" o method no es una cadena-32600 ligado a ese id
Sin id (una notificación) — cualquier método, incluso uno que lanza excepciónprocesado, nunca respondido
method que empieza con notifications/ (incluso con id)sin respuesta
Método desconocido con id-32601
tools/call que nombra una herramienta fuera de la lista expuesta de 15-32602
La herramienta lanza excepción a mitad de llamada-32603 ligado al id de ese mensaje; los hermanos no se afectan; si no hay id recuperable la respuesta se descarta (regla de notificación)

Los mensajes de un lote se ejecutan secuencialmente, en orden del arreglo, y las respuestas vuelven en el mismo orden (solo las peticiones, las notificaciones no aportan nada). Un cuerpo de solo notificaciones produce por tanto un conjunto de respuestas vacío → HTTP 202 sin cuerpo. Cada invocación corre bajo un maxDuration de función de 30 s.

Mecánica del límite de tasa: el limitador corre antes del handler:

  • Clave: mcp:<client-ip>, donde la IP es x-real-ip / la entrada más a la derecha de x-forwarded-for: nunca la más a la izquierda, que es falsificable.
  • El tope de tamaño de lote se verifica antes de cobrar, así que un arreglo sobredimensionado es un único -32600 barato que consume cero presupuesto. Cada mensaje contenido cobra entonces una unidad; en cuanto un cobro dispara el límite se deja de cobrar (los viajes restantes al store solo empujarían el contador) y toda la petición se rechaza con el Retry-After de ese cobro; las unidades ya cobradas quedan consumidas.
  • Backend: un viaje de ida y vuelta con pipeline INCR + EXPIRE … NX + PTTL a Upstash/Vercel-KV cuando UPSTASH_REDIS_REST_* / KV_REST_API_* está configurado (techo entre instancias); si no, un limitador en memoria acotado (tope duro de 50.000 entradas, barridos cada 30 s, expulsión del más antiguo primero). El limitador nunca lanza excepción; lo que cambia es qué veredicto devuelve cuando el store falla (abajo).
  • Cortacircuitos. Tras 5 errores consecutivos del store, el limitador deja de llamarlo por un enfriamiento de 30 s y luego lo vuelve a sondear con la siguiente petición. Sin esto, cada petición añadiría un viaje condenado al mismo store que se necesita de vuelta (el middleware y los contadores de scraping comparten esa Redis). El cortacircuitos nunca ensancha en silencio un tope: mientras está abierto, quien pide fail-open recibe límite por instancia y quien pide fail-closed recibe el techo degradado.
  • Fail-closed: store configurado que no responde → techo degradado, luego 503. /api/mcp cobra con failClosed: true. Si el store está configurado y el viaje falla (o el cortacircuitos está abierto), el endpoint no degrada al tope completo por instancia — en serverless eso significa tope × número de instancias vivas, es decir ningún tope, sobre una superficie pública y sin autenticación cuyos clientes son agentes en bucle de reintento — pero tampoco rechaza todo: cae a 10 mensajes por minuto y por IP (en vez de 60), contados en la memoria de la instancia. Un agente en bucle es justo el cliente que conviene estrechar, no cortar. Al rebasar ese techo la respuesta es HTTP 503 con -32000 y el mensaje "rate limiting is temporarily unavailable; retry shortly", con Retry-After: 30 (el enfriamiento del cortacircuitos). Es un rechazo distinto del 429 de sobre-cuota a propósito: 429 significa que quien llama realmente excedió su cuota, 503 que el degradado es nuestro y el cliente no hizo nada mal (un agente hace backoff distinto en cada caso). RATE_LIMIT_REQUIRE_STORE=1 devuelve el rechazo duro sin techo degradado.
  • Store nunca configurado → sigue fail-open. "Sin configurar" no es lo mismo que "fallando": es un estado estable de operación (dev local, un self-hoster, vercel dev), no una transición alcanzable por un atacante. En ese caso el limitador cae al modo por instancia y avisa una vez en los logs, porque 503-ear cada petición por una variable de entorno faltante sería una interrupción peor que el gasto que evita. Un operador que ya confirmó que el store está provisionado pone RATE_LIMIT_REQUIRE_STORE=1 para convertir también el store ausente en falta dura (solo en un despliegue gestionado: VERCEL_ENV=development y una máquina local quedan siempre fail-open).

Adónde van las lecturas de las herramientas. El handler construye un FetchDataSource apuntado al propio origen del deployment, resuelto por selfOrigin(): prefiere la URL de deployment que fija la plataforma, y solo cae a las cabeceras x-forwarded-proto / x-forwarded-host fuera de Vercel (desarrollo local). El orden importa y es una defensa, no un detalle: como esa lectura es la verdad de base detrás de cada respuesta de herramienta, un x-forwarded-host o un host falsificado no puede reapuntar el fetch del servidor a un host elegido por un atacante. Una llamada de herramienta lee así el mismo JSON de public/data cacheado en el CDN que usa la web app, sin base de datos, sin un tercer host, y las lecturas repetidas dentro de una invocación golpean una caché LRU.

Normalización de argumentos. Antes del despacho, el ejecutor reconcilia los argumentos de país y pilar contra el catálogo real: "Brasil" → BRA, "mex" → MEX, "educación" → educacion. Puramente correctivo, los valores no resolubles pasan sin tocar, y las herramientas de texto libre o direccionadas por dataset no se ven afectadas.

Conectar un cliente ​

Cree una clave en /cuenta/claves y guárdela: se muestra una sola vez.

Claude Code ​

bash
claude mcp add --transport http futuros https://futuros.xyz/api/mcp \
  --header "Authorization: Bearer $FUTUROS_API_KEY"

Cursor ​

En .cursor/mcp.json (por proyecto) o ~/.cursor/mcp.json (global):

jsonc
{
  "mcpServers": {
    "futuros": {
      "url": "https://futuros.xyz/api/mcp",
      "headers": { "Authorization": "Bearer $FUTUROS_API_KEY" }
    }
  }
}

Claude Desktop / cualquier cliente con archivo de config ​

Añada a la config de su cliente MCP (el claude_desktop_config.json de Claude Desktop, o equivalente). Un cliente nativo con soporte HTTP:

jsonc
{
  "mcpServers": {
    "futuros": {
      "type": "http",
      "url": "https://futuros.xyz/api/mcp",
      "headers": { "Authorization": "Bearer $FUTUROS_API_KEY" }
    }
  }
}

Para un cliente que solo habla stdio, use el puente mcp-remote:

jsonc
{
  "mcpServers": {
    "futuros": {
      "command": "npx",
      "args": [
        "-y", "mcp-remote", "https://futuros.xyz/api/mcp",
        "--header", "Authorization: Bearer $FUTUROS_API_KEY"
      ]
    }
  }
}

Claves de API ​

SuperficieCredencialDónde se crea
GET /api/v1/**, /feeds/** (datos abiertos)Clave de cuenta (o sesión de navegador)/cuenta/claves
Chat en futuros.xyz (navegador con sesión)Cookie de sesión—
POST /api/mcpClave de cuenta/cuenta/claves
POST /api/chat desde scripts / otros orígenesClave de cuenta. El turno se guarda en el historial del dueño de la clave, en una conversación titulada por el cliente (title en el cuerpo, máximo 60 caracteres) o "API"/cuenta/claves

Límites: 600 solicitudes por minuto por clave, cobradas en cada solicitud en el Routing Middleware (variable API_KEY_MAX_PER_MINUTE), y, para el chat, 500 mensajes por día por cuenta (variable CHAT_USER_DAILY_MAX). A eso se suma el límite propio de este endpoint (60 mensajes por minuto), que con una clave se cuenta por clave (mcp:key:<id>) en lugar de por IP. Un 429 trae Retry-After. Una clave nunca es una sesión de navegador: solo sirve en las superficies de la tabla. La creación y la revocación de cada clave quedan en el registro de auditoría.

Modo prepago (ACCESS_ENABLED=true, paquetes en /acceso): apagado en producción desde la release de cuentas. Su código y sus claves access:* quedan intactos para poder reactivarlo como capa de medición sobre las claves de cuenta.

En initialize el servidor devuelve su cadena instructions, que le dice al agente cómo conducir el corpus: resolver una métrica nombrada en palabras con resolve_metric, luego llamar get_parameter_data / compare_countries / compute; cada cifra lleva un citation_id; si el corpus carece de una cifra, las herramientas lo dicen en vez de inventarla.

Herramientas ​

La exposición son 15 herramientas de corpus de solo lectura: las 13 de la lista blanca del toolset del chat, más 2 exclusivas de MCP (server/mcp/tools.ts) que el chat deliberadamente no tiene, get_series (el panel país-año completo de un indicador; el chat trunca la serie a los últimos 6 puntos por presupuesto de tokens) y resolve_citations (resolver citation_ids guardados sin salir de MCP hacia la API de archivos). Las herramientas de web abierta (web_search, fetch_url) y la paga perplexity_search, respaldada por LLM, nunca se exponen, el endpoint no puede fungir como proxy SSRF, no puede gastar el presupuesto de búsqueda externa del deployment, y queda acotado a datos citados de Futuros. El diseño falla cerrado en ambas vías: una herramienta nueva añadida al toolset del chat no se filtra a MCP hasta que se agrega explícitamente a la lista blanca, y una herramienta exclusiva de MCP solo existe si está definida en MCP_EXTRA_TOOLS.

Cada herramienta acepta además un parámetro opcional lang (es | en | pt, por defecto es: el idioma fuente del corpus). Localiza los campos de prosa (narrativas, nombres de pilotos, claims); los números, ids y citas son independientes del idioma. tools/list lo anuncia explícitamente: el handler inyecta el enum lang en el input schema de cada objeto anunciado, así un cliente puede descubrir que existe salida en otros idiomas en vez de tener que saberlo; por llamada, lang se lee de los argumentos de la herramienta, y cualquier otro valor cae de vuelta a es.

Las quince herramientas:

HerramientaQué devuelveParámetros clave
resolve_metricMapea una métrica nombrada en palabras a un id de indicador gobernado (del registro de métricas) — candidatos ordenados con id canónico, unidad, good_direction, pilar, cobertura, vintage. Llámela primero cuando necesite un id.query (req), pillar, limit
get_parameter_dataEl cuadro completo de un pilar en un país: narrativa citada, todos los indicadores (valor/tendencia/año/citation_id), gobernanza, correlaciones, grado de evidencia.parameter (req), country (req)
compare_countriesLos indicadores de un pilar en 2–8 países en una sola llamada, cada cifra citada.parameter (req), countries[] (req, 2–8)
get_newsNoticias recientes curadas para un pilar+país; el id de cada ítem es su citation_id.parameter (req), country (req), limit
get_regional_pulseAgregado regional LATAM de un pilar: valor, tendencia, líderes/rezagados, movimientos, contradicciones documentadas entre fuentes.parameter (req)
computeUn cálculo reproducible citado a sus celdas de entrada exactas. Ops: change, cagr, rank, gap_to_frontier, correlation, forecast, explain_change.op+parameter+indicator (req); luego country/countries[]/from_year/to_year/year/order/frontier/method/indicator_b…
get_data_healthQué tan frescos/confiables/completos son los datos. Con parameter+country: puntaje de confianza, % de provenance, vintage, anomalías, brechas de cobertura. Sin argumentos: el resumen regional + la lista de trabajo de refresco.parameter, country (ambos opcionales)
get_discoveriesAflora proactivamente las mayores anomalías y contradicciones entre fuentes, ordenadas por magnitud, cada una citada.parameter, country, limit (todos opcionales)
find_pilotsBusca en el portafolio de pilotos bancables; devuelve slug, pitch, país, madurez, capex, base del impacto.country, parameter, query, limit (todos opcionales)
get_pilotLa ficha de diseño completa de un piloto: economía, evidencia de bancabilidad, riesgos, financiamiento, tomadores de decisión.slug (req)
recommend_actionLa cadena de acción no copiable para un país+pilar: piloto más adecuado → instrumentos de financiamiento → tomadores de decisión ordenados por relevancia, con pedidos a la medida.country (req), pillar (req)
search_corpusBúsqueda léxica BM25 sobre el corpus entero (personas, pilotos, casos, señales, financiamiento, regulación…); devuelve hits con dataset_id + claves para traer vía get_dataset. Sobre MCP el brazo semántico queda apagado por el servidor y el campo mode de la respuesta siempre vuelve bm25 (ver la sección siguiente).query (req), datasets[], country, parameter, limit
get_datasetAccesor genérico de cualquier dataset catalogado por dataset_id + las claves de ese dataset.dataset_id (req), country/slug/id/role/layer/status…
get_seriesExclusiva de MCP. El panel país-año completo de un indicador gobernado en 1–8 países — cada punto anual con su valor, más fuente, deep-link y citation_id por país. (get_parameter_data devuelve solo los últimos 6 puntos como series_tail.) Los países sin el dato se listan bajo missing, nunca se rellenan.parameter+indicator+countries[] (req), from_year, to_year
resolve_citationsExclusiva de MCP. Resuelve citation_ids guardados (de una respuesta anterior o de la API pública) a su registro de fuente completo: source, title, url, year, retrieved_at. Los ids que el registro global no tiene vuelven bajo unresolved — o ids sintéticos por-respuesta (news-*, pulse-*, civic-*…), que solo existen dentro de la respuesta que los acuñó, o ids cuyo registro viaja inline dentro del payload de un dataset (vuelva a llamar la herramienta que devolvió el id para recibir su registro).ids[] (req, 1–50)

Costo cero por llamada: search_corpus corre en BM25 ​

La regla de diseño de este endpoint: toda herramienta alcanzable por /api/mcp no puede costar nada más allá de lecturas estáticas del corpus.search_corpus era la única excepción. Su brazo por defecto es híbrido y manda la consulta a la API de embeddings de Voyage y el pool de candidatos a su reranker, ambas medidas y facturadas a la llave de Futuros. Como este endpoint solía ser público y sin autenticación, ese camino era una primitiva de amplificación de gasto: texto elegido por quien llama, cobrado a nosotros, al ritmo completo de peticiones del endpoint. Las claves de cuenta (véase Claves de API) ahora filtran quién puede llamar al MCP; el pin BM25 se mantiene para que ni un cliente de pago pueda enrutar gasto de Voyage por esta superficie.

Por eso el handler fija search_corpus a su brazo de solo palabras clave (BM25):

  • El campo mode de la respuesta siempre vale bm25 sobre MCP, nunca hybrid-rrf, bm25+rerank ni hybrid-rrf+rerank. Tampoco aparece semantic_index_drift, que solo lo produce el brazo semántico.
  • El override se aplica después de los argumentos del cliente, así que mandar keyword_only: false no lo desactiva. El perfil de costo de /api/mcp es una decisión del servidor, no algo negociable por el cliente; el flag no se anuncia en ningún inputSchema.
  • El texto de consulta que puede llegar a una API medida está topado en 512 caracteres (MAX_QUERY_CHARS). Sobre MCP ese camino ni siquiera corre: el ranking BM25 es cómputo local sobre los archivos de índice.
  • No se pierde cobertura. BM25 ordena los índices vivos, así que si algo cambia es que resulta más fresco que los embeddings precalculados de los que depende el brazo semántico.

La invariante que se preserva al agregar herramientas: cualquier herramienta con un camino medido queda fuera de la lista blanca, o fijada a un brazo gratuito que el cliente no pueda desfijar.

La restricción es solo de MCP. El chat de Futuros (/api/chat) conserva el camino completo, híbrido semántico + léxico con rerank de cross-encoder: es una superficie limitada por IP y globalmente, y es el consumidor de pago previsto.

Semántica de citas ​

tools/call devuelve el contenido de herramienta MCP como un único bloque de texto cuyo payload es una cadena JSON:

json
{ "result": { /* la respuesta de la herramienta */ }, "citations": [ /* fuentes resueltas */ ] }

isError es true cuando una herramienta reporta un problema (p. ej. un indicador desconocido); el payload es entonces {"result":{"error":"…"},"citations":[]}: el mensaje de fallo viaja dentro de result.error como error de nivel herramienta, no como objeto error JSON-RPC de nivel protocolo (esos quedan reservados para los códigos -32xxx de arriba). Cada cifra numérica dentro de result está ligada a un citation_id que aparece en citations (o resuelve contra el citations.json público). El contrato de diseño: nunca mostrar una cifra de Futuros sin su cita. MCP no tiene herramienta de web en vivo en absoluto, así que todo lo que devuelve una herramienta está verificado contra el corpus.

Ejemplo completo ​

Listar las herramientas, luego resolver una métrica y traer una comparación citada.

1, initialize

bash
curl -s https://futuros.xyz/api/mcp \
  -H 'Authorization: Bearer $FUTUROS_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize",
       "params":{"protocolVersion":"2025-06-18","capabilities":{},
                 "clientInfo":{"name":"demo","version":"0"}}}'
jsonc
{ "jsonrpc": "2.0", "id": 1, "result": {
    "protocolVersion": "2025-06-18",
    "capabilities": { "tools": { "listChanged": false } },
    "serverInfo": { "name": "futuros-corpus", "version": "1.0.0",
                    "title": "Futuros — LATAM governed-data corpus" },
    "instructions": "Read-only access to the Futuros corpus: …" } }

2, tools/list

bash
curl -s https://futuros.xyz/api/mcp -H 'Authorization: Bearer $FUTUROS_API_KEY' -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'

3, tools/call (resolve_metric)

bash
curl -s https://futuros.xyz/api/mcp -H 'Authorization: Bearer $FUTUROS_API_KEY' -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":3,"method":"tools/call",
       "params":{"name":"resolve_metric","arguments":{"query":"homicidios"}}}'

4, tools/call (compare_countries) con el id resuelto

bash
curl -s https://futuros.xyz/api/mcp -H 'Authorization: Bearer $FUTUROS_API_KEY' -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":4,"method":"tools/call",
       "params":{"name":"compare_countries",
                 "arguments":{"parameter":"seguridad","countries":["MEX","BRA","COL"]}}}'
jsonc
{ "jsonrpc": "2.0", "id": 4, "result": {
    "content": [ { "type": "text",
      "text": "{\"result\":{ …cifras por país… },\"citations\":[ … ]}" } ],
    "isError": false } }

5 (tools/call (get_series)) el panel país-año completo

bash
curl -s https://futuros.xyz/api/mcp -H 'Authorization: Bearer $FUTUROS_API_KEY' -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":5,"method":"tools/call",
       "params":{"name":"get_series",
                 "arguments":{"parameter":"salud","indicator":"sp_dyn_le00_in",
                              "countries":["MEX","COL"],"from_year":2010}}}'

Devuelve, por país, cada punto anual {year, value} desde 2010 con la fuente, el deep-link y el citation_id del indicador: la serie completa que get_parameter_data trunca a series_tail.

6 (tools/call (resolve_citations)) resolver un id guardado

bash
curl -s https://futuros.xyz/api/mcp -H 'Authorization: Bearer $FUTUROS_API_KEY' -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":6,"method":"tools/call",
       "params":{"name":"resolve_citations",
                 "arguments":{"ids":["wb-sp-dyn-le00-in-mex-2024"]}}}'

Lote: envíe un arreglo para correr varias llamadas en un solo viaje; la respuesta es un arreglo de resultados en orden. Un lote está topado en 20 mensajes (MAX_BATCH); un arreglo mayor se rechaza con un único -32600 antes de que corra herramienta alguna. Cada mensaje contenido cobra una unidad del presupuesto de 60/min, y una excepción de herramienta dentro de un lote produce -32603 solo para ese id: los hermanos igual responden.

Para descargas de archivos planos del mismo corpus (CSV/SDMX/XLSX), use la API pública v1.

Cada cifra con su fuente — la trazabilidad es el contrato.