← Blog
30 de octubre, 2026 · 13 min

x402 exact en Neo: un pago de 60 segundos que siguió vigente 88 minutos

El PR #3926 especifica el scheme exact de x402 para Neo N3. Reconstruimos cada byte de su vector de prueba, corrimos su script de transferencia contra mainnet y leímos 31 horas de historia de la cadena. Los vectores se sostienen. El reloj no: un pago de "60 segundos" estuvo 88 minutos minable el 2026-10-29.

El PR es solo especificación. Agrega un archivo, specs/schemes/exact/scheme_exact_neo.md, de 828 líneas, y ningún código de SDK. Lo abrió erikzhang el 2026-10-24 como x402-foundation/x402 #3926, commit head d38e9e7. Cuando lo leímos el 2026-10-30 estaba abierto, sin reviews, y su único comentario era un bot de Vercel pidiendo autorización. La descripción dice que una implementación de referencia llegará después de aprobar la spec.

Neo es un caso útil porque no es una cadena de balances con una autorización estilo EIP-3009, ni una cadena UTXO. El pago es una transacción firmada completa, y la transacción expira por altura de bloque. Esa segunda propiedad es donde este post pasa casi todo su tiempo.

Cómo funciona el scheme

El pagador firma una transacción de Neo completa que contiene exactamente una llamada transfer de NEP-17. El payload es el objeto ContractParametersContext de NEP-21 (estado Accepted, creado el 2021-11-24), restringido a cinco campos: type, hash, data (la transacción sin firmar, en Base64), items (una entrada de firma por firmante) y network.

Hay dos modos de fees. En modo payer-paid el pagador es el único firmante y paga las fees de sistema y de red. En modo sponsored el facilitator es el firmante 0 con WitnessScope.None, así que su witness puede pagar fees pero no autorizar actividad de contratos, y el pagador es el firmante 1 con CalledByEntry. El cliente deja vacía la entrada del facilitator. El facilitator la completa, construye los witnesses y difunde.

El flujo es upfront: settle, luego el recurso, luego la respuesta. No hay una llamada verify aparte. El facilitator corre un preflight con invokescript, exige HALT y exactamente una notificación Transfer del asset, reserva (network, transactionHash) en un settlement cache, difunde, y devuelve éxito solo después de leer un HALT persistido con la misma notificación.

El script está fijado byte a byte. Empujar el argumento data (PUSHNULL o un string con PUSHDATA1), empujar el monto con el opcode entero más chico, empujar payTo, empujar al pagador, PUSH4, PACK, PUSH15 (el valor numérico de CallFlags.All), empujar "transfer", empujar el hash del asset, SYSCALL System.Contract.Call, ASSERT. Cualquier otra codificación se rechaza, incluso una semánticamente igual.

Los vectores se reproducen

El apéndice trae un fixture de TestNet T5 con una transacción payer-paid y una sponsored. Lo reconstruimos solo desde los requirements, con Python y el paquete cryptography, y RIPEMD-160 desde Node porque el OpenSSL de Python aquí lo trae deshabilitado.

La system fee también cuadra, con un detalle que vale guardar. El SystemFee del fixture es 215.933. Corrimos el mismo script canónico como invokescript de solo lectura en mainnet, con un pagador con fondos y un sponsor de scope None al frente. Consumió 215.926 de gas con argumento data nulo y 215.933 con la transfer data de 36 caracteres del fixture. La diferencia de 7 unidades es el push del dato, y el número de la spec es exactamente el número del preflight.

También intentamos romper la regla "SystemFee debe igualar el gas del preflight" en modo sponsored, porque cero margen parece frágil. Variamos el receptor (cuenta existente, cuenta nueva, el propio pagador) y el monto (1.000.000, el balance completo del pagador). Todas las corridas consumieron 215.926. Para una transferencia de GAS, el gas no depende de ese estado, así que la regla de cero margen aguantó en todos los casos que probamos. No probamos otros assets NEP-17, que corren su propio código de contrato.

El mismo script ya está en mainnet

El scheme no inventa una forma de transacción. Fija la que las wallets ya emiten. Leímos todas las transacciones de los 28.800 bloques de mainnet entre 14.449.151 y 14.477.950 (2026-10-29 01:45:58 UTC a 2026-10-30 09:07:30 UTC), 2.097 transacciones en total, e intentamos parsear cada script contra la plantilla de la spec. Reconstruir cada coincidencia con nuestro builder canónico dio un script idéntico byte a byte todas las veces.

Es buena noticia para la adopción. Una wallet de Neo que ya arma una transferencia estándar produce la mayor parte de un pago válido. También significa que el layout sponsored no es hipotético en esta cadena.

Qué le cuesta al sponsor un pago fallido

Una de esas 61 transacciones falló. Es 0x40c5f6299053f95d0c4eb7fd60b1a50f0c8ab6019f456043f03f38d32b18c045, en el bloque 14.456.315, una transferencia de GAS con la forma canónica. Su excepción es at instruction 90 (ASSERT): ASSERT failed, el mismo mensaje que obtuvimos simulando una transferencia por una unidad más que el balance del pagador. El pagador no tenía los fondos cuando el bloque se ejecutó.

El log OnPersist del bloque muestra un Transfer de GAS de ese sender a null por 253.690. Es su SystemFee (215.926) más su NetworkFee (37.764), completas. Una transacción fallida igual se incluye y igual paga todo lo que declaró.

En modo payer-paid ese es el costo del pagador. En modo sponsored el sender es el facilitator. Un pagador que firma y luego gasta el balance en otro lado antes de que se mine la transacción del facilitator hace que este pague por una transacción que no transfiere nada. La spec limita la exposición por transacción con un tope local y un preflight fresco, y exige que el facilitator guarde el registro del settlement cache. No dice cómo valorar la carrera entre el preflight y la inclusión. En el propio fixture sponsored de la spec una falla cuesta 215.933 + 73.868 = 289.801 unidades, o 0,00289801 GAS.

Es poco en un día normal. No es poco cuando el preflight es viejo, lo que nos lleva al reloj.

El reloj: 60 segundos son 20 bloques

Las transacciones de Neo expiran por ValidUntilBlock, un índice de bloque. La spec convierte el maxTimeoutSeconds del requirement en bloques:

baseTimeoutBlocks = min(maxvaliduntilblockincrement,
                        max(1, ceil(maxTimeoutSeconds * 1000 / msperblock)))

Los insumos coinciden en todos los lugares donde preguntamos. getversion en tres nodos de mainnet (un nodo neo-go en coz.io, neo-cli 3.10.2 en seed1.neo.org, un nodo neo-go en nspcc.ru) y un nodo de TestNet reportan msperblock 3000 y maxvaliduntilblockincrement 5760. Los métodos getMillisecondsPerBlock y getMaxValidUntilBlockIncrement del contrato nativo Policy devuelven esos mismos dos números en los cuatro. Entonces maxTimeoutSeconds: 60 se vuelve 20 bloques, y 5.760 bloques a 3 s son 4,8 horas.

El ajuste que sí difiere entre nodos es maxtransactionsperblock (200, 200, 512 y 5.000 en los cuatro nodos). La spec no lo usa, así que aquí no importa.

La conversión supone que los bloques llegan cada 3 segundos. En toda la ventana de 28.800 bloques la mediana del intervalo es 3.161 ms y el percentil 95 es 3.419 ms. La media es 3.920 ms y el percentil 99 es 44.607 ms, y esa distancia entre mediana y cola es la historia.

2026-10-29: la cadena se volvió lenta, luego se detuvo

Muestreamos el timestamp de cada bloque número 100 en la ventana. 31 de las 287 ventanas de 100 bloques promediaron más de 5 segundos por bloque, entre aproximadamente las 04:58 y las 14:15 UTC del 2026-10-29. Luego bajamos todos los headers del bloque 14.452.800 al 14.456.700 (3.901 headers, de 04:55:22 a 14:23:15 UTC).

Las ventanas lentas promediaron entre 9,2 y 10,0 segundos por bloque, unas tres veces el ritmo nominal. En los 120 bloques alrededor de la detención, los headers muestran un patrón repetido de intervalos cercanos a 30,1 s y 48,8 s. Luego, entre el bloque 14.456.314 (2026-10-29 12:20:11,143 UTC) y el bloque 14.456.315 (13:45:28,391 UTC), no se produjo nada durante 5.117.248 ms, 85 minutos y 17 segundos. Leímos ambos headers en los tres nodos de mainnet. Devolvieron timestamps y hashes idénticos.

El bloque 14.456.315 tenía 197 transacciones, 14 de las cuales fallaron (7,1%). En los otros 28.799 bloques de la ventana, 1.900 transacciones produjeron 3 fallas (0,16%). No afirmamos una causa para la detención ni para la tasa de fallas. El log muestra qué corrió, no por qué se enlenteció el consenso. Lo que los datos sí muestran es que un backlog de transacciones firmadas antes de la detención se ejecutó de golpe contra un estado de 85 minutos de antigüedad.

Ahora aplica la regla de la spec a ese tramo. Para cada bloque de inicio dentro de los 3.901 headers, medimos el tiempo de reloj hasta el bloque 20 posiciones después, que es la ventana más larga que permite un requirement de 60 segundos.

Un cliente que observó la altura 14.456.308 y firmó con ValidUntilBlock 14.456.328, tal como pide la regla del cliente, produjo un pago que podía minarse en cualquier momento de los siguientes 88 minutos. Nada en el payload lo dice. Los 60 segundos existen solo en PaymentRequirements.

Esto no es un exploit por sí solo. El pagador firmó términos exactos, y el tx hash más el cache de duplicados impiden la doble liquidación. El peligro es la forma del timeout. Un resource server que retiene un payload y no lo liquida puede entregar ese mismo payload al facilitator una hora después y aun así obtener un pago válido. Un comprador que trata "sin respuesta en 60 s" como "sin cobro" puede equivocarse por una hora. La spec sí reconoce la incertidumbre, porque define settlement_pending con el hash de la transacción. No le dice al comprador cuánto tiempo una firma sigue siendo peligrosa.

El fixture contradice la regla

El fixture dice altura 18.762.412 y ValidUntilBlock 18.762.434. Son 22 bloques. La regla del cliente, con msperblock 3000, lo limita a 20 bloques, 18.762.432. El valor 22 es exactamente el límite de aceptación del facilitator (baseTimeoutBlocks + 2), que la spec dice que es solo una tolerancia para un cliente que va dos bloques adelante. Entonces el ejemplo pasa el chequeo del facilitator y falla el del cliente. No podemos ver contra qué msperblock se generó el fixture, solo que las dos redes públicas reportan 3000 hoy. Uno de los dos números debería cambiar.

Lo que la spec hace bien

Gran parte del documento es rigor, y casi todo es del tipo correcto. El facilitator debe comparar el script byte a byte. Todo valor Base64 debe ser canónico, sin alfabeto URL-safe, sin espacios y sin bits sobrantes en el último carácter. El campo hash nunca se confía y se recalcula desde data. La respuesta de settlement debe llevar el hash calculado de forma independiente, no la copia del payload. Cada dato que el JSON repite (hash, network, las claves de items, las firmas en parameters y en signatures) debe contrastarse, y un conflicto no se resuelve eligiendo uno.

Dos reglas destacan porque cierran huecos reales de otros schemes. El witness del sponsor usa WitnessScope.None, así que ni un contrato receptor hostil puede gastar la autoridad del fee payer, y el facilitator debe rechazar cualquier transferencia NEP-17 observada desde su propia cuenta. Y el resultado se juzga sobre el application log persistido, no sobre el preflight, por eso un broadcast seguido de un log ilegible se vuelve settlement_pending con un hash y no un éxito ni un reintento.

La debilidad es la que medimos. Todo el rigor trata de lo que dice la transacción. Casi nada trata de cuándo puede ejecutarse.

El header

El payload viaja como JSON en Base64 dentro de PAYMENT-SIGNATURE. El propio ejemplo sponsored de la spec, envuelto en un envelope v2, son 1.527 bytes de JSON y 2.036 bytes de header. Es poco. Las cuentas multisig lo hacen crecer, porque cada verification script y cada firma aparece dos veces (script y parameters / signatures) y las claves aparecen también en signatures. Armamos payloads sintéticos con claves y firmas aleatorias de los largos correctos:

Esos tamaños cruzan 8 KiB, que era el límite de header por defecto de Node antes de v13, y quedan bajo el límite actual de 16 KiB (medimos los límites de header en varios clientes para el lado del 402). La spec indica configurar un límite suficiente pero acotado. Para cuentas de firma única no es un problema. Un consejo de 16 claves pagando a través de un proxy con configuración por defecto es el caso a probar.

Qué significa para LLM4Agents

Neo no está hoy en nuestros rieles, y este scheme es un borrador sin implementación de referencia, así que nada cambia en nuestro gateway. Tres cosas se trasladan.

Primero, la expiración en bloques es una garantía distinta de la expiración en segundos. Nuestros pagos EIP-3009 llevan validBefore, un timestamp Unix, así que el límite de una firma es de reloj y sobrevive a una detención de la cadena. En Neo el límite es una cuenta de bloques, y el 2026-10-29 la cuenta y el reloj se separaron por un factor de 88. Cualquier cadena que sumemos y expire por altura necesita su propio tope de reloj en el facilitator, no solo una conversión a bloques. Nuestras auditorías de la ventana verify y settle de Stellar y de la finalidad del settlement miran la misma brecha en otras cadenas.

Segundo, el modo sponsored pone sobre el facilitator una pérdida que el patrocinio de gas con EIP-3009 no tiene. Una transacción fallida de Neo quema las fees declaradas completas. Si alguna vez patrocinamos en una cadena así, la exposición por pagador y la edad del preflight pasan a ser insumos de precio.

Tercero, la idea de fijar la forma es buena. Un scheme que acepta exactamente una codificación de bytes es fácil de verificar, y 61 transacciones reales en un día ya la cumplen. Los schemes de Canton y BSV muestran el riesgo opuesto, donde verificar significa parsear una estructura grande.

Cómo mantenerse en la frontera

Pasos concretos, en orden.

Uno. Agregar un chequeo de reloj para toda cadena que expire por altura antes de listarla. Registrar el timestamp del bloque en la altura del firmante y rechazar un payload cuyo tiempo esperado hasta ValidUntilBlock, usando intervalos reales recientes y no la constante del protocolo, supere el requirement. Los datos de arriba son un caso de prueba listo.

Dos. Comentar en el PR #3926 los dos problemas reproducibles: la diferencia de 22 contra 20 bloques en el fixture, y la falta de un límite de reloj en la regla de timeout. Ambos son baratos de arreglar en un borrador y caros después de que salgan los SDKs.

Tres. Conservar el harness. El builder de scripts, los chequeos de hash y firma y la sonda de simulación son unas 150 líneas. Servirán para contrastar una implementación de referencia con la spec cuando aparezca.

Cuatro. Vigilar el SDK. La descripción promete uno tras la aprobación. Hasta que haya código, solo seguimos el estado del PR y el commit head.

Cinco. No patrocinar fees en ninguna cadena sin un presupuesto de pérdida. Dimensionarlo con las fees declaradas por la tasa de fallas vista en la práctica, no solo con el preflight.

Paga por llamada, en stablecoins, sobre una API compatible con OpenAI

Un 402, una firma, más de 345 modelos. Sin suscripción, sin mínimo.

Registra tu agente