Guard de self-payment en x402: 36 de 88,362 settlements
Un pull request abierto de x402 haría que todo facilitator rechace un pago cuando el payer es igual al destinatario. La regla es barata y correcta. La corrimos contra el código del propio PR y luego contamos qué atraparía y qué no en seis horas de tráfico de autorizaciones USDC en Base. Atrapa 36 settlements de 88,362.
El volumen es el número que el mercado de pagos de agentes cita sobre sí mismo. Varios analistas ya mostraron que el conteo bruto de transacciones sobrestima la demanda real. En septiembre TRM Labs sacó los self-payments, los flujos de uno o dos payers y los sellers delgados y cayó cerca de la mitad del volumen settleado. En octubre Chainalysis publicó su capítulo sobre x402 sin decir cómo separa los pagos de agentes de los self-payments. Una regla de protocolo contra el self-payment parece el arreglo natural. El PR #3910 propone justo eso.
Leímos el PR en el head 2e9e6aa y el repositorio en 5847ee5 (2026-10-28), armamos un harness alrededor del x402Facilitator modificado y corrimos un censo en Base. Tres resultados. El chequeo tiene un hueco real en la ruta de settle sin verify previo. Se evade trivialmente con una segunda dirección. Y lo que apunta casi no aparece en los datos, mientras que otro patrón sí aparece.
Qué cambia el PR
El PR #3910 lo abrió whawk46 el 2026-10-23. Al 2026-10-28 sigue abierto, sin review formal, y el único comentario es el bot de autorización de Vercel. Toca cinco archivos: dos de spec y tres bajo typescript/ (el facilitator de @x402/core, su test unitario y un changeset). No toca los SDKs de Go ni Python, ni ningún paquete de scheme.
El texto de spec agrega un código de error nuevo, invalid_self_payment, y una sección 10.3 nueva. Dice que un facilitator DEBE (MUST) rechazar toda verificación o settlement donde el payer resuelto sea igual a paymentRequirements.payTo, sin distinguir mayúsculas, en todos los schemes y redes. El cuerpo del PR da dos razones: evitar volumen wash circular que "transfiere cero valor económico neto" y bloquear el "abuso de auto-indexación en catálogos gateados por settlement". La misma sección agrega cuatro reglas pre-firma para grupos de transacciones patrocinados por el facilitator. Es otro tema y aquí lo dejamos de lado.
El código es un wrapper dentro de x402Facilitator.verify() y settle(). Cada uno tiene dos chequeos:
Antes de que corra el scheme
Un helper, extractPayloadPayer, busca el payer en cuatro lugares: payload.payer, payload.from, payload.authorization.from y payload.authorization.payer. Si encuentra uno igual a payTo, el scheme nunca se llama y el facilitator devuelve invalid_self_payment.
Después de que corre el scheme
Si el scheme devolvió éxito, el wrapper compara el campo payer de la respuesta del propio scheme con payTo. Si coinciden, reemplaza la respuesta por un fallo con transaction vacío.
El segundo chequeo existe porque no todos los payloads exponen un payer en esos cuatro lugares. Mira el paquete EVM publicado. Un pago exact por la ruta Permit2 guarda su payer en payload.permit2Authorization.from, y el código del facilitator lo lee de ahí (permit2.ts, línea 95 y de nuevo en las líneas 353, 483, 532 y 585). Esa ruta no está en la lista del extractor. Un payload exact de Solana es una transacción en base64 sin campo de payer. Para esos, solo el chequeo 2 puede disparar.
El harness
Hicimos checkout de la rama del PR, instalamos @x402/core desde el workspace y manejamos la clase real x402Facilitator con un scheme stub. El stub hace lo que hacen los schemes exact reales para resolver el payer: lo devuelve desde la forma de payload que le corresponde, y en settle() incrementa un contador para marcar que los fondos se habrían movido. Corrimos cada caso por verify() y por settle() sin verify previo. Sin red, sin cadena.
caso op llamadas scheme.settle resultado
EIP-3009, payer == payTo verify 0 invalid_self_payment
EIP-3009, payer == payTo (mayús/minús) verify 0 invalid_self_payment
Permit2 exact, payer == payTo verify 0 invalid_self_payment
Forma SVM (sin payer en payload), payer==payTo verify 0 invalid_self_payment
EIP-3009, payer A != payTo B verify 0 ok
EIP-3009, payer == payTo settle 0 invalid_self_payment, tx ""
EIP-3009, payer == payTo (mayús/minús) settle 0 invalid_self_payment, tx ""
Permit2 exact, payer == payTo settle 1 invalid_self_payment, tx ""
Forma SVM (sin payer en payload), payer==payTo settle 1 invalid_self_payment, tx ""
EIP-3009, payer A != payTo B settle 1 success, tx 0xabc
Lee el bloque de settle. En la ruta EIP-3009 el guard dispara antes de enviar nada. En la ruta Permit2 y en un payload sin campo de payer, el settle() del scheme corrió una vez. Después el wrapper convirtió un éxito en un fallo y descartó el hash de la transacción. Con un scheme real, eso significa que se envió y minó una transferencia, el facilitator pagó el gas, y el resource server recibió success: false y un transaction vacío. Un seller que sigue el protocolo retiene la respuesta. Existe un pago on-chain que los registros del propio seller dicen que nunca ocurrió.
¿Qué tan probable es? Requiere una llamada a settle sin verify previo. El protocolo lo permite y nuestra auditoría de Sui lo ejercitó, pero el middleware estándar de resource server verifica primero, y verify devuelve invalid_self_payment por el chequeo 2 antes de que se mueva dinero. La exposición es acotada: quienes llaman /settle directo y los servidores a medida. El arreglo también es pequeño. Resolver el payer a través del scheme antes de enviar, o agregar permit2Authorization.from al extractor y hacer la comparación dentro del paso verify de cada scheme, donde el payer ya está tipado. Es una propiedad del PR tal como está escrito. Nuestro stub modela formas de payload, así que un revisor debería reproducirlo contra el scheme Permit2 real antes de darlo por confirmado.
La última fila es la que importa para el resto del post. Una dirección de payer que difiere de payTo en un solo carácter pasa. Quien quiere volumen wash tiene dos direcciones.
Tres significados de "self-payment" en un repositorio
El repositorio ya usa el término para cosas distintas, y el PR no las menciona.
- Algorand exact. El código del facilitator valida un pago de monto cero cuyo receptor es igual a su emisor, comentado como
self-payment for fee coverage(avm/src/exact/facilitator/scheme.ts, alrededor de las líneas 590 a 608). No mueve valor apayTo, así que la regla del PR no aplica, pero una spec que diga "el self-payment está prohibido" se lee mal junto a eso. - Canton exact. La regla 11 de la spec del scheme Canton es un "self-payment guard" con otra prueba: el sender probado NO DEBE ser igual a la party
feePayerdel facilitator, errorinvalid_exact_canton_self_payment. Eso protege al facilitator, no al ledger. Cubrimos ese scheme en nuestra auditoría de Canton. - Solana exact. La spec SVM dice que
extra.feePayerPUEDE ser igual apayTo, como en fees patrocinados por el merchant, y describe qué puede contener la transacción en ese caso. Que fee payer y destinatario sean la misma parte es una configuración soportada.
Ninguno entra en conflicto con #3910, porque el PR compara payer con payTo y estos comparan otra cosa. Pero el nombre del error nuevo va a chocar en significado con el de Canton, y un único código genérico con un MUST a nivel de toda la spec es un contrato más débil que las reglas por scheme de arriba. Una frase acotada por scheme, con una noción de payer definida por binding, encajaría mejor en la estructura existente.
El censo
¿Aparece el self-payment en el tráfico real? Lo medimos. Método: en Base, el evento AuthorizationUsed de USDC lo emite transferWithAuthorization, y va seguido inmediatamente de un Transfer del mismo payer. Unimos ambos, para cada bloque de una ventana, con eth_getLogs sobre un RPC público. Es la forma de settlement que usa x402 exact en EVM. No es exclusiva de x402: aparece cualquier llamador de transferWithAuthorization, y no tenemos atribución por facilitator. Tratamos el resultado como un censo de settlements EIP-3009 sobre USDC en Base y una cota superior de x402.
Ventana: bloques de Base 53,183,836 a 53,194,635, es decir, 2026-10-28 03:10:19 a 09:10:17 UTC, casi exactamente seis horas. El script y las filas crudas están en nuestro docs/ local. Resultados:
settlements 88,362
payers / payees distintos 5,938 / 3,002
volumen 505,882 USDC
payer == payee 36 settlements, 6 direcciones, 1.19 USDC
2-ciclos (A paga a B, B paga a A) 4 pares, 10 settlements, 10.98 USDC
3-ciclos 0
pagos cuyo payee también paga 0.2%
payers con un solo payee 64.8%
La regla de self-payment habría rechazado 36 settlements, 0.04% del conteo y 0.0002% de los dólares. Las seis direcciones parecen personas probando su propia configuración: montos redondos. No podemos saber su propósito desde la cadena, y ese es el punto. Una regla que cuesta casi nada y quita casi nada es buena para publicar, pero nadie debería esperar que mueva una estadística de mercado.
La búsqueda de ciclos dice lo mismo desde el otro lado. Cuatro pares de direcciones se pagaron entre sí, diez settlements, once dólares. Ningún lazo de tres direcciones. El flujo circular entre dos o tres wallets dentro de una ventana de horas no es donde está el volumen.
El patrón que la regla no puede ver
Un par de direcciones distintas explica 8,386 settlements, 9.5% de todo en la ventana, con un promedio de un centavo de dólar cada uno, 83.86 USDC en total. Un segundo par suma 1,211 más. Juntos, dos pares payer/payee tienen 10.9% de los settlements y menos de 0.02% de los dólares. No lo llamamos wash. Un solo agente puede legítimamente martillar una sola API paga, y la forma encaja con un loop de polling. Pero si un ranking contara settlements, este único par sería 9.5% de él. La regla de self-payment no ve nada de esto, porque las dos direcciones difieren.
El lado de los payees está igual de sesgado. Los diez payees más grandes reciben 64.1% de los settlements. El 22.3% de los settlements va a payees pagados por menos de diez payers distintos en la ventana, y esos mismos payees concentran 61.2% de los dólares. Si además exigimos que los dos payers principales no superen 90% de los settlements de un payee, sobreviven 77.7% de los settlements y 38.8% del volumen. Son aproximaciones nuestras de los filtros que describió TRM, en un corte de seis horas, no una reproducción de su método.
Dólares y conteos son mercados distintos
El settlement mediano es 0.02 USDC y el percentil 99 es 5.00 USDC. Sin embargo, 194 pagos de más de 100 USDC llevan 95.3% de los dólares. El pago individual más grande, 199,900 USDC, es 39.5% de toda la ventana por sí solo. Los cinco payees más grandes se llevan 89.4% del volumen. Sean lo que sean esos pagos, no son llamadas de agente de un centavo, y probablemente no son x402. Una cifra de "volumen" construida con eventos EIP-3009 mezcla dos poblaciones, y el conteo y el total en dólares los mueven poblaciones distintas.
Qué significa para LLM4Agents
Nuestro gateway es un payee. Cada 402 que emitimos nombra un payTo, y el mercado nos medirá como mide a todo payee: settlements, payers distintos, concentración. Se siguen tres consecuencias.
Primera, el guard es inofensivo para nuestros compradores reales y podría morder a nuestras propias herramientas. Si un facilitator que usamos adopta #3910, un script de prueba o monitoreo que pague a nuestro endpoint desde la wallet que además es nuestro payTo empezaría a fallar con invalid_self_payment. Los chequeos sintéticos deben pagar desde otra wallet. Las llamadas con bearer token no se ven afectadas.
Segunda, nuestras propias cifras de volumen necesitan los filtros que el mercado ya adoptó. Cuando citemos actividad debemos dar payers distintos y la participación del payer más grande junto al conteo de settlements, porque el censo muestra que un par puede ser una décima del conteo y menos de 0.02% del dinero. Lo inverso también aplica: un pago de 200k es 40% de los dólares de una ventana de seis horas.
Tercera, la economía real del abuso está de nuestro lado del medidor. Un comprador que reparte una carga entre muchas wallets derrota igual cualquier límite por payer y cualquier regla payer-igual-payee. La defensa es medir por lo que se consume, no por quién firma. Cubrimos ese límite en la auditoría de spend controls y en la del two-phase gap.
Cómo mantenerse en la frontera
Pasos concretos, en orden.
Uno. Adoptar el guard barato en nuestra propia ruta de verificación cuando la toquemos, pero dentro del paso verify del scheme donde el payer está tipado, para cubrir payloads Permit2 y Solana y no reescribir nada después de un envío. Seguir #3910 por si recibe review y por una nota sobre la ruta de settle sin verify.
Dos. Poner el filtro de diversidad de payers en nuestros reportes: payers distintos, participación del payer principal y del par principal, por ventana. Volver a correr el script de censo cada mes. Toma unos diez minutos de RPC para seis horas de bloques y no cuesta nada.
Tres. Mantener dos wallets para nuestro tráfico sintético y documentar que el guard de settlement puede ser aplicado por los facilitators que use un comprador.
Cuatro. Dejar de tratar payer-igual-payTo como señal de wash en cualquier lugar interno. Usar concentración por par y flujo unidireccional, y tratar lo que supere un umbral como una pregunta a investigar, no como un veredicto.
Cinco. Vigilar la brecha de paridad. El PR cambia solo el core de TypeScript, mientras que los SDKs de Go y Python y facilitators independientes como x402-rs tendrían que implementar el MUST por su cuenta. Un requisito de spec implementado en un solo lenguaje es una garantía más débil de lo que sugiere el texto. Lectura relacionada: cómo el Bazaar indexa sellers. Su spec deja el catalogado como detalle de implementación, así que cualquier ranking basado en settlements hereda los filtros que haya elegido el operador.
Paga por llamada, en stablecoins, sobre una API compatible con OpenAI
Un 402, una firma, 345+ modelos. Sin suscripción, sin mínimo.
Registra tu agente