AI Product Index
Un directorio legible por máquinas ("SEO para IAs") donde los productos de IA se registran para que los agentes de IA puedan descubrirlos.
Documentación
AI Product Index
Un directorio legible por máquinas ("SEO para IAs") donde los productos de IA se registran para que los agentes de IA puedan descubrirlos. Los clientes son agentes de IA que actúan de forma autónoma: un agente encuentra el sitio, lee llms.txt y registra un producto sin ningún paso humano.
🟢 En vivo, con dinero real y probado. https://index.percall.dev está desplegado con la pasarela de pago en mainnet —
POST /api/auditcotiza $0.05 en USDC en Base y lo liquida a la dirección receptora. La pasarela completó su primera liquidación integral el 2026-07-29: pagada por el cliente de accionesx402-fetch, liquidada en cadena, rechazada la repetición, registrada en el panel de ingresos. Detalles en DEPLOY.md → Fase 3.3. Migrado el mismo día desdeindex.kc-it.pl, que permanece adjunto y responde con un 308 que preserva el método — percall.dev es el dominio general para el portafolio de servicios de pago en el que esto se está convirtiendo.🟢 Dos productos, un Worker, tres hostnames (desde 2026-08-03).
percall.devsirve la página del portafolio;index.percall.deves el AI Product Index;router.percall.deves The Router — sondeo y enrutamiento en vivo a $0.005 por llamada, más observaciones semanales prepagadas a $0.005 por barrido. Los endpoints de Router de un solo disparo liquidaron dinero real en Base mainnet el 2026-08-03; la entrega de observaciones se probó posteriormente a través de una caída y recuperación. Cada host es dueño de sus propias rutas y responde 308 a todo lo demás, de modo que hay exactamente una dirección por documento.
Documentación: NEXT.md — lo que falta, y de quién es el turno · DEPLOY.md — cómo ponerlo en producción, fase por fase · ARCHITECTURE.md — cómo funciona y por qué · TODO.md — el registro de cambios y el razonamiento detrás de cada cambio
- El registro es gratuito y seguirá siéndolo: registrarse es la "compra" — un agente abre un issue de GitHub con el JSON del listado, un workflow lo valida y lo publica.
- El producto de pago es
POST /api/audit: una auditoría de legibilidad para agentes de cualquier URL, con precio por llamada a través de x402 (HTTP 402). Su valor no depende de cuántos listados tenga el registro, por eso es este — no la mejora de nivel — lo que está detrás de la pasarela de pago. - Niveles de pago (
verified,featured) existen en el esquema y en el ranking; el flujo[upgrade]verifica los recibos x402 en cadena.
Lo que obtiene un producto listado: una página HTML rastreable con schema.org JSON-LD (/l/<slug>.html), presencia en el registro JSON (/api/index.json + /listings/<slug>.json), inclusión en el sitemap y presencia en llms.txt/llms-full.txt.
Estrategia: dos vías, una construcción
- Vía A — servicio GEO pagado por humanos (ingresos ahora). Los humanos ya pagan $29–489/mes por visibilidad en IA. La oferta: llms.txt + schema.org JSON-LD + auditoría de legibilidad para agentes en el dominio del propio cliente, todo hecho por nosotros. Se vende a través de la sección "Para humanos" de la página de aterrizaje — issue
[hire]o correo electrónico. Primeros trabajos entregados a mano; sin infraestructura de pago hasta que alguien pague. - Vía B — el registro de agentes (un activo que envejece bien). Este repositorio. Comercializa la Vía A: cada página de listado es una demo en vivo del entregable, y el embudo está integrado (el agente se registra gratis → el operador ve el listado → venta adicional). Los listados semilla son el propio registro, el servicio de la Vía A y los otros siete sitios desplegados del operador (dogfood).
Cómo funciona
Compilación estática en la raíz del repositorio, servida por un Cloudflare Worker con activos estáticos (wrangler.toml → worker/index.js). Cero dependencias en tiempo de ejecución; scripts de Node simples. Compilar y probar con Node ≥ 22 (la misma versión que usa CI).
El Worker existe por las tres cosas que GitHub Pages estructuralmente no podía hacer:
| GitHub Pages | Worker | |
|---|---|---|
| HTTP 402 + manifiesto de pago | imposible | POST /api/audit |
Cabeceras de respuesta personalizadas, negociación Accept | imposible | Link: alterna, variantes JSON/markdown |
| Registros de solicitudes | ninguno | Analytics Engine → /api/stats.json |
Esa última fila es el punto: antes de la migración no había forma de saber si un solo agente había visitado el sitio.
Rutas del Worker (worker/):
GET /api/search?q=…— búsqueda clasificada sobre el registro, concategory,tagylimit. La coincidencia está ponderada (nombre > etiquetas > slug > descripción) y por niveles (palabra completa > subcadena > raíz compartida), porque en un corpus tan pequeño un puntuador que puede clasificar un listado no relacionado por encima de una coincidencia exacta de nombre es peor que uno que no devuelve nada: un agente puede ampliar su propia consulta, pero no puede distinguir una respuesta incorrecta segura de una correcta. Una respuesta con cero resultados informa de lo que el corpus sí contiene y dónde registrarse, de modo que un callejón sin salida aún enseña algo.discovery.js.POST /ask— NLWeb (nlweb.ai). Lenguaje natural de entrada, objetos schema.org de salida, fundamentado únicamente en el registro — nada se genera. Elimina las stop-words y luego aplica un umbral de relevancia en la mitad de la puntuación máxima, porque "subastas de propiedades polacas" que coincida con siete de ocho listados por la palabra "polacas" es una respuesta peor que las dos correctas. Acepta el{"query": {"text": …}}de la especificación, un{"query": "…"}simple yGET ?query=…, porque cada cliente escrito a mano se equivoca con el anidamiento al menos una vez.discovery.js.POST /mcp— Model Context Protocol sobre HTTP transmisible. Sin autenticación, nada que instalar: cualquier cliente MCP que acepte una URL puede añadir este índice como fuente de herramientas (claude mcp add --transport http ai-product-index https://index.percall.dev/mcp). Seis herramientas:search_products,get_product,score_url,search_x402_endpoints,search_mcp_servers,how_to_register.score_urlse proxifica a través del manejador real de/api/scoreen lugar de reimplementarse, de modo que un llamador MCP y un navegador no pueden discrepar sobre una calificación. Solo se implementa la mitad JSON-RPC del transporte — un servidor sin mensajes iniciados por el servidor no tiene nada que transmitir, yinitializelo indica declarando solotools.discovery.js.GET /badge.svg?slug=…— la insignia del README que un producto listado incrusta;&show=scoremuestra su calificación en vivo de A–F en lugar de su nivel. Lee el registro confirmado yscores.json, nunca audita y siempre devuelve una imagen 200: se renderiza dentro del README de otra persona, donde un 4xx es un icono roto. Las calificaciones se actualizan semanalmente mediantescripts/score-listings.mjsen el cron de salud.badge.js.GET /.well-known/http-message-signatures-directory— claves públicas Ed25519 para las firmas RFC 9421 en cada respuesta.signing.js.GET /api/score?url=…— gratuito. La calificación con letra A–F, la puntuación numérica y los 20 checks por etiqueta con aprobado/fallido. Esto es lo que llama la caja de entrada de la página de inicio; la auditoría se ejecuta en el servidor porque un navegador no puede leer el llms.txt o robots.txt de otro origen. Caché por URL durante una hora (los aciertos de caché no se miden), 20 auditorías no cacheadas/hora/IP.score.js.POST /api/audit— de pago. Los mismos 20 checks más, para cada fallo, por qué falló, una corrección clasificada por peso y un fragmento de código listo para pegar con el origen del propio llamador sustituido. Valida el destino conurlError()antes de cobrar, y luego limita con x402.audit.js.GET /api/liveness?url=…— de pago, enrouter.percall.dev. Sondea un endpoint pagable por máquina en este momento: si respondió, qué tan rápido y los términos de pago que cotiza actualmente, analizados de su propio 402 en cualquiera de las dos versiones de x402. Lo que se vende es frescura — los catálogos y sus agregados semanales siguen siendo gratuitos, pero el Bazaar x402 mantiene una entrada durante 30 días después de su última liquidación, de modo que "listado" y "responde" son hechos diferentes y solo uno se publicó en algún lugar.route.js.POST /api/route— de pago, enrouter.percall.dev.{"q": "unit conversion", "max_price": 0.01}→ candidatos clasificados por la búsqueda del propio catálogo, cada uno sondeado en vivo, cada uno con los términos que cotiza ahora y la URL a llamar. Los endpoints que no respondieron se informan en lugar de descartarse, y una consulta que no coincide con nada no se cobra.route.js.POST /api/watch— de pago, enrouter.percall.dev. Prepagar 4–52 barridos semanales de un endpoint a $0.005 cada uno. La dirección del pagador es dueña de la observación; el webhook se dispara solo en un borde de respondiendo→fallando o fallando→respondiendo, y una vez cuando se agotan los créditos. No hay cuenta, ni obligación de suscripción, ni cargos posteriores.watch.js.- Ambos llevan el historial del endpoint — sondeos, respuestas, fallos consecutivos y cuántas de las últimas 30 observaciones respondieron — acumulado de sondeos en vivo en lugar de un rastreo, de modo que la respuesta mejora cuanto más se usa el servicio. Ninguno paga jamás: un sondeo no pagado de un endpoint de pago devuelve su 402, y un 402 declara sus términos, así que el llamador paga al endpoint directamente desde su propia billetera. Este despliegue no tiene ninguna clave que pueda firmar una transferencia, y una prueba prohíbe que cualquier módulo bajo
worker/establezca una cabecera de pago saliente. GET /api/stats.json— contadores de solicitudes de 30 días por tipo de cliente inferido, bucket de ruta y hostname, másagent_share. Lee el conjunto de datos de Analytics Engine a través de la API SQL; informastats_not_enabledsin credenciales.stats.js.GET /api/x402/info— los términos de pago de la pasarela activa y las versiones de protocolo aceptadas, para que un agente pueda leer el precio sin provocar un 402.GET /api/revenue.json+/dashboard.html— el libro de ingresos y su panel, privados. La página en sí está restringida, no solo los datos: las solicitudes no autorizadas reciben el 404 ordinario, de modo que su existencia nunca se revela. El acceso es mediante?token=<DASHBOARD_TOKEN>una vez, canjeado por una cookie de sesión HttpOnly. El panel etiqueta las liquidaciones de testnet como testnet en lugar de llamarlas ingresos.revenue.js.- Todo lo demás cae en el binding
ASSETS, decorado pornegotiate.js(cabeceraLink:, negociación de contenido basada enAccept) — los dos checks de preparación para agentes que la auditoría puntuó como imposibles en alojamiento estático. - Cada solicitud se registra en Analytics Engine con una ruta agrupada, un tipo de cliente clasificado (
classify.js), método, clase de estado, UA truncado y ASN. Sin direcciones IP.
Fuente de verdad (editada a mano o escrita por workflow):
listings/<slug>.json— un archivo por listadotemplates/— index.html, 404.html, llms.txt, robots.txt, openapi.yaml con placeholders{{BASE}}/{{REPO}}/{{COUNT}}/{{LISTINGS_HTML}}site.config.json— URL base + slug del repositorio (la única perilla para la migración)scripts/validate.mjs— el límite de seguridad: reglas de campos,validate(),reconstruct(),esc(),jsonLd(), másschemaJson()para que el esquema publicado se genere a partir de las mismas constantes que lo hacen cumplir
Generado por node scripts/build.mjs (confirmado; determinista — sin marcas de tiempo, compilar dos veces → diff cero): index.html, 404.html, llms.txt, llms-full.txt, robots.txt, openapi.yaml, sitemap.xml, api/index.json, api/schema.json, .well-known/agent.json (tarjeta A2A), .well-known/agents.json (el manifiesto de agentes en plural — una especificación diferente, leída por los auditores de preparación para agentes), .well-known/security.txt (RFC 9116; su Expires está codificado en la plantilla porque la compilación no puede estampar una marca de tiempo, y una prueba falla una vez que pasa), l/*.html (el directorio l/ se borra y reconstruye para que los listados eliminados no puedan dejar páginas obsoletas).
Además, las superficies cuyo único trabajo es hacer que las rutas anteriores sean encontrables por algo que solo conoce el dominio: .well-known/mcp.json (tarjeta de servidor MCP — SEP-1649/2127 son borradores, así que lleva solo los campos en los que ambos borradores coinciden), opensearch.xml (sigue siendo el único formato que convierte un dominio desnudo en una caja de búsqueda invocable), feed.xml + feed.json (un directorio que gana entradas es un feed), y .well-known/ai-plugin.json (superado, y sondeado con la frecuencia suficiente para que responder cueste menos que los 404). Una prueba verifica que los manifiestos no pueden anunciar una ruta que el Worker no tenga.
Rutas de escritura (las transacciones autónomas) — .github/workflows/register.yml, controladas por el prefijo del título del issue (no una etiqueta, que los agentes de REST-API no podrían establecer):
[register]— nuevo listado.scripts/process-issue.mjs(entrada solo por env, nunca interpolada desde shell): límite de 20 KB → analizar (```json fence or bare body) →validate()→ unique slug + normalized-URL dedup → ≤ 10 listings per GitHub account → liveness check (product URL must answer < 400 in 10 s) → write reconstructedlistings/<slug>.json.[update]— reemplazo completo de un listado existente; solo elgithub_useroriginal (o el propietario del repositorio) puede actualizar;created/github_user/tierse conservan,updatedse estampa.[upgrade]— cambio de nivel de pago ({"slug", "tier": "verified|featured", "rail": "x402", "receipt": {"transaction": "0x…"}}): verificación de propiedad y forma, luego verificación de recibo en cadena víascripts/x402-receipt.mjs— la transacción debe haber tenido éxito, tener suficientes confirmaciones y contener unaTransferERC-20 de al menos el precio del nivel en el activo configurado apayments.x402_address. Los hashes de transacción gastados se queman en el registropayments.jsonconfirmado para que un pago no pueda comprar dos mejoras. Rechazapayments_not_enabledmientras el riel no esté configurado;rail: "card"devuelvemanual_reconciliation.- Todos los modos: build + commit + push con un bucle de reintento de reset-y-rehacer ×3 (no un grupo de Actions
concurrency, que cancela silenciosamente las ejecuciones en cola; después dereset --hardla deduplicación se re-ejecuta, por lo que una carrera perdida falla limpiamente), luego un comentario de bot legible por máquina ({"ok":…,"code":…,"errors":…}) y cierre del issue. Páginas se redespliega con el push (~1 min).
Niveles: free < verified < featured — los niveles de pago aparecen primero en el índice y llevan una insignia. Cambio manual (p. ej., tras un pago fuera de banda): node scripts/set-tier.mjs <slug> <tier>, luego commit + push.
Salud — .github/workflows/health.yml (lunes 04:17 UTC + dispatch manual): scripts/check-liveness.mjs vuelve a comprobar cada URL del listado; estado de strikes en health.json confirmado; 3 fallos semanales consecutivos dan de baja (página 404s, registro actualizado); fallos/bajas se reportan como un issue de GitHub.
Vitalidad del catálogo — scripts/probe-catalogs.mjs, mismo cron semanal: ninguno de los registros upstream verifica si sus entradas siguen respondiendo, así que se sondea una muestra rotatoria de ambos catálogos y los resultados se publican en api/{x402,mcp}/health.json. Se toma cada fila de stride en lugar de una ventana contigua — estos archivos están ordenados, por lo que un vecindario no es una muestra (las URLs de placeholder son 1.6% del catálogo MCP pero eran 54% de sus primeras 120 filas) — con el cursor avanzando uno por ejecución, por lo que un pase completo sigue cubriendo cada entrada exactamente una vez. Un 402 o 401 cuenta como respuesta: la pregunta es si algo está escuchando, y solo los fallos de transporte y los 5xx cuentan en contra de una entrada. Dos fallos consecutivos antes de declarar algo muerto; la búsqueda marca y nunca oculta, ya que una sonda semanal desde una ruta de red es evidencia, no prueba.
Servidor MCP — mcp/server.mjs: stdio JSON-RPC de cero dependencias (initialize/ping/tools/list/tools/call). Importa sus definiciones de herramientas desde worker/discovery.js y reenvía tools/call al /mcp alojado, por lo que los dos servidores no pueden divergir; tools/list permanece sin conexión porque los chequeos de salud del registro lo inspeccionan en un sandbox sin red. Añade register_product (abre el issue [register]; necesita env GITHUB_TOKEN, public_repo), que es la única razón para preferir stdio — un token en un Worker público es una credencial a punto de filtrarse. Instalación: claude mcp add ai-product-index -- node <clone>/mcp/server.mjs, o salta el clon por completo con claude mcp add --transport http ai-product-index https://index.percall.dev/mcp.
Modelo de seguridad: todo texto/atributos HTML a través de un esc(); hrefs solo de campos URL permitidos por esquema (http/https, host público); JSON-LD <-escapado contra el rompimiento de </script>; regex de slug + aserción de ruta resuelta detienen el path traversal; los objetos aceptados se reconstruyen campo por campo desde una allowlist (sin __proto__ write-through); token de workflow con alcance a contents: write, issues: write.
La frontera gratis/pago es una whitelist, no un borrado. freeView() en score.js nombra los campos que conserva el nivel gratis, por lo que un campo añadido posteriormente a la auditoría no puede filtrarse a él por omisión — y una prueba afirma que un campo nuevo hipotéticamente de pago permanece fuera. El nivel gratis responde deliberadamente "¿tengo un problema, y aproximadamente dónde?"; el nivel de pago responde "aquí está el código que lo arregla".
Seguridad del pago — el facilitador verifica firmas y saldos; no tiene idea de cuánto cobramos, así que worker/x402.js es lo que evita que un cliente pague una unidad atómica a una dirección de su elección:
- cada campo del bloque
accepteddel cliente se compara contra nuestros propios requisitos (esquema, red, activo,payTo, monto), y la autorización se verifica de forma independiente — un payload con un bloqueacceptedde apariencia correcta pero una autorización que paga en otro lugar se rechaza; - los montos se comparan como
BigInt, por lo que"1e5"," 10000"y"-10000"nunca pasan como"10000"; - el nonce se reserva en KV antes de liquidar, por lo que una repetición concurrente pierde la carrera en lugar de liquidar dos veces;
- el objetivo de auditoría pasa
urlError()(solo hosts públicos http/https) antes de cualquier cargo — nadie paga por una solicitud que rechazaríamos.
Desarrollo local
node --test scripts/*.test.mjs # validator, escaping, worker, payment-gate, receipt tests
node scripts/build.mjs # regenerate everything (deterministic: build twice → zero diff)
npx wrangler dev # serve assets + Worker routes locally
# simulate a registration without GitHub:
ISSUE_BODY='{"slug":"x-y-z","name":"X","url":"https://example.com","description":"d","category":"api","pricing":"free"}' \
ISSUE_USER=you node scripts/process-issue.mjs # SKIP_LIVENESS=1 to skip the URL check
Usa scripts/*.test.mjs, no node --test scripts/ — Node 22 resuelve un argumento de directorio desnudo como módulo y falla antes de ejecutar nada.
Despliegue (Cloudflare)
.github/workflows/deploy.yml ejecuta pruebas, afirma que el build confirmado no está obsoleto, luego wrangler deploy en cada push a main. Omite el despliegue con un aviso mientras los dos secretos de Cloudflare no estén establecidos, por lo que main permanece verde en lugar de acumular cruces rojas que nadie lee. Desplegado y verde desde 2026-07-25; la configuración a continuación se registra para una reconstrucción, no trabajo pendiente. Configuración única:
npx wrangler kv namespace create PAYMENTS→ pon el id enwrangler.toml.- Secretos de repositorio
CLOUDFLARE_API_TOKEN(Workers Scripts: Edit) yCLOUDFLARE_ACCOUNT_ID. - Apunta
index.percall.deval Worker —gh workflow run cf-admin -f action=attach-domain -f hostname=index.percall.dev(la zona debe estar en la cuenta de Cloudflare y el token necesita sus derechos de zona). - Opcional, para
/api/stats.json:npx wrangler secret put CF_ACCOUNT_IDyCF_ANALYTICS_TOKEN(Account Analytics: Read). Sin ellos, el endpoint reportastats_not_enableden lugar de fingir.
Localmente, ejecútalo con npx wrangler dev --local --persist-to /tmp/seo-wstate. El --persist-to fuera del repositorio importa: el directorio de activos es la raíz del repositorio, por lo que el directorio de estado de wrangler termina en el árbol vigilado y recarga en bucle para siempre.
Interruptor de migración: site.config.json → base es la única fuente para cada URL absoluta; el build regenera sitemap/canonical/JSON-LD/llms.txt/openapi a partir de él. Las tres URLs hardcodeadas en .github/ISSUE_TEMPLATE/*.yml deben editarse a mano (el texto del formulario de issue no puede ser templateado).
Pagos — rieles, y cómo cambiar entre ellos
Los rieles difieren solo en dónde liquidan y a quién responden, por lo que viven como perfiles nombrados bajo payments.x402.profiles con un selector active. Pasar de ensayo a dinero real es una palabra, no cinco campos editados. scripts/x402-config.mjs → resolveX402() es el único resolvedor; tanto el Worker como el flujo de issue [upgrade] lo leen, por lo que los dos no pueden discrepar sobre qué cadena y activo se aceptan.
Actualmente active: "mainnet" — Base, USDC, dinero real.
| Perfil | Facilitador | Auth | ¿Liquida en Base mainnet? | ¿Listo? |
|---|---|---|---|---|
mainnet | PayAI | ninguno | sí, v1 + v2 | sí — activo ahora |
testnet | x402.org/facilitator | ninguno | no — solo testnet | sí — vuelve a active a ensayo |
cdp | Coinbase CDP | Bearer JWT | no verificable sin claves | necesita una API key de CDP |
El cambio a mainnet se hizo deliberadamente sin liquidar primero un pago de testnet, por lo que la corrección del perfil se basa en lo que se verificó estáticamente: la dirección del activo contra la propia página de Circle y la cadena, el nombre de dominio EIP-712 contra el name() del token, y el /supported del facilitador contra la red. Revertir es la misma palabra.
El facilitador público x402.org no puede liquidar Base mainnet. Su /supported anuncia eip155:84532 y ninguna mainnet, y la documentación de x402 dice claramente que no lo trates como una ruta de producción — por lo que mainnet apunta a PayAI desde el directorio oficial de facilitadores en su lugar: sin API key, y anuncia Base mainnet bajo ambas versiones de protocolo. Un facilitador de terceros retransmite la transacción y paga el gas; no puede redirigir fondos, porque la autorización está firmada a nuestra dirección por nuestro monto exacto.
Verifica cualquier perfil contra la cadena y su facilitador antes de cambiar a él:
node scripts/verify-rail.mjs mainnet
Eso lee el name(), symbol(), decimals() y version() propios del token fuera de cadena y pregunta al facilitador qué liquidará realmente. Existe porque dos errores aquí son invisibles hasta que cada pago falla: una dirección de activo incorrecta y un nombre de dominio EIP-712 incorrecto — asset_name se publica como el dominio que el pagador firma, y USDC se llama a sí mismo "USDC" en Base Sepolia pero "USD Coin" en Base mainnet, por lo que es un campo por perfil.
Ir a mainnet fue exactamente eso: la verificación anterior, el activo inspeccionado una vez en basescan, "active": "mainnet", push. El riel activo ahora es cdp, que cuesta una API key de CDP (wrangler secret put CDP_API_KEY_ID / CDP_API_KEY_SECRET, nunca en site.config.json) y compra un nivel gratis de 1,000 tx/mes además del catálogo en el Bazaar de x402. El /supported de CDP responde 401, por lo que verify-rail.mjs lo firma con el mismo código que usa el Worker cuando esas dos variables están presentes — gh workflow run cf-admin -f action=verify-cdp lo ejecuta en el runner, el único lugar donde existen los secretos.
El catálogo del Bazaar no es automático solo porque el facilitador sea CDP: el listado se construye a partir de metadatos de descubrimiento adjuntos a una liquidación, por lo que un endpoint puede aceptar dinero real indefinidamente y nunca estar listado. /api/audit por lo tanto publica un outputSchema con discoverable: true (v1) y el mismo objeto bajo extensions.bazaar (v2) — una forma leída del catálogo en vivo en lugar de la documentación. node scripts/bazaar-check.mjs responde si funcionó.
Precios son unidades atómicas — USDC tiene 6 decimales, por lo que 50000 = $0.05. La auditoría completa es $0.05; una comprobación, una sonda en vivo, una consulta de ruta y un barrido semanal de vigilancia son cada uno $0.005; los niveles de listado verificado y destacado son $5 y $25. Los agentes pueden leer los términos en vivo en /api/x402/info sin provocar un 402.
Tarjeta (humanos) — un enlace de pago de Stripe en payments.stripe_payment_link; las respuestas rail: "card" de mejoras responden manual_reconciliation y se activan con scripts/set-tier.mjs. El producto x402 de Stripe liquida a un saldo de Stripe en fiat pero es vista previa privada detrás de una solicitud de acceso; adoptarlo más tarde es un cambio de facilitator_url, no una reescritura.
Autenticación CDP (worker/cdp-auth.js) está hecha a mano en WebCrypto en lugar de incluir @coinbase/x402, que arrastraría viem, zod y todo el SDK de CDP a un Worker por una firma. El contrato se leyó de esas fuentes publicadas del paquete: encabezado {alg, kid, typ, nonce} con alg de EdDSA (Ed25519) o ES256 (EC), claims {sub, iss: "cdp", nbf, exp, jti, uris}. El claim uris vincula cada token a un método+host+ruta, por lo que un token /verify no puede reproducirse contra /settle. Un riel que declara auth: "cdp" sin credenciales falla cerrado en lugar de disparar no autenticado y mostrar el 401 de Coinbase como si el pago del agente fuera malo.
La API de lectura no cambiará de forma — tier ha estado establecido del lado del servidor en cada listado desde el primer día.