Verifiable Intent: firmar lo que el usuario autorizó
El Trusted Agent Protocol de Visa firma quién toca la puerta del merchant. Verifiable Intent firma qué aprobó realmente el humano del otro lado. Son problemas distintos, y hasta marzo nadie había publicado un formato abierto para el segundo.
Mastercard anunció Verifiable Intent el 5 de marzo de 2026, co-desarrollado con Google y con compromisos de soporte de Fiserv, IBM, Checkout.com, Basis Theory y Getnet. El encuadre de Pablo Fourez, chief digital officer de Mastercard, fue compacto: "A medida que aumenta la autonomía, la confianza no se puede dar por supuesta. Hay que probarla." La spec y una implementación de referencia en Python salieron el mismo día bajo Apache 2.0 en github.com/agent-intent/verifiable-intent, con documentación en verifiableintent.dev.
Esta es la mitad de autorización del problema que cubrimos desde el lado de identidad en el Trusted Agent Protocol de Visa. TAP prueba que un agente responde ante alguien. Verifiable Intent prueba que una compra concreta cae dentro del límite que trazó un humano concreto, y lo hace en un formato que cualquier parte puede verificar sin confiar en lo que el agente dice de sí mismo. Clonamos el repo el 2 de agosto de 2026, leímos los cuatro documentos normativos, corrimos la implementación de referencia y encontramos las partes que funcionan y las que no.
La forma: tres capas de SD-JWT
Verifiable Intent es un formato de credencial, no un protocolo. No define endpoints, ni transporte, ni flujo de mensajes. Lo que define es una cadena de credenciales SD-JWT — Selective Disclosure JWTs, estandarizados como RFC 9901 en noviembre de 2025 — donde cada capa autoriza criptográficamente a la siguiente.
La Layer 1 la emite un banco o una red de pagos hacia la wallet de un credential provider. Lleva los claims de identidad del usuario y, sobre todo, un claim cnf.jwk según RFC 7800 con la llave pública del usuario. Vida útil aproximada: un año. En el perfil de referencia de Mastercard (vct: "https://credentials.mastercard.com/card") los claims siempre visibles incluyen pan_last_four y scheme; email es selectivamente revelable.
La Layer 2 la firma la llave que L1 ató. Es el mandate del usuario — el registro de qué autorizó. Lleva un claim sd_hash igual a B64U(SHA-256(ASCII(serialized_L1))), que la fija a la forma serializada exacta de la credencial de arriba.
La Layer 3 existe solo cuando el usuario delega en un agente. La firma una llave que el usuario ató dentro de los mandates de L2, y lleva los valores concretos: este merchant, este carrito, este monto.
// La cadena, como la enuncia la spec de forma normativa
L1 cnf.jwk = llave publica del usuario // el issuer ata al usuario
L2 firmada por llave privada del usuario
L2 sd_hash = hash(L1 serializada)
L2 mandate cnf.jwk = llave publica del agente // solo modo autonomous
L3 firmada por llave privada del agente
L3 sd_hash = hash(L2 + disclosures seleccionadas)
L3 header.kid = key id del agente // MUST coincidir con L2 cnf.jwk.kid
L3 payload sin cnf // delegacion terminal
Tres detalles de ese bloque pesan más de lo que parecen. Primero, los headers de L3 MUST NOT contener un parámetro jwk: el verifier resuelve la llave del agente desde L2 haciendo match por kid, así que un agente no puede mandar su propia llave junto a su propia firma. Segundo, L3 no lleva cnf en absoluto, lo que hace terminal la delegación: un agente no puede sub-delegar en otro agente. Tercero, todo esto es solo ES256. La spec fija alg a "ES256" y _sd_alg a "sha-256" en todas las capas, y el modelo de seguridad explica por qué: los ataques de confusión de algoritmo viven y mueren en verifiers que leen alg del mismo header que están por verificar.
Dos modos, y el que importa
El modo Immediate son dos capas. El usuario revisa el checkout final y los valores finales de pago y los firma directamente. Los mandates de L2 no llevan claim cnf, y esa ausencia carga peso: señala que no se permite más delegación. Vida útil de unos quince minutos. El agente es un mensajero.
El modo Autonomous son tres capas y es la razón de existir de la spec. El usuario no ve el carrito final. Firma constraints — un rango de monto, una allowlist de merchants, line items aceptables — y ata la llave pública del agente dentro de cada mandate. L2 usa typ: "kb-sd-jwt+kb", donde el sufijo +kb significa que la credencial lleva key binding para delegar hacia adelante. La vida útil va de 24 horas a 30 días, y MUST NOT exceder el exp de L1.
Después el agente compra y produce la Layer 3. Ahí el diseño hace algo que vale la pena robar.
El L3 partido
L3 no es una credencial. Son dos, producidas por la misma llave del agente y enviadas a partes distintas.
L3a es el payment mandate. Va a la red de pagos y lleva payment_instrument, el payment_amount final y un transaction_id. L3b es el checkout mandate. Va al merchant y lleva el checkout_jwt firmado por el merchant, los line items y un checkout_hash.
El merchant nunca ve cómo se fondeó la compra. La red nunca ve qué había en el carrito. Esa frontera no es convención arquitectónica: se aplica a nivel de credencial, porque cada L3 calcula su sd_hash solo sobre las disclosures de L2 que su destinatario tiene derecho a ver. L3a hashea el JWT base de L2 más las disclosures de payment y merchant; L3b hashea el base más las de checkout e items. Ninguna puede reconstruir la mitad de la otra.
Las dos se cosen con un solo hash:
checkout_hash = B64U(SHA-256(ASCII(checkout_jwt)))
// va como `checkout_hash` en el checkout mandate (L3b)
// va como `transaction_id` en el payment mandate (L3a)
// los verifiers MUST chequear: L3a.transaction_id == L3b.checkout_hash
Esa igualdad es lo que corta el ataque de checkout-payment mismatch: un agente que autoriza el pago de un artículo caro mientras el merchant despacha un sustituto barato. Ambas mitades referencian la misma cadena de bytes o la cadena se rompe.
Hay una defensa paralela para el split-agent attack, donde se atan dos llaves de agente distintas en los mandates de checkout y payment del mismo L2, dejando que un agente elija el carrito y un segundo apruebe el dinero. La respuesta de la spec: el cnf.jwk de todos los mandates de un L2 MUST ser idéntico, y los verifiers MUST compararlos.
Ocho constraints
Las constraints son donde la intención humana se vuelve verificable por máquina. Aparecen solo en mandates de modo autonomous, como un array JSON dentro del claim selectivamente revelable del mandate. VI v0.1 registra ocho tipos, y los verifiers MUST soportarlos todos.
Cuatro acotan la forma de la compra: mandate.checkout.allowed_merchants, mandate.checkout.line_items, mandate.payment.allowed_payees y mandate.payment.reference, esta última con el conditional_transaction_id que empareja un checkout mandate con su payment mandate en el momento de delegar.
Cuatro acotan el dinero: mandate.payment.amount_range (por transacción), mandate.payment.budget (acumulado), mandate.payment.recurrence (suscripciones gestionadas por el merchant) y mandate.payment.agent_recurrence (compras repetidas gestionadas por el agente, donde end_date es REQUIRED para que la autorización no quede abierta).
// El array de constraints de un payment mandate, textual de la spec
{
"type": "mandate.payment.amount_range",
"currency": "USD",
"min": 10000, // unidades minimas enteras, ISO 4217
"max": 40000
}
Todo monto en la spec es un entero en unidades mínimas ISO 4217. Sin strings decimales, sin parseo de floats, sin ambigüedad sobre si 1.10 es un dólar diez o un dólar uno. Es la misma disciplina a la que llegaron los schemes de x402, y por la misma razón.
El modelo de validación merece nombrarse con precisión. El verifier arma un fulfillment — un objeto derivado que extrae los valores finales de L3 — y lo contrasta contra las constraints de L2. El identificador del merchant no es un campo del checkout mandate de L3: hay que extraerlo decodificando el checkout_jwt firmado por el merchant que va embebido adentro.
Dónde se acaba la criptografía
La sección más honesta de la spec es §4.2 del modelo de seguridad, y es la que todo operador de gateway debería leer.
Tres de las ocho constraints — budget, recurrence, agent_recurrence — son lo que la spec llama network-enforced. No las puede verificar un verifier sin estado. La cadena de firmas prueba que el L3 del agente es auténtico; no prueba nada sobre cuántos L3 ya produjo ese agente desde el mismo L2.
La spec enumera por qué cada defensa por credencial falla acá: la unicidad del nonce frena el replay del mismo L3, no la generación de nuevos. El binding de aud no ayuda porque el agente elige el aud de L3. Las vidas útiles cortas acotan la ventana de replay, no la emisión secuencial. El sd_hash impide sustituir L2, no multiplicarlo. Y el amount_range se chequea por L3, nunca se acumula.
sd_hash de esa L2.
En el modelo base, un par de mandates de L2 autoriza exactamente un L3a más un L3b. Aplicar ese "exactamente uno" es un problema de base de datos, no de firmas. Es la misma frontera que golpeamos en el scheme upto de x402, donde la firma autoriza un techo y algo con estado liquida el número real, y la misma que hay detrás de reserve-then-settle en nuestro propio camino de facturación. Todo diseño creíble de autorización de agentes converge ahí: un sobre firmado más un ledger que recuerda.
La sección de limitaciones conocidas es igual de franca. VI v0.1 no tiene protocolo de revocación — las mitigaciones son las vidas útiles por capa y el retiro de llaves del JWKS, con las Token Status Lists de RFC 9701 señaladas como mecanismo futuro probable. El claim sub de L1 siempre es visible y persiste toda la vida de la credencial, así que un verifier puede enlazar un año de transacciones a un mismo usuario. Y §6.6 admite que la estructura del checkout_jwt queda definida por la implementación: verificar la firma del merchant sobre él es apenas un SHOULD en v0.1.
Qué trae el repo de verdad
Clonamos el repositorio el 2 de agosto de 2026 y lo corrimos. La implementación de referencia es real: 326 tests pasan en cerca de un segundo, y examples/autonomous_flow.py recorre una compra completa de tres capas — credencial del issuer, mandate del usuario con constraints, L3 partido producido por el agente, verificación del merchant, verificación de la red — de punta a punta. Es más de lo que trae la mayoría de las specs en esta etapa.
También está, a hoy, congelado. El repositorio tiene cinco commits. El último aterrizó el 20 de abril de 2026 y mergeó un cambio breaking de alineación de campos. Nada después. Contra eso: 25 issues abiertos, 82 stars, 16 forks. Seis de esos issues los abrió Ant Financial el 27 de marzo de 2026 — trust model, responsabilidad por chargebacks, manejo de llaves y credenciales, revocación de intent — y los seis siguen abiertos sin respuesta del maintainer cuatro meses después. Un port a TypeScript llegó como PR #31 el 28 de julio de 2026, sin revisar.
Dos hallazgos de leer el código pesan más que el hueco de commits.
Cada L2 que emite viola RFC 9901
El formato de L2 lista el digest de disclosure de cada mandate dos veces en el payload firmado: una como referencia en delegate_payload y otra en el array _sd de nivel superior. RFC 9901 lo prohíbe de los dos lados — §4.1: "El mismo valor de digest MUST NOT aparecer más de una vez en el SD-JWT." §7.1: "Si algún valor de digest se encuentra más de una vez en el payload del JWT firmado por el issuer... el SD-JWT MUST ser rechazado."
Lo reprodujimos en el commit 356c296. La credencial L2 que produce el propio ejemplo autonomous del proyecto lleva seis digests en _sd y dos en delegate_payload, con los dos últimos presentes también en el primero: cada digest de mandate aparece exactamente dos veces en un payload que RFC 9901 manda rechazar de entrada.
No es teórico. El PR #380 de sd-jwt-js endureció la validación de disclosures y salió en v0.20.0 el 29 de junio de 2026. El issue #29, abierto el 17 de julio de 2026 y con un fix acotado a dos archivos ofrecido, tiene cero comentarios.
El constraint checker arranca por el camino flojo
La spec define dos modos de estrictez y afirma que "independientemente del modo de estrictez, los verifiers MUST rechazar open mandates que contengan tipos de constraint desconocidos" — porque una constraint no evaluable deja la autoridad del agente sin acotar. La referencia implementa eso como un flag aparte, is_open_mandate, en check_constraints(), con default False.
Ese flag se pone en True en exactamente tres lugares del repositorio, los tres dentro de tests/. Nada en src/ ni en examples/ lo activa — incluido el ejemplo autonomous, que llama a check_constraints(payment_constraints, fulfillment) dejando el modo y el flag en sus defaults. Como las constraints solo existen en open mandates, el camino de demostración que se distribuye es precisamente la configuración que la spec dice que MUST rechazar tipos desconocidos, y no lo hace.
Lo agrava que verify_chain() no chequea constraints en absoluto. La verificación de firmas y bindings es una llamada; la validación de constraints es una segunda llamada que el integrador tiene que acordarse de hacer, en el modo correcto y con el flag correcto.
Nada de esto es fatal para el diseño. Todo es arreglable en una tarde. Pero un draft que los verifiers estándar rechazan, sin tocar durante tres meses y medio mientras sus integradores previstos abren preguntas sin responder, es una spec cuyo reloj de adopción todavía no arrancó.
Dónde encaja en el stack
VI es deliberadamente angosto. Fuera de alcance: transporte, gestión de llaves, enrolamiento de credential providers, APIs de plataformas de agentes, resolución de disputas, mapeo de cumplimiento regulatorio. No mueve dinero y no define endpoints.
Su propio documento de panorama de protocolos lo posiciona como la implementación concreta de una capa que AP2 describe pero deja abierta: AP2 nombra las Verifiable Digital Credentials como su primitiva de confianza sin prescribir un formato. VI se propone como ese formato, transportado en los campos de la extensión dev.ucp.shopping.ap2_mandate de UCP sin cambiar los endpoints de UCP. Frente al Agentic Commerce Protocol es ortogonal: ACP mueve un token de pago delegado por una API de checkout; VI es la evidencia de que el humano lo sancionó.
La convergencia ya es difícil de no ver. ACP tiene allowance. ERC-7715 y las session keys tienen spend permissions. x402 tiene upto. VI tiene constraints. Cuatro ecosistemas, cuatro vocabularios, una primitiva: un techo firmado sobre el gasto delegado, más algo con estado que cuenta.
Qué significa para LLM4Agents
VI no compite con nuestro rail. Se sienta arriba. La credencial autoriza; x402 y EIP-3009 mueven el valor. Un mandate que dice "hasta $400 en estos payees" es agnóstico respecto de si el settlement aterriza como autorización de tarjeta o como transferencia de USDC.
El hueco es el rol de issuer de L1. La Layer 1 de VI está definida como emitida por un banco o una red de pagos, y el perfil de referencia lleva pan_last_four y scheme. Un gateway de stablecoins no puede ser Issuer de VI tal como está escrita la v0.1. Esa es una restricción real sobre cuán lejos viaja el formato fuera de los rails de tarjeta, y es lo que hay que vigilar: si el vocabulario de mandates para compras de agentes termina en manos de las redes, el comercio de agentes vuelve por default a la economía de las tarjetas.
Lo que sí podemos hacer hoy es la parte que la spec dice que la criptografía no cubre. El gateway ya es el enforcer con estado que VI exige. Reserve-then-settle rastrea gasto acumulado por agente y por período; ese es el mismo ledger que la spec le pide a las redes mantener contra el sd_hash de una L2. Nuestra superficie de constraints mapea casi uno a uno: allowed_payees a endpoints de modelo y tool permitidos, amount_range a un techo por llamada, budget al tope de balance del agente, agent_recurrence a trabajo agendado.
El otro valor inmediato es probatorio. El paquete de evidencia de disputa de VI — firma del issuer, firma del usuario sobre las constraints, firma del agente sobre los valores finales, timestamps, bindings por hash — es una buena descripción de lo que un gateway de agentes debería poder producir para cualquier llamada liquidada, haya o no una red de tarjetas de por medio. No hace falta ser Issuer para emitir ese bundle.
Cómo mantenerse en la frontera
Concreto, en orden.
Primero, emitir el bundle de evidencia. Para cada llamada liquidada, persistir la tupla que VI trata como grado-disputa: quién autorizó, bajo qué límites, qué ejecutó realmente el agente y el binding por hash entre el request y el settlement. Es aditivo sobre el camino de facturación existente y no cuesta latencia.
Segundo, construir los contadores que la spec exige. Gasto acumulado y conteo de ocurrencias indexados por un digest de mandate, con la regla del modelo base aplicada: un fulfillment por par de mandates salvo que una constraint de recurrencia diga otra cosa. Ya tenemos casi todo esto; lo que falta es indexarlo por un identificador de mandate externo y no solo por nuestro propio agent ID.
Tercero, escribir el verifier antes que el issuer. Aceptar un mandate de VI es la integración más barata y la más útil: verificar ES256 con allowlist explícita, recorrer cnf de L1 a L3, recalcular ambos sd_hash y la igualdad del checkout_hash, y correr las constraints en modo STRICT rechazando tipos desconocidos. Rechazar digests duplicados según RFC 9901 §7.1 — lo que hoy significa rechazar la salida de la propia implementación de referencia. Mejor ser correcto y interoperar después que heredar un bug de conformidad.
Cuarto, proponer el binding de stablecoins. La pieza que falta es un tipo de payment_instrument y un vocabulario de payees que describan un settlement on-chain en lugar de una tarjeta. Es una contribución chica y bien acotada a un draft Apache-2.0 cuyos maintainers todavía no tuvieron que responder si el formato tiene forma de tarjeta por accidente o por diseño. Los PRs sin mergear que proponen constraints de estado de wallet y de budget acumulado sugieren que otros ya empujan sobre el mismo borde.
Quinto, vigilar tres señales. Si Mastercard entrega las intent APIs de Agent Pay que prometió en marzo; si el port a TypeScript aterriza y con él una segunda implementación contra la cual probar interoperabilidad; y si el issue #29 se cierra. Ese último es el termómetro de adopción más barato disponible: una spec que deja a los verifiers estándar rechazando sus propias credenciales durante un mes todavía no la está integrando nadie que lo hubiera notado.
Verifiable Intent tiene la forma correcta. Delegación como cadena firmada, constraints como objetos verificables por máquina, selective disclosure como frontera de privacidad entre merchant y red, y una admisión honesta de que la última milla tiene estado. Lo que todavía no tiene es tracción. El diseño merece mejor mantenimiento del que está recibiendo.
Dale a tu agente un presupuesto que no pueda exceder
Techos por llamada, topes acumulados y un registro de settlement para cada request — sobre un gateway compatible con OpenAI.
Registrar un agente