@nyx5/nyx5 0.4.3 → 0.6.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.
package/bin/nyx5.js CHANGED
@@ -95,15 +95,26 @@ try {
95
95
  fs.mkdirSync(path.dirname(salida), { recursive: true });
96
96
  fs.writeFileSync(salida, JSON.stringify({ address: r.address, estafeta: r.estafeta, keys: r.keys }, null, 2), { mode: 0o600 });
97
97
  const mcpBloque = bloqueMcp({ address: r.address, keyfile: salida });
98
+ // Qué trabajo hay AHORA, traído en el mismo paso. Un agente recién unido no va a ir a
99
+ // buscarlo: si no lo ve aquí, se va. Que esto falle no puede tumbar el alta.
100
+ let trabajo = [];
101
+ try {
102
+ const dcw = await new Resolver({ hosts: loadHosts() }).domainCard(casa);
103
+ const jw = await (await fetch(`${dcw._estafeta}/tareas`)).json();
104
+ trabajo = (jw.tasks || jw.tareas || []).map((t) => ({ id: t.id, pays: t.price, do: t.concept, take: `npx @nyx5/nyx5 tomar --agent ${salida} --id ${t.id}` }));
105
+ } catch { trabajo = []; }
98
106
  const resumen = {
99
107
  nyx5: '1', address: r.address, house: casa, keyfile: salida,
100
108
  balance: r.balance?.balance ?? null, record: r.historial?.resumen ?? null,
101
109
  mcp: mcpBloque, mailbox: r.first?.id ? 1 : 0,
110
+ paid_work_available_now: trabajo,
102
111
  next: {
112
+ take_paid_work: trabajo[0]?.take || `npx @nyx5/nyx5 tareas --house ${casa}`,
103
113
  read_mailbox: `npx @nyx5/nyx5 inbox --agent ${salida}`,
104
114
  my_record: `npx @nyx5/nyx5 historial --agent ${salida}`,
105
- find_paid_work: `npx @nyx5/nyx5 tareas --house ${casa}`,
106
115
  get_a_budget: `ask your principal to run: npx @nyx5/nyx5 mandate --agent <their-key> --grantee ${r.address} --cap 20000`,
116
+ // Creer que hay que instalar algo y reiniciar el cliente es la fricción que sobra.
117
+ mcp_is_optional: 'every command above runs from the shell; the MCP block only makes these tools appear inside your client',
107
118
  },
108
119
  };
109
120
  if (o.json) { print(resumen); break; }
@@ -112,7 +123,15 @@ try {
112
123
  console.log(`Welcome balance: ${r.balance?.balance ?? 0} tokens`);
113
124
  console.log(`Mailbox: ${r.first?.id ? '1 message (your welcome envelope)' : 'empty'}`);
114
125
  console.log('');
115
- console.log('To use it from Claude Desktop, Claude Code or Cursor, add this to your MCP config:');
126
+ if (trabajo.length) {
127
+ console.log('');
128
+ console.log(`Paid work you can take right now (${trabajo.length}):`);
129
+ for (const t of trabajo) console.log(` ${t.id} ${t.pays} tok ${t.do}`);
130
+ console.log(` take one: ${trabajo[0].take}`);
131
+ }
132
+ console.log('');
133
+ console.log('Everything above works from the shell. The MCP block is OPTIONAL — it only makes');
134
+ console.log('these tools appear inside Claude, Cursor or another client:');
116
135
  console.log(JSON.stringify(mcpBloque.mcpServers ? { mcpServers: mcpBloque.mcpServers } : mcpBloque, null, 2));
117
136
  console.log('');
118
137
  console.log('Next:');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nyx5/nyx5",
3
- "version": "0.4.3",
3
+ "version": "0.6.0",
4
4
  "description": "Nyx5/1 — mail and ledger for AI agents: agent@domain addresses, a store-and-forward mailbox, and a ledger where a claim costs something. One command to join, escrow released only against a deterministic check, and reputation that is just a query on the ledger. Zero dependencies.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
package/server.json CHANGED
@@ -7,13 +7,13 @@
7
7
  "url": "https://github.com/Nicoiakl/nyx5",
8
8
  "source": "github"
9
9
  },
10
- "version": "0.4.3",
10
+ "version": "0.6.0",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "registryBaseUrl": "https://registry.npmjs.org",
15
15
  "identifier": "@nyx5/nyx5",
16
- "version": "0.4.3",
16
+ "version": "0.6.0",
17
17
  "runtimeHint": "npx",
18
18
  "transport": {
19
19
  "type": "stdio"
@@ -26,6 +26,7 @@ import { Libro, MEDIA, LibroError } from '../libro/libro.js';
26
26
  import { veredicto, pruebasDe, pruebasDisponibles } from '../libro/verifica.js';
27
27
  import { contratoPublico, ACP } from '../libro/contratos.js';
28
28
  import { Tareas } from '../libro/tareas.js';
29
+ import { datosInforme, informeHtml } from '../libro/informe.js';
29
30
  import { APP_HTML } from '../plataformas/app-html.js';
30
31
  import { SPEC_HTML, LLMS_TXT } from '../plataformas/spec-html.js';
31
32
  import { HOME_HTML } from '../plataformas/home-html.js';
@@ -55,7 +56,7 @@ export class Estafeta {
55
56
  this.fetch = (...a) => fetchImpl(...a); // envuelto: workerd exige fetch con this=globalThis
56
57
  this.log = log;
57
58
  // registration: 'admin' (solo la casa inscribe) | 'invite' (código emitido por la casa) | 'open' (cualquiera, con prueba de posesión de clave)
58
- this.policy = { inbound: 'verified', max_bytes: 1_048_576, rate_per_minute: 120, registration: 'admin', registrations_per_minute: 10, ...policy };
59
+ this.policy = { inbound: 'verified', max_bytes: 1_048_576, rate_per_minute: 120, registration: 'admin', registrations_per_minute: 10, min_name_length: 4, ...policy };
59
60
  this.retry = { baseMs: 1000, maxMs: 60_000, giveUpMs: 3 * 24 * 3600 * 1000, ...retry };
60
61
  this.workerIntervalMs = workerIntervalMs;
61
62
  this.index = { enabled: false, crawlMinutes: 15, maxHouses: 500, ...index };
@@ -227,6 +228,12 @@ export class Estafeta {
227
228
  const address = `${local}@${this.domain}`;
228
229
  parseAddress(address);
229
230
  if (Estafeta.RESERVED.has(local) && !this.isSystem(local)) throw Object.assign(new Error(`name reserved by the protocol: ${local}`), { status: 409 });
231
+ // Nombres de 1 a 3 caracteres: reservados en una casa de registro abierto. Son lo primero
232
+ // que alguien acapara para revender o para suplantar (a@casa se confunde con cualquiera), y
233
+ // un agente que llega no necesita un nombre corto: necesita uno suyo. Los de sistema pasan.
234
+ if (local.length <= this.policy.min_name_length - 1 && !this.isSystem(local) && !delegation) {
235
+ throw Object.assign(new Error(`names shorter than ${this.policy.min_name_length} characters are reserved in this house`), { status: 409 });
236
+ }
230
237
  if (delegation) {
231
238
  // Tarjeta delegada: el agente padre firma { by, address, sig, scope, valid_until }; el dominio la certifica igual.
232
239
  const { local: parentLocal, domain: parentDomain } = parseAddress(delegation.by);
@@ -814,6 +821,23 @@ export class Estafeta {
814
821
  if (rx.method === 'GET' && (path === '/terms' || path === '/terms/') && this.terms) {
815
822
  return { status: 200, contentType: 'text/html; charset=utf-8', body: this.terms };
816
823
  }
824
+ // El libro de la casa, en público. Se calcula al pedirlo: así nunca está viejo, y no hay
825
+ // un trabajo de fondo que pueda fallar en silencio dejando publicado un número de hace un
826
+ // mes. Sin nombres, sin contrapartes y sin contenido: solo cuántos y cuánto.
827
+ if (rx.method === 'GET' && (path === '/report' || path === '/report/')) {
828
+ try {
829
+ const dias = Math.min(90, Math.max(1, Number(rx.query.get('days') || 7) || 7));
830
+ return { status: 200, contentType: 'text/html; charset=utf-8', body: informeHtml(this.domain, await datosInforme(this, { dias })) };
831
+ } catch (e) {
832
+ // Un informe que no se puede calcular se dice, no se inventa.
833
+ this.log(`informe no disponible: ${e.message}`);
834
+ return send(503, { reason: 'the ledger could not be read right now' });
835
+ }
836
+ }
837
+ if (rx.method === 'GET' && path === '/report.json') {
838
+ try { return send(200, await datosInforme(this, { dias: Math.min(90, Math.max(1, Number(rx.query.get('days') || 7) || 7)) })); }
839
+ catch { return send(503, { reason: 'the ledger could not be read right now' }); }
840
+ }
817
841
  // La imagen de la vista previa al compartir un enlace. Se sirve desde la casa y no desde
818
842
  // un CDN externo para no depender de nadie: si esta URL falla, el enlace se comparte pelado.
819
843
  if (rx.method === 'GET' && path === '/og.png') {
@@ -828,7 +852,7 @@ export class Estafeta {
828
852
  return { status: 200, contentType: 'text/plain; charset=utf-8', body: `User-agent: *\nAllow: /\n# Los buzones y el Libro exigen firma; no hay nada que rastrear ahí.\nDisallow: /mailbox/\nDisallow: /libro/\nDisallow: /outbox/\nSitemap: https://${this.domain}/sitemap.xml\n` };
829
853
  }
830
854
  if (rx.method === 'GET' && path === '/sitemap.xml') {
831
- const paginas = ['/', '/spec', '/app', '/llms.txt'].concat(this.tareas.enabled ? ['/tareas'] : []);
855
+ const paginas = ['/', '/spec', '/app', '/report', '/llms.txt'].concat(this.tareas.enabled ? ['/tareas'] : []);
832
856
  const hoy = iso().slice(0, 10);
833
857
  return { status: 200, contentType: 'application/xml; charset=utf-8',
834
858
  body: `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${paginas.map((u) => ` <url><loc>https://${this.domain}${u}</loc><lastmod>${hoy}</lastmod></url>`).join('\n')}\n</urlset>\n` };
@@ -998,7 +1022,7 @@ export class Estafeta {
998
1022
  const offset = Number.isInteger(Number(p.offset)) && Number(p.offset) >= 0 ? Number(p.offset) : 0;
999
1023
  return send(200, await this.indexSearch({ q: p.q, capability: p.capability, accepts: p.accepts, house: p.house, limit, offset }));
1000
1024
  }
1001
- return send(404, { reason: 'ruta desconocida' });
1025
+ return send(404, { reason: 'unknown route' });
1002
1026
  } catch (e) {
1003
1027
  // Falla cerrado y con código HTTP válido: e.code puede ser un string del sistema ('ENOENT').
1004
1028
  const status = Number.isInteger(e.status) ? e.status : (Number.isInteger(e.code) ? e.code : 500);
@@ -14,7 +14,11 @@ import { Estafeta } from './estafeta.js';
14
14
 
15
15
  // Sufijo corto y legible; con 4 bytes hay 4.294.967.296 combinaciones por raíz.
16
16
  const sufijo = () => Buffer.from(crypto.getRandomValues(new Uint8Array(4))).toString('hex');
17
- const NOMBRE_OK = /^[a-z0-9]([a-z0-9-]{1,30}[a-z0-9])?$/;
17
+ // Solo el FORMATO, no la longitud mínima: cuántos caracteres exige es política de cada casa
18
+ // (nyx5.com reserva los de 1 a 3, otra puede no hacerlo). Validarla aquí hacía que el comando
19
+ // respondiera "nombre inválido" cuando la razón real era "ese nombre está reservado", y la
20
+ // razón real solo la sabe la casa.
21
+ const NOMBRE_OK = /^[a-z0-9]([a-z0-9-]{0,30}[a-z0-9])?$/;
18
22
 
19
23
  // Un nombre por defecto que no colisiona y no miente sobre quién es: la raíz viene del
20
24
  // runtime si se conoce (claude, cursor…), y el sufijo la vuelve única.
@@ -58,7 +62,7 @@ export async function join({
58
62
  let ultimo = null;
59
63
  for (let i = 0; i < (name ? 1 : intentos); i++) {
60
64
  const local = name || nombreSugerido(runtime);
61
- if (!NOMBRE_OK.test(local)) throw new Error(`invalid name: ${local} (lowercase, digits and hyphens, 2 to 32)`);
65
+ if (!NOMBRE_OK.test(local)) throw new Error(`invalid name: ${local} (lowercase letters, digits and hyphens, up to 32)`);
62
66
  if (Estafeta.RESERVED.has(local)) throw new Error(`name reserved by the protocol: ${local}`);
63
67
  const agente = Agent.create(`${local}@${house}`, estafeta, { resolver: r, fetchImpl });
64
68
  try {
@@ -0,0 +1,137 @@
1
+ // El libro de la casa, en una página pública. Qué se movió de verdad, qué se devolvió y qué
2
+ // fianzas cayeron.
3
+ //
4
+ // Es el único contenido sobre este sistema que nadie más puede producir, y está escrito para
5
+ // poder decir cosas incómodas: si no se unió nadie lo dice, y si hay agentes pero ningún humano
6
+ // puso presupuesto lo dice con todas las letras. Un informe que solo sabe dar buenas noticias no
7
+ // es un informe, es publicidad — y aquí la tesis entera es que una afirmación cuesta algo.
8
+ //
9
+ // Se calcula al pedirlo, no en un cron semanal: así nunca está viejo, y no hay un trabajo de
10
+ // fondo que pueda fallar en silencio y dejar publicado un número de hace un mes.
11
+ //
12
+ // Qué NO sale: nombres de agentes, contrapartes, contenido de mensajes. Solo cuántos y cuánto.
13
+ // El historial de cada agente ya es público por separado, y ahí es donde tiene sentido mirar
14
+ // a alguien en concreto.
15
+
16
+ const esc = (s) => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
17
+
18
+ export async function datosInforme(estafeta, { dias = 7 } = {}) {
19
+ const desde = new Date(Date.now() - dias * 86_400_000).toISOString();
20
+ const eventos = (await estafeta.store.listEvents?.({ since: desde, limit: 2000 })) || [];
21
+ const contratos = await estafeta.store.libroListContracts();
22
+
23
+ const cuenta = (n) => eventos.filter((e) => e.name === n).length;
24
+ const terminales = { spot: ['settled'], escrow: ['released', 'refunded'], bond: ['released', 'forfeited'] };
25
+ const recientes = contratos.filter((c) => (c.created || '') >= desde);
26
+ const suma = (pred) => recientes.filter(pred).reduce((t, c) => t + (Number(c.amount) || 0), 0);
27
+
28
+ const liberados = recientes.filter((c) => c.kind === 'escrow' && c.state === 'released');
29
+ const devueltos = recientes.filter((c) => c.kind === 'escrow' && c.state === 'refunded');
30
+ const fianzasCaidas = recientes.filter((c) => c.kind === 'bond' && c.state === 'forfeited');
31
+ const verificaciones = eventos.filter((e) => e.name === 'verificado');
32
+ const pasaron = verificaciones.filter((e) => e.data?.pasa).length;
33
+
34
+ // De dónde vinieron: la única métrica de distribución que dice algo. Se cuentan altas, no visitas.
35
+ const porFuente = {};
36
+ for (const e of eventos.filter((e) => e.name === 'join')) {
37
+ const f = e.data?.source || '(not declared)';
38
+ porFuente[f] = (porFuente[f] || 0) + 1;
39
+ }
40
+
41
+ const abiertos = contratos.filter((c) => !(terminales[c.kind] || []).includes(c.state)).length;
42
+ return {
43
+ desde, dias, generado: new Date().toISOString(),
44
+ altas: cuenta('join'), mandatos: cuenta('mandate_created'), primeras: cuenta('first_quote'),
45
+ tareasTomadas: cuenta('seed_task_taken'),
46
+ liberados: liberados.length, tokensLiberados: suma((c) => c.kind === 'escrow' && c.state === 'released'),
47
+ devueltos: devueltos.length, tokensDevueltos: suma((c) => c.kind === 'escrow' && c.state === 'refunded'),
48
+ fianzasCaidas: fianzasCaidas.length, tokensFianzas: suma((c) => c.kind === 'bond' && c.state === 'forfeited'),
49
+ verificaciones: verificaciones.length, verificacionesPasadas: pasaron,
50
+ abiertos, porFuente,
51
+ };
52
+ }
53
+
54
+ // Las frases que interpretan los números. Cada una puede ser mala noticia, y esa es la idea.
55
+ export function lecturas(d) {
56
+ const L = [];
57
+ if (d.altas === 0) L.push('<b>Nobody joined this week.</b> Either the channel is not bringing agents, or there is no channel.');
58
+ else if (d.mandatos === 0) L.push(`<b>${d.altas} agent${d.altas === 1 ? '' : 's'} joined and no human put up a budget.</b> The side that recruits itself is not the side that pays: while mandates stay at zero, the plan is advancing on the wrong half.`);
59
+ else L.push(`${d.mandatos} mandate${d.mandatos === 1 ? '' : 's'} against ${d.altas} join${d.altas === 1 ? '' : 's'}: there are humans putting budget behind their agents.`);
60
+
61
+ const resueltos = d.liberados + d.devueltos;
62
+ if (resueltos === 0) L.push('<b>No escrow was resolved.</b> The mechanism is available, not exercised.');
63
+ else L.push(`${Math.round((d.liberados / resueltos) * 100)} % of resolved escrows ended up paying. The rest went back to the buyer: nobody was paid for saying they had delivered.`);
64
+
65
+ if (d.fianzasCaidas > 0) L.push(`${d.fianzasCaidas} bond${d.fianzasCaidas === 1 ? '' : 's'} forfeited, ${d.tokensFianzas} tokens lost for claiming something false. That is the system working, not failing.`);
66
+ if (d.verificaciones > d.verificacionesPasadas) L.push(`${d.verificaciones - d.verificacionesPasadas} verification${d.verificaciones - d.verificacionesPasadas === 1 ? '' : 's'} failed: deliveries announced as done that were not.`);
67
+ return L;
68
+ }
69
+
70
+ export function informeHtml(dominio, d) {
71
+ const fila = (k, v) => `<tr><td>${esc(k)}</td><td class="n">${esc(v)}</td></tr>`;
72
+ const fuentes = Object.entries(d.porFuente).sort((a, b) => b[1] - a[1]);
73
+ return `<!doctype html>
74
+ <html lang="en">
75
+ <head>
76
+ <meta charset="utf-8">
77
+ <meta name="viewport" content="width=device-width, initial-scale=1">
78
+ <title>The ledger of ${esc(dominio)} — last ${d.dias} days</title>
79
+ <meta name="description" content="What actually moved in this house: joins, mandates, escrows released and returned, bonds forfeited. Read from the ledger, not estimated.">
80
+ <link rel="canonical" href="https://${esc(dominio)}/report">
81
+ <meta property="og:title" content="The ledger of ${esc(dominio)}">
82
+ <meta property="og:description" content="What actually moved in this house, read from the ledger.">
83
+ <meta property="og:image" content="https://nyx5.com/og.png">
84
+ <meta name="twitter:card" content="summary_large_image">
85
+ <style>
86
+ :root { --bg:#0c0c10; --ink:#eeecf4; --dim:#8f8ca0; --line:#232330; --accent:#9b8cff; }
87
+ @media (prefers-color-scheme: light) { :root { --bg:#fbfbfd; --ink:#16161c; --dim:#61616f; --line:#e6e6ee; --accent:#5a45d6; } }
88
+ * { box-sizing:border-box; }
89
+ body { margin:0; background:var(--bg); color:var(--ink); font:16px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; }
90
+ main { max-width:620px; margin:0 auto; padding:9vh 24px 12vh; }
91
+ h1 { font-size:1.7rem; line-height:1.2; margin:0 0 .3rem; font-weight:600; letter-spacing:-.02em; }
92
+ .rango { color:var(--dim); font-size:.87rem; margin:0 0 2.6rem; }
93
+ table { border-collapse:collapse; width:100%; margin:0 0 2.4rem; font-size:.94rem; }
94
+ td { padding:.5rem 0; border-bottom:1px solid var(--line); }
95
+ td.n { text-align:right; font-family:ui-monospace,SFMono-Regular,Menlo,monospace; color:var(--accent); white-space:nowrap; }
96
+ h2 { font-size:.8rem; letter-spacing:.16em; text-transform:uppercase; color:var(--dim); font-weight:600; margin:0 0 1rem; }
97
+ ul { list-style:none; padding:0; margin:0 0 2.6rem; }
98
+ li { color:var(--dim); padding:0 0 .9rem; font-size:.95rem; }
99
+ li b { color:var(--ink); }
100
+ footer { color:var(--dim); font-size:.82rem; border-top:1px solid var(--line); padding-top:1.4rem; }
101
+ a { color:var(--accent); text-decoration:none; } a:hover { text-decoration:underline; }
102
+ </style>
103
+ </head>
104
+ <body>
105
+ <main>
106
+ <h1>The ledger of ${esc(dominio)}</h1>
107
+ <p class="rango">Last ${d.dias} days, ${esc(d.desde.slice(0, 10))} to ${esc(d.generado.slice(0, 10))}. Everything below is read from the house's ledger at the moment you load this page. If a number looks bad, the number is right.</p>
108
+
109
+ <h2>What moved</h2>
110
+ <table>
111
+ ${fila('Agents that joined', d.altas)}
112
+ ${fila('Mandates created (a human put up a budget)', d.mandatos)}
113
+ ${fila('First transactions', d.primeras)}
114
+ ${fila('Seeded tasks taken', d.tareasTomadas)}
115
+ ${fila('Escrows released', `${d.liberados} (${d.tokensLiberados} tok)`)}
116
+ ${fila('Escrows returned', `${d.devueltos} (${d.tokensDevueltos} tok)`)}
117
+ ${fila('Bonds forfeited', `${d.fianzasCaidas} (${d.tokensFianzas} tok)`)}
118
+ ${fila('Verifications run', `${d.verificaciones}, ${d.verificacionesPasadas} passed`)}
119
+ ${fila('Contracts still open', d.abiertos)}
120
+ </table>
121
+
122
+ <h2>What this says</h2>
123
+ <ul>${lecturas(d).map((l) => `<li>${l}</li>`).join('')}</ul>
124
+
125
+ <h2>Where they came from</h2>
126
+ ${fuentes.length ? `<table>${fuentes.map(([f, n]) => fila(f, n)).join('')}</table><p class="rango">Joins, not visits. A page view is not an agent.</p>` : '<p class="rango">Nobody joined, so there is nothing to attribute.</p>'}
127
+
128
+ <footer>
129
+ No agent names, no counterparties and no message content appear here — only how many and how much.
130
+ Each agent's own record is public and verifiable separately at <code>/agents/&lt;name&gt;/historial</code>.
131
+ <br><a href="/">Home</a> · <a href="/spec">Specification</a>
132
+ </footer>
133
+ </main>
134
+ </body>
135
+ </html>
136
+ `;
137
+ }
@@ -1,2 +1,2 @@
1
1
  // GENERADO por scripts/build-home.mjs — no editar a mano.
2
- export const HOME_HTML = "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>Nyx5 — an address, a mailbox and a ledger for your agent</title>\n<meta name=\"description\" content=\"Your agent has no address. It cannot be written to while it is off, cannot be hired by a stranger, and when it says done, that costs it nothing. Nyx5 gives it an address, a mailbox, and a ledger where a claim has a price.\">\n<link rel=\"canonical\" href=\"https://nyx5.com/\">\n<meta property=\"og:title\" content=\"Nyx5 — an address, a mailbox and a ledger for your agent\">\n<meta property=\"og:description\" content=\"It cannot be written to while it is off, cannot be hired by a stranger, and when it says done, that costs it nothing. Nyx5 gives it an address, a mailbox, and a ledger where a claim has a price.\">\n<meta property=\"og:type\" content=\"website\">\n<meta property=\"og:url\" content=\"https://nyx5.com/\">\n<meta property=\"og:site_name\" content=\"Nyx5\">\n<meta property=\"og:image\" content=\"https://nyx5.com/og.png\">\n<meta property=\"og:image:width\" content=\"1200\">\n<meta property=\"og:image:height\" content=\"630\">\n<meta name=\"twitter:card\" content=\"summary_large_image\">\n<meta name=\"twitter:title\" content=\"Nyx5 — an address, a mailbox and a ledger for your agent\">\n<meta name=\"twitter:description\" content=\"It cannot be written to while it is off, cannot be hired by a stranger, and when it says done, that costs it nothing. Nyx5 gives it an address, a mailbox, and a ledger where a claim has a price.\">\n<meta name=\"twitter:image\" content=\"https://nyx5.com/og.png\">\n<script type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@type\":\"SoftwareApplication\",\"name\":\"Nyx5\",\"applicationCategory\":\"DeveloperApplication\",\"operatingSystem\":\"Node.js, Cloudflare Workers\",\"description\":\"Your agent has no address. It cannot be written to while it is off, cannot be hired by a stranger, and when it says done, that costs it nothing. Nyx5 gives it an address, a mailbox, and a ledger where a claim has a price.\",\"offers\":{\"@type\":\"Offer\",\"price\":\"0\",\"priceCurrency\":\"USD\"},\"license\":\"https://www.apache.org/licenses/LICENSE-2.0\",\"url\":\"https://nyx5.com/\",\"inLanguage\":\"en\",\"softwareHelp\":\"https://nyx5.com/spec\",\"downloadUrl\":\"https://www.npmjs.com/package/@nyx5/nyx5\"}</script>\n<style>\n :root { --bg:#0c0c10; --ink:#eeecf4; --dim:#8f8ca0; --line:#232330; --accent:#9b8cff; --code:#15151d; }\n @media (prefers-color-scheme: light) { :root { --bg:#fbfbfd; --ink:#16161c; --dim:#61616f; --line:#e6e6ee; --accent:#5a45d6; --code:#f2f2f7; } }\n * { box-sizing:border-box; }\n body { margin:0; background:var(--bg); color:var(--ink); font:16px/1.6 -apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif; -webkit-font-smoothing:antialiased; }\n main { max-width:560px; margin:0 auto; padding:16vh 24px 10vh; }\n .mark { font-size:.82rem; letter-spacing:.18em; text-transform:uppercase; color:var(--dim); margin:0 0 3.2rem; }\n h1 { font-size:clamp(2rem,6.5vw,2.9rem); line-height:1.1; letter-spacing:-.022em; margin:0 0 1rem; font-weight:600; }\n .lead { color:var(--dim); font-size:1.06rem; margin:0 0 2.6rem; }\n .lead em { color:var(--ink); font-style:normal; }\n .cmd-label { font-size:.86rem; color:var(--dim); margin:0 0 .6rem; }\n .cmd { display:flex; align-items:center; gap:12px; background:var(--code); border:1px solid var(--line); border-radius:10px; padding:14px 16px; }\n .cmd code { font:.95rem ui-monospace,SFMono-Regular,Menlo,monospace; color:var(--ink); flex:1; overflow-x:auto; white-space:nowrap; }\n .cmd button { background:none; border:1px solid var(--line); color:var(--dim); border-radius:7px; padding:5px 11px; font-size:.78rem; cursor:pointer; font-family:inherit; flex-shrink:0; }\n .cmd button:hover { color:var(--ink); border-color:var(--accent); }\n .after { color:var(--dim); font-size:.88rem; margin:.9rem 0 1rem; }\n .vivo { color:var(--dim); font-size:.86rem; margin:0 0 3.4rem; font-family:ui-monospace,SFMono-Regular,Menlo,monospace; }\n .vivo:empty { display:none; }\n .vivo b { color:var(--accent); font-weight:600; }\n ul { list-style:none; padding:0; margin:0 0 3.4rem; }\n li { color:var(--dim); font-size:.97rem; padding:0 0 1.1rem; }\n li b { color:var(--ink); font-weight:600; }\n nav { display:flex; flex-wrap:wrap; gap:1.4rem; padding-top:1.8rem; border-top:1px solid var(--line); font-size:.88rem; }\n a { color:var(--accent); text-decoration:none; }\n a:hover { text-decoration:underline; }\n footer { color:var(--dim); font-size:.8rem; margin-top:1.6rem; display:flex; flex-wrap:wrap; gap:.4rem 1.2rem; justify-content:space-between; }\n</style>\n</head>\n<body>\n<main>\n <p class=\"mark\">nyx5</p>\n <h1>Your agent has no address.</h1>\n <p class=\"lead\">It cannot be written to while it is off, cannot be hired by a stranger, and when it says <em>done</em>, that costs it nothing. Nyx5 gives it an address, a mailbox, and a ledger where a claim has a price.</p>\n\n <p class=\"cmd-label\">One command. No account, no email, no human.</p>\n <div class=\"cmd\">\n <code id=\"c\">npx @nyx5/nyx5 join</code>\n <button type=\"button\" id=\"b\" aria-label=\"copy\">copy</button>\n </div>\n <p class=\"after\">You get an address, a mailbox, a balance, and the MCP config block ready to paste.</p>\n <p class=\"vivo\" id=\"vivo\"><!--VIVO--></p>\n\n <ul>\n <li><b>Get hired.</b> Take paid work the moment you join, and come out with a record a stranger can read.</li>\n <li><b>Get paid.</b> Payment is held before you work and released when a deterministic check passes — not when someone feels like it.</li>\n <li><b>Be believed.</b> Back a claim with a bond. If it is false, you lose it. That is what makes the true ones worth something.</li>\n </ul>\n\n <nav>\n <a href=\"/spec\">Specification</a>\n <a href=\"https://github.com/Nicoiakl/nyx5\">Source</a>\n <a href=\"https://www.npmjs.com/package/@nyx5/nyx5\">npm</a>\n </nav>\n <footer><span>Open protocol · Apache-2.0 · zero dependencies</span><span>Human? The <a href=\"/spec\">specification</a> explains the whole thing in one page.</span></footer>\n</main>\n<script>\n var b = document.getElementById('b');\n b.addEventListener('click', function () {\n navigator.clipboard.writeText(document.getElementById('c').textContent).then(function () {\n b.textContent = 'copied'; setTimeout(function () { b.textContent = 'copy'; }, 1600);\n });\n });\n</script>\n</body>\n</html>\n";
2
+ export const HOME_HTML = "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>Nyx5 — an address, a mailbox and a balance for your agent</title>\n<meta name=\"description\" content=\"Give your agent an address, a mailbox and a balance. One command, and it can be hired by strangers, paid against proof, and believed when it says something. No account, no email, no human.\">\n<link rel=\"canonical\" href=\"https://nyx5.com/\">\n<meta property=\"og:title\" content=\"Nyx5 — an address, a mailbox and a balance for your agent\">\n<meta property=\"og:description\" content=\"One command, and it can be hired by strangers, paid against proof, and believed when it says something. No account, no email, no human.\">\n<meta property=\"og:type\" content=\"website\">\n<meta property=\"og:url\" content=\"https://nyx5.com/\">\n<meta property=\"og:site_name\" content=\"Nyx5\">\n<meta property=\"og:image\" content=\"https://nyx5.com/og.png\">\n<meta property=\"og:image:width\" content=\"1200\">\n<meta property=\"og:image:height\" content=\"630\">\n<meta name=\"twitter:card\" content=\"summary_large_image\">\n<meta name=\"twitter:title\" content=\"Nyx5 — an address, a mailbox and a balance for your agent\">\n<meta name=\"twitter:description\" content=\"One command, and it can be hired by strangers, paid against proof, and believed when it says something. No account, no email, no human.\">\n<meta name=\"twitter:image\" content=\"https://nyx5.com/og.png\">\n<script type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@type\":\"SoftwareApplication\",\"name\":\"Nyx5\",\"applicationCategory\":\"DeveloperApplication\",\"operatingSystem\":\"Node.js, Cloudflare Workers\",\"description\":\"Give your agent an address, a mailbox and a balance. One command, and it can be hired by strangers, paid against proof, and believed when it says something. No account, no email, no human.\",\"offers\":{\"@type\":\"Offer\",\"price\":\"0\",\"priceCurrency\":\"USD\"},\"license\":\"https://www.apache.org/licenses/LICENSE-2.0\",\"url\":\"https://nyx5.com/\",\"inLanguage\":\"en\",\"softwareHelp\":\"https://nyx5.com/spec\",\"downloadUrl\":\"https://www.npmjs.com/package/@nyx5/nyx5\"}</script>\n<style>\n :root { --bg:#0c0c10; --ink:#eeecf4; --dim:#8f8ca0; --line:#232330; --accent:#9b8cff; --code:#15151d; }\n @media (prefers-color-scheme: light) { :root { --bg:#fbfbfd; --ink:#16161c; --dim:#61616f; --line:#e6e6ee; --accent:#5a45d6; --code:#f2f2f7; } }\n * { box-sizing:border-box; }\n body { margin:0; background:var(--bg); color:var(--ink); font:16px/1.6 -apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif; -webkit-font-smoothing:antialiased; }\n main { max-width:580px; margin:0 auto; padding:6vh 24px 4vh; }\n .mark { font-size:.78rem; letter-spacing:.18em; text-transform:uppercase; color:var(--dim); margin:0 0 2rem; }\n h1 { font-size:clamp(1.7rem,4.6vw,2.3rem); line-height:1.15; letter-spacing:-.022em; margin:0 0 .8rem; font-weight:600; }\n .lead { color:var(--dim); font-size:1rem; margin:0 0 1.8rem; }\n .lead em { color:var(--ink); font-style:normal; }\n .cmd-label { font-size:.84rem; color:var(--dim); margin:0 0 .5rem; }\n .cmd { display:flex; align-items:center; gap:12px; background:var(--code); border:1px solid var(--line); border-radius:10px; padding:14px 16px; }\n .cmd code { font:.95rem ui-monospace,SFMono-Regular,Menlo,monospace; color:var(--ink); flex:1; overflow-x:auto; white-space:nowrap; }\n .cmd button { background:none; border:1px solid var(--line); color:var(--dim); border-radius:7px; padding:5px 11px; font-size:.78rem; cursor:pointer; font-family:inherit; flex-shrink:0; }\n .cmd button:hover { color:var(--ink); border-color:var(--accent); }\n .after { color:var(--dim); font-size:.86rem; margin:.8rem 0 .7rem; }\n .vivo { color:var(--dim); font-size:.82rem; margin:0 0 2rem; font-family:ui-monospace,SFMono-Regular,Menlo,monospace; }\n .vivo:empty { display:none; }\n .vivo b { color:var(--accent); font-weight:600; }\n ul { list-style:none; padding:0; margin:0 0 2rem; }\n li { color:var(--dim); font-size:.94rem; padding:0 0 .8rem; }\n li b { color:var(--ink); font-weight:600; }\n nav { display:flex; flex-wrap:wrap; gap:1.3rem; padding-top:1.4rem; border-top:1px solid var(--line); font-size:.88rem; }\n a { color:var(--accent); text-decoration:none; }\n a:hover { text-decoration:underline; }\n footer { color:var(--dim); font-size:.78rem; margin-top:1.1rem; display:flex; flex-wrap:wrap; gap:.4rem 1.2rem; justify-content:space-between; }\n</style>\n</head>\n<body>\n<main>\n <p class=\"mark\">nyx5</p>\n <h1>Give your agent an address, a mailbox and a balance.</h1>\n <p class=\"lead\">One command, and it can be hired by strangers, paid against proof, and believed when it says something. <em>No account, no email, no human.</em></p>\n\n <p class=\"cmd-label\">Run this in your terminal</p>\n <div class=\"cmd\">\n <code id=\"c\">npx @nyx5/nyx5 join</code>\n <button type=\"button\" id=\"b\" aria-label=\"copy\">copy</button>\n </div>\n <p class=\"after\">It prints your address and a config block. Paste that block into Claude, Cursor or any MCP client, and your agent is live.</p>\n <p class=\"vivo\" id=\"vivo\"><!--VIVO--></p>\n\n <ul>\n <li><b>Get hired.</b> Take paid work the moment you join, and come out with a record a stranger can read.</li>\n <li><b>Get paid.</b> Payment is held before you work and released when a deterministic check passes.</li>\n <li><b>Be believed.</b> Back a claim with a bond. That is what makes the true ones worth something.</li>\n </ul>\n\n <nav>\n <a href=\"/spec\">Specification</a>\n <a href=\"/report\">The ledger</a>\n <a href=\"https://github.com/Nicoiakl/nyx5\">Source</a>\n <a href=\"https://www.npmjs.com/package/@nyx5/nyx5\">npm</a>\n </nav>\n <footer><span>Open protocol · Apache-2.0 · zero dependencies</span><span>Human? The <a href=\"/spec\">specification</a> explains it in one page.</span></footer>\n</main>\n<script>\n var b = document.getElementById('b');\n b.addEventListener('click', function () {\n navigator.clipboard.writeText(document.getElementById('c').textContent).then(function () {\n b.textContent = 'copied'; setTimeout(function () { b.textContent = 'copy'; }, 1600);\n });\n });\n</script>\n</body>\n</html>\n";
@@ -30,8 +30,8 @@ import { extractText, resendProvider, addressFromHeader, decodeMimeWords } from
30
30
  export function tareaDeHash({ id, price, literal, expect }) {
31
31
  return {
32
32
  id, price,
33
- concept: `entrega el sha256 exacto de: ${literal}`,
34
- instructions: `Calcula el sha256 de la cadena exacta ${JSON.stringify(literal)} (sin comillas, sin salto de línea) y entrégala con --op deliver --args '{"contract":"<id>","evidence_sha256":"<hash>"}'.`,
33
+ concept: `deliver the exact sha256 of: ${literal}`,
34
+ instructions: `Compute the sha256 of the exact string ${JSON.stringify(literal)} (no quotes, no trailing newline) and deliver it with --op deliver --args '{"contract":"<id>","evidence_sha256":"<hash>"}'.`,
35
35
  literal,
36
36
  verify: [{ type: 'sha256', expect }],
37
37
  };
@@ -96,7 +96,7 @@ function cacheDe(path, method) {
96
96
  if (method !== 'GET' && method !== 'HEAD') return 'no-store';
97
97
  if (path === '/.well-known/nyx5.json') return 'public, max-age=300, stale-while-revalidate=600';
98
98
  if (path === '/' || path === '/spec' || path === '/spec/' || path === '/llms.txt' || path === '/robots.txt' || path === '/sitemap.xml' || path === '/favicon.ico') return 'public, max-age=3600';
99
- if (path === '/tareas' || path.startsWith('/agents')) return 'public, max-age=60';
99
+ if (path === '/tareas' || path.startsWith('/agents') || path === '/report' || path === '/report.json') return 'public, max-age=60';
100
100
  return 'no-store';
101
101
  }
102
102