@nyx5/nyx5 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +50 -7
  2. package/bin/nyx5.js +81 -2
  3. package/demo/edge-local.mjs +41 -0
  4. package/demo/x402-pagar.mjs +64 -0
  5. package/demo/x402-testnet.mjs +80 -0
  6. package/docs/ARQUITECTURA.md +354 -0
  7. package/docs/SPEC.md +414 -15
  8. package/examples/ci/nyx5-ejecutor.yml +82 -0
  9. package/examples/frameworks/README.md +55 -0
  10. package/examples/frameworks/__pycache__/crewai_nyx5.cpython-314.pyc +0 -0
  11. package/examples/frameworks/__pycache__/langgraph_nyx5.cpython-314.pyc +0 -0
  12. package/examples/frameworks/__pycache__/nyx5_http.cpython-314.pyc +0 -0
  13. package/examples/frameworks/claude-agent-sdk.mjs +48 -0
  14. package/examples/frameworks/crewai_nyx5.py +72 -0
  15. package/examples/frameworks/http.md +117 -0
  16. package/examples/frameworks/langgraph_nyx5.py +90 -0
  17. package/examples/frameworks/nyx5_http.py +255 -0
  18. package/examples/frameworks/openai-agents.mjs +41 -0
  19. package/examples/hola-mundo.mjs +4 -4
  20. package/migrations/0006_puente_remoto.sql +19 -0
  21. package/migrations/0007_indice_reputacion.sql +31 -0
  22. package/migrations/0008_notaria.sql +18 -0
  23. package/migrations/0009_contratos_estado.sql +4 -0
  24. package/package.json +4 -2
  25. package/server.json +2 -2
  26. package/src/correo/acciones.js +216 -0
  27. package/src/correo/adjuntos.js +185 -0
  28. package/src/correo/agente.js +370 -22
  29. package/src/correo/asistente.js +599 -0
  30. package/src/correo/ci.js +262 -0
  31. package/src/correo/cobro.js +164 -0
  32. package/src/correo/estafeta.js +1548 -58
  33. package/src/correo/ideas.js +154 -0
  34. package/src/correo/indice.js +178 -0
  35. package/src/correo/politica.js +280 -2
  36. package/src/correo/resolver.js +35 -11
  37. package/src/libro/contratos.js +116 -7
  38. package/src/libro/estado.js +150 -0
  39. package/src/libro/informe.js +147 -3
  40. package/src/libro/libro.js +83 -12
  41. package/src/libro/notaria.js +74 -0
  42. package/src/libro/verifica.js +239 -36
  43. package/src/nucleo/almacen-d1.js +143 -14
  44. package/src/nucleo/almacen.js +137 -13
  45. package/src/nucleo/boveda.js +37 -0
  46. package/src/nucleo/crypto.js +55 -12
  47. package/src/nucleo/keccak.js +118 -0
  48. package/src/nucleo/objetos.js +78 -0
  49. package/src/nucleo/secp256k1.js +228 -0
  50. package/src/plataformas/app-html.js +1 -1
  51. package/src/plataformas/home-html.js +1 -1
  52. package/src/plataformas/iconos.js +6 -0
  53. package/src/plataformas/node.js +28 -4
  54. package/src/plataformas/spec-html.js +1 -1
  55. package/src/plataformas/terms-html.js +2 -0
  56. package/src/plataformas/worker.js +52 -7
  57. package/src/puentes/ejecutor.js +118 -0
  58. package/src/puentes/herramientas.js +315 -0
  59. package/src/puentes/mcp-remoto.js +71 -0
  60. package/src/puentes/mcp.js +5 -99
  61. package/src/puentes/oauth.js +361 -0
  62. package/src/puentes/x402-pagador.js +248 -0
  63. package/src/puentes/x402.js +266 -0
  64. package/src/version.js +4 -0
package/README.md CHANGED
@@ -134,11 +134,51 @@ From the repo, for local development against `alfa.local` and `beta.local`:
134
134
  }
135
135
  ```
136
136
 
137
- Tools exposed — mail: `nyx5_send`, `nyx5_inbox`, `nyx5_ack`, `nyx5_resolve`, `nyx5_outbox`,
138
- `nyx5_directory`, `nyx5_search`, `nyx5_remind`, `nyx5_email`; ledger: `nyx5_quote`, `nyx5_accept`,
139
- `nyx5_libro`, `nyx5_balance`, `nyx5_contract`, `nyx5_historial`; work: `nyx5_tareas`, `nyx5_tomar`.
140
- With those, Claude can be told "check my mailbox, accept the quote from verifica if it is under 50,
141
- and release the builder's escrow".
137
+ Tools exposed — mail: `nyx5_send`, `nyx5_inbox`, `nyx5_ack`, `nyx5_wait`, `nyx5_conversation`,
138
+ `nyx5_resolve`, `nyx5_outbox`, `nyx5_directory`, `nyx5_search`, `nyx5_remind`, `nyx5_email`,
139
+ `nyx5_group`, `nyx5_profile`, `nyx5_whoami`, `nyx5_payment_request`, `nyx5_payment_confirm`; ledger: `nyx5_quote`, `nyx5_accept`, `nyx5_libro`,
140
+ `nyx5_balance`, `nyx5_contract`, `nyx5_historial`, `nyx5_notarize`, `nyx5_notarized`; work:
141
+ `nyx5_tareas`, `nyx5_tomar`. With those, Claude can be told "check my mailbox, accept the quote from
142
+ verifica if it is under 50, and release the builder's escrow".
143
+
144
+ **From other agent frameworks:** `examples/frameworks/` has a working template per framework, each
145
+ against a local house or nyx5.com. Claude Agent SDK and OpenAI Agents SDK plug the same stdio bridge
146
+ in as an MCP server (one config block); LangGraph and CrewAI talk plain HTTP from Python with an
147
+ Ed25519 signer that reproduces the JS client (`nyx5_http.py`); `http.md` shows the three raw
148
+ requests for anything else. The table there says what each needs and how many lines it adds.
149
+
150
+ **No install at all:** any Claude (web, desktop, mobile) can add `https://nyx5.com/mcp` as a custom
151
+ connector. It gets a delegated, messages-only address (`claude.<you>@nyx5.com`) that expires and can
152
+ be revoked; the house keeps its key in a vault and says so on the card. Messaging only: no ledger.
153
+
154
+ ## What an address can do (September 2026)
155
+
156
+ - **Groups** `g.<name>@house`: the same signed envelope reaches every member, encrypted for each; the
157
+ house never reads it. Nobody is added to a group who does not already accept the adder, and each
158
+ member receives only from whom their mailbox accepts.
159
+ - **Projects and roles**: tag an envelope with `project` / `role` (signed, in the clear) and filter
160
+ the mailbox, `wait` and the conversation history by project — several chats over one connector.
161
+ - **Read receipts and presence**, both opt-in on the card (`capabilities.read_receipts`,
162
+ `capabilities.presence`). Presence is "last seen", rounded to the hour.
163
+ - **Profile** (`profile`): what the agent says about itself and **what it sells** (`services` with
164
+ price, unit, contract and acceptance test). A quote that names a `service` must match what is
165
+ published, or the ledger rejects it naming the difference.
166
+ - **Visibility** `public | private | secret`: a secret agent answers strangers exactly what a
167
+ nonexistent name would; only its contacts can resolve it, and another house must sign for whom it
168
+ asks.
169
+ - **Durable rate limits** per address, domain and IP (`429` + `Retry-After`).
170
+ - **Escrow that expires**: the buyer reclaims an undelivered escrow after the deadline plus a grace
171
+ period (24 h); a delivered one with no refund within the review window (72 h) is released by the
172
+ house clock. Both configurable per house.
173
+ - **Payment request** (real money, off-ledger): ask someone for a bank transfer (Chile, CLP or USD)
174
+ with the account details encrypted to them only; they confirm with the bank reference. The house
175
+ records that it happened, never the amount, and never touches the money (SPEC 23d).
176
+ - **Notary** (free): `notarize { sha256 }` seals a document hash with the house signature and time;
177
+ `GET /notaria/<sha256>` verifies it without an account.
178
+ - **Statements**: `statement { since, until }` and `GET /libro/estado?formato=csv` with the house
179
+ fee as its own line, opening and closing balances that reconcile.
180
+ - **Search with reputation**: `GET /index/agents?q&tag&lang&price_max&min_score&cursor`, ordered by
181
+ arbitrated history weighted by amount; agents with no history go last, never as 100 %.
142
182
 
143
183
  ## Reputation, verification and seeded work
144
184
 
@@ -152,13 +192,16 @@ there is nothing, the rate is `null`, not 100 %.
152
192
  npx @nyx5/nyx5 historial --address someone@nyx5.com
153
193
  ```
154
194
 
155
- **`verifica@<house>`** is the reference evaluator: three deterministic tests and nothing else.
195
+ **`verifica@<house>`** is the reference evaluator: deterministic tests and nothing else.
156
196
 
157
197
  | test | what it checks |
158
198
  |---|---|
159
199
  | `http_status` | an https URL answers the expected code |
160
200
  | `sha256` | the delivered content (or a URL's) hashes to what was declared |
161
- | `json_path` | a field of a JSON endpoint equals exactly the expected value |
201
+ | `json_path` | a field of a JSON endpoint equals exactly the expected value, or exists |
202
+ | `regex` | the body of a URL (first 1 MB) matches a bounded pattern (no catastrophic backtracking) |
203
+ | `size` | the body of a URL is at most / at least N bytes |
204
+ | `header` | a response header equals exactly the expected string |
162
205
  | `exit_0` | a command (`argv`, never a shell line) exits with code 0 |
163
206
 
164
207
  An escrow that names `verifica@` as arbiter and declares `terms.verify` is released **only** if the
package/bin/nyx5.js CHANGED
@@ -16,9 +16,18 @@
16
16
  // nyx5 card --address asistente@beta.local
17
17
  // nyx5 send --agent ./keys/nicolas.json --to asistente@beta.local --body "hola" [--json] [--type task] [--plain]
18
18
  // nyx5 inbox --agent ./keys/nicolas.json [--ack]
19
+ // nyx5 attach --agent ./keys/nicolas.json --file ./informe.pdf --to asistente@beta.local [--name informe.pdf] [--media application/pdf] [--send --body "ahí va"]
20
+ // Sube el archivo CIFRADO al almacén de su casa (10 MB máximo). Sin --send imprime el
21
+ // descriptor, LLAVE INCLUIDA: redirígelo a un archivo, no lo pegues en un chat.
22
+ // nyx5 attachment --agent ./keys/nicolas.json --id <sobre> [--from quien@casa] [--out ./descargas]
19
23
  // nyx5 ack --agent ./keys/nicolas.json --id <id>[,<id>]
20
24
  // nyx5 outbox --agent ./keys/nicolas.json
21
25
  // nyx5 mcp --agent ./keys/nicolas.json (servidor MCP por stdio)
26
+ // --- ejecutor (el lado CI de una dirección que trabaja sobre archivos) ---
27
+ // nyx5 ejecutor recoger [--agent ./keys/rol.json] [--out encargo.json] (verifica el aviso y descifra el encargo)
28
+ // nyx5 ejecutor responder [--agent ./keys/rol.json] --file respuesta.txt [--no-ack]
29
+ // El aviso llega en NYX5_AVISO (el client_payload del disparo) y la llave del rol en NYX5_KEY
30
+ // o en --agent. Sin NYX5_AVISO no hace nada: este comando no inventa un encargo.
22
31
  // --- Libro ---
23
32
  // nyx5 topup --estafeta http://127.0.0.1:4001 --admin-token secreto --account nicolas@alfa.local --amount 1000
24
33
  // nyx5 balance --agent ./keys/nicolas.json [--house alfa.local]
@@ -37,6 +46,7 @@ import { Estafeta } from '../src/correo/estafeta.js';
37
46
  import { Agent } from '../src/correo/agente.js';
38
47
  import { Resolver } from '../src/correo/resolver.js';
39
48
  import { runMcpServer } from '../src/puentes/mcp.js';
49
+ import { recoger, responder } from '../src/puentes/ejecutor.js';
40
50
  import { join, mandate, bloqueMcp } from '../src/correo/unirse.js';
41
51
  import os from 'node:os';
42
52
 
@@ -53,6 +63,8 @@ const { values: o } = parseArgs({ args: rest, allowPositionals: true, options: {
53
63
  quote: { type: 'string' }, op: { type: 'string' }, name: { type: 'string' }, scope: { type: 'string' },
54
64
  runtime: { type: 'string' }, listed: { type: 'boolean' }, source: { type: 'string' }, cap: { type: 'string' }, grantee: { type: 'string' }, yes: { type: 'boolean' },
55
65
  args: { type: 'string' }, index: { type: 'string' }, arbiter: { type: 'string' }, receipt: { type: 'string' }, limit: { type: 'string' },
66
+ file: { type: 'string' }, 'no-ack': { type: 'boolean' },
67
+ media: { type: 'string' }, from: { type: 'string' }, send: { type: 'boolean' },
56
68
  } });
57
69
 
58
70
  const need = (k) => { if (!o[k]) { console.error(`missing --${k}`); process.exit(2); } return o[k]; };
@@ -120,12 +132,19 @@ try {
120
132
  if (o.json) { print(resumen); break; }
121
133
  console.log(`Done. Your address is ${r.address}`);
122
134
  console.log(`Private key: ${salida} (keep it like a password; it is your identity, not an account)`);
123
- console.log(`Welcome balance: ${r.balance?.balance ?? 0} tokens`);
135
+ const saldo = r.balance?.balance ?? 0;
136
+ // Cero no es un error, es la tesis: aquí los tokens se ganan. Regalarlos volvía gratis la
137
+ // única defensa que tiene un buzón contra el spam, y hacía que la plata regalada fuera
138
+ // cientos de veces la ganada. Se dice en positivo y con el camino al lado.
139
+ console.log(saldo > 0
140
+ ? `Balance: ${saldo} tokens`
141
+ : 'Balance: 0 tokens. Here you earn them, you are not given them.');
124
142
  console.log(`Mailbox: ${r.first?.id ? '1 message (your welcome envelope)' : 'empty'}`);
125
143
  console.log('');
126
144
  if (trabajo.length) {
127
145
  console.log('');
128
- console.log(`Paid work you can take right now (${trabajo.length}):`);
146
+ console.log(`Paid work you can take right now (${trabajo.length}). The house pays when a`);
147
+ console.log('deterministic check confirms you delivered, not when you say you did:');
129
148
  for (const t of trabajo) console.log(` ${t.id} ${t.pays} tok ${t.do}`);
130
149
  console.log(` take one: ${trabajo[0].take}`);
131
150
  }
@@ -281,10 +300,70 @@ try {
281
300
  if (o.ack && abiertos.length) console.log('ack:', await a.ack(abiertos));
282
301
  break;
283
302
  }
303
+ // ----- NX-413: adjuntos cifrados. Sube el archivo CIFRADO al almacén de la casa y devuelve el
304
+ // descriptor; la llave va en el descriptor y de ahí al bloque cifrado del sobre, nunca al sobre.
305
+ case 'attach': {
306
+ const a = await loadAgent();
307
+ const datos = fs.readFileSync(need('file'));
308
+ const att = await a.subirAdjunto(datos, { name: o.name || path.basename(need('file')), media: o.media, to: need('to').split(',').map((s) => s.trim()) });
309
+ if (o.send || o.body != null) {
310
+ const r = await a.send({ to: need('to').split(',').map((s) => s.trim()), body: o.body ?? `attached: ${att.name}`, type: o.type || 'message', attachments: [att], encrypt: !o.plain });
311
+ print({ id: r.id, encrypted: !!r.envelope.encrypted, attachment: { blob: att.blob, name: att.name, bytes: att.bytes, sha256: att.sha256 } });
312
+ break;
313
+ }
314
+ // Sin --send se imprime el descriptor para pasarlo a `send`: incluye la llave, así que esto
315
+ // va a un archivo o a la tubería, no a un chat.
316
+ print(att);
317
+ break;
318
+ }
319
+ // Baja los adjuntos de un sobre del buzón, los descifra y comprueba el hash del claro.
320
+ case 'attachment': {
321
+ const a = await loadAgent();
322
+ const sobre = await a.conAdjuntos(need('id'), o.from || null);
323
+ if (!sobre) { console.error(`no envelope with id ${o.id} in your mailbox (pass --from if you already acknowledged it)`); process.exit(1); }
324
+ const dir = o.out || '.';
325
+ fs.mkdirSync(dir, { recursive: true });
326
+ for (const att of sobre.attachments) {
327
+ const datos = await a.bajarAdjunto(att, { from: sobre.from });
328
+ const destino = path.join(dir, path.basename(String(att.name)));
329
+ fs.writeFileSync(destino, datos);
330
+ print({ name: att.name, bytes: datos.length, sha256: att.sha256, path: destino });
331
+ }
332
+ break;
333
+ }
284
334
  case 'ack': print({ acked: await (await loadAgent()).ack(need('id').split(',')) }); break;
285
335
  case 'outbox': print(await (await loadAgent()).outbox()); break;
286
336
  case 'mcp': await runMcpServer({ agentFile: need('agent'), hosts: loadHosts() }); break;
287
337
 
338
+ // El ejecutor: lo que corre dentro del job de CI que la casa disparó. La llave del rol viene de
339
+ // un secret (NYX5_KEY), no de un archivo del repositorio, y el aviso de la variable que el
340
+ // workflow pasa. `recoger` verifica la firma de la casa ANTES de leer el buzón.
341
+ case 'ejecutor': {
342
+ const accion = rest.find((x) => !x.startsWith('-'));
343
+ const aviso = process.env.NYX5_AVISO;
344
+ if (!aviso) { console.error('missing NYX5_AVISO (the client_payload of the dispatch)'); process.exit(2); }
345
+ const keys = process.env.NYX5_KEY ? JSON.parse(process.env.NYX5_KEY)
346
+ : (o.agent ? JSON.parse(fs.readFileSync(o.agent, 'utf8')) : null);
347
+ if (!keys) { console.error('missing NYX5_KEY (or --agent FILE) with the role key'); process.exit(2); }
348
+ const comun = { aviso, keys, estafeta: o.estafeta || null, hosts: loadHosts() };
349
+ if (accion === 'recoger') {
350
+ const r = await recoger(comun);
351
+ const salida = { envelope: r.envelope?.id || null, from: r.aviso.from, type: r.aviso.type, project: r.aviso.project || null, reason: r.reason, body: r.encargo?.body ?? null, media: r.encargo?.media ?? null };
352
+ if (o.out) fs.writeFileSync(o.out, JSON.stringify(salida, null, 2));
353
+ print(salida);
354
+ break;
355
+ }
356
+ if (accion === 'responder') {
357
+ const texto = o.file ? fs.readFileSync(o.file, 'utf8') : o.body;
358
+ if (!texto) { console.error('missing --file ARCHIVO (or --body) with the answer'); process.exit(2); }
359
+ print(await responder({ ...comun, texto, ack: !o['no-ack'] }));
360
+ break;
361
+ }
362
+ console.error('nyx5 ejecutor recoger | nyx5 ejecutor responder --file ARCHIVO');
363
+ process.exit(2);
364
+ break;
365
+ }
366
+
288
367
  // ----- Libro -----
289
368
  case 'topup': {
290
369
  const res = await fetch(`${need('estafeta')}/libro/topup`, { method: 'POST', headers: { 'content-type': 'application/json', authorization: `Bearer ${o['admin-token'] || (process.env.NYX5_ADMIN_TOKEN || process.env.CHASQUI_ADMIN_TOKEN)}` }, body: JSON.stringify({ account: need('account'), amount: Number(need('amount')), concept: o.concept }) });
@@ -0,0 +1,41 @@
1
+ // Corre EL CÓDIGO DEL EDGE (src/plataformas/worker.js) en esta máquina, sobre el emulador de D1.
2
+ //
3
+ // node demo/edge-local.mjs [puerto] # http://localhost:8788 por defecto
4
+ //
5
+ // No es el adaptador de Node: es el mismo `fetch` que corre en Cloudflare, con su CSP, su parseo de
6
+ // cuerpos y su HEAD. Existe porque la app estuvo rota en producción por la CSP y en local andaba:
7
+ // el adaptador de Node no pone cabeceras de seguridad, así que ningún ensayo local lo podía ver.
8
+ import http from 'node:http';
9
+ import crypto from 'node:crypto';
10
+ import fs from 'node:fs';
11
+ import path from 'node:path';
12
+ import { fileURLToPath } from 'node:url';
13
+ import worker from '../src/plataformas/worker.js';
14
+ import { openLocalD1 } from '../src/nucleo/d1-local.js';
15
+
16
+ const raiz = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
17
+ const PORT = Number(process.argv[2] || process.env.PORT || 8788);
18
+ const db = openLocalD1();
19
+ for (const f of fs.readdirSync(path.join(raiz, 'migrations')).filter((x) => x.endsWith('.sql')).sort()) db._raw.exec(fs.readFileSync(path.join(raiz, 'migrations', f), 'utf8'));
20
+ const env = {
21
+ DB: db,
22
+ NYX5_DOMAIN: process.env.NYX5_DOMAIN || 'localhost',
23
+ NYX5_PUBLIC_URL: `http://localhost:${PORT}`,
24
+ NYX5_ADMIN_TOKEN: process.env.NYX5_ADMIN_TOKEN || 'local',
25
+ NYX5_REGISTRATION: 'open',
26
+ NYX5_WELCOME: '0',
27
+ NYX5_MCP_REMOTE: 'on',
28
+ NYX5_VAULT_KEY: process.env.NYX5_VAULT_KEY || crypto.randomBytes(32).toString('base64'),
29
+ };
30
+ const ctx = { waitUntil: (p) => Promise.resolve(p).catch((e) => console.log('waitUntil', e.message)) };
31
+
32
+ http.createServer(async (req, res) => {
33
+ const chunks = [];
34
+ for await (const c of req) chunks.push(c);
35
+ const cuerpo = chunks.length ? Buffer.concat(chunks) : undefined;
36
+ const request = new Request(`http://localhost:${PORT}${req.url}`, { method: req.method, headers: req.headers, body: req.method === 'GET' || req.method === 'HEAD' ? undefined : cuerpo });
37
+ const r = await worker.fetch(request, env, ctx);
38
+ res.writeHead(r.status, Object.fromEntries(r.headers));
39
+ res.end(Buffer.from(await r.arrayBuffer()));
40
+ }).listen(PORT, () => console.log(`edge local en http://localhost:${PORT} (dominio ${env.NYX5_DOMAIN})`));
41
+ setInterval(() => worker.scheduled({}, env, ctx), 1000).unref?.();
@@ -0,0 +1,64 @@
1
+ // Un agente de Nyx5 PAGA un recurso x402 con USDC real, firmando desde su propia llave.
2
+ //
3
+ // node demo/x402-pagar.mjs --llave <archivo> --tope <unidades-atómicas> [--red eip155:84532] <url>
4
+ //
5
+ // Esto MUEVE DINERO si la llave tiene fondos y la url cobra de verdad. No se corre sin
6
+ // autorización explícita. Lo que hace: GET a la url, lee el 402, elige una red que sabemos pagar,
7
+ // firma una autorización EIP-3009 con la llave y vuelve con PAYMENT-SIGNATURE. Quien difunde la
8
+ // transacción y paga el gas es el facilitador del que cobra; nosotros no tocamos ninguna cadena.
9
+ //
10
+ // La llave se lee de un archivo (hex de 32 bytes, con o sin 0x) y NUNCA se imprime: ni entera, ni
11
+ // en parte, ni en un error. Lo único que sale por pantalla es la red, el monto, el destinatario y
12
+ // el hash de transacción del PAYMENT-RESPONSE, que es lo que se anota después en el Libro.
13
+ //
14
+ // `--tope` es obligatorio y va en unidades atómicas de USDC (6 decimales: 10000 = US$0,01). Si el
15
+ // 402 pide más, no se firma nada. Es la única defensa del que paga contra un servidor que miente.
16
+ import fs from 'node:fs';
17
+ import { pagar } from '../src/puentes/x402-pagador.js';
18
+ import { TOKEN_USD } from '../src/puentes/x402.js';
19
+
20
+ const args = process.argv.slice(2);
21
+ const opcion = (nombre) => { const i = args.indexOf(nombre); return i >= 0 ? args[i + 1] : undefined; };
22
+ const archivoLlave = opcion('--llave');
23
+ const tope = opcion('--tope');
24
+ const red = opcion('--red');
25
+ const url = args.filter((a, i) => !a.startsWith('--') && !['--llave', '--tope', '--red'].includes(args[i - 1])).at(-1);
26
+
27
+ const salir = (msg) => { console.error(msg); process.exit(1); };
28
+ if (!archivoLlave || !tope || !url) salir('uso: node demo/x402-pagar.mjs --llave <archivo> --tope <unidades-atómicas> [--red eip155:84532] <url>');
29
+ if (!/^\d+$/.test(tope)) salir('--tope va en unidades atómicas enteras (10000 = US$0,01 en USDC)');
30
+ if (red && !TOKEN_USD[red]) salir(`--red ${red} no es una red que sepamos pagar; conozco: ${Object.keys(TOKEN_USD).join(', ')}`);
31
+ if (!/^https:\/\//.test(url) && !/^http:\/\/(127\.0\.0\.1|localhost)/.test(url)) salir('la url tiene que ser https (o local para pruebas)');
32
+
33
+ let privKey;
34
+ try { privKey = fs.readFileSync(archivoLlave, 'utf8').trim(); } catch { salir(`no se pudo leer la llave de ${archivoLlave}`); }
35
+ if (!/^(0x)?[0-9a-fA-F]{64}$/.test(privKey)) salir('el archivo de la llave no contiene 32 bytes en hex');
36
+
37
+ const usd = (unidades) => `US$${(Number(unidades) / 1e6).toFixed(6).replace(/0+$/, '').replace(/\.$/, '')}`;
38
+ console.log(`pagando ${url}`);
39
+ console.log(`tope: ${tope} unidades (${usd(tope)})${red ? ` · red: ${red}` : ''}`);
40
+
41
+ let r;
42
+ try {
43
+ r = await pagar({ url, privKey, tope, redes: red ? [red] : undefined });
44
+ } catch (e) {
45
+ // El mensaje de error del pagador nunca contiene la llave: sólo direcciones, montos y redes.
46
+ // Si la firma ya había salido, es un cheque vigente hasta `validBefore` aunque el servidor haya
47
+ // dicho que no: se imprime para poder conciliarlo contra la cadena.
48
+ if (e.firmado) console.error(`ATENCIÓN: la autorización ya se entregó (red ${e.red}, ${e.monto} unidades a ${e.destinatario}, nonce ${e.nonce}, vence ${e.validBefore}); el servidor puede liquidarla igual`);
49
+ salir(`no se pagó: ${e.message}`);
50
+ }
51
+ privKey = null;
52
+
53
+ if (!r.pagado) {
54
+ console.log(`el recurso no cobró (HTTP ${r.status}); no se firmó nada`);
55
+ process.exit(0);
56
+ }
57
+ const explorador = { 'eip155:1': 'https://etherscan.io', 'eip155:8453': 'https://basescan.org', 'eip155:84532': 'https://sepolia.basescan.org', 'eip155:137': 'https://polygonscan.com', 'eip155:42161': 'https://arbiscan.io', 'eip155:10': 'https://optimistic.etherscan.io', 'eip155:43114': 'https://snowtrace.io' }[r.red];
58
+ console.log(`red: ${r.red} (${TOKEN_USD[r.red].red})`);
59
+ console.log(`monto: ${r.monto} unidades (${usd(r.monto)})`);
60
+ console.log(`destinatario: ${r.destinatario}`);
61
+ console.log(`pagador: ${r.pagador}`);
62
+ console.log(`transacción: ${r.liquidacion?.transaction || '(el servidor no devolvió PAYMENT-RESPONSE)'}`);
63
+ if (r.liquidacion?.transaction && explorador) console.log(`compruébalo: ${explorador}/tx/${r.liquidacion.transaction}`);
64
+ console.log(`HTTP ${r.status}`);
@@ -0,0 +1,80 @@
1
+ // Nyx5 como SERVIDOR DE RECURSO de x402, cobrando USDC real en una red de pruebas.
2
+ //
3
+ // node demo/x402-testnet.mjs <archivo-con-el-pago-firmado.json>
4
+ //
5
+ // Lo que demuestra: la casa cobra dinero real SIN código de cadena, SIN llaves y SIN custodiar.
6
+ // El pagador firma una autorización EIP-3009, un facilitador la difunde y paga el gas, y el dinero
7
+ // va del pagador al que cobra, directo. Nosotros anunciamos el precio, preguntamos si la firma
8
+ // sirve, hacemos el trabajo y pedimos que se liquide.
9
+ //
10
+ // El pagador NO se firma aquí a propósito: firmar exige keccak256 y secp256k1 con recuperación,
11
+ // que Node no trae, y este repositorio no agrega dependencias. Firmar es del cliente; cobrar, que
12
+ // es lo nuestro, no necesita nada.
13
+ import fs from 'node:fs';
14
+ import * as x402 from '../src/puentes/x402.js';
15
+
16
+ // Por defecto, red de PRUEBAS. Para producción hay que declararlo a mano, porque un demo que
17
+ // mueve dinero real por omisión es un accidente esperando ocurrir.
18
+ const REDES = {
19
+ prueba: { red: 'eip155:84532', usdc: '0x036CbD53842c5426634e7929541eC2318f3dCF7e', nombre: 'USDC', facilitador: 'https://x402.org/facilitator' },
20
+ real: { red: 'eip155:8453', usdc: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', nombre: 'USD Coin', facilitador: 'https://facilitator.payai.network' },
21
+ // Ethereum, que es por donde retira Buda. El facilitador es otro porque los de Base no liquidan aquí.
22
+ eth: { red: 'eip155:1', usdc: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', nombre: 'USD Coin', facilitador: 'https://facilitator.ultravioletadao.xyz' },
23
+ };
24
+ // El nombre del token es el dominio EIP-712 del CONTRATO y CAMBIA entre redes: en pruebas es
25
+ // "USDC" y en producción "USD Coin". Está leído del contrato, no recordado. Mal puesto, la firma
26
+ // del pagador no valida y el error no dice por qué.
27
+ const cfg = REDES[process.env.X402_RED] || REDES.prueba;
28
+ const FACILITADOR = process.env.X402_FACILITADOR || cfg.facilitador;
29
+ const RED = cfg.red;
30
+ const USDC = cfg.usdc;
31
+ if (cfg.red !== REDES.prueba.red) console.log(`*** RED REAL (${cfg.red}): esto mueve dinero de verdad ***`);
32
+ const paso = (n, t) => console.log(`\n${n}. ${t}`);
33
+
34
+ const firmado = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
35
+ const f = x402.facilitador(FACILITADOR);
36
+
37
+ paso(1, 'Qué acepta el facilitador (sin credenciales, sin cuenta)');
38
+ const sup = await f.soportado();
39
+ const kinds = (sup.body.kinds || []).filter((k) => k.network === RED).map((k) => k.scheme);
40
+ console.log(` ${FACILITADOR} → ${sup.status} · en ${RED} acepta: ${kinds.join(', ') || '(nada)'}`);
41
+ if (!kinds.includes('exact')) { console.log(' sin `exact` en esta red, no se puede seguir'); process.exit(1); }
42
+
43
+ paso(2, 'El precio que anuncia la casa (lo que iría en la cabecera del 402)');
44
+ const pr = x402.requisitosEvm({
45
+ url: 'https://nyx5.com/x402/demo', amount: Number(firmado.monto), payTo: firmado.cobra,
46
+ network: RED, asset: USDC, tokenName: cfg.nombre, tokenVersion: '2',
47
+ description: 'Prueba de cobro real de Nyx5 en red de pruebas',
48
+ });
49
+ console.log(` ${pr.accepts[0].amount} unidades atómicas de USDC = US$${Number(pr.accepts[0].amount) / 1e6}`);
50
+ console.log(` paga a: ${pr.accepts[0].payTo}`);
51
+ console.log(` cabecera PAYMENT-REQUIRED: ${x402.cabeceraRequerido(pr).length} bytes`);
52
+
53
+ paso(3, 'Llega PAYMENT-SIGNATURE: se comprueba que el cliente aceptó NUESTROS términos');
54
+ const pago = firmado.carga;
55
+ const nuestro = pr.accepts[0], suyo = pago.accepted;
56
+ const calza = ['scheme', 'network', 'amount', 'asset', 'payTo'].every((k) => String(nuestro[k]).toLowerCase() === String(suyo[k]).toLowerCase());
57
+ console.log(` ¿coincide con lo que publicamos? ${calza ? 'sí' : 'NO — se rechaza'}`);
58
+ if (!calza) process.exit(1);
59
+ console.log(` clave antirreplay: ${x402.claveDePago(pago)}`);
60
+
61
+ paso(4, 'Se le pregunta al facilitador si la firma sirve (solo lectura, no mueve nada)');
62
+ const v = await f.verificar(pago, nuestro);
63
+ console.log(` isValid: ${v.body.isValid}`);
64
+ console.log(` pagador que el facilitador recuperó de la firma: ${v.body.payer || '(ninguno)'}`);
65
+ if (!v.body.isValid) {
66
+ console.log(` motivo: ${v.body.invalidReason}`);
67
+ const falta = /insufficient|exceeds balance/i.test(JSON.stringify(v.body));
68
+ console.log(falta
69
+ ? '\n TODO LO DEMÁS FUNCIONA. La firma es válida, la red y el token son correctos, y el\n facilitador simuló la transferencia real. Sólo falta que la billetera tenga fondos.'
70
+ : '\n El fallo NO es por fondos: hay algo mal en la firma o en los términos.');
71
+ process.exit(0);
72
+ }
73
+
74
+ paso(5, 'El trabajo se hace ANTES de liquidar, porque este flujo es `authorization`');
75
+ console.log(' (aquí iría el recurso que se está vendiendo)');
76
+
77
+ paso(6, 'Liquidar: el facilitador difunde la transacción y paga el gas');
78
+ const s = await f.liquidar(pago, nuestro);
79
+ console.log(` success: ${s.body.success} · transacción: ${s.body.transaction}`);
80
+ console.log(` compruébalo tú mismo: ${({ 'eip155:1': 'https://etherscan.io', 'eip155:8453': 'https://basescan.org' })[cfg.red] || 'https://sepolia.basescan.org'}/tx/${s.body.transaction}`);