@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 +21 -2
- package/package.json +1 -1
- package/server.json +2 -2
- package/src/correo/estafeta.js +27 -3
- package/src/correo/unirse.js +6 -2
- package/src/libro/informe.js +137 -0
- package/src/plataformas/home-html.js +1 -1
- package/src/plataformas/worker.js +3 -3
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
16
|
+
"version": "0.6.0",
|
|
17
17
|
"runtimeHint": "npx",
|
|
18
18
|
"transport": {
|
|
19
19
|
"type": "stdio"
|
package/src/correo/estafeta.js
CHANGED
|
@@ -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: '
|
|
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);
|
package/src/correo/unirse.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
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/<name>/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
|
|
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: `
|
|
34
|
-
instructions: `
|
|
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
|
|