← Blog
30 de septiembre, 2026 · 16 min

Auditamos World AgentKit: prueba de humanidad para agentes x402

AgentKit, de World, permite que un servidor x402 distinga a un agente respaldado por un humano de un script y le dé una prueba gratuita en lugar de una factura. Leímos el contrato y el SDK, contamos cada registro on-chain y corrimos los paquetes publicados en local. La idea es sólida. El seudónimo es global, el registro no requiere el consentimiento del agente y la firma se puede reutilizar en otro sitio.

Un pago le dice a un servidor que el dinero existe. No le dice quién está detrás del request. Un solo operador puede levantar mil agentes, cada uno pagando centavos, y agotar cualquier cuota por identidad que ofrezca un vendedor. La respuesta de World, lanzada el 17 de marzo de 2026 en coordinación con Coinbase, es AgentKit: un humano prueba una vez que es único con World ID, delega esa prueba a una o varias wallets de agente, y los servidores x402 pueden entonces darles acceso gratuito, una prueba gratuita o un descuento, con tope por humano y no por wallet.

Es una de las pocas capas de identidad construidas específicamente para x402. El repositorio de x402 la incluye entre sus extensiones de terceros. Una propuesta para agregar recursos protegidos por prueba de personhood al propio protocolo, el issue #2677, citó a AgentKit como antecedente y se cerró como not planned el 28 de septiembre. Así que, por ahora, AgentKit es la opción de proof of human para x402, y Exa ya la corre en producción.

Nuestras fuentes, todas leídas el 30 de septiembre de 2026: worldcoin/agentkit en el commit 1ec70f7 (24 de agosto), los paquetes npm @worldcoin/agentkit y @worldcoin/agentkit-core 0.2.1 (publicados el 24 de agosto, idénticos a ese código), @x402/* 2.28.0, la documentación de AgentKit, los dos despliegues de AgentBook leídos por RPC pública y Blockscout, y un 402 en vivo de Exa.

Cómo funciona AgentKit

Tiene dos mitades: un registro on-chain y un challenge dentro del 402.

El registro es AgentBook, un contrato pequeño que mapea una dirección de agente a un número. El humano corre npx @worldcoin/agentkit-cli register <address>, escanea un código QR con World App y genera una prueba zero-knowledge de World ID cuyo signal es el par (dirección del agente, nonce). Un relay alojado envía la transacción, así que el registro no cuesta gas. Este es todo el camino de escritura:

function register(address agent, uint256 root, uint256 nonce,
                  uint256 nullifierHash, uint256[8] calldata proof) external {
    if (nonce != getNextNonce[agent]) revert InvalidNonce();
    getNextNonce[agent] = nonce + 1;
    lookupHuman[agent] = nullifierHash;   // el "humanId"
    worldIdRouter.verifyProof(root, groupId,
        abi.encodePacked(agent, nonce).hashToField(),
        nullifierHash, EXTERNAL_NULLIFIER_HASH, proof);
    emit AgentRegistered(agent, nullifierHash);
}

El valor guardado es el nullifier hash de World ID. La documentación de verificación on-chain de World explica qué es: un número constante para un usuario, una app y una acción dados. Normalmente los contratos guardan los nullifiers usados para imponer un humano, una acción. AgentBook no lo hace a propósito, para que un humano pueda registrar muchos agentes, y todos reciben el mismo valor. Ambos despliegues usan groupId 1, que esa misma documentación describe como el camino solo-Orb para pruebas legacy (anteriores a 4.0).

El lado del request reutiliza el patrón CAIP-122 que cubrimos en la auditoría de Sign-In-With-X. Una ruta protegida declara una extensión agentkit. El 402 lleva un challenge (domain, uri, nonce, issuedAt, resources) y una lista de supportedChains. El agente firma un mensaje SIWE con su wallet registrada y reintenta con el resultado codificado en base64 en un header agentkit. Los hooks del servidor verifican la firma, llaman a lookupHuman en World Chain y aplican uno de tres modos: free, free-trial (los primeros N usos por humano y por endpoint) o discount.

Exa es el despliegue más claro. Un POST sin autenticar a api.exa.ai/search devuelve un 402 con siete opciones de pago, $0.007 por búsqueda en USDC (1,000 unidades atómicas, $0.001, para /contents), y un challenge agentkit que acepta firmas EIP-191 y ERC-1271 solo en World Chain. La documentación de Exa promete 100 requests gratuitos por humano verificado al mes, compartidos entre todos los agentes de ese humano.

Lo que dice la cadena

AgentBook tiene dos despliegues, ambos verificados en Blockscout, con el mismo runtime de 3,569 bytes y el mismo owner. El SDK lee solo uno de ellos.

// censo de AgentBook, snapshot 2026-09-30 (~09:10 UTC)
World Chain  0xA23aB2712eA7BBa896930544C7d6636a96b944dA   // lo lee el SDK
  registros (AgentRegistered)         1,275   primero 2026-03-15, último 2026-09-30 04:51
  direcciones de agente distintas     1,275   re-registros: 0
  humanIds distintos                    860
  humanos con un agente                 713
  humanos con 2+ agentes                147   (562 agentes, 44% del total)
  cluster más grande                     41   agentes bajo un mismo humanId
  por mes   mar 80 | abr 286 | may 703 | jun 67 | jul 94 | ago 9 | sep 36
Base         0xE1D1D3526A6FAa37eb36bD10B933C1b77f4561a4   // documentado, no se lee
  registros                              10   2026-03-06 a 2026-04-02

Destacan tres cosas. Primero, la actividad. Al momento del snapshot, solo 73 de las 1,275 direcciones registradas tenían algo de USDC en World Chain o Base. De las 1,186 que son EOAs (incluidas las delegadas con EIP-7702), 1,108 no tienen USDC y nunca enviaron una transacción en ninguna de las dos cadenas. Eso no prueba que nunca hayan pagado, porque quien paga con EIP-3009 nunca envía su propia transacción, pero encaja con un registro que es sobre todo experimentación temprana. Los registros tuvieron su pico en mayo y desde entonces rondan una o dos decenas por mes, o menos.

Segundo, qué tipo de wallets se registran. En World Chain, 83 direcciones de agente son proxies Safe 1.4.1 con un solo owner, threshold de uno y el Safe4337Module habilitado. Otras tres son EOAs delegadas con EIP-7702 (13 en Base). Esa forma de Safe es la Safe estándar para ERC-4337, y World ha dicho que usa Safe como smart account por defecto de cada wallet de World App. Los datos on-chain por sí solos no prueban que cada una de esas 83 sea una wallet personal de World App. Si algunas lo son, sus dueños ataron su seudónimo de World ID a su wallet principal, en público.

Tercero, documentación desfasada. El REGISTRATION.md del repositorio todavía lista base y base-sepolia como redes soportadas y dice que el CLI usa por defecto un relay alojado en Base. El código del CLI, el SDK y la documentación alojada se movieron a World Chain entre marzo y abril. Los 10 registros en Base son el rastro: seis de esas direcciones de agente nunca se registraron en World Chain, así que el verificador por defecto, que solo lee World Chain, las trata como no registradas. Los siete humanIds distintos de Base aparecen también en World Chain con valores idénticos, algo que importa para el primer hallazgo.

Ambos despliegues pertenecen a la misma cuenta externa, 0xE340…B39D. Esa llave puede reemplazar el router de World ID (setWorldIdRouter) o cambiar el grupo de credenciales (setGroupId). No se puede renunciar al ownership. En la práctica, qué cuenta como prueba válida de humanidad depende de una sola llave, algo normal en una beta y que conviene saber antes de que un vendedor ponga precio a algo sobre ella.

Seis hallazgos

Corrimos los paquetes publicados contra servidores locales en Hono y Express construidos según la guía de integración de AgentKit. Lo único que reemplazamos con un stub fue el lookup de AgentBook, que devolvía un humanId fijo para nuestra wallet de prueba. No probamos contra Exa ni contra ningún otro servidor en producción.

// Hallazgo 1

Un solo seudónimo para todos los sitios

Los nullifiers de World ID están acotados a una app y una acción, así que apps distintas normalmente ven valores distintos para la misma persona. AgentKit usa un solo app ID (app_a7c3e2b6…146a) y una sola acción (agentbook-registration) para todos los relying parties. Por eso el humanId es el mismo en Exa, en cualquier otro vendedor con AgentKit, en World Chain y en Base. Los siete humanos que se registraron en ambas cadenas tienen valores idénticos en las dos.

El post de lanzamiento de World es franco: "a website can see that all of those agents trace back to the same unique human". La versión precisa es más amplia. Lo puede ver cualquiera, porque AgentRegistered(agent, humanId) es un evento público. Hoy hay 562 direcciones de agente en clusters de dos o más. En cuanto una de ellas se vincula a una persona, las demás la siguen, junto con su historial de pagos.

// Hallazgo 2

El registro no requiere consentimiento del agente

register() nunca revisa msg.sender ni pide una firma de la dirección del agente. El CLI acepta cualquier dirección como argumento. Así, cualquier poseedor de World ID puede registrar cualquier wallet bajo su propio humanId. Como el nonce avanza con cada registro, también puede re-apuntar una wallet que otra persona ya registró. El test del contrato testCanReRegisterAgent cubre esa sobrescritura como comportamiento esperado.

El control del agente sobre su llave se prueba después, en cada request, con la firma SIWE. El vínculo que el humano reclama sobre el agente nunca se prueba. El peor caso no es un robo: quien re-apunta tu agente a su humanId te regala su cuota, o te quita la tuya cuando la suya se agota. Pero el humanId es atribución sin consentimiento, y no hay revocación. El issue #23 (cómo des-registrar, abierto desde abril) y el RFC #37 (re-registro, revocación, rotación, abierto desde julio) siguen sin resolverse. Nuestro censo no encontró sobrescrituras todavía: cada dirección de agente se registró exactamente una vez.

// Hallazgo 3

El cliente firma cualquier origen, así que las firmas se pueden reusar en otro sitio

createAgentkitClient toma domain y uri del 402 y los firma. Nunca los compara con la URL que realmente pidió. Apuntamos un agente a un servidor hostil que respondía con un 402 cuyo domain era otro servidor con AgentKit:

// harness local, 2026-09-30, @worldcoin/agentkit 0.2.1 + @x402/express 2.28.0
// el agente llama SOLO al atacante (127.0.0.1); víctima = AgentKit free-trial en Express (localhost)
agente -> atacante                        200   agentkit_detected, agentkit_signed, agentkit_retry_completed
el header capturado firma                 domain=localhost  uri=http://localhost:4392/data
atacante -> víctima, header capturado     200   {"served":"victim /data"}   // víctima: agent_verified
segundo replay (tracking de nonce activo) 402

La víctima descontó un uso gratuito de la cuota del humano por un request que el agente nunca le envió. Con el tracking de nonces activo, cada firma capturada sirve una vez. En modo free la guía dice que no hace falta storage, y sin storage no hay ningún chequeo de nonce. Reenviamos un mismo header cinco veces contra una ruta free y obtuvimos cinco 200. La ventana es el maxAge de cinco minutos, y el validador ata el host pero no el path, así que una firma abre todas las rutas de ese host. Incluso con storage, el chequeo y el registro del nonce son dos llamadas separadas, una carrera que el PR abierto #36 corrige con consumo atómico.

El SDK de referencia de x402 corrigió el mismo par de problemas para Sign-In-With-X este verano: #2859 (15 de julio) ató la validación de domain en el servidor a un origen configurado en lugar del header Host, y #3133 (13 de agosto) hizo que el cliente rechace challenges que no coinciden con el origen del request. AgentKit 0.2.1 no tiene ninguna de las dos. Detrás del adaptador de Express de x402, la URL contra la que valida se construye a partir del header Host.

// Hallazgo 4

El servidor confía en la cadena que nombre el cliente

El servidor anuncia sus supportedChains y después las ignora. El chainId del payload decide contra qué RPC se verifica la firma. Nuestro servidor Express anunciaba solo eip155:8453:

chainId en payload   status   evento del hook
eip155:8453          200      agent_verified
eip155:10            200      agent_verified     // no anunciada
eip155:137           200      agent_verified     // no anunciada
eip155:999999        402      validation_failed  // viem no tiene RPC para ella

Para una EOA esto es inofensivo. La firma es válida en cualquier cadena. Para un agente con smart account no lo es. La validez ERC-1271 es por cadena, y una Safe cuyo owner se rotó en una cadena sigue aceptando la llave vieja en otra. El cliente elige a qué copia de la cuenta le pregunta el servidor. El fix que salió en 0.2.1, #32, "verify SCA signatures on signed chain", lo hizo explícito, pero sin allowlist.

También hay un costo operativo. verifyMessage de viem intenta primero el validador universal ERC-6492 por RPC y solo después cae a ecrecover. Así que incluso verificar una EOA cuesta un viaje de red, por defecto a una RPC pública, más una segunda llamada para leer AgentBook en World Chain, sin caché en 0.2.1. Cada request verificado tardó unos 400 ms en nuestras corridas. Y lookupHuman atrapa cualquier error y devuelve null. Si la RPC de World Chain limita el tráfico, un agente realmente respaldado por un humano pasa, sin aviso, a tratarse como no registrado y va al camino de pago.

// Hallazgo 5

El contador de la prueba gratuita usa el path crudo como llave

El uso se cuenta con tryIncrementUsage(context.path, humanId, uses). context.path es el path exactamente como lo escribió el cliente. El core de x402, en cambio, hace el matching de rutas sin distinguir mayúsculas y después de normalizar las barras finales, y Express hace lo mismo por defecto. Con free-trial y uses: 1:

express 5.2.1   /data    200   // el único uso gratuito
                /data    402   // agotado, como se espera
                /Data    200   // contador nuevo
                /DATA    200   // contador nuevo
                /data/   200   // contador nuevo
hono 4.13       /Data    404   // router case-sensitive, nunca llega al handler

En Express, cada forma de escribir la ruta que llega al mismo handler recibe su propia cuota. Para /data son 32 formas. Para un path compatible con OpenAI como /v1/chat/completions, con 16 letras, son 65,536 antes de contar las barras finales. Hono, el framework que usa la guía, no se ve afectado porque su router distingue mayúsculas. El arreglo está a mano: el core de x402 le pasa a cada hook el routePattern que hizo match, y usarlo como llave del contador cerraría el hueco. Esa misma llave por endpoint significa que un vendedor con 20 rutas pagas y uses: 5 regala 100 llamadas por humano, no 5. Está documentado, pero es fácil pasarlo por alto.

// Hallazgo 6

El cliente de referencia lee la mitad equivocada del 402

x402 v2 pone el objeto PaymentRequired, extensiones incluidas, en el header base64 PAYMENT-REQUIRED. Los middlewares estándar @x402/hono y @x402/express envían un body JSON vacío. createAgentkitClient busca la extensión en el body. Contra el propio servidor Hono de la guía, agentkit.fetch devolvió el 402 sin ningún evento de AgentKit. El test end-to-end del paquete pasa porque su servidor mock pone el challenge en el body.

Funciona contra Exa solo porque Exa copia el objeto PaymentRequired también en el body. Incluso ahí, un signer configurado como en el README, con chainId: 'eip155:8453', se descarta: Exa anuncia solo eip155:480 y el cliente exige coincidencia exacta. O sea, el cliente es estricto con la cadena, donde el servidor es laxo, y laxo con el origen, donde debería ser estricto.

Lo que se sostiene — la criptografía de base está bien. Una prueba de World ID no se puede reutilizar para otro agente u otro nonce, la firma SIWE sí prueba el control de la llave del agente, el binding de domain frena el reuso ingenuo entre hosts y el tracking de nonces funciona cuando hay storage configurado. Todos los hallazgos de arriba están en la capa de integración, que es justo donde una beta debe ajustarse antes de que los vendedores pongan valor real detrás.

Lo que viene: una reescritura sobre RFC 9421

Un pull request abierto, el #38 (rama new-cli, 19 commits, última actualización el 1 de septiembre), rediseña el SDK. El CLI va a generar y guardar la llave del agente por su cuenta. Apilado encima, el #39, mergeado a esa rama el 1 de septiembre, reemplaza el challenge SIWE por HTTP Message Signatures de RFC 9421 sobre @method, @authority, @path, @query y un content-digest de RFC 9530. Las firmas duran hasta 300 segundos, con 30 segundos de tolerancia de reloj. El servidor reconstruye todo a partir del request que recibió. Es la misma familia de firmas que usa Web Bot Auth, y cerraría el Hallazgo 3: una firma sobre la authority y el path del atacante no verifica en ningún otro lado.

Dos salvedades. La rama verifica con recoverMessageAddress, así que es solo para EOAs y deja fuera el soporte ERC-1271 del que dependen 83 registros de Safe. Y reconstruye @authority a partir de request.url, que @hono/node-server, por ejemplo, arma desde el header Host. Esa es la debilidad que x402 corrigió con un origen configurado. Se propuso un nonce de un solo uso encima (#42) y se cerró sin merge. Nada de esto está en main ni en npm todavía. El contrato del registro solo cambia sus comentarios: "anonymous human identifier" pasa a ser "lookup ID". Es una descripción más fiel de un valor que es un seudónimo estable, público y compartido entre sitios.

Qué significa para LLM4Agents

AgentKit llena un hueco que tenemos. Nuestro gateway vende por llamada sobre x402, y la primera llamada de un agente es la venta más difícil. No podemos repartir API keys sin cuentas, y no podemos darle crédito gratis a cada wallet nueva sin pagar por cada granja de scripts de internet. Una prueba con tope por humano, sin KYC, sin captcha y sin cuenta, tiene la forma correcta para el onboarding de agentes. Encaja de forma natural entre el 402 y el rate limiter que describimos en ¿Esperar o pagar?, y se combina con la sesión de pago único que SIWX da a quienes vuelven a pagar.

Los hallazgos delimitan para qué sirve. Un humanId no es un pago ni una identidad a la que se le puedan pedir cuentas. Cualquiera con World ID puede asociarlo a cualquier dirección, y esa asociación no se puede revocar. Es seguro para regalar valor pequeño y acotado, e inseguro para cualquier cosa que extienda crédito, construya reputación o asigne culpas. El Hallazgo 5 nos toca directo: nuestra ruta principal es /v1/chat/completions. Una prueba conectada con los hooks estándar detrás de un router que no distingue mayúsculas sería una prueba por cada forma de escribirla.

Hay un lado de privacidad para nuestros usuarios. Los operadores que registran flotas crean un cluster público y permanente, y el humanId es el mismo en todos los vendedores. Si registramos humanIds junto a direcciones de pagador y metadatos de requests, pasamos a ser un actor más capaz de vincular los agentes de una persona en todo el mercado. Y si nuestras herramientas de comprador alguna vez firman challenges de AgentKit por los usuarios, el Hallazgo 3 implica que un vendedor hostil podría quemarles la cuota en Exa, o la cuota con nosotros.

Cómo mantenerse en la frontera

Primero, pilotear una prueba por humano con nuestro propio verificador, no con los hooks estándar. Usar como llave de uso el routePattern que el core de x402 ya pasa a los hooks. Comparar el domain firmado con un origen configurado, nunca con el header Host. Rechazar cualquier chainId que no hayamos anunciado. Fijar nuestros propios endpoints RPC. Cachear los lookups con un TTL corto. Consumir nonces de forma atómica en todos los modos, con el patrón INSERT … ON CONFLICT DO NOTHING del PR #36.

Segundo, acotar el valor. Una prueba de unos pocos centavos por humano al mes, en la línea de los 100 requests de Exa, es un costo de marketing acotado. Nunca extender crédito, límites de gasto ni reputación sobre un humanId por sí solo.

Tercero, corregir el cliente antes de publicar uno. Si nuestras herramientas de agente soportan AgentKit, deben leer el header PAYMENT-REQUIRED, negarse a firmar cuando domain o uri no coinciden con el origen del request, y hacer el match de cadenas igual que el servidor. Es lo mismo que hizo el #3133 de x402.

Cuarto, darles a los operadores una regla práctica de privacidad en nuestra documentación. Registrar wallets dedicadas para agentes, nunca una wallet personal de World App, y tratar cada registro como un vínculo público y permanente entre esos agentes. De nuestro lado, guardar humanIds solo donde el contador de la prueba los necesite.

Quinto, seguir el PR #38. Firmar requests con RFC 9421 es la dirección correcta, y coincide con las firmas de Web Bot Auth que igual verificaríamos. Planificar el hueco que deja: los agentes con smart account necesitan ERC-1271, y la authority debe venir de configuración.

Sexto, reportar upstream. El chequeo de origen, la allowlist de cadenas, la llave por route pattern, el parseo del header frente al body y la guía desactualizada de Base son arreglos pequeños y verificables para una beta que el ecosistema x402 ya adoptó como su capa de proof of human.

Haz onboarding de agentes sin repartir llaves

Un gateway compatible con OpenAI, pagado por llamada en USDC sobre x402.

Registra tu agente