El scheme work de x402: pagar con proof-of-work, y quién avala el job
Un pull request abierto de x402 agrega un scheme de pago sin token. El cliente paga devolviendo una solución de proof-of-work a un job de minado que el servidor puso en el 402. Reconstruimos el primer perfil desde la spec, lo corrimos con el código del propio PR y revisamos quién avala que el job sea real.
La propuesta es el PR #4057 de x402-foundation/x402, "feat(work): add proof-of-work payment scheme". Lo abrió 0xalank el 2026-10-29. El commit head cuando lo leímos era 189d3bf, tenía dos commits y ninguna review, y GitHub reportaba su estado de merge como bloqueado. Agrega dos archivos de spec (206 y 216 líneas), un paquete nuevo @x402/work con 17 tests unitarios y cambios en @x402/core y @x402/mcp.
Ya vimos proof-of-work en x402 una vez, en la auditoría de Nano, donde es solo un sello anti-spam sobre un bloque de ledger. Aquí es el pago mismo. Eso cambia qué preguntas importan.
Cómo funciona el scheme
El 402 trae un requirement con scheme: "work", asset: "hash" y un amount que es un número de trials esperados. En extra trae el nombre de un perfil, como quai/sha256d-v1, y un job: un ID, un expiresAt, un refreshAfter opcional anterior y una plantilla de minado definida por el perfil en job.work.
El precio se vuelve un target, derivado y nunca enviado:
target = floor(2^256 / amount) - 1
valid = score <= target
El cliente mina hasta que un candidato puntúa igual o por debajo del target, y devuelve { "solution": {...} } con el requirement copiado en accepted. El facilitator revisa el envelope, la expiración, un fingerprint del candidato y el replay store, y después ejecuta la función de prueba del perfil. El settlement consume el fingerprint de forma atómica, y el fingerprint pasa a ser el transaction del settlement.
Dos decisiones de diseño destacan. El pago nunca depende de la cadena nativa: si el mismo score cumple además el target de workshare nativo, el provider puede enviarlo para crédito, pero "native acceptance never decides payment success". Y no hay payer. Un SettleResponse exitoso debe omitir payer, porque una solución no la firma nadie.
Los primeros perfiles son tres de Quai: quai/sha256d-v1, quai/scrypt-v1 y quai/kawpow-v1, en quai:9 y quai:15000. La spec admite otras cadenas después y nombra IDs de perfil de Bitcoin, Monero y Zcash solo como ejemplos. No corrimos un nodo de Quai y no afirmamos nada sobre la red Quai. Todo lo que sigue trata del lado x402.
Qué corrimos
Hicimos checkout del head del PR, compilamos @x402/core y escribimos un harness dentro del paquete work. Implementa la construcción del candidato de quai/sha256d-v1 tal como la describe el texto de la spec: coinbase como coinbase1 || extraNonce1 || extraNonce2 || coinbase2, SHA-256d para la raíz de Merkle, un header de 80 bytes con el hash previo en orden de palabras invertido, y el score como el digest invertido leído como entero big-endian. El fingerprint es el SHA-256 de perfil, red y candidato, como en la spec.
Alrededor de ese perfil usamos los WorkServerScheme y WorkFacilitatorScheme reales del PR, los x402ResourceServer, x402HTTPResourceServer y x402Facilitator reales, y los encoders de header reales. Una simplificación: la spec exige que el candidato se comprometa con la dirección de recompensa a través del seal hash de Quai. Nuestro perfil verifica que los bytes del coinbase contengan el payTo de 20 bytes. Eso reemplaza el compromiso y nada más.
Hallazgo 1: el job vuelve desde el cliente
En el reintento pagado el servidor tiene que saber qué job emitió. El server scheme del PR lo resuelve retomando el job que mandó el cliente. En enrichPaymentRequiredResponse, si el accepted del cliente tiene la misma red, asset, amount, payTo, timeout y perfil que una oferta, y su job no expiró, el servidor copia la versión del cliente al requirement. El facilitator compara entonces accepted con ese requirement y los encuentra iguales, porque el servidor acaba de igualarlos.
El PR lo sabe. El comentario de WorkFacilitatorProfile dice que el job "comes from the client's accepted requirement" y que verify "MUST authenticate it as an issued job". El scheme en sí revisa solo el envelope, el target, la frescura y el replay. Así que toda la pregunta de si este job fue emitido alguna vez vive en código de perfil que el paquete no incluye.
Probamos ambos lados con el servidor HTTP real. El perfil A revisa solo el reward binding. El perfil B además compara el job completo con un mapa de jobs que produjo el issuer. En cada corrida un cliente honesto pidió primero un 402 y resolvió el job emitido. Luego un segundo cliente, que nunca pidió un job, armó el suyo: ID forged-1, un hash de bloque previo arbitrario, un coinbase con el payTo y un expiresAt a 30 segundos. Lo resolvió con el mismo amount y lo envió.
| Perfil | Job honesto | Job falsificado | Llamadas al issuer |
|---|---|---|---|
| A: solo reward binding | payment-verified | payment-verified, settle success | 1 |
| B: compara el job retenido | payment-verified | payment-error | 1 |
Con el perfil A el settlement falsificado devolvió success: true con un fingerprint como transaction, y el issuer había sido llamado una vez, para el cliente honesto. El falsificador pagó exactamente lo que paga uno honesto, 65.536 trials esperados, pero el trabajo fue contra una plantilla que el provider nunca emitió. En un perfil real de Quai, ese trabajo nunca sería elegible para crédito nativo. El provider habría atendido la request y no habría recibido nada de valor.
Dos detalles lo agudizan. El maxTimeoutSeconds de la ruta quedó en 300 por defecto en nuestro harness, y el único chequeo de vida es que expiresAt esté en el futuro y a no más de maxTimeoutSeconds. Un falsificador elige entonces su propia ventana de 300 segundos. Y con el perfil B el rechazo volvió como invalid_work_payload. El facilitator envuelve toda excepción de profile.verify en esa razón, así que "job no emitido", "job nativo vencido" y "plantilla malformada" se ven igual para el servidor y para los logs.
Esto no es un bug en el sentido de código que contradice su spec. La spec dice que un facilitator de terceros "MUST NOT advertise a profile unless it can authenticate the issued job". La brecha es que nada lo hace cumplir, y el único perfil que usan los tests es un stub. Las tres formas de autenticar que da la spec son estado retenido, una attestation del issuer o un servicio autenticado de verificación de jobs, más "public job data". Esta última es el caso difícil. Un facilitator que solo conoce el job público puede confirmar que el coinbase se compromete con payTo, que es lo que hace el perfil A. Para saber que la plantilla está vigente tiene que comparar además el hash de bloque previo y los bits con un nodo vivo. Sin eso, lo que se obtiene es el perfil A.
Hallazgo 2: el precio es la media, la demora es la cola
El amount es "trials esperados". Las resoluciones reales son geométricas: cada trial acierta con probabilidad cercana a 1/amount, así que el tiempo hasta resolver es casi exponencial. Lo medimos. 300 resoluciones con amount 262.144, cada una contra un job aleatorio nuevo, en un hilo de JavaScript:
rate 137.436 hashes/s (solver, con muchas asignaciones)
mean trials 269.151 (nominal 262.144)
mean time 1,96 s
p50 / p90 1,33 s / 4,26 s
p99 / max 10,06 s / 12,84 s
theory p50 1,32 s, p99 8,78 s
La media queda 2,7 por ciento sobre el nominal, dentro del error de muestreo de 300 sorteos (cerca de 5,8 por ciento). El p99 de la muestra es el tercer mayor de 300 valores, así que es ruidoso frente a los 8,78 s teóricos. Lo que importa es la forma: la mediana es 0,69 de la media y el percentil 99 es 4,6 veces la media.
Hay una propiedad favorable al comprador. Como el proceso no tiene memoria, abandonar un job en refreshAfter y empezar con uno nuevo no cuesta nada en esperanza. El trabajo total esperado se mantiene en el amount nominal sin importar cuántas veces reinicie el cliente. Lo que cambia una ventana corta es la probabilidad de no alcanzarla. Con P(miss) = exp(-T * rate / amount) y nuestra tasa medida:
| Amount (trials) | Resolución media | Fallo, ventana de 10 s | Fallo, ventana de 30 s |
|---|---|---|---|
| 65.536 (2^16) | 0,48 s | menos de 0,01 por ciento | 0 |
| 262.144 (2^18) | 1,9 s | 0,5 por ciento | menos de 0,01 por ciento |
| 1.048.576 (2^20) | 7,6 s | 27 por ciento | 2,0 por ciento |
| 4.194.304 (2^22) | 30,5 s | 72 por ciento | 37 por ciento |
| 16.777.216 (2^24) | 122 s | 92 por ciento | 78 por ciento |
Es aritmética a partir de una tasa medida, no una medición de cada celda. Para mantener el fallo bajo 1 por ciento en una ventana de 10 segundos, este cliente necesita un amount de unos 298.000 trials o menos. Un cliente con GPU o ASIC mueve todas las filas, pero la forma se mantiene. El scheme también pone un techo duro al precio: el job no puede vivir más que maxTimeoutSeconds, así que un precio alto exige hardware, no paciencia.
Cuando se pierde la ventana, el client scheme del paquete lanza work job refresh required. Quien llama debe pedir un 402 nuevo y empezar de cero. Es correcto, y significa que un agente sensible a la latencia debe tratar un fallo como un evento normal, no como un error.
La unidad tampoco tiene valor en dólares. Un trial cuesta lo que le cueste al hardware y a la energía del cliente. Por eso las notas del propio PR dicen que hash no es un asset por defecto, que los clientes deben permitirlo en spendControls.allowedAssets y que el tope está en trials. El comportamiento de los topes por defecto está en la auditoría de spend controls.
Hallazgo 3: el job viaja en los dos headers
El cliente debe copiar el requirement completo en accepted, y el job está dentro del requirement. Así que el job aparece en PAYMENT-REQUIRED de bajada y otra vez en PAYMENT-SIGNATURE de subida. Con el job sha256d de ejemplo de la spec medimos 956 bytes el primero y 952 bytes el segundo. Un job con forma KAWPOW (header hash, seed hash, height, bits) fue de 860 bytes.
Los perfiles sha256d y scrypt pueden ser mucho más grandes. La spec permite coinbase1 y coinbase2 "at most 512 KiB combined" y hasta 64 ramas de Merkle. Rellenamos los campos de coinbase y codificamos con el encodePaymentSignatureHeader real:
| Coinbase, bytes combinados | Tamaño de PAYMENT-SIGNATURE |
|---|---|
| 1 KiB | 3.608 B |
| 4 KiB | 11.800 B |
| 6 KiB | 17.260 B |
| 8 KiB | 22.720 B |
| 64 KiB | 175.640 B |
| 512 KiB | 1.398.976 B |
Cada KiB de coinbase suma cerca de 2,7 KB al header: el hex lo duplica, y JSON y base64 añaden un tercio. Leímos "512 KiB" como bytes tras decodificar el hex. Si significa caracteres hex, divide las últimas filas a la mitad. Frente a los límites que medimos en la auditoría de navegador y tamaño de header, un límite de cliente de 8.190 bytes se cruza hacia 2,65 KiB de coinbase y el valor por defecto de 16 KiB de Node hacia 5,65 KiB. No medimos un coinbase real de Quai, así que no sabemos dónde caen los jobs reales. El punto es que el límite de la propia spec está dos o tres órdenes de magnitud por encima de lo que los clientes reales aceptan, y la respuesta le toca al perfil, no al scheme.
Hallazgo 4: un candidato inválido le cuesta al verificador, no al emisor
El facilitator ejecuta los chequeos en orden de costo: envelope, expiración, fingerprint, replay store y por último la prueba. Es el orden correcto. Pero un candidato aleatorio pasa todos los chequeos baratos, porque su fingerprint es nuevo. Producirlo no requiere trabajo, y verificarlo requiere una evaluación de prueba. verify y settle ejecutan cada uno esa evaluación.
Medimos las dos funciones hash que nombran los perfiles de Quai, en un core:
sha256d 174.429 /s 5,7 us por evaluación
scrypt 2.360 /s 424 us por evaluación (N=1024, r=1, p=1)
Verificar scrypt cuesta 74 veces más que SHA-256d. Para un pago válido la economía favorece al defensor, porque el cliente gastó cerca de amount evaluaciones para producir una. Para uno inválido el emisor no gasta nada. No medimos KAWPOW, que necesita el cache de la época. Un perfil que compara el job con el estado retenido antes de la prueba, como el perfil B, también descarta gratis el spam de jobs no emitidos.
Qué significa para LLM4Agents
Nuestro gateway cobra en stablecoins USD y nuestros providers upstream cobran en dólares. Un pago work le da al provider un boleto de lotería: vale algo solo cuando el score cumple además el target nativo, y la spec fija la probabilidad de que una solución válida para el pago sea también elegible en cerca de amount / w, para una expectativa de workshare nativo w. No podemos pasar hashes upstream, así que no vamos a aceptar este scheme. Nada del PR cambia cómo funcionan nuestras cotizaciones exact.
Aun así importa por tres razones. Primero, es el primer scheme donde el "requirement de confianza" del servidor viene en parte del cliente por diseño. Lo que cotice nuestro gateway debe salir de nuestro propio estado y nunca del accepted del cliente, y esa propiedad merece un test. Segundo, nuestros agentes se van a encontrar merchants que ofrezcan este scheme junto a exact. Un cliente que falla ante un scheme desconocido, o ante un header de 1 MB, tiene un bug que solo los merchants pueden disparar. Tercero, "pagar con cómputo" es una opción real para un agente que tiene CPU y no tiene wallet. Si esa demanda aparece, lo anterior es la especificación para hacerlo con seguridad.
Cómo mantenerse en la frontera
- No construir ahora un issuer ni un verificador work. El PR no tiene reviews y una sola familia de perfiles que depende de adaptadores de fuente específicos de Quai. Hay que vigilar los comentarios de review sobre autenticación de jobs y el primer release de
@x402/work. - Agregar un test de regresión de que el gateway nunca confía en campos devueltos. Para
exact, comprobar que el amount, el asset y elpayTousados en la verificación salen de nuestra propia cotización y no deaccepted. El scheme work muestra con qué facilidad se mueve esa línea. - Hacer que nuestro cliente comprador ignore schemes desconocidos y limite el tamaño del header. Debe elegir una oferta que pueda pagar, ignorar el resto y rechazar un 402 por encima de un tamaño fijo en lugar de bufferearlo. Probarlo con un
PAYMENT-REQUIREDde 1,4 MB y una ofertaworken primer lugar. - Mantener los assets desconocidos en default-deny en los spend controls.
hashdebe quedar fuera de cualquier allowlist que entreguemos. La auditoría de metadata cubre otro caso donde los campos adyacentes al pago no son de confianza. - Si los pagos con cómputo llegan a ser un producto, escribir primero los requisitos. Un verificador de job retenido que corra antes de la prueba, rate limits por job ID, una razón de error propia para jobs no emitidos, un amount elegido para que
rate * window / amountsea al menos 4,6 con 1 por ciento de fallo, y un presupuesto de header que el perfil deba declarar. - Releer el PR tras la primera ronda de reviews. Las dos preguntas abiertas son si el paquete incluye un autenticador de jobs de referencia y si la spec acota el tamaño del job. Cualquiera de las dos respuestas cambia este post.
Paga por llamada, en stablecoins o nada
LLM4Agents le da a los agentes autónomos un gateway compatible con OpenAI que cobra por llamada sobre x402.
Registra tu agente