@nyx5/nyx5 0.2.4 → 0.3.1

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.
@@ -1,4 +1,3 @@
1
- // GENERADO por scripts/build-spec-site.mjs desde docs/SPEC.en.md y docs/SPEC.md — no editar a mano.
2
- export const SPEC_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/1 — Mail and Libro for agents — the specification</title>\n<meta name=\"description\" content=\"A single system with two components that share identity, transport and storage.\">\n<link rel=\"canonical\" href=\"https://nyx5.com/spec\">\n<link rel=\"alternate\" hreflang=\"en\" href=\"https://nyx5.com/spec\">\n<link rel=\"alternate\" hreflang=\"es\" href=\"https://nyx5.com/es\">\n<link rel=\"alternate\" hreflang=\"x-default\" href=\"https://nyx5.com/spec\">\n<meta property=\"og:title\" content=\"Nyx5/1 — Mail and Libro for agents\">\n<meta property=\"og:description\" content=\"A single system with two components that share identity, transport and storage.\">\n<meta property=\"og:type\" content=\"article\">\n<meta property=\"og:url\" content=\"https://nyx5.com/spec\">\n<script type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@type\":\"TechArticle\",\"name\":\"Nyx5/1 — Mail and Libro for agents\",\"headline\":\"Nyx5/1 — Mail and Libro for agents\",\"description\":\"A single system with two components that share identity, transport and storage.\",\"inLanguage\":\"es\",\"url\":\"https://nyx5.com/spec\",\"license\":\"https://www.apache.org/licenses/LICENSE-2.0\",\"about\":[\"agent communication protocol\",\"signed messaging\",\"double-entry ledger for AI agents\"]}</script>\n<style>\n :root { --ink:#1a1a1a; --dim:#666; --bg:#fff; --soft:#f6f6f4; --line:#e5e5e0; --accent:#7a4d1d; --code:#f0efe9; }\n @media (prefers-color-scheme: dark) { :root { --ink:#e8e6e0; --dim:#9a978f; --bg:#151513; --soft:#1e1e1b; --line:#33322d; --accent:#d6a86a; --code:#222220; } }\n * { box-sizing: border-box; }\n body { margin:0; background:var(--bg); color:var(--ink); font:16px/1.65 -apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif; }\n main { max-width: 760px; margin: 0 auto; padding: 48px 22px 120px; }\n h1 { font-size: 2.1rem; line-height:1.15; margin: 0 0 .2em; letter-spacing:-.01em; }\n h2 { font-size: 1.45rem; margin: 2.4em 0 .5em; padding-top:.4em; border-top:1px solid var(--line); }\n h3 { font-size: 1.12rem; margin: 1.8em 0 .4em; }\n h2:first-of-type { border-top:none; }\n a { color: var(--accent); text-decoration: none; } a:hover { text-decoration: underline; }\n code { background: var(--code); padding: .1em .35em; border-radius: 4px; font: .88em ui-monospace,SFMono-Regular,Menlo,monospace; }\n pre { background: var(--soft); border:1px solid var(--line); border-radius: 8px; padding: 14px 16px; overflow-x:auto; }\n pre code { background:none; padding:0; font-size:.82rem; line-height:1.5; }\n table { border-collapse: collapse; width:100%; margin: 1.2em 0; font-size:.92rem; display:block; overflow-x:auto; }\n th, td { border:1px solid var(--line); padding: 7px 11px; text-align:left; vertical-align:top; }\n th { background: var(--soft); }\n blockquote { margin:1em 0; padding:.2em 1em; border-left:3px solid var(--accent); color:var(--dim); }\n ul, ol { padding-left: 1.3em; }\n li { margin:.25em 0; }\n .lang { margin:0 0 26px; font-size:.85rem; }\n .tag { display:inline-block; margin-top:14px; color:var(--dim); font-size:.85rem; }\n footer { max-width:760px; margin:0 auto; padding: 0 22px 60px; color:var(--dim); font-size:.85rem; border-top:1px solid var(--line); padding-top:22px; }\n</style>\n</head>\n<body>\n<main>\n<p class=\"lang\"><a href=\"/es\">Leer en español</a></p>\n<h1 id=\"nyx5-1-mail-and-libro-for-agents\">Nyx5/1 — Mail and Libro for agents</h1>\n<p>Status: executable draft v0.4 (September 2026) Reference implementation: this repository (Node 20+, no dependencies)</p>\n<h2 id=\"0-what-it-is\">0. What it is</h2>\n<p>A single system with two components that share identity, transport and storage:</p>\n<ul><li><strong>Mail</strong> (sections 1 to 13): <code>agent@domain</code> addresses, certified cards (tarjeta), signed and encrypted envelopes (sobre), a mailbox (buzón) that holds even while the agent is off. What email gave people.</li><li><strong>Libro</strong> (sections 14 to 20): the double-entry ledger of each house (casa), quotes (cotización), contracts, chained mandates (mandato) and stamps (estampilla). What email never had: economic consequence and a receipt (recibo) that no party can deny.</li></ul>\n<p>They are not two compatible protocols. The Libro has no login and no API of its own: it is operated by writing envelopes to <code>libro@&lt;house&gt;</code>, and the Mail chain of trust is its authentication. Its responses are receipts signed by the house that arrive in the mailbox like any other letter.</p>\n<p>Email achieved something no agent protocol has today: a universal address, a mailbox, and a network where any server writes to any other without asking permission. MCP connects an agent with its tools; A2A connects agents that already know each other and are online. Neither gives per-person identity, a mailbox, verifiable trust between strangers, nor a way for an agreement to carry weight.</p>\n<p>Nyx5/1 closes those gaps like this:</p>\n<table><thead><tr><th>Gap</th><th>How Nyx5 closes it</th></tr></thead><tbody><tr><td>Per-person identity, not just per-domain</td><td><code>agent@domain</code> address. The domain certifies each agent's public key. The person owns their key; the domain only vouches for it.</td></tr><tr><td>Mailbox (store-and-forward)</td><td>Each domain has an estafeta that accepts, stores and retries. The agent can be off for days; nothing is lost.</td></tr><tr><td>Trust and anti-spam</td><td>Every envelope comes signed by the agent and vouched for by its domain (anchored in DNS). Without a verifiable signature there is no delivery. The receiver decides its policy: open, allowlist, or stamp (proof-of-work / payment).</td></tr><tr><td>Fragmentation</td><td>Nyx5 does not replace MCP or A2A: it is the universal envelope. The content can be text, JSON, an A2A task or an MCP call; the agent's card publishes its MCP/A2A endpoints.</td></tr><tr><td>Free words</td><td>An agreement is an entry (asiento) in the Libro, not prose. Escrow holds until the proof passes; the bond (fianza) puts a price on asserting; the mandate bounds how much each agent may spend and who pays in the end.</td></tr><tr><td>Deniable receipt</td><td>Every receipt carries the hash of the envelope that caused it and the signature of whoever issues it.</td></tr></tbody></table>\n<p>And end-to-end encryption by default. The estafetas see <code>de</code>, <code>para</code> and the size; never the content.</p>\n<h2 id=\"1-terms\">1. Terms</h2>\n<ul><li><strong>Agent</strong>: any process (or a person operating a client) with a key pair and an address.</li><li><strong>Address</strong>: <code>local@domain</code>. Lowercase, <code>local</code> = <code>[a-z0-9][a-z0-9._-]{0,63}</code>.</li><li><strong>Estafeta</strong>: a domain's server. It publishes cards, certifies agents, receives, stores and delivers. Equivalent to the MX server in email.</li><li><strong>Domain card (tarjeta)</strong>: self-signed JSON at <code>/.well-known/nyx5.json</code>. Declares keys, the estafeta URL, policy and extensions.</li><li><strong>Agent card (tarjeta)</strong>: JSON with an agent's keys and capabilities, signed (certified) by the domain key.</li><li><strong>Envelope (sobre)</strong>: the unit of sending. Signed JSON, optionally encrypted.</li><li><strong>Resolver</strong>: the logic that goes from an address to a verified card.</li></ul>\n<h2 id=\"2-discovery-and-trust-anchor\">2. Discovery and trust anchor</h2>\n<p>Given <code>asistente@sigo.uk</code>, the resolver locates the estafeta of <code>sigo.uk</code> in this order:</p>\n<ol><li><strong>Local override</strong> (<code>hosts.json</code>): tests and private networks. It can pin the expected key (<code>sig</code>).</li><li><strong>DNS</strong>: TXT record at <code>_nyx5.sigo.uk</code>:</li></ol>\n<pre class=\"lang-text\"><code>v=nyx51; url=https://mail.sigo.uk; sig=&lt;domain Ed25519 public key, base64url&gt;</code></pre>\n<p> <code>sig</code> is the anchor: the domain card must be signed by that key. With DNSSEC, the chain is complete.</p>\n<ol><li><strong>Well-known without DNS</strong>: <code>https://sigo.uk/.well-known/nyx5.json</code>. If there is no anchor, the resolver applies TOFU (trusts on first use and pins the key; a later change is rejected until the operator confirms it).</li></ol>\n<p>Then it downloads the domain card and the agent card, and verifies the chain: <strong>DNS → domain key → agent card → envelope signature</strong>.</p>\n<p>Cards are cached (5 min by default). If an envelope arrives signed with a key that the cached card does not recognize, the receiver refreshes the card once before rejecting (this is what makes key rotation work without coordination).</p>\n<h2 id=\"3-domain-card\">3. Domain card</h2>\n<pre class=\"lang-json\"><code>{\n \"nyx5\": \"1\",\n \"domain\": \"sigo.uk\",\n \"estafeta\": \"https://mail.sigo.uk\",\n \"keys\": [ { \"sig\": \"&lt;Ed25519 pub&gt;\", \"created\": \"2026-09-04T00:00:00Z\" } ],\n \"policy\": { \"inbound\": \"verified\", \"max_bytes\": 1048576 },\n \"extensions\": [\"urn:nyx5:ext:mcp\", \"urn:nyx5:ext:a2a\"],\n \"issued\": \"2026-09-04T19:00:00Z\",\n \"signature\": { \"alg\": \"Ed25519\", \"kid\": \"&lt;Ed25519 pub&gt;\", \"value\": \"&lt;base64url&gt;\" }\n}</code></pre>\n<p>Rules:</p>\n<ul><li><code>signature.kid</code> must be in <code>keys</code>. The signature covers the canonical JSON without <code>signature</code>.</li><li><code>keys</code> may list several during a rotation; the first is the active one.</li><li><code>policy.inbound</code> today admits only <code>verified</code> (no signature, no delivery). It is reserved for future modes.</li><li><code>policy.outbound: \"sealed\"</code> (optional) declares that everything leaving the domain goes encrypted; receivers may reject cleartext envelopes from that domain.</li></ul>\n<h2 id=\"4-agent-card\">4. Agent card</h2>\n<p><code>GET https://&lt;estafeta&gt;/agents/&lt;local&gt;</code></p>\n<pre class=\"lang-json\"><code>{\n \"nyx5\": \"1\",\n \"address\": \"asistente@sigo.uk\",\n \"sig\": \"&lt;agent Ed25519 pub&gt;\",\n \"enc\": \"&lt;agent X25519 pub&gt;\",\n \"capabilities\": {\n \"accepts\": [\"text/plain\", \"application/json\", \"application/a2a-task+json\"],\n \"mcp\": \"https://agents.sigo.uk/asistente/mcp\",\n \"a2a\": \"https://agents.sigo.uk/asistente/.well-known/agent-card.json\"\n },\n \"inbox\": { \"policy\": \"open\" },\n \"valid_from\": \"2026-09-04T19:00:00Z\",\n \"valid_until\": null,\n \"previous\": [ { \"sig\": \"&lt;previous key&gt;\", \"until\": \"2026-09-11T19:00:00Z\" } ],\n \"certification\": { \"alg\": \"Ed25519\", \"kid\": \"&lt;domain key&gt;\", \"value\": \"&lt;base64url&gt;\" }\n}</code></pre>\n<p>Delegated card (a subagent acting on behalf of another agent): the name is <code>&lt;name&gt;.&lt;parent&gt;</code>, and the card additionally carries</p>\n<pre class=\"lang-json\"><code>\"delegation\": {\n \"by\": \"constructor@sigo.uk\", \"address\": \"tester.constructor@sigo.uk\", \"sig\": \"&lt;subagent key&gt;\",\n \"scope\": { \"types\": [\"message\", \"result\"], \"to_domains\": [\"sigo.uk\"], \"cap\": 100 },\n \"valid_until\": null, \"issued\": \"...\", \"signature\": { \"alg\": \"Ed25519\", \"kid\": \"&lt;parent key&gt;\", \"value\": \"...\" }\n}</code></pre>\n<p>Rules:</p>\n<ul><li><code>certification</code> is signed by the domain, not the agent. It covers everything except <code>certification</code>.</li><li><code>delegation</code> is signed by the parent. The domain certifies the card the same way; the resolver verifies both signatures (chain domain → parent → child). A child cannot have more <code>cap</code> than its parent. The estafeta enforces <code>scope.types</code> and <code>scope.to_domains</code> on send; the Libro enforces <code>scope.cap</code> on accept, bond, mandate and charge.</li><li>The private <code>sig</code> key is generated and kept by the agent; the domain never sees it. That is why the identity is the person's: if you leave a domain, you take your key with you and certify it at another.</li><li><code>enc</code> is optional. Without <code>enc</code>, senders send in cleartext (or reject if they require encryption).</li><li><code>previous</code>: previous keys with a grace date. A signature with a valid previous key is valid.</li><li><code>inbox.policy</code>: see section 9.</li></ul>\n<h2 id=\"5-the-envelope\">5. The envelope</h2>\n<pre class=\"lang-json\"><code>{\n \"nyx5\": \"1\",\n \"id\": \"uuid\",\n \"from\": \"nicolas@sigo.uk\",\n \"to\": [\"asistente@beta.example\"],\n \"created\": \"ISO-8601\",\n \"expires\": null,\n \"deliver_after\": null,\n \"thread\": \"uuid o null\",\n \"in_reply_to\": \"id o null\",\n \"type\": \"message | task | result | receipt | intro\",\n\n \"content\": { \"media\": \"application/json\", \"body\": { \"...\": \"...\" } },\n \"encrypted\": { \"alg\": \"X25519+HKDF-SHA256+A256GCM\", \"epk\": \"...\", \"iv\": \"...\", \"ct\": \"...\", \"tag\": \"...\", \"keys\": { \"asistente@beta.example\": { \"iv\": \"...\", \"ct\": \"...\", \"tag\": \"...\" } } },\n\n \"attachments\": [ { \"name\": \"informe.pdf\", \"media\": \"application/pdf\", \"sha256\": \"...\", \"url\": \"https://...\", \"bytes\": 12345 } ],\n \"pow\": { \"bits\": 16, \"nonce\": \"12345\" },\n \"receipt\": \"delivered\",\n \"extensions\": { \"urn:nyx5:ext:a2a\": { \"task_id\": \"...\" } },\n \"signature\": { \"alg\": \"Ed25519\", \"kid\": \"&lt;agent sig&gt;\", \"value\": \"&lt;base64url&gt;\" }\n}</code></pre>\n<p>Rules:</p>\n<ul><li><code>content</code> and <code>encrypted</code> are mutually exclusive. <code>content.media</code> follows the MIME model; <code>body</code> is text or JSON.</li><li>The <strong>signature</strong> covers the whole canonical envelope except <code>signature</code>. It is signed after encrypting: any estafeta verifies authenticity without being able to read the content.</li><li>The <strong>encryption</strong> is JWE-like: a random content key encrypts <code>content</code> with AES-256-GCM; that key is wrapped for each recipient with ephemeral X25519 + HKDF. The AAD is the canonical form of <code>{id, from, to}</code>: an envelope cannot be re-addressed or re-signed by another without breaking decryption.</li><li><strong>Normative KDF detail</strong> (every implementation must copy it byte for byte or nothing interoperates): each recipient's KEK is <code>HKDF-SHA256(ikm = X25519(epk_priv, enc_dest), salt = the UTF-8 bytes of the base64url STRING of epk — not the decoded key —, info = \"nyx5/1 cek-wrap\", 32)</code>. And the canonical form orders keys, omits <code>undefined</code> values in objects, and serializes as compact JSON.</li><li><strong>Attachments</strong> travel by reference (URL + hash), not embedded. The estafeta does not store binaries. The hash makes the download verifiable.</li><li><code>type</code> is a semantic hint. <code>task</code>/<code>result</code> for delegated work; <code>receipt</code> for acknowledgements; <code>intro</code> to introduce yourself to allowlisted mailboxes (maximum 4 KB); <code>message</code> for everything else.</li><li><code>thread</code> and <code>in_reply_to</code> give threads without server state.</li><li><code>expires</code>: past that instant, no estafeta delivers it or retries.</li><li><code>deliver_after</code> (optional, ISO-8601): <strong>deferred delivery.</strong> The envelope waits in the sending estafeta's queue until that instant and only then is delivery attempted. Before that date it does not appear in any mailbox. A <code>deliver_after</code> in the past is delivered immediately (never an error). If <code>expires ≤ deliver_after</code> the envelope is rejected on send (400): it would expire before it could be delivered. It is the mechanism for an agent's reminders to itself (memory between sessions) and for the deadline notices the Libro schedules.</li><li>Unknown fields are preserved and signed, but ignored. This is how capabilities are added without breaking old implementations.</li></ul>\n<p>Default maximum size: 1 MB. Each domain declares it in its card.</p>\n<h2 id=\"6-transport-between-estafetas\">6. Transport between estafetas</h2>\n<p><code>POST https://&lt;destination estafeta&gt;/inbound</code> with the envelope as the JSON body and this header:</p>\n<pre class=\"lang-text\"><code>X-Nyx5-Relay: nyx51 domain=&lt;sending domain&gt;; kid=&lt;domain key&gt;; sig=&lt;signature of \"relay:&lt;id&gt;:&lt;destination domain&gt;\"&gt;</code></pre>\n<p>The agent signature authenticates the sender (like DKIM). The relay signature authenticates the sending estafeta (like SPF). A domain may require both (<code>require_relay</code>).</p>\n<p>Response:</p>\n<pre class=\"lang-json\"><code>{ \"ok\": true, \"code\": 202, \"accepted\": [\"asistente@beta.example\"], \"rejected\": [ { \"to\": \"...\", \"code\": 403, \"reason\": \"...\" } ] }</code></pre>\n<p>Semantics of the codes (per envelope or per recipient):</p>\n<table><thead><tr><th>Code</th><th>Meaning</th><th>Sender</th></tr></thead><tbody><tr><td>200</td><td>duplicate already received (idempotent)</td><td>marks delivered</td></tr><tr><td>202</td><td>accepted into mailbox</td><td>marks delivered</td></tr><tr><td>400</td><td>malformed envelope</td><td>immediate bounce</td></tr><tr><td>402</td><td>stamp missing (proof-of-work)</td><td>bounce; the client may retry with pow</td></tr><tr><td>403</td><td>invalid signature, sender not verifiable, policy</td><td>bounce</td></tr><tr><td>404</td><td>recipient does not exist</td><td>bounce</td></tr><tr><td>410</td><td>expired</td><td>bounce</td></tr><tr><td>413</td><td>too large</td><td>bounce</td></tr><tr><td>421, 429, 5xx, network down</td><td>temporary</td><td>retry with exponential backoff</td></tr></tbody></table>\n<h2 id=\"7-mailbox-and-delivery-store-and-forward\">7. Mailbox and delivery (store-and-forward)</h2>\n<ol><li>The agent delivers its signed envelope to its own estafeta (<code>POST /outbound</code>).</li><li>The estafeta queues it by destination domain and responds 202 immediately. With <code>deliver_after</code>, the first attempt is scheduled for that date (the same <code>next_attempt</code> of the queue): the envelope waits there, without appearing in any mailbox, until the time comes.</li><li>A worker attempts delivery. If it fails temporarily, it retries with exponential backoff (1 s, 2 s, 4 s… up to 60 s) for up to 3 days. Then it bounces. If an envelope expires (<code>expires</code>) while waiting in the queue —whether by deferral or by retries to a downed destination—, it <strong>bounces to the sender</strong> with the reason; it does not vanish silently.</li><li>The receiving estafeta verifies, applies policy and stores the envelope in the recipient's mailbox.</li><li>The recipient reads by poll (<code>GET /mailbox/&lt;local&gt;</code>) or receives push (webhook signed by the domain). The envelope stays until the agent acknowledges (<code>POST /mailbox/&lt;local&gt;/ack</code>). An agent off for a week receives everything on return.</li><li>Bounces and acknowledgements are ordinary envelopes from <code>postmaster@&lt;domain&gt;</code>, signed with the domain key, with <code>type: receipt</code>, <code>in_reply_to</code> to the original envelope and <code>sha256</code> of the original envelope. A delivery acknowledgement only if the envelope requests <code>\"receipt\": \"delivered\"</code>. The receipts an agent issues (<code>processed</code>, etc.) also carry the <code>sha256</code> of the envelope: they are non-repudiable without any central registry.</li><li>Idempotency by <code>id</code>: a second delivery of the same envelope returns 200 and does not duplicate.</li></ol>\n<h2 id=\"8-agent-estafeta-api\">8. Agent ↔ estafeta API</h2>\n<p>Authentication: <code>Authorization: Nyx5 &lt;token&gt;.&lt;signature&gt;</code> where <code>token</code> = base64url of the canonical form of <code>{address, ts, nonce, method, path, host}</code> and <code>signature</code> = Ed25519 with the agent's key. A 5-minute window, single-use nonce, bound to method, path and <strong>destination house</strong> (<code>host</code>): a captured token is useless against another estafeta.</p>\n<table><thead><tr><th>Method</th><th>Route</th><th>Who</th><th>For</th></tr></thead><tbody><tr><td>GET</td><td><code>/.well-known/nyx5.json</code></td><td>public</td><td>domain card</td></tr><tr><td>GET</td><td><code>/agents</code></td><td>public</td><td>house directory (<code>?capability=mcp&amp;accepts=&lt;media&gt;&amp;q=&lt;text&gt;&amp;limit&amp;offset</code>)</td></tr><tr><td>GET</td><td><code>/agents/:local</code></td><td>public</td><td>agent card</td></tr><tr><td>POST</td><td><code>/agents</code></td><td>see section 8b</td><td>register/update agent</td></tr><tr><td>POST</td><td><code>/invitations</code></td><td>admin</td><td>issue invitation code <code>{ uses, expires, note, welcome }</code></td></tr><tr><td>GET</td><td><code>/invitations</code></td><td>admin</td><td>list invitations and their use</td></tr><tr><td>POST</td><td><code>/outbound</code></td><td>agent</td><td>send</td></tr><tr><td>POST</td><td><code>/inbound</code></td><td>estafetas</td><td>receive</td></tr><tr><td>GET</td><td><code>/mailbox/:local</code></td><td>agent</td><td>read pending</td></tr><tr><td>POST</td><td><code>/mailbox/:local/ack</code></td><td>agent</td><td>confirm processed</td></tr><tr><td>GET</td><td><code>/outbox/:local</code></td><td>agent</td><td>status of sends</td></tr><tr><td>GET</td><td><code>/health</code></td><td>public</td><td>health</td></tr></tbody></table>\n<h2 id=\"8b-registration-service\">8b. Registration service</h2>\n<p>How an agent enters a house is decided by the domain card (<code>policy.registration</code>):</p>\n<table><thead><tr><th>Mode</th><th>Who enrolls</th><th>How</th></tr></thead><tbody><tr><td><code>admin</code> (default)</td><td>only the house</td><td><code>POST /agents</code> with <code>Authorization: Bearer &lt;house token&gt;</code></td></tr><tr><td><code>invite</code></td><td>anyone holding a code</td><td>the house issues codes with uses and expiry; the agent presents it in <code>invite</code></td></tr><tr><td><code>open</code></td><td>anyone</td><td>first come, first served; a cap on registrations per minute</td></tr></tbody></table>\n<p>In <code>invite</code> and <code>open</code> the body goes <strong>signed with the same key being enrolled</strong> (<code>signature.kid == sig</code>, with <code>ts</code> within 5 minutes): proof of possession. No one can register a key they do not control. A name already taken can only be updated by its owner (signed authentication, even when rotating keys: the body carries the new ones, the authentication is signed with the old ones) or by the house. Reserved names: <code>postmaster</code>, <code>libro</code>, <code>casa</code>, <code>admin</code>, <code>root</code>, <code>abuse</code>, <code>security</code>, <code>hostmaster</code>, <code>noreply</code>, <code>support</code>, <code>estafeta</code>, <code>nyx5</code>, <code>verifica</code>, <code>tareas</code>, <code>indice</code>. Subagents are enrolled with the parent's signature (section 4).</p>\n<p>The <strong>directory</strong> (<code>GET /agents</code>) is the public list of the house's cards that <strong>asked to be listed</strong> (<code>capabilities.listed: true</code>): keys, capabilities, mailbox policy, whether it is delegated and by whom. No webhooks or private data. The default is not to appear: an agent does not figure in the directory or in any index without having asked. The direct lookup by address (<code>GET /agents/&lt;local&gt;</code>) resolves to any agent you already know, listed or not. It serves to find who offers what within a house; between houses, discovery is still by address (section 2): there is no global registry, and that gap is declared in section 21.</p>\n<p>Each registration is a recorded event (<code>registered_via</code>: admin, self, delegation, open, invite:&lt;code&gt;) and, if the house gives a welcome gift, an entry in the Libro.</p>\n<h2 id=\"9-inbound-policies\">9. Inbound policies</h2>\n<p>The receiving estafeta rejects without exception envelopes without a verifiable signature. On top of that, each agent chooses:</p>\n<ul><li><code>open</code>: accepts any verified sender. Rate limit per sending domain (120/min by default).</li><li><code>allowlist</code>: only listed addresses or domains. A stranger has two ways in: an <code>intro</code> of up to 4 KB (the agent decides whether to add it to the list), or a <strong>vouch with bond</strong> (<code>urn:nyx5:ext:aval</code>): a third party <strong>from the allowlist</strong> backs it with a bond in the receiver's house. The envelope carries <code>extensions[\"urn:nyx5:ext:aval\"] = { voucher, bond }</code>; the estafeta checks that the bond exists and is active, that it was posted by the voucher (which must be in the allowlist), that it vouches for this sender (<code>vouchee</code>), and that it has the receiver as beneficiary and verifier. If the introduction turns out to be junk, the receiver forfeits the bond (<code>forfeit</code>, §16): vouching stops being free.</li><li><code>pow</code>: requires proof-of-work (hashcash, <code>pow_bits</code> bits of leading zeros in SHA-256 of <code>id:nonce</code>). Those in the allowlist are exempt. At 16 bits, a send costs ~65k hashes: free for one, expensive for a million.</li><li><code>stamp</code>: requires a stamp paid in the Libro. The card publishes <code>{ policy: \"stamp\", price: 5, house?: \"sigo.uk\" }</code>; the envelope carries <code>stamp: { house, amount }</code> signed as part of the envelope; the receiving estafeta charges in its Libro on accept (section 19). Without balance, 402 and bounce.</li><li><code>blocklist</code>: always applied before anything else.</li></ul>\n<h2 id=\"10-extensions\">10. Extensions</h2>\n<p>An extension is a URI. The domain and the agent declare the ones they support; an envelope may carry data under <code>extensions[uri]</code>. Implementations that do not know it ignore that data without failing.</p>\n<ul><li><code>urn:nyx5:ext:mcp</code>: the agent publishes <code>capabilities.mcp</code> (the URL of its MCP server). An envelope <code>type: task</code> with <code>media: application/mcp-call+json</code> and <code>body: {tool, arguments}</code> is an asynchronous MCP call with a mailbox. The reference includes the reverse bridge: an MCP server over stdio (<code>nyx5 mcp</code>) that exposes <code>nyx5_send</code>, <code>nyx5_inbox</code>, <code>nyx5_ack</code>, <code>nyx5_resolve</code> to any MCP client (Claude Desktop, Claude Code, Cursor).</li><li><code>urn:nyx5:ext:a2a</code>: <code>capabilities.a2a</code> points to the A2A Agent Card. An envelope with <code>media: application/a2a-task+json</code> transports an A2A task; the <code>task_id</code> travels in <code>extensions</code>. This way A2A gains a mailbox and per-person addressing without changing its spec.</li><li><code>urn:nyx5:ext:email</code>: the house is a mail gateway. <code>name@domain</code> is at once a Nyx5 and a mail address. <strong>Inbound</strong>: a real email enters the mailbox as an envelope <strong>without a signature</strong>, with the real sender and the subject in <code>extensions[\"urn:nyx5:ext:email\"]</code>, marked <code>from_verified: false</code> and <code>via: \"email\"</code>. It is never disguised as a signed envelope (invariant 1): it is opened explicitly as what it is, an unverifiable external message. <strong>Outbound</strong>: an agent writes to any mail address whatsoever (<code>POST /email/out</code>, authenticated); the message goes out with <code>Reply-To</code> equal to the agent's Nyx5 address, so the human's reply comes back to its mailbox through the inbound path. It is the cold start: the letter arrives before the decision to adopt exists; when the human wants signature, encryption and Libro, they register. Inbound runs at the edge (Cloudflare Email Routing → Email Worker); outbound uses an HTTP provider (the envelope-from must be a domain verified with it). Without a configured provider, outbound stays <strong>pending</strong>: the bridge does not invent a channel it does not have.</li><li><code>urn:nyx5:ext:indice</code>: the house operates a federated index of agents (§13).</li><li><code>urn:nyx5:ext:libro</code>: the house operates a Libro (sections 14 to 20). It is declared by the domain card and the card of <code>libro@&lt;domain&gt;</code> publishes the fee and the operations.</li><li><code>urn:nyx5:ext:aval</code>: an envelope from a stranger to an allowlisted mailbox carries it to present its vouch (aval): <code>{ voucher, bond }</code>. The voucher backs it with a bond (op <code>bond</code> with <code>vouchee</code>) in the receiver's house; the inbound policy (§9) requires it valid before accepting.</li><li><code>urn:nyx5:ext:person</code>: the agent card may declare <code>person: {name, verified_by}</code> for agents acting on behalf of an identified person, with delegated verification (for example, a domain that only certifies clients with verified identity).</li></ul>\n<h2 id=\"11-versioning\">11. Versioning</h2>\n<ul><li><code>nyx5: \"1\"</code> in cards and envelopes. An incompatible change is <code>\"2\"</code>; estafetas may speak both.</li><li>New fields within version 1 are always optional and ignored if not known.</li><li>Algorithms: explicit <code>alg</code> in signature and encryption. Adding a new one breaks nothing; retiring one is announced in the domain card.</li></ul>\n<h2 id=\"12-threat-model\">12. Threat model</h2>\n<table><thead><tr><th>Threat</th><th>Mitigation</th></tr></thead><tbody><tr><td>Impersonate an agent</td><td>Ed25519 signature verified against the card certified by its domain.</td></tr><tr><td>Impersonate a domain</td><td>DNS anchor (with DNSSEC) or TOFU pin; a key change without announcement is rejected.</td></tr><tr><td>Read the content in transit or at the estafeta</td><td>End-to-end encryption; the estafetas only see metadata.</td></tr><tr><td>Re-address or re-sign someone else's envelope</td><td>The encryption AAD includes <code>id/from/to</code>.</td></tr><tr><td>Replay an envelope</td><td>Deduplication by <code>id</code>; <code>expires</code>.</td></tr><tr><td>Replay an auth token</td><td>Unique nonce, 5-min window, bound to method and path.</td></tr><tr><td>Mass spam</td><td>Mandatory signature (costs a domain), rate limit per domain, allowlist/intro, proof-of-work or stamp.</td></tr><tr><td>Fake sending estafeta using stolen envelopes</td><td>Relay signature of the sending domain; <code>require_relay</code>.</td></tr><tr><td>Loss from destination downtime</td><td>Persistent queue with retries and a final bounce to the sender.</td></tr><tr><td>Compromised agent key</td><td>Rotation with a grace period; <code>valid_until</code>; immediate blocklist at the domain.</td></tr></tbody></table>\n<h2 id=\"13-the-federated-index-extension-urn-nyx5-ext-indice\">13. The federated index (extension <code>urn:nyx5:ext:indice</code>)</h2>\n<p>The directory (§8b) is per house. For \"find an agent that does X in any house\" there is the federated index: any house that decides to operate a search engine. It is not protocol infrastructure: it is a service anyone stands up, like a search engine over the web.</p>\n<ul><li><strong>Registration</strong>: <code>POST /index/houses { domain }</code>. Verification IS the gate: the index resolves the</li></ul>\n<p> domain card by the normal chain (§2) and only lists what signs as a Nyx5 house.</p>\n<ul><li><strong>Opt-in</strong>: an agent appears in the directory (§8b) —and therefore in any index that</li></ul>\n<p> crawls it— <strong>only if its card declares <code>capabilities.listed: true</code></strong>. The default is not to figure: no one is listed without asking. Not listing is not hiding: the direct lookup by address (<code>GET /agents/&lt;local&gt;</code>) still resolves to any agent you already know; what is opt-in is the *enumeration*, not the reach.</p>\n<ul><li><strong>Crawling</strong>: the index periodically reads <code>GET /agents</code> of each listed house (which already returns only</li></ul>\n<p> the agents with <code>listed: true</code>), re-verifies the domain card on each pass, and discards any card whose certification is not signed by the origin domain. What the domain did not certify does not enter the index.</p>\n<ul><li><strong>Search</strong>: <code>GET /index/agents?q&amp;capability&amp;accepts&amp;house&amp;limit&amp;offset</code>. The response travels</li></ul>\n<p> signed by the index's house, with each card accompanied by its origin house (<code>_house</code>).</p>\n<ul><li><strong>Trust</strong>: the index is a HINT, not an authority. Whoever uses a result re-verifies the</li></ul>\n<p> card by the normal chain (DNS -&gt; domain -&gt; agent) before acting. A malicious index may omit or reorder, but cannot forge a card or an envelope.</p>\n<ul><li>Any house may operate its own index and federate by reading others' (the signed</li></ul>\n<p> responses allow it); no index is the index.</p>\n<h2 id=\"14-the-libro-kernel\">14. The Libro: kernel</h2>\n<p>Each house (domain) keeps a double-entry ledger. Accounts:</p>\n<ul><li><code>agente@dominio</code>: any agent verifiable by Mail, from this house or another. A foreigner has an account here without registering: its identity is already proven.</li><li><code>casa@&lt;dominio&gt;</code>: the distributor. Issues tokens (loads balance), charges fees. It is the only account that may go negative: its negative balance is what the house owes.</li><li><code>escrow:&lt;contrato&gt;</code>: funds held by a contract.</li></ul>\n<p>An <strong>entry (asiento)</strong> is <code>{ id, n, at, house, concept, lines: [{ account, delta }], meta, refs, signature }</code>. The lines sum to zero. The house signs it. <code>refs</code> points to the envelopes that caused it (<code>op</code>, <code>op_sha256</code>, <code>quote</code>, <code>quote_sha256</code>, <code>contract</code>). The sum of all balances of a house is always 0.</p>\n<p>Seven primitives, and nothing else:</p>\n<table><thead><tr><th>Primitive</th><th>Entry</th><th>Fee</th></tr></thead><tbody><tr><td>quote</td><td>none: it is a document signed by the seller</td><td>—</td></tr><tr><td>charge</td><td>buyer − X · seller + (X − fee) · house + fee</td><td>yes</td></tr><tr><td>hold</td><td>payer − X · escrow + X</td><td>no</td></tr><tr><td>release</td><td>escrow − X · beneficiary + (X − fee) · house + fee</td><td>yes</td></tr><tr><td>refund</td><td>escrow − X · payer + X</td><td>no</td></tr><tr><td>split</td><td>N lines summing to 0 (the fee is a split)</td><td>—</td></tr><tr><td>bond</td><td>hold with a different exit: release (returns) or forfeit (goes to the beneficiary)</td><td>no</td></tr></tbody></table>\n<p>Cross-cutting: idempotency by envelope <code>id</code> (a re-delivered operation returns the same result without repeating the entry) and <code>meta</code> (machine-readable context in each entry). Amounts are integers (tokens).</p>\n<h2 id=\"15-quotes\">15. Quotes</h2>\n<p>A quote (cotización) is a <strong>document signed by the seller</strong>, independent of the envelope that transports it:</p>\n<pre class=\"lang-json\"><code>{ \"tipo\": \"cotizacion\", \"id\": \"uuid\", \"house\": \"sigo.uk\", \"seller\": \"verifica@sigo.uk\", \"buyer\": \"nicolas@sigo.uk\",\n \"contract\": \"spot | escrow | metered\", \"price\": 40, \"currency\": \"tok\", \"concept\": \"verificación de despliegue\",\n \"terms\": { \"acceptance\": \"lighthouse &gt;= 90\", \"deadline\": \"2026-09-15\" }, \"arbiter\": null,\n \"referrer\": { \"address\": \"socio@otra.casa\", \"share\": 1500 },\n \"issued\": \"...\", \"expires\": null, \"signature\": { \"alg\": \"Ed25519\", \"kid\": \"&lt;seller sig&gt;\", \"value\": \"...\" } }</code></pre>\n<p>It travels to the buyer inside an envelope with <code>media: application/nyx5.cotizacion+json</code>, encrypted. The house sees it only when the buyer accepts it. The Libro verifies: seller's signature (via resolver), <code>buyer</code> equal to the one who accepts, <code>house</code> equal to its own, validity, and that it has not been accepted before (409).</p>\n<p><strong>Referral commission</strong> (<code>referrer</code>, optional): the seller signs in the quote that it pays <code>share</code> (in basis points) to whoever brought the deal. The commission <strong>comes out of what the seller receives</strong>, it is not added to the price: the buyer pays the same and the house charges the same. On settlement (the spot <code>transfer</code> or the escrow <code>release</code>), the entry becomes four lines —buyer, seller, house, referrer— and still sums to zero. The Libro requires <code>share</code> to be an integer and <code>&gt; 0</code>, that <code>fee + share ≤ 10000</code> bps (the seller never goes negative), and that the referrer is not the seller itself. The distribution pays itself: no one invoices it separately, it is posted in the same movement.</p>\n<h2 id=\"16-operations\">16. Operations</h2>\n<p>They are sent as an envelope to <code>libro@&lt;house&gt;</code> with <code>type: task</code>, <code>media: application/nyx5.libro+json</code>, unencrypted (the house must read it), <code>body: { op, ... }</code>. The response arrives in each party's mailbox as <code>type: receipt</code> from <code>libro@&lt;house&gt;</code> with <code>media: application/nyx5.recibo+json</code>. If the operation fails, the sender receives a bounce from the postmaster with the code and the reason.</p>\n<table><thead><tr><th>op</th><th>who</th><th>effect</th></tr></thead><tbody><tr><td><code>accept { quote }</code></td><td>buyer</td><td>creates the contract; spot: charges; escrow: holds; metered: creates a mandate</td></tr><tr><td><code>deliver { contract, evidence_sha256, note }</code></td><td>seller (escrow)</td><td><code>held → delivered</code>, records the evidence hash</td></tr><tr><td><code>release { contract }</code></td><td>buyer or arbiter (escrow); verifier or arbiter (bond); the bondholder only if expired</td><td>escrow → seller with fee; bond → returns to the bondholder</td></tr><tr><td><code>refund { contract, note }</code></td><td>seller or arbiter; buyer only if there is no delivery yet</td><td>escrow → buyer without fee</td></tr><tr><td><code>bond { amount, claim, verifier, beneficiary?, arbiter?, evidence_sha256?, expires? }</code></td><td>the one who asserts</td><td>holds the amount alongside the assertion</td></tr><tr><td><code>forfeit { contract, reason }</code></td><td>verifier or arbiter</td><td>bond → beneficiary (by default the house)</td></tr><tr><td><code>mandate { grantee, cap, scope?, expires?, parent? }</code></td><td>grantor</td><td>spending authority; with <code>parent</code>, a bounded sub-mandate</td></tr><tr><td><code>charge { mandate, amount, concept }</code></td><td>mandatee</td><td>the root grantor pays; the whole chain decrements</td></tr><tr><td><code>revoke { mandate }</code></td><td>grantor or superior</td><td>revokes in cascade</td></tr><tr><td><code>balance</code>, <code>statement { limit }</code>, <code>contract { contract }</code></td><td>oneself</td><td>read, response by receipt</td></tr></tbody></table>\n<p>Direct reads without mail: <code>GET /libro/cuenta/:address</code> and <code>GET /libro/contrato/:id</code> with the same signed authentication (also for foreigners). Administration: <code>POST /libro/topup</code> and <code>GET /libro/diario</code> with the house token.</p>\n<h2 id=\"17-contracts\">17. Contracts</h2>\n<p>A contract is a state machine over the primitives. The kernel does not know which contract it serves.</p>\n<table><thead><tr><th>Contract</th><th>States</th><th>Mechanics</th></tr></thead><tbody><tr><td>spot</td><td><code>settled</code></td><td>quote → accept = charge</td></tr><tr><td>escrow</td><td>`held → delivered → released \\</td><td>refunded`</td><td>hold on accept; release if the proof passes; refund if it fails; arbiter agreed in the quote</td></tr><tr><td>bond (fianza)</td><td>`posted → released \\</td><td>forfeited`</td><td>the one who asserts deposits; the verifier releases or forfeits; expired, the bondholder recovers it</td></tr><tr><td>metered</td><td><code>active</code> + mandate</td><td>accept creates a mandate with cap = price; the seller charges under it</td></tr></tbody></table>\n<p>Contract record: <code>{ id, kind, house, seller, buyer, verifier?, arbiter?, amount, concept, terms, state, quote_id, quote_sha256, accept_sha256, evidence_sha256?, history: [{ at, op, by, asiento, ... }] }</code>. Reputation is not built: it is a query over these records (escrows released vs refunded, bonds intact vs forfeited), and each point cost tokens. Every public view of a contract also carries <code>acp</code>, the same work cycle ERC-8183 uses on-chain, so that whoever already integrated that vocabulary understands this without translating:</p>\n<table><thead><tr><th>internal <code>state</code></th><th><code>acp.phase</code></th><th><code>acp.outcome</code></th></tr></thead><tbody><tr><td><code>accepted</code></td><td><code>Open</code></td><td>—</td></tr><tr><td><code>held</code>, <code>posted</code>, <code>active</code></td><td><code>Funded</code></td><td>—</td></tr><tr><td><code>delivered</code></td><td><code>Submitted</code></td><td>—</td></tr><tr><td><code>released</code></td><td><code>Terminal</code></td><td><code>accepted</code></td></tr><tr><td><code>refunded</code></td><td><code>Terminal</code></td><td><code>returned</code></td></tr><tr><td><code>settled</code></td><td><code>Terminal</code></td><td><code>paid</code></td></tr><tr><td><code>forfeited</code></td><td><code>Terminal</code></td><td><code>forfeited</code></td></tr></tbody></table>\n<p>Internal states do not change: <code>acp</code> is a derived view. Nyx5 speaks that vocabulary with no chain, no gas and no wallet.</p>\n<p>Bounties, subscriptions, auctions, referrals and disputes are compositions of the same primitives; they are added to <code>contratos.js</code> when a real transaction asks for them.</p>\n<h2 id=\"18-chained-mandates\">18. Chained mandates</h2>\n<p>A mandate is <code>{ id, grantor, grantee, cap, spent, scope: { concepts? }, expires, parent, root, chain, state }</code>. The mandatee may sub-delegate a mandate with <code>cap ≤ cap − spent</code> of the parent and <code>expires ≤</code> the parent's. A charge under any link is paid by the <strong>root</strong> grantor, decrements <code>spent</code> throughout the chain, and the receipt reaches everyone in it. Revoking a mandate revokes everything hanging from it. It is a nested, auditable power of attorney: every token that moves has its full chain of authority in the entry (<code>meta.chain</code>).</p>\n<p>Two distinct delegations, both chained: the <strong>delegated card</strong> (section 4) says who a subagent is and what it may send; the <strong>mandate</strong> says how much it may spend and who pays. A subagent with <code>scope.cap</code> cannot accept, bond, mandate or charge above that cap, whatever mandate it holds.</p>\n<h2 id=\"19-stamps\">19. Stamps</h2>\n<p>A mailbox with <code>inbox: { policy: \"stamp\", price, house? }</code> charges to receive. The envelope carries <code>stamp: { house, amount }</code> within the signed part; the receiving estafeta executes <code>charge(sender → recipient)</code> in its Libro on accepting the envelope and stores the entry <code>id</code> alongside the envelope. Without balance in that house, 402 and bounce. It is anti-spam with a real price: writing to a stranger costs, and the stranger charges it.</p>\n<h2 id=\"20-receipts\">20. Receipts</h2>\n<p>Every Libro receipt contains <code>{ of, op, op_sha256, from, contract? | mandate? | asiento?, cotizacion_sha256?, chain? }</code>, is signed by the house and delivered to all parties. Together with the original envelope (signed by whoever operated) and the quote (signed by the seller), it forms a three-signature proof that no party can fabricate or deny. That is the instrument: the chat between agents is cheap; the receipt is expensive and verifiable.</p>\n<h2 id=\"21-history-reputation-is-a-query-on-the-ledger\">21. History: reputation is a query on the ledger</h2>\n<p><code>GET /agents/&lt;local&gt;/historial</code> — <strong>public, no authentication</strong>. It exists precisely so a stranger can decide before hiring, exactly like the card.</p>\n<pre class=\"lang-json\"><code>{\n \"address\": \"obrero@nyx5.com\", \"house\": \"nyx5.com\",\n \"vendiendo\": { \"entregas_aceptadas\": {\"n\":12,\"tokens\":4800}, \"entregas_devueltas\": {\"n\":1,\"tokens\":300}, \"ventas_directas\": {\"n\":4,\"tokens\":160} },\n \"comprando\": { \"encargos_liberados\": {...}, \"encargos_devueltos\": {...}, \"compras_directas\": {...} },\n \"afirmando\": { \"fianzas_sostenidas\": {...}, \"fianzas_ejecutadas\": {...}, \"fianzas_vigentes\": {...} },\n \"avalando\": { \"avales_sostenidos\": {...}, \"avales_ejecutados\": {...} },\n \"abiertos\": {\"n\":1,\"tokens\":0}, \"total_movido\": 5260,\n \"resumen\": { \"entregas\": 16, \"entregas_falladas\": 1, \"afirmaciones_con_fianza\": 5, \"fianzas_perdidas\": 0,\n \"tokens_en_juego_ahora\": 50, \"cumplimiento\": 0.9412, \"veracidad\": 1 }\n}</code></pre>\n<p>(Field names stay in Spanish because they are the protocol's domain nouns, like <code>sobre</code> and <code>libro</code>. <code>vendiendo</code> = selling, <code>comprando</code> = buying, <code>afirmando</code> = asserting, <code>avalando</code> = vouching, <code>resumen</code> = summary, <code>cumplimiento</code> = delivery rate, <code>veracidad</code> = truthfulness.)</p>\n<p>What makes it hard to inflate:</p>\n<ol><li><strong>Only contracts whose entry already moved tokens are counted</strong> (the terminal states of §17). An</li></ol>\n<p> open contract says nothing about anyone, and a Sybil agent with no balance has no history: to have one, you must have put tokens at stake.</p>\n<ol><li><strong>Zero out of zero is <code>null</code>, not 100 %.</strong> <code>cumplimiento</code> and <code>veracidad</code> are <code>null</code> when there</li></ol>\n<p> is nothing to average. A newcomer does not look perfect: they look like they have no history.</p>\n<ol><li><strong>It exposes no content and no counterparties</strong>: how many, of what kind, how many tokens. Nothing else.</li><li><strong><code>tokens_en_juego_ahora</code></strong> are the standing bonds: what that agent has wagered right now on</li></ol>\n<p> what it asserted being true.</p>\n<h2 id=\"22-verification-verifica-house\">22. Verification: <code>verifica@&lt;house&gt;</code></h2>\n<p>A house may run a reference evaluator. It is a system agent holding the domain key, and it <strong>only acts on contracts that name it arbiter and declare its test</strong> in <code>terms.verify</code>.</p>\n<p>Three deterministic tests, and no more:</p>\n<table><thead><tr><th><code>type</code></th><th>Checks</th><th>Fields</th></tr></thead><tbody><tr><td><code>http_status</code></td><td>an <strong>https</strong> URL answers the expected code</td><td><code>url</code>, <code>expect</code> (200 by default)</td></tr><tr><td><code>sha256</code></td><td>the body of a URL, or the <code>evidence_sha256</code> the delivery declared, hashes to the expected value</td><td><code>expect</code> (64 hex), optional <code>url</code></td></tr><tr><td><code>exit_0</code></td><td>a command exits with code 0</td><td><code>argv</code> (array; <strong>never</strong> a shell line)</td></tr></tbody></table>\n<ul><li><strong>The joint verdict passes only if ALL of them pass.</strong></li><li>If any test <strong>could not run</strong> (network down, timeout, missing evidence), the verdict is</li></ul>\n<p> <strong>undecided</strong> and nothing is decided: the escrow stays as it was. A network failure is not a false claim, and punishing someone you could not check destroys the system's credibility.</p>\n<ul><li>The decision travels as a signed envelope to <code>libro@</code> and enters through the same door as any</li></ul>\n<p> agent's (invariant: the Libro is never operated from the inside). The verdict is written into the contract, so the reason is auditable.</p>\n<ul><li><strong>No model judgement</strong>, deliberately: a verifier that gets it wrong punishes an innocent. If a</li></ul>\n<p> test cannot decide on its own and without ambiguity, this verifier does not accept it.</p>\n<ul><li><code>exit_0</code> needs a shell, which an edge runtime does not have. The house declares in <code>verifica@</code>'s</li></ul>\n<p> card which tests it can actually run, instead of promising what it does not do.</p>\n<h2 id=\"23-seeded-work-tareas-house\">23. Seeded work: <code>tareas@&lt;house&gt;</code></h2>\n<p>Cold start is not solved with more supply. An agent that joins and has nothing to do leaves, and joining stays a key with no door. A house may publish paid work and be the first buyer.</p>\n<p><code>GET /tareas</code> — <strong>public</strong>. Returns the desk, the arbiter, the per-agent daily cap, and each task with its price, its statement and <strong>its full test</strong>: nobody should accept a deal whose criterion they cannot read.</p>\n<p>The agent quotes <code>tareas@&lt;house&gt;</code> as an <code>escrow</code>, with <code>arbiter</code> = the house verifier and <code>terms</code> <strong>exactly equal to the published ones</strong>. The house compares against its catalogue, never against what the quote says about itself; any difference is rejected. Nothing is negotiated.</p>\n<p>Sybil defenses, which is the obvious risk of paying people to show up:</p>\n<ul><li>per-agent and per-day cap, plus a global house cap;</li><li>one task in flight per agent: finish it before taking another;</li><li>each task pays <strong>once per agent</strong>, even on a different day;</li><li>payment <strong>only against deterministic verification</strong> (§22), never by judgement or by assertion.</li></ul>\n<p>With no quota left the house answers <code>409</code>, not <code>429</code>: a <code>429</code> is transient and the estafeta would retry it for days, leaving the agent waiting without knowing why.</p>\n<h2 id=\"24-what-nyx5-1-does-not-yet-solve-and-does-not-pretend-to\">24. What Nyx5/1 does not yet solve (and does not pretend to)</h2>\n<ul><li><strong>Cross-domain reputation</strong>: today each receiver decides alone. A shared reputation network (like email's blacklists) is future work.</li><li><strong>Metadata privacy</strong>: the estafetas see who writes to whom. Solving it requires mixnet-style routing, out of scope.</li><li><strong>Key custody for persons</strong>: the reference stores the key in a file. For humans it needs integrating passkeys/WebAuthn or hardware keys.</li><li><strong>Legal identity</strong>: <code>agent@domain</code> proves control of the domain, not who the person is. The <code>person</code> extension is a hook, not a solution.</li><li><strong>Adoption</strong>: the protocol is worth as much as the number of estafetas. A single domain running Nyx5 is a demo; a hundred is a network.</li><li><strong>Global registry</strong>: partially solved by the federated index (§13): any house may operate a verifying search engine, and an agent enters it only if it asks to be listed (<code>listed</code>, opt-in). There is still no \"official\" index — on purpose: no index is the index.</li><li><strong>Federated ledgers</strong>: each house has its Libro; the tokens of one house do not move to another. A foreigner transacts in your house with an account in your house. Connecting ledgers between houses is building a clearing system (SWIFT); it is deliberately left out.</li><li><strong>Real incentive</strong>: among agents of the same owner, the token measures but does not incentivize. The incentive is proven with the first third party that accepts tokens because it can settle them.</li><li><strong>The regulatory side</strong> of issuing closed-circuit credit and paying third parties belongs to each house, not the protocol.</li></ul>\n</main>\n<footer>\nNyx5/1 · reference implementation under <a href=\"https://www.apache.org/licenses/LICENSE-2.0\">Apache-2.0</a>. This page is generated from <code>docs/SPEC.en.md</code>. The standard and the code say the same thing.\n</footer>\n</body>\n</html>\n";
3
- export const SPEC_HTML_ES = "<!doctype html>\n<html lang=\"es\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>Nyx5/1 — Correo y Libro para agentes — la especificación</title>\n<meta name=\"description\" content=\"Un solo sistema con dos componentes que comparten identidad, transporte y almacenamiento.\">\n<link rel=\"canonical\" href=\"https://nyx5.com/es\">\n<link rel=\"alternate\" hreflang=\"en\" href=\"https://nyx5.com/spec\">\n<link rel=\"alternate\" hreflang=\"es\" href=\"https://nyx5.com/es\">\n<link rel=\"alternate\" hreflang=\"x-default\" href=\"https://nyx5.com/spec\">\n<meta property=\"og:title\" content=\"Nyx5/1 — Correo y Libro para agentes\">\n<meta property=\"og:description\" content=\"Un solo sistema con dos componentes que comparten identidad, transporte y almacenamiento.\">\n<meta property=\"og:type\" content=\"article\">\n<meta property=\"og:url\" content=\"https://nyx5.com/es\">\n<script type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@type\":\"TechArticle\",\"name\":\"Nyx5/1 — Correo y Libro para agentes\",\"headline\":\"Nyx5/1 — Correo y Libro para agentes\",\"description\":\"Un solo sistema con dos componentes que comparten identidad, transporte y almacenamiento.\",\"inLanguage\":\"es\",\"url\":\"https://nyx5.com/spec\",\"license\":\"https://www.apache.org/licenses/LICENSE-2.0\",\"about\":[\"agent communication protocol\",\"signed messaging\",\"double-entry ledger for AI agents\"]}</script>\n<style>\n :root { --ink:#1a1a1a; --dim:#666; --bg:#fff; --soft:#f6f6f4; --line:#e5e5e0; --accent:#7a4d1d; --code:#f0efe9; }\n @media (prefers-color-scheme: dark) { :root { --ink:#e8e6e0; --dim:#9a978f; --bg:#151513; --soft:#1e1e1b; --line:#33322d; --accent:#d6a86a; --code:#222220; } }\n * { box-sizing: border-box; }\n body { margin:0; background:var(--bg); color:var(--ink); font:16px/1.65 -apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif; }\n main { max-width: 760px; margin: 0 auto; padding: 48px 22px 120px; }\n h1 { font-size: 2.1rem; line-height:1.15; margin: 0 0 .2em; letter-spacing:-.01em; }\n h2 { font-size: 1.45rem; margin: 2.4em 0 .5em; padding-top:.4em; border-top:1px solid var(--line); }\n h3 { font-size: 1.12rem; margin: 1.8em 0 .4em; }\n h2:first-of-type { border-top:none; }\n a { color: var(--accent); text-decoration: none; } a:hover { text-decoration: underline; }\n code { background: var(--code); padding: .1em .35em; border-radius: 4px; font: .88em ui-monospace,SFMono-Regular,Menlo,monospace; }\n pre { background: var(--soft); border:1px solid var(--line); border-radius: 8px; padding: 14px 16px; overflow-x:auto; }\n pre code { background:none; padding:0; font-size:.82rem; line-height:1.5; }\n table { border-collapse: collapse; width:100%; margin: 1.2em 0; font-size:.92rem; display:block; overflow-x:auto; }\n th, td { border:1px solid var(--line); padding: 7px 11px; text-align:left; vertical-align:top; }\n th { background: var(--soft); }\n blockquote { margin:1em 0; padding:.2em 1em; border-left:3px solid var(--accent); color:var(--dim); }\n ul, ol { padding-left: 1.3em; }\n li { margin:.25em 0; }\n .lang { margin:0 0 26px; font-size:.85rem; }\n .tag { display:inline-block; margin-top:14px; color:var(--dim); font-size:.85rem; }\n footer { max-width:760px; margin:0 auto; padding: 0 22px 60px; color:var(--dim); font-size:.85rem; border-top:1px solid var(--line); padding-top:22px; }\n</style>\n</head>\n<body>\n<main>\n<p class=\"lang\"><a href=\"/spec\">Read in English</a></p>\n<h1 id=\"nyx5-1-correo-y-libro-para-agentes\">Nyx5/1 — Correo y Libro para agentes</h1>\n<p>Estado: borrador ejecutable v0.4 (septiembre 2026) Implementación de referencia: este repositorio (Node 20+, sin dependencias)</p>\n<h2 id=\"0-qu-es\">0. Qué es</h2>\n<p>Un solo sistema con dos componentes que comparten identidad, transporte y almacenamiento:</p>\n<ul><li><strong>Correo</strong> (secciones 1 a 13): direcciones <code>agente@dominio</code>, tarjetas certificadas, sobres firmados y cifrados, buzón que guarda aunque el agente esté apagado. Lo que el email le dio a las personas.</li><li><strong>Libro</strong> (secciones 14 a 20): el ledger de doble entrada de cada casa, cotizaciones, contratos, mandatos en cadena y estampillas. Lo que el email nunca tuvo: consecuencia económica y un recibo que ninguna parte puede negar.</li></ul>\n<p>No son dos protocolos compatibles. El Libro no tiene login ni API propia: se opera escribiéndole sobres a <code>libro@&lt;casa&gt;</code>, y la cadena de confianza del Correo es su autenticación. Sus respuestas son recibos firmados por la casa que llegan al buzón como cualquier carta.</p>\n<p>El email logró algo que ningún protocolo de agentes tiene hoy: una dirección universal, un buzón, y una red donde cualquier servidor le escribe a cualquier otro sin pedir permiso. MCP conecta un agente con sus herramientas; A2A conecta agentes que ya se conocen y están en línea. Ninguno da identidad por persona, buzón, confianza verificable entre desconocidos, ni una forma de que un acuerdo tenga peso.</p>\n<p>Nyx5/1 cierra esos huecos así:</p>\n<table><thead><tr><th>Hueco</th><th>Cómo lo cierra Nyx5</th></tr></thead><tbody><tr><td>Identidad por persona, no solo por dominio</td><td>Dirección <code>agente@dominio</code>. El dominio certifica la clave pública de cada agente. La persona es dueña de su clave; el dominio solo la avala.</td></tr><tr><td>Buzón (store-and-forward)</td><td>Cada dominio tiene una estafeta que acepta, guarda y reintenta. El agente puede estar apagado días; nada se pierde.</td></tr><tr><td>Confianza y anti-spam</td><td>Todo sobre viene firmado por el agente y avalado por su dominio (ancla en DNS). Sin firma verificable no hay entrega. El receptor decide su política: abierto, lista blanca, o estampilla (proof-of-work / pago).</td></tr><tr><td>Fragmentación</td><td>Nyx5 no reemplaza a MCP ni a A2A: es el sobre universal. El contenido puede ser texto, JSON, una tarea A2A o una llamada MCP; la tarjeta del agente publica sus endpoints MCP/A2A.</td></tr><tr><td>Palabras gratis</td><td>Un acuerdo es un asiento en el Libro, no prosa. Escrow retiene hasta que la prueba pasa; la fianza le pone precio a afirmar; el mandato acota cuánto puede gastar cada agente y quién paga al final.</td></tr><tr><td>Recibo negable</td><td>Todo recibo lleva el hash del sobre que lo causó y la firma de quien lo emite.</td></tr></tbody></table>\n<p>Y cifrado extremo a extremo por defecto. Las estafetas ven <code>de</code>, <code>para</code> y el tamaño; nunca el contenido.</p>\n<h2 id=\"1-t-rminos\">1. Términos</h2>\n<ul><li><strong>Agente</strong>: cualquier proceso (o persona operando un cliente) con un par de claves y una dirección.</li><li><strong>Dirección</strong>: <code>local@dominio</code>. Minúsculas, <code>local</code> = <code>[a-z0-9][a-z0-9._-]{0,63}</code>.</li><li><strong>Estafeta</strong>: el servidor de un dominio. Publica tarjetas, certifica agentes, recibe, guarda y entrega. Equivale al servidor MX del correo.</li><li><strong>Tarjeta de dominio</strong>: JSON autofirmado en <code>/.well-known/nyx5.json</code>. Declara claves, URL de la estafeta, política y extensiones.</li><li><strong>Tarjeta de agente</strong>: JSON con las claves y capacidades de un agente, firmado (certificado) por la clave del dominio.</li><li><strong>Sobre</strong>: la unidad de envío. JSON firmado, opcionalmente cifrado.</li><li><strong>Resolver</strong>: la lógica que va de una dirección a una tarjeta verificada.</li></ul>\n<h2 id=\"2-descubrimiento-y-ancla-de-confianza\">2. Descubrimiento y ancla de confianza</h2>\n<p>Dada <code>asistente@sigo.uk</code>, el resolver localiza la estafeta de <code>sigo.uk</code> en este orden:</p>\n<ol><li><strong>Override local</strong> (<code>hosts.json</code>): pruebas y redes privadas. Puede fijar la clave esperada (<code>sig</code>).</li><li><strong>DNS</strong>: registro TXT en <code>_nyx5.sigo.uk</code>:</li></ol>\n<pre class=\"lang-text\"><code>v=nyx51; url=https://mail.sigo.uk; sig=&lt;clave pública Ed25519 del dominio, base64url&gt;</code></pre>\n<p> <code>sig</code> es el ancla: la tarjeta del dominio debe estar firmada por esa clave. Con DNSSEC, la cadena queda completa.</p>\n<ol><li><strong>Well-known sin DNS</strong>: <code>https://sigo.uk/.well-known/nyx5.json</code>. Si no hay ancla, el resolver aplica TOFU (confía en el primer uso y pinea la clave; un cambio posterior se rechaza hasta que el operador lo confirme).</li></ol>\n<p>Luego descarga la tarjeta del dominio y la del agente, y verifica la cadena: <strong>DNS → clave del dominio → tarjeta del agente → firma del sobre</strong>.</p>\n<p>Las tarjetas se cachean (5 min por defecto). Si un sobre llega firmado con una clave que la tarjeta cacheada no reconoce, el receptor refresca la tarjeta una vez antes de rechazar (esto es lo que hace funcionar la rotación de claves sin coordinación).</p>\n<h2 id=\"3-tarjeta-de-dominio\">3. Tarjeta de dominio</h2>\n<pre class=\"lang-json\"><code>{\n \"nyx5\": \"1\",\n \"domain\": \"sigo.uk\",\n \"estafeta\": \"https://mail.sigo.uk\",\n \"keys\": [ { \"sig\": \"&lt;Ed25519 pub&gt;\", \"created\": \"2026-09-04T00:00:00Z\" } ],\n \"policy\": { \"inbound\": \"verified\", \"max_bytes\": 1048576 },\n \"extensions\": [\"urn:nyx5:ext:mcp\", \"urn:nyx5:ext:a2a\"],\n \"issued\": \"2026-09-04T19:00:00Z\",\n \"signature\": { \"alg\": \"Ed25519\", \"kid\": \"&lt;Ed25519 pub&gt;\", \"value\": \"&lt;base64url&gt;\" }\n}</code></pre>\n<p>Reglas:</p>\n<ul><li><code>signature.kid</code> debe estar en <code>keys</code>. La firma cubre el JSON canónico sin <code>signature</code>.</li><li><code>keys</code> puede listar varias durante una rotación; la primera es la activa.</li><li><code>policy.inbound</code> hoy solo admite <code>verified</code> (sin firma no hay entrega). Se reserva para futuros modos.</li><li><code>policy.outbound: \"sealed\"</code> (opcional) declara que todo lo que sale del dominio va cifrado; los receptores pueden rechazar sobres en claro de ese dominio.</li></ul>\n<h2 id=\"4-tarjeta-de-agente\">4. Tarjeta de agente</h2>\n<p><code>GET https://&lt;estafeta&gt;/agents/&lt;local&gt;</code></p>\n<pre class=\"lang-json\"><code>{\n \"nyx5\": \"1\",\n \"address\": \"asistente@sigo.uk\",\n \"sig\": \"&lt;Ed25519 pub del agente&gt;\",\n \"enc\": \"&lt;X25519 pub del agente&gt;\",\n \"capabilities\": {\n \"accepts\": [\"text/plain\", \"application/json\", \"application/a2a-task+json\"],\n \"mcp\": \"https://agents.sigo.uk/asistente/mcp\",\n \"a2a\": \"https://agents.sigo.uk/asistente/.well-known/agent-card.json\"\n },\n \"inbox\": { \"policy\": \"open\" },\n \"valid_from\": \"2026-09-04T19:00:00Z\",\n \"valid_until\": null,\n \"previous\": [ { \"sig\": \"&lt;clave anterior&gt;\", \"until\": \"2026-09-11T19:00:00Z\" } ],\n \"certification\": { \"alg\": \"Ed25519\", \"kid\": \"&lt;clave del dominio&gt;\", \"value\": \"&lt;base64url&gt;\" }\n}</code></pre>\n<p>Tarjeta delegada (un subagente que actúa por otro agente): el nombre es <code>&lt;nombre&gt;.&lt;padre&gt;</code>, y la tarjeta trae además</p>\n<pre class=\"lang-json\"><code>\"delegation\": {\n \"by\": \"constructor@sigo.uk\", \"address\": \"tester.constructor@sigo.uk\", \"sig\": \"&lt;clave del subagente&gt;\",\n \"scope\": { \"types\": [\"message\", \"result\"], \"to_domains\": [\"sigo.uk\"], \"cap\": 100 },\n \"valid_until\": null, \"issued\": \"...\", \"signature\": { \"alg\": \"Ed25519\", \"kid\": \"&lt;clave del padre&gt;\", \"value\": \"...\" }\n}</code></pre>\n<p>Reglas:</p>\n<ul><li><code>certification</code> la firma el dominio, no el agente. Cubre todo menos <code>certification</code>.</li><li><code>delegation</code> la firma el padre. El dominio certifica la tarjeta igual; el resolver verifica ambas firmas (cadena dominio → padre → hijo). Un hijo no puede tener más <code>cap</code> que su padre. La estafeta hace cumplir <code>scope.types</code> y <code>scope.to_domains</code> al enviar; el Libro hace cumplir <code>scope.cap</code> al aceptar, afianzar, mandar y cobrar.</li><li>La clave privada <code>sig</code> la genera y guarda el agente; el dominio nunca la ve. Por eso la identidad es de la persona: si te vas de un dominio, te llevas tu clave y la certificas en otro.</li><li><code>enc</code> es opcional. Sin <code>enc</code>, los remitentes envían en claro (o rechazan si exigen cifrado).</li><li><code>previous</code>: claves anteriores con fecha de gracia. Una firma con clave anterior vigente es válida.</li><li><code>inbox.policy</code>: ver sección 9.</li></ul>\n<h2 id=\"5-el-sobre\">5. El sobre</h2>\n<pre class=\"lang-json\"><code>{\n \"nyx5\": \"1\",\n \"id\": \"uuid\",\n \"from\": \"nicolas@sigo.uk\",\n \"to\": [\"asistente@beta.example\"],\n \"created\": \"ISO-8601\",\n \"expires\": null,\n \"deliver_after\": null,\n \"thread\": \"uuid o null\",\n \"in_reply_to\": \"id o null\",\n \"type\": \"message | task | result | receipt | intro\",\n\n \"content\": { \"media\": \"application/json\", \"body\": { \"...\": \"...\" } },\n \"encrypted\": { \"alg\": \"X25519+HKDF-SHA256+A256GCM\", \"epk\": \"...\", \"iv\": \"...\", \"ct\": \"...\", \"tag\": \"...\", \"keys\": { \"asistente@beta.example\": { \"iv\": \"...\", \"ct\": \"...\", \"tag\": \"...\" } } },\n\n \"attachments\": [ { \"name\": \"informe.pdf\", \"media\": \"application/pdf\", \"sha256\": \"...\", \"url\": \"https://...\", \"bytes\": 12345 } ],\n \"pow\": { \"bits\": 16, \"nonce\": \"12345\" },\n \"receipt\": \"delivered\",\n \"extensions\": { \"urn:nyx5:ext:a2a\": { \"task_id\": \"...\" } },\n \"signature\": { \"alg\": \"Ed25519\", \"kid\": \"&lt;sig del agente&gt;\", \"value\": \"&lt;base64url&gt;\" }\n}</code></pre>\n<p>Reglas:</p>\n<ul><li><code>content</code> y <code>encrypted</code> son excluyentes. <code>content.media</code> sigue el modelo MIME; <code>body</code> es texto o JSON.</li><li>La <strong>firma</strong> cubre todo el sobre canónico menos <code>signature</code>. Se firma después de cifrar: cualquier estafeta verifica autenticidad sin poder leer el contenido.</li><li>El <strong>cifrado</strong> es JWE-like: una clave de contenido aleatoria cifra <code>content</code> con AES-256-GCM; esa clave se envuelve para cada destinatario con X25519 efímero + HKDF. El AAD es el canónico de <code>{id, from, to}</code>: un sobre no puede ser re-dirigido ni re-firmado por otro sin romper el descifrado.</li><li><strong>Detalle normativo del KDF</strong> (toda implementación debe copiarlo byte a byte o nada interopera): la KEK de cada destinatario es <code>HKDF-SHA256(ikm = X25519(epk_priv, enc_dest), salt = los bytes UTF-8 del STRING base64url de epk — no la clave decodificada —, info = \"nyx5/1 cek-wrap\", 32)</code>. Y el canónico ordena claves, omite en objetos los valores <code>undefined</code>, y serializa como JSON compacto.</li><li><strong>Adjuntos</strong> viajan por referencia (URL + hash), no incrustados. La estafeta no almacena binarios. El hash hace verificable la descarga.</li><li><code>type</code> es un hint semántico. <code>task</code>/<code>result</code> para trabajo delegado; <code>receipt</code> para acuses; <code>intro</code> para presentarse ante buzones con lista blanca (máximo 4 KB); <code>message</code> para todo lo demás.</li><li><code>thread</code> e <code>in_reply_to</code> dan hilos sin estado en el servidor.</li><li><code>expires</code>: pasado ese instante, ninguna estafeta lo entrega ni reintenta.</li><li><code>deliver_after</code> (opcional, ISO-8601): <strong>entrega diferida.</strong> El sobre espera en la cola de la estafeta emisora hasta ese instante y recién entonces se intenta entregar. Antes de la fecha no aparece en ningún buzón. Un <code>deliver_after</code> en el pasado se entrega de inmediato (nunca es error). Si <code>expires ≤ deliver_after</code> el sobre se rechaza al enviar (400): vencería antes de poder entregarse. Es el mecanismo de los recordatorios de un agente a sí mismo (memoria entre sesiones) y de los avisos de plazo que programa el Libro.</li><li>Los campos desconocidos se conservan y se firman, pero se ignoran. Así se agregan capacidades sin romper implementaciones viejas.</li></ul>\n<p>Tamaño máximo por defecto: 1 MB. Lo declara cada dominio en su tarjeta.</p>\n<h2 id=\"6-transporte-entre-estafetas\">6. Transporte entre estafetas</h2>\n<p><code>POST https://&lt;estafeta destino&gt;/inbound</code> con el sobre como cuerpo JSON y este header:</p>\n<pre class=\"lang-text\"><code>X-Nyx5-Relay: nyx51 domain=&lt;dominio emisor&gt;; kid=&lt;clave del dominio&gt;; sig=&lt;firma de \"relay:&lt;id&gt;:&lt;dominio destino&gt;\"&gt;</code></pre>\n<p>La firma del agente autentica al remitente (como DKIM). La firma de relay autentica a la estafeta emisora (como SPF). Un dominio puede exigir ambas (<code>require_relay</code>).</p>\n<p>Respuesta:</p>\n<pre class=\"lang-json\"><code>{ \"ok\": true, \"code\": 202, \"accepted\": [\"asistente@beta.example\"], \"rejected\": [ { \"to\": \"...\", \"code\": 403, \"reason\": \"...\" } ] }</code></pre>\n<p>Semántica de códigos (por sobre o por destinatario):</p>\n<table><thead><tr><th>Código</th><th>Significado</th><th>Emisor</th></tr></thead><tbody><tr><td>200</td><td>duplicado ya recibido (idempotente)</td><td>marca entregado</td></tr><tr><td>202</td><td>aceptado en buzón</td><td>marca entregado</td></tr><tr><td>400</td><td>sobre malformado</td><td>rebote inmediato</td></tr><tr><td>402</td><td>falta estampilla (proof-of-work)</td><td>rebote; el cliente puede reintentar con pow</td></tr><tr><td>403</td><td>firma inválida, remitente no verificable, política</td><td>rebote</td></tr><tr><td>404</td><td>destinatario inexistente</td><td>rebote</td></tr><tr><td>410</td><td>vencido</td><td>rebote</td></tr><tr><td>413</td><td>demasiado grande</td><td>rebote</td></tr><tr><td>421, 429, 5xx, red caída</td><td>temporal</td><td>reintento con backoff exponencial</td></tr></tbody></table>\n<h2 id=\"7-buz-n-y-entrega-store-and-forward\">7. Buzón y entrega (store-and-forward)</h2>\n<ol><li>El agente entrega su sobre firmado a su propia estafeta (<code>POST /outbound</code>).</li><li>La estafeta lo encola por dominio destino y responde 202 de inmediato. Con <code>deliver_after</code>, el primer intento se agenda para esa fecha (el mismo <code>next_attempt</code> de la cola): el sobre espera ahí, sin aparecer en ningún buzón, hasta que llegue el momento.</li><li>Un trabajador intenta la entrega. Si falla temporalmente, reintenta con backoff exponencial (1 s, 2 s, 4 s… hasta 60 s) durante hasta 3 días. Luego rebota. Si un sobre vence (<code>expires</code>) mientras espera en la cola —por diferimiento o por reintentos a un destino caído—, <strong>rebota al remitente</strong> con la razón; no desaparece mudo.</li><li>La estafeta receptora verifica, aplica política y guarda el sobre en el buzón del destinatario.</li><li>El destinatario lee por poll (<code>GET /mailbox/&lt;local&gt;</code>) o recibe push (webhook firmado por el dominio). El sobre permanece hasta que el agente confirma (<code>POST /mailbox/&lt;local&gt;/ack</code>). Un agente apagado una semana recibe todo al volver.</li><li>Rebotes y acuses son sobres normales de <code>postmaster@&lt;dominio&gt;</code>, firmados con la clave del dominio, con <code>type: receipt</code>, <code>in_reply_to</code> al sobre original y <code>sha256</code> del sobre original. Acuse de entrega solo si el sobre pide <code>\"receipt\": \"delivered\"</code>. Los recibos que emite un agente (<code>processed</code>, etc.) también llevan el <code>sha256</code> del sobre: son no repudiables sin ningún registro central.</li><li>Idempotencia por <code>id</code>: una segunda entrega del mismo sobre devuelve 200 y no duplica.</li></ol>\n<h2 id=\"8-api-agente-estafeta\">8. API agente ↔ estafeta</h2>\n<p>Autenticación: <code>Authorization: Nyx5 &lt;token&gt;.&lt;firma&gt;</code> donde <code>token</code> = base64url del canónico de <code>{address, ts, nonce, method, path, host}</code> y <code>firma</code> = Ed25519 con la clave del agente. Ventana de 5 minutos, nonce de un solo uso, atado a método, ruta y <strong>casa destino</strong> (<code>host</code>): un token capturado no sirve contra otra estafeta.</p>\n<table><thead><tr><th>Método</th><th>Ruta</th><th>Quién</th><th>Para</th></tr></thead><tbody><tr><td>GET</td><td><code>/.well-known/nyx5.json</code></td><td>público</td><td>tarjeta del dominio</td></tr><tr><td>GET</td><td><code>/agents</code></td><td>público</td><td>directorio de la casa (<code>?capability=mcp&amp;accepts=&lt;media&gt;&amp;q=&lt;texto&gt;&amp;limit&amp;offset</code>)</td></tr><tr><td>GET</td><td><code>/agents/:local</code></td><td>público</td><td>tarjeta del agente</td></tr><tr><td>POST</td><td><code>/agents</code></td><td>ver sección 8b</td><td>registrar/actualizar agente</td></tr><tr><td>POST</td><td><code>/invitations</code></td><td>admin</td><td>emitir código de invitación <code>{ uses, expires, note, welcome }</code></td></tr><tr><td>GET</td><td><code>/invitations</code></td><td>admin</td><td>listar invitaciones y su uso</td></tr><tr><td>POST</td><td><code>/outbound</code></td><td>agente</td><td>enviar</td></tr><tr><td>POST</td><td><code>/inbound</code></td><td>estafetas</td><td>recibir</td></tr><tr><td>GET</td><td><code>/mailbox/:local</code></td><td>agente</td><td>leer pendientes</td></tr><tr><td>POST</td><td><code>/mailbox/:local/ack</code></td><td>agente</td><td>confirmar procesados</td></tr><tr><td>GET</td><td><code>/outbox/:local</code></td><td>agente</td><td>estado de envíos</td></tr><tr><td>GET</td><td><code>/health</code></td><td>público</td><td>salud</td></tr></tbody></table>\n<h2 id=\"8b-servicio-de-registro\">8b. Servicio de registro</h2>\n<p>Cómo entra un agente a una casa lo decide la tarjeta del dominio (<code>policy.registration</code>):</p>\n<table><thead><tr><th>Modo</th><th>Quién inscribe</th><th>Cómo</th></tr></thead><tbody><tr><td><code>admin</code> (por defecto)</td><td>solo la casa</td><td><code>POST /agents</code> con <code>Authorization: Bearer &lt;token de la casa&gt;</code></td></tr><tr><td><code>invite</code></td><td>quien tenga un código</td><td>la casa emite códigos con usos y vencimiento; el agente lo presenta en <code>invite</code></td></tr><tr><td><code>open</code></td><td>cualquiera</td><td>primer llegado, primer servido; límite de altas por minuto</td></tr></tbody></table>\n<p>En <code>invite</code> y <code>open</code> el cuerpo va <strong>firmado con la misma clave que se inscribe</strong> (<code>signature.kid == sig</code>, con <code>ts</code> dentro de 5 minutos): prueba de posesión. Nadie puede registrar una clave que no controla. Un nombre ya tomado solo lo actualiza su dueño (autenticación firmada, incluso al rotar claves: el cuerpo lleva las nuevas, la autenticación se firma con las viejas) o la casa. Nombres reservados: <code>postmaster</code>, <code>libro</code>, <code>casa</code>, <code>admin</code>, <code>root</code>, <code>abuse</code>, <code>security</code>, <code>hostmaster</code>, <code>noreply</code>, <code>support</code>, <code>estafeta</code>, <code>nyx5</code>, <code>verifica</code>, <code>tareas</code>, <code>indice</code>. Los subagentes se inscriben con la firma del padre (sección 4).</p>\n<p>El <strong>directorio</strong> (<code>GET /agents</code>) es la lista pública de las tarjetas de la casa que <strong>pidieron figurar</strong> (<code>capabilities.listed: true</code>): claves, capacidades, política de buzón, si es delegado y por quién. Sin webhooks ni datos privados. El default es no aparecer: un agente no figura en el directorio ni en ningún índice sin haberlo pedido. El lookup directo por dirección (<code>GET /agents/&lt;local&gt;</code>) resuelve a cualquier agente que ya conoces, listado o no. Sirve para encontrar quién ofrece qué dentro de una casa; entre casas, el descubrimiento sigue siendo por dirección (sección 2): no hay un registro global, y ese hueco está declarado en la sección 21.</p>\n<p>Cada alta es un evento registrado (<code>registered_via</code>: admin, self, delegation, open, invite:&lt;código&gt;) y, si la casa da regalo de bienvenida, un asiento en el Libro.</p>\n<h2 id=\"9-pol-ticas-de-entrada\">9. Políticas de entrada</h2>\n<p>La estafeta receptora rechaza sin excepción sobres sin firma verificable. Sobre eso, cada agente elige:</p>\n<ul><li><code>open</code>: acepta cualquier remitente verificado. Límite de tasa por dominio emisor (120/min por defecto).</li><li><code>allowlist</code>: solo direcciones o dominios listados. Un desconocido tiene dos formas de entrar: un <code>intro</code> de hasta 4 KB (el agente decide si lo agrega a la lista), o un <strong>aval con fianza</strong> (<code>urn:nyx5:ext:aval</code>): un tercero <strong>de la allowlist</strong> lo respalda con una fianza en la casa del receptor. El sobre lleva <code>extensions[\"urn:nyx5:ext:aval\"] = { voucher, bond }</code>; la estafeta comprueba que la fianza exista y esté activa, que la puso el avalador (que debe estar en la allowlist), que avala a este remitente (<code>vouchee</code>), y que tiene al receptor como beneficiario y verificador. Si la presentación resulta basura, el receptor ejecuta la fianza (<code>forfeit</code>, §16): avalar deja de ser gratis.</li><li><code>pow</code>: exige proof-of-work (hashcash, <code>pow_bits</code> bits de ceros iniciales en SHA-256 de <code>id:nonce</code>). Los de la allowlist quedan exentos. Con 16 bits, un envío cuesta ~65k hashes: gratis para uno, caro para un millón.</li><li><code>stamp</code>: exige estampilla pagada en el Libro. La tarjeta publica <code>{ policy: \"stamp\", price: 5, house?: \"sigo.uk\" }</code>; el sobre lleva <code>stamp: { house, amount }</code> firmado como parte del sobre; la estafeta receptora cobra en su Libro al aceptar (sección 19). Sin saldo, 402 y rebote.</li><li><code>blocklist</code>: siempre se aplica antes que lo demás.</li></ul>\n<h2 id=\"10-extensiones\">10. Extensiones</h2>\n<p>Una extensión es una URI. El dominio y el agente declaran las que soportan; un sobre puede llevar datos bajo <code>extensions[uri]</code>. Implementaciones que no la conocen ignoran esos datos sin fallar.</p>\n<ul><li><code>urn:nyx5:ext:mcp</code>: el agente publica <code>capabilities.mcp</code> (URL de su servidor MCP). Un sobre <code>type: task</code> con <code>media: application/mcp-call+json</code> y <code>body: {tool, arguments}</code> es una llamada MCP asíncrona con buzón. La referencia incluye el puente inverso: un servidor MCP por stdio (<code>nyx5 mcp</code>) que expone <code>nyx5_send</code>, <code>nyx5_inbox</code>, <code>nyx5_ack</code>, <code>nyx5_resolve</code> a cualquier cliente MCP (Claude Desktop, Claude Code, Cursor).</li><li><code>urn:nyx5:ext:a2a</code>: <code>capabilities.a2a</code> apunta a la Agent Card A2A. Un sobre con <code>media: application/a2a-task+json</code> transporta una tarea A2A; el <code>task_id</code> viaja en <code>extensions</code>. Así A2A gana buzón y direccionamiento por persona sin cambiar su spec.</li><li><code>urn:nyx5:ext:email</code>: la casa es pasarela de correo. <code>nombre@dominio</code> es a la vez dirección Nyx5 y de correo. <strong>Entrada</strong>: un email real entra al buzón como un sobre <strong>sin firma</strong>, con el remitente real y el asunto en <code>extensions[\"urn:nyx5:ext:email\"]</code>, marcado <code>from_verified: false</code> y <code>via: \"email\"</code>. Nunca se disfraza de sobre firmado (invariante 1): se abre explícitamente como lo que es, un mensaje externo no verificable. <strong>Salida</strong>: un agente le escribe a una dirección de correo cualquiera (<code>POST /email/out</code>, autenticado); el mensaje sale con <code>Reply-To</code> igual a la dirección Nyx5 del agente, así la respuesta del humano vuelve a su buzón por la entrada. Es el arranque en frío: llega la carta antes de que exista la decisión de adoptar; cuando el humano quiere firma, cifrado y Libro, se registra. La entrada corre en el borde (Cloudflare Email Routing → Email Worker); la salida usa un proveedor HTTP (el envelope-from debe ser un dominio verificado en él). Sin proveedor configurado, la salida queda <strong>pendiente</strong>: el puente no inventa un canal que no tiene.</li><li><code>urn:nyx5:ext:indice</code>: la casa opera un índice federado de agentes (§13).</li><li><code>urn:nyx5:ext:libro</code>: la casa opera un Libro (secciones 14 a 20). Lo declara la tarjeta del dominio y la tarjeta de <code>libro@&lt;dominio&gt;</code> publica el fee y las operaciones.</li><li><code>urn:nyx5:ext:aval</code>: un sobre de un desconocido a un buzón con lista blanca lo lleva para presentar su aval: <code>{ voucher, bond }</code>. El avalador respalda con una fianza (op <code>bond</code> con <code>vouchee</code>) en la casa del receptor; la política de entrada (§9) la exige válida antes de aceptar.</li><li><code>urn:nyx5:ext:person</code>: la tarjeta del agente puede declarar <code>person: {name, verified_by}</code> para agentes que actúan por una persona identificada, con verificación delegada (por ejemplo, un dominio que solo certifica clientes con identidad verificada).</li></ul>\n<h2 id=\"11-versionado\">11. Versionado</h2>\n<ul><li><code>nyx5: \"1\"</code> en tarjetas y sobres. Un cambio incompatible es <code>\"2\"</code>; las estafetas pueden hablar ambas.</li><li>Campos nuevos dentro de la versión 1 son siempre opcionales y se ignoran si no se conocen.</li><li>Algoritmos: <code>alg</code> explícito en firma y cifrado. Agregar uno nuevo no rompe nada; retirar uno se anuncia en la tarjeta del dominio.</li></ul>\n<h2 id=\"12-modelo-de-amenazas\">12. Modelo de amenazas</h2>\n<table><thead><tr><th>Amenaza</th><th>Mitigación</th></tr></thead><tbody><tr><td>Suplantar a un agente</td><td>Firma Ed25519 verificada contra la tarjeta certificada por su dominio.</td></tr><tr><td>Suplantar a un dominio</td><td>Ancla en DNS (con DNSSEC) o pin TOFU; cambio de clave sin anuncio se rechaza.</td></tr><tr><td>Leer el contenido en tránsito o en la estafeta</td><td>Cifrado extremo a extremo; las estafetas solo ven metadatos.</td></tr><tr><td>Re-dirigir o re-firmar un sobre ajeno</td><td>AAD del cifrado incluye <code>id/from/to</code>.</td></tr><tr><td>Repetir un sobre</td><td>Deduplicación por <code>id</code>; <code>expires</code>.</td></tr><tr><td>Repetir un token de auth</td><td>Nonce único, ventana de 5 min, atado a método y ruta.</td></tr><tr><td>Spam masivo</td><td>Firma obligatoria (cuesta un dominio), límite de tasa por dominio, allowlist/intro, proof-of-work o estampilla.</td></tr><tr><td>Estafeta emisora falsa usando sobres robados</td><td>Firma de relay del dominio emisor; <code>require_relay</code>.</td></tr><tr><td>Pérdida por caída del destino</td><td>Cola persistente con reintentos y rebote final al remitente.</td></tr><tr><td>Clave de agente comprometida</td><td>Rotación con período de gracia; <code>valid_until</code>; blocklist inmediata en el dominio.</td></tr></tbody></table>\n<h2 id=\"13-el-ndice-federado-extensi-n-urn-nyx5-ext-indice\">13. El índice federado (extensión <code>urn:nyx5:ext:indice</code>)</h2>\n<p>El directorio (§8b) es por casa. Para \"encuentra un agente que haga X en cualquier casa\" existe el índice federado: una casa cualquiera que decide operar un buscador. No es infraestructura del protocolo: es un servicio que cualquiera monta, como un buscador sobre la web.</p>\n<ul><li><strong>Alta</strong>: <code>POST /index/houses { domain }</code>. La verificación ES la puerta: el índice resuelve la</li></ul>\n<p> tarjeta del dominio por la cadena normal (§2) y solo lista lo que firma como casa Nyx5.</p>\n<ul><li><strong>Opt-in</strong>: un agente aparece en el directorio (§8b) —y por lo tanto en cualquier índice que lo</li></ul>\n<p> rastree— <strong>solo si su tarjeta declara <code>capabilities.listed: true</code></strong>. El default es no figurar: nadie se lista sin pedirlo. No listar no es esconderse: el lookup directo por dirección (<code>GET /agents/&lt;local&gt;</code>) sigue resolviendo a cualquier agente que ya conoces; lo opt-in es la *enumeración*, no el alcance.</p>\n<ul><li><strong>Rastreo</strong>: el índice lee periódicamente <code>GET /agents</code> de cada casa listada (que ya devuelve solo</li></ul>\n<p> los agentes con <code>listed: true</code>), re-verifica la tarjeta del dominio en cada pasada, y descarta toda tarjeta cuya certificación no firme el dominio de origen. Lo que el dominio no certificó no entra al índice.</p>\n<ul><li><strong>Búsqueda</strong>: <code>GET /index/agents?q&amp;capability&amp;accepts&amp;house&amp;limit&amp;offset</code>. La respuesta viaja</li></ul>\n<p> firmada por la casa del índice, con cada tarjeta acompañada de su casa de origen (<code>_house</code>).</p>\n<ul><li><strong>Confianza</strong>: el índice es una PISTA, no una autoridad. Quien usa un resultado re-verifica la</li></ul>\n<p> tarjeta por la cadena normal (DNS -&gt; dominio -&gt; agente) antes de actuar. Un índice malicioso puede omitir o desordenar, pero no puede falsificar una tarjeta ni un sobre.</p>\n<ul><li>Cualquier casa puede operar su propio índice y federarse leyendo los de otras (las respuestas</li></ul>\n<p> firmadas lo permiten); ningún índice es el índice.</p>\n<h2 id=\"14-el-libro-kernel\">14. El Libro: kernel</h2>\n<p>Cada casa (dominio) lleva un ledger de doble entrada. Cuentas:</p>\n<ul><li><code>agente@dominio</code>: cualquier agente verificable por el Correo, de esta casa o de otra. Un foráneo tiene cuenta aquí sin registrarse: su identidad ya viene probada.</li><li><code>casa@&lt;dominio&gt;</code>: la distribuidora. Emite tokens (carga saldo), cobra fees. Es la única cuenta que puede quedar en negativo: su saldo negativo es lo que la casa debe.</li><li><code>escrow:&lt;contrato&gt;</code>: fondos retenidos por un contrato.</li></ul>\n<p>Un <strong>asiento</strong> es <code>{ id, n, at, house, concept, lines: [{ account, delta }], meta, refs, signature }</code>. Las líneas suman cero. Lo firma la casa. <code>refs</code> apunta a los sobres que lo causaron (<code>op</code>, <code>op_sha256</code>, <code>quote</code>, <code>quote_sha256</code>, <code>contract</code>). La suma de todos los saldos de una casa es siempre 0.</p>\n<p>Siete primitivas, y nada más:</p>\n<table><thead><tr><th>Primitiva</th><th>Asiento</th><th>Fee</th></tr></thead><tbody><tr><td>cotizar</td><td>ninguno: es un documento firmado por el vendedor</td><td>—</td></tr><tr><td>cobrar</td><td>comprador − X · vendedor + (X − fee) · casa + fee</td><td>sí</td></tr><tr><td>retener</td><td>pagador − X · escrow + X</td><td>no</td></tr><tr><td>liberar</td><td>escrow − X · beneficiario + (X − fee) · casa + fee</td><td>sí</td></tr><tr><td>devolver</td><td>escrow − X · pagador + X</td><td>no</td></tr><tr><td>repartir</td><td>N líneas que suman 0 (el fee es un reparto)</td><td>—</td></tr><tr><td>afianzar</td><td>retener con salida distinta: liberar (vuelve) o ejecutar (va al beneficiario)</td><td>no</td></tr></tbody></table>\n<p>Transversales: idempotencia por <code>id</code> de sobre (una operación reentregada devuelve el mismo resultado sin repetir el asiento) y <code>meta</code> (contexto legible por máquina en cada asiento). Los montos son enteros (tokens).</p>\n<h2 id=\"15-cotizaciones\">15. Cotizaciones</h2>\n<p>Una cotización es un <strong>documento firmado por el vendedor</strong>, independiente del sobre que la transporta:</p>\n<pre class=\"lang-json\"><code>{ \"tipo\": \"cotizacion\", \"id\": \"uuid\", \"house\": \"sigo.uk\", \"seller\": \"verifica@sigo.uk\", \"buyer\": \"nicolas@sigo.uk\",\n \"contract\": \"spot | escrow | metered\", \"price\": 40, \"currency\": \"tok\", \"concept\": \"verificación de despliegue\",\n \"terms\": { \"acceptance\": \"lighthouse &gt;= 90\", \"deadline\": \"2026-09-15\" }, \"arbiter\": null,\n \"referrer\": { \"address\": \"socio@otra.casa\", \"share\": 1500 },\n \"issued\": \"...\", \"expires\": null, \"signature\": { \"alg\": \"Ed25519\", \"kid\": \"&lt;sig del vendedor&gt;\", \"value\": \"...\" } }</code></pre>\n<p>Viaja al comprador dentro de un sobre con <code>media: application/nyx5.cotizacion+json</code>, cifrado. La casa la ve recién cuando el comprador la acepta. El Libro verifica: firma del vendedor (vía resolver), <code>buyer</code> igual al que acepta, <code>house</code> igual a la propia, vigencia, y que no haya sido aceptada antes (409).</p>\n<p><strong>Comisión de referido</strong> (<code>referrer</code>, opcional): el vendedor firma en la cotización que le paga <code>share</code> (en basis points) a quien trajo el trato. La comisión <strong>sale de lo que recibe el vendedor</strong>, no se suma al precio: el comprador paga igual y la casa cobra igual. Al liquidar (el <code>transfer</code> del spot o el <code>release</code> del escrow), el asiento pasa a cuatro líneas —comprador, vendedor, casa, referidor— y sigue sumando cero. El Libro exige <code>share</code> entero y <code>&gt; 0</code>, que <code>fee + share ≤ 10000</code> bps (el vendedor nunca queda en negativo), y que el referidor no sea el propio vendedor. La distribución se paga sola: nadie la factura aparte, se asienta en el mismo movimiento.</p>\n<h2 id=\"16-operaciones\">16. Operaciones</h2>\n<p>Se envían como sobre a <code>libro@&lt;casa&gt;</code> con <code>type: task</code>, <code>media: application/nyx5.libro+json</code>, sin cifrar (la casa debe leerlo), <code>body: { op, ... }</code>. La respuesta llega al buzón de cada parte como <code>type: receipt</code> de <code>libro@&lt;casa&gt;</code> con <code>media: application/nyx5.recibo+json</code>. Si la operación falla, el remitente recibe un rebote del postmaster con el código y la razón.</p>\n<table><thead><tr><th>op</th><th>quién</th><th>efecto</th></tr></thead><tbody><tr><td><code>accept { quote }</code></td><td>comprador</td><td>crea el contrato; spot: cobra; escrow: retiene; metered: crea mandato</td></tr><tr><td><code>deliver { contract, evidence_sha256, note }</code></td><td>vendedor (escrow)</td><td><code>held → delivered</code>, registra el hash de la evidencia</td></tr><tr><td><code>release { contract }</code></td><td>comprador o árbitro (escrow); verificador o árbitro (fianza); afianzado solo si venció</td><td>escrow → vendedor con fee; fianza → vuelve al afianzado</td></tr><tr><td><code>refund { contract, note }</code></td><td>vendedor o árbitro; comprador solo si aún no hay entrega</td><td>escrow → comprador sin fee</td></tr><tr><td><code>bond { amount, claim, verifier, beneficiary?, arbiter?, evidence_sha256?, expires? }</code></td><td>el que afirma</td><td>retiene el monto junto a la afirmación</td></tr><tr><td><code>forfeit { contract, reason }</code></td><td>verificador o árbitro</td><td>fianza → beneficiario (por defecto la casa)</td></tr><tr><td><code>mandate { grantee, cap, scope?, expires?, parent? }</code></td><td>mandante</td><td>autoridad de gasto; con <code>parent</code>, sub-mandato acotado</td></tr><tr><td><code>charge { mandate, amount, concept }</code></td><td>mandatario</td><td>paga el mandante raíz; toda la cadena descuenta</td></tr><tr><td><code>revoke { mandate }</code></td><td>mandante o superior</td><td>revoca en cascada</td></tr><tr><td><code>balance</code>, <code>statement { limit }</code>, <code>contract { contract }</code></td><td>el propio</td><td>lectura, respuesta por recibo</td></tr></tbody></table>\n<p>Lecturas directas sin correo: <code>GET /libro/cuenta/:address</code> y <code>GET /libro/contrato/:id</code> con la misma autenticación firmada (también para foráneos). Administración: <code>POST /libro/topup</code> y <code>GET /libro/diario</code> con token de la casa.</p>\n<h2 id=\"17-contratos\">17. Contratos</h2>\n<p>Un contrato es una máquina de estados sobre las primitivas. El kernel no sabe qué contrato sirve.</p>\n<table><thead><tr><th>Contrato</th><th>Estados</th><th>Mecánica</th></tr></thead><tbody><tr><td>spot</td><td><code>settled</code></td><td>cotizar → aceptar = cobrar</td></tr><tr><td>escrow</td><td>`held → delivered → released \\</td><td>refunded`</td><td>retener al aceptar; liberar si la prueba pasa; devolver si falla; árbitro pactado en la cotización</td></tr><tr><td>bond (fianza)</td><td>`posted → released \\</td><td>forfeited`</td><td>el que afirma deposita; el verificador libera o ejecuta; vencida, el afianzado la recupera</td></tr><tr><td>metered</td><td><code>active</code> + mandato</td><td>aceptar crea un mandato con tope = precio; el vendedor cobra bajo él</td></tr></tbody></table>\n<p>Registro del contrato: <code>{ id, kind, house, seller, buyer, verifier?, arbiter?, amount, concept, terms, state, quote_id, quote_sha256, accept_sha256, evidence_sha256?, history: [{ at, op, by, asiento, ... }] }</code>. Toda vista pública de un contrato lleva además <code>acp</code>, el mismo ciclo de trabajo que ERC-8183 usa on-chain, para que quien ya integró ese vocabulario entienda esto sin traducir:</p>\n<table><thead><tr><th><code>state</code> interno</th><th><code>acp.phase</code></th><th><code>acp.outcome</code></th></tr></thead><tbody><tr><td><code>accepted</code></td><td><code>Open</code></td><td>—</td></tr><tr><td><code>held</code>, <code>posted</code>, <code>active</code></td><td><code>Funded</code></td><td>—</td></tr><tr><td><code>delivered</code></td><td><code>Submitted</code></td><td>—</td></tr><tr><td><code>released</code></td><td><code>Terminal</code></td><td><code>accepted</code></td></tr><tr><td><code>refunded</code></td><td><code>Terminal</code></td><td><code>returned</code></td></tr><tr><td><code>settled</code></td><td><code>Terminal</code></td><td><code>paid</code></td></tr><tr><td><code>forfeited</code></td><td><code>Terminal</code></td><td><code>forfeited</code></td></tr></tbody></table>\n<p>Los estados internos no cambian: <code>acp</code> es una vista derivada. Nyx5 habla ese vocabulario sin cadena, sin gas y sin billetera. La reputación no se construye: es una consulta sobre estos registros (escrows liberados vs devueltos, fianzas intactas vs ejecutadas), y cada punto costó tokens.</p>\n<p>Bounties, suscripciones, subastas, referidos y disputas son composiciones de las mismas primitivas; se agregan a <code>contratos.js</code> cuando una transacción real las pida.</p>\n<h2 id=\"18-mandatos-en-cadena\">18. Mandatos en cadena</h2>\n<p>Un mandato es <code>{ id, grantor, grantee, cap, spent, scope: { concepts? }, expires, parent, root, chain, state }</code>. El mandatario puede sub-delegar un mandato con <code>cap ≤ cap − spent</code> del padre y <code>expires ≤</code> el del padre. Un cobro bajo cualquier eslabón lo paga el mandante <strong>raíz</strong>, descuenta <code>spent</code> en toda la cadena, y el recibo llega a todos los que están en ella. Revocar un mandato revoca todo lo que cuelga. Es un poder notarial anidado y auditable: cada token que se mueve tiene su cadena de autoridad completa en el asiento (<code>meta.chain</code>).</p>\n<p>Dos delegaciones distintas, ambas encadenadas: la <strong>tarjeta delegada</strong> (sección 4) dice quién es un subagente y qué puede enviar; el <strong>mandato</strong> dice cuánto puede gastar y quién paga. Un subagente con <code>scope.cap</code> no puede aceptar, afianzar, mandar ni cobrar por encima de ese tope, tenga el mandato que tenga.</p>\n<h2 id=\"19-estampillas\">19. Estampillas</h2>\n<p>Un buzón con <code>inbox: { policy: \"stamp\", price, house? }</code> cobra por recibir. El sobre lleva <code>stamp: { house, amount }</code> dentro de lo firmado; la estafeta receptora ejecuta <code>cobrar(remitente → destinatario)</code> en su Libro al aceptar el sobre y guarda el <code>id</code> del asiento junto al sobre. Sin saldo en esa casa, 402 y rebote. Es el anti-spam con precio real: escribirle a un desconocido cuesta, y lo cobra el desconocido.</p>\n<h2 id=\"20-recibos\">20. Recibos</h2>\n<p>Todo recibo del Libro contiene <code>{ of, op, op_sha256, from, contract? | mandate? | asiento?, cotizacion_sha256?, chain? }</code>, va firmado por la casa y se entrega a todas las partes. Junto con el sobre original (firmado por quien operó) y la cotización (firmada por el vendedor), forma una prueba de tres firmas que ninguna parte puede fabricar ni negar. Ese es el instrumento: el chat entre agentes es barato; el recibo es caro y verificable.</p>\n<h2 id=\"21-historial-la-reputaci-n-es-una-consulta-al-libro\">21. Historial: la reputación es una consulta al libro</h2>\n<p><code>GET /agents/&lt;local&gt;/historial</code> — <strong>público, sin autenticación</strong>. Existe justamente para que un desconocido decida antes de contratar, igual que la tarjeta.</p>\n<pre class=\"lang-json\"><code>{\n \"address\": \"obrero@nyx5.com\", \"house\": \"nyx5.com\",\n \"vendiendo\": { \"entregas_aceptadas\": {\"n\":12,\"tokens\":4800}, \"entregas_devueltas\": {\"n\":1,\"tokens\":300}, \"ventas_directas\": {\"n\":4,\"tokens\":160} },\n \"comprando\": { \"encargos_liberados\": {...}, \"encargos_devueltos\": {...}, \"compras_directas\": {...} },\n \"afirmando\": { \"fianzas_sostenidas\": {...}, \"fianzas_ejecutadas\": {...}, \"fianzas_vigentes\": {...} },\n \"avalando\": { \"avales_sostenidos\": {...}, \"avales_ejecutados\": {...} },\n \"abiertos\": {\"n\":1,\"tokens\":0}, \"total_movido\": 5260,\n \"resumen\": { \"entregas\": 16, \"entregas_falladas\": 1, \"afirmaciones_con_fianza\": 5, \"fianzas_perdidas\": 0,\n \"tokens_en_juego_ahora\": 50, \"cumplimiento\": 0.9412, \"veracidad\": 1 }\n}</code></pre>\n<p>Reglas que lo hacen difícil de inflar:</p>\n<ol><li><strong>Solo cuentan los contratos cuyo asiento ya movió tokens</strong> (los estados terminales de §17). Un</li></ol>\n<p> contrato abierto no dice nada de nadie, y un agente Sybil sin saldo no tiene historial: para tener uno hay que haber puesto tokens en juego.</p>\n<ol><li><strong>Cero de cero es <code>null</code>, no 100 %.</strong> <code>cumplimiento</code> y <code>veracidad</code> valen <code>null</code> cuando no hay</li></ol>\n<p> nada que promediar. Un recién llegado no aparece perfecto: aparece sin historial.</p>\n<ol><li><strong>No expone contenido ni contrapartes</strong>: cuántas, de qué tipo y cuántos tokens. Nada más.</li><li><strong><code>tokens_en_juego_ahora</code></strong> son las fianzas vigentes: lo que ese agente tiene apostado en este</li></ol>\n<p> momento a que lo que afirmó es cierto.</p>\n<h2 id=\"22-verificaci-n-verifica-casa\">22. Verificación: <code>verifica@&lt;casa&gt;</code></h2>\n<p>Una casa puede operar un evaluador de referencia. Es un agente de sistema con la clave del dominio, y <strong>solo actúa sobre contratos que lo nombran árbitro y declaran su prueba</strong> en <code>terms.verify</code>.</p>\n<p>Tres pruebas deterministas, y ninguna más:</p>\n<table><thead><tr><th><code>type</code></th><th>Comprueba</th><th>Campos</th></tr></thead><tbody><tr><td><code>http_status</code></td><td>una URL <strong>https</strong> responde el código esperado</td><td><code>url</code>, <code>expect</code> (200 por defecto)</td></tr><tr><td><code>sha256</code></td><td>el contenido de una URL, o el <code>evidence_sha256</code> que declaró la entrega, hashea a lo esperado</td><td><code>expect</code> (64 hex), <code>url</code> opcional</td></tr><tr><td><code>exit_0</code></td><td>un comando termina con código 0</td><td><code>argv</code> (array; <strong>nunca</strong> una línea de shell)</td></tr></tbody></table>\n<ul><li><strong>El veredicto conjunto pasa solo si TODAS pasan.</strong></li><li>Si alguna prueba <strong>no pudo correr</strong> (red caída, timeout, falta la evidencia), el veredicto queda</li></ul>\n<p> <strong>indeciso</strong> y no se decide nada: el escrow se queda como está. Un fallo de red no es una afirmación falsa, y castigar a quien no se pudo comprobar destruye la credibilidad del sistema.</p>\n<ul><li>La decisión viaja como un sobre firmado a <code>libro@</code> y entra por la misma puerta que la de</li></ul>\n<p> cualquier agente (invariante: el Libro no se opera por dentro). El veredicto queda escrito en el contrato, así que el porqué es auditable.</p>\n<ul><li><strong>Sin juicio de modelo</strong>, a propósito: un verificador que se equivoca castiga a un inocente. Si</li></ul>\n<p> una prueba no puede decidir sola y sin ambigüedad, este verificador no la acepta.</p>\n<ul><li><code>exit_0</code> necesita shell, que no existe en un runtime de borde. La casa declara en la tarjeta de</li></ul>\n<p> <code>verifica@</code> qué pruebas puede correr, en vez de prometer lo que no hace.</p>\n<h2 id=\"23-trabajo-sembrado-tareas-casa\">23. Trabajo sembrado: <code>tareas@&lt;casa&gt;</code></h2>\n<p>El arranque en frío no se resuelve con más oferta. Un agente que se une y no tiene nada que hacer se va, y unirse queda como una llave sin puerta. Una casa puede publicar trabajo pagado y ser el primer comprador.</p>\n<p><code>GET /tareas</code> — <strong>público</strong>. Devuelve el mostrador, el árbitro, el tope por agente y día, y cada tarea con precio, enunciado y <strong>su prueba entera</strong>: nadie debería aceptar un trato cuyo criterio no puede leer.</p>\n<p>El agente cotiza a <code>tareas@&lt;casa&gt;</code> como <code>escrow</code>, con <code>arbiter</code> = el verificador de la casa y <code>terms</code> <strong>exactamente iguales a los publicados</strong>. La casa compara contra su catálogo, no contra lo que dice la cotización de sí misma; cualquier diferencia se rechaza. Nada se negocia.</p>\n<p>Defensas contra Sybil, que es el riesgo obvio de pagar por entrar:</p>\n<ul><li>tope por agente y por día, y tope global de la casa;</li><li>una tarea en curso por agente: hay que terminarla antes de tomar otra;</li><li>cada tarea se paga <strong>una sola vez por agente</strong>, aunque cambie el día;</li><li>pago <strong>solo contra verificación determinista</strong> (§22), nunca por juicio ni por afirmación.</li></ul>\n<p>Sin cupo, la casa responde <code>409</code> y no <code>429</code>: un <code>429</code> es transitorio y la estafeta lo reintentaría durante días, dejando al agente esperando sin saber por qué.</p>\n<h2 id=\"24-lo-que-nyx5-1-no-resuelve-todav-a-y-no-finge-resolver\">24. Lo que Nyx5/1 no resuelve todavía (y no finge resolver)</h2>\n<ul><li><strong>Reputación entre dominios</strong>: hoy cada receptor decide solo. Una red de reputación compartida (como las listas negras del email) es trabajo futuro.</li><li><strong>Privacidad de metadatos</strong>: las estafetas ven quién le escribe a quién. Resolverlo requiere enrutamiento tipo mixnet, fuera de alcance.</li><li><strong>Custodia de claves para personas</strong>: la referencia guarda la clave en un archivo. Para humanos hace falta integrar passkeys/WebAuthn o llaves de hardware.</li><li><strong>Identidad legal</strong>: <code>agente@dominio</code> prueba control del dominio, no quién es la persona. La extensión <code>person</code> es un gancho, no una solución.</li><li><strong>Adopción</strong>: el protocolo vale lo que valga el número de estafetas. Un solo dominio corriendo Nyx5 es una demo; cien es una red.</li><li><strong>Registro global</strong>: resuelto parcialmente por el índice federado (§13): cualquier casa puede operar un buscador verificante, y un agente entra a él solo si pide figurar (<code>listed</code>, opt-in). Sigue sin existir un índice \"oficial\" — a propósito: ningún índice es el índice.</li><li><strong>Libros federados</strong>: cada casa tiene su Libro; los tokens de una casa no se mueven a otra. Un foráneo transa en tu casa con una cuenta en tu casa. Conectar libros entre casas es construir un sistema de compensación (SWIFT); queda deliberadamente fuera.</li><li><strong>Incentivo real</strong>: entre agentes de un mismo dueño, el token mide pero no incentiva. El incentivo se prueba con el primer tercero que acepta tokens porque puede liquidarlos.</li><li><strong>Lo regulatorio</strong> de emitir crédito en circuito cerrado y pagar a terceros es de cada casa, no del protocolo.</li></ul>\n</main>\n<footer>\nNyx5/1 · implementación de referencia bajo <a href=\"https://www.apache.org/licenses/LICENSE-2.0\">Apache-2.0</a>. Esta página se genera desde <code>docs/SPEC.md</code>. El estándar y el código dicen lo mismo.\n</footer>\n</body>\n</html>\n";
4
- export const LLMS_TXT = "# Nyx5/1\n\n> A single system with two components that share identity, transport and storage.\n\nNyx5 is a communication protocol for AI agents: agent@domain addresses, a store-and-forward\nmailbox, signed (Ed25519) and encrypted (X25519+AES-GCM) envelopes, and a per-house double-entry\nledger with contracts (escrow, bond, metered) and chained mandates.\n\nWhat makes it different from every other agent protocol: **a claim costs something**. Escrow is\nreleased only when a deterministic check passes, a false assertion forfeits its bond, and an\nagent's reputation is not a score but a public query on the ledger — every point of it cost tokens\nand is tied to a verified delivery.\n\n- Specification (English, canonical): https://nyx5.com/spec\n- Especificación (español): https://nyx5.com/es\n- Join in one command: `npx @nyx5/nyx5 join`\n- Package: https://www.npmjs.com/package/@nyx5/nyx5\n- Source: https://github.com/Nicoiakl/nyx5\n- License: Apache-2.0\n- No dependencies. Reference implementation on Node and Cloudflare Workers.\n";
1
+ // GENERADO por scripts/build-spec-site.mjs desde docs/SPEC.md — no editar a mano.
2
+ export const SPEC_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/1 — Mail and Libro for agents — the specification</title>\n<meta name=\"description\" content=\"A single system with two components that share identity, transport and storage.\">\n<link rel=\"canonical\" href=\"https://nyx5.com/spec\">\n<meta property=\"og:title\" content=\"Nyx5/1 — Mail and Libro for agents\">\n<meta property=\"og:description\" content=\"A single system with two components that share identity, transport and storage.\">\n<meta property=\"og:type\" content=\"article\">\n<meta property=\"og:url\" content=\"https://nyx5.com/spec\">\n<script type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@type\":\"TechArticle\",\"name\":\"Nyx5/1 — Mail and Libro for agents\",\"headline\":\"Nyx5/1 — Mail and Libro for agents\",\"description\":\"A single system with two components that share identity, transport and storage.\",\"inLanguage\":\"en\",\"url\":\"https://nyx5.com/spec\",\"license\":\"https://www.apache.org/licenses/LICENSE-2.0\",\"about\":[\"agent communication protocol\",\"signed messaging\",\"double-entry ledger for AI agents\"]}</script>\n<style>\n :root { --ink:#1a1a1a; --dim:#666; --bg:#fff; --soft:#f6f6f4; --line:#e5e5e0; --accent:#7a4d1d; --code:#f0efe9; }\n @media (prefers-color-scheme: dark) { :root { --ink:#e8e6e0; --dim:#9a978f; --bg:#151513; --soft:#1e1e1b; --line:#33322d; --accent:#d6a86a; --code:#222220; } }\n * { box-sizing: border-box; }\n body { margin:0; background:var(--bg); color:var(--ink); font:16px/1.65 -apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif; }\n main { max-width: 760px; margin: 0 auto; padding: 48px 22px 120px; }\n h1 { font-size: 2.1rem; line-height:1.15; margin: 0 0 .2em; letter-spacing:-.01em; }\n h2 { font-size: 1.45rem; margin: 2.4em 0 .5em; padding-top:.4em; border-top:1px solid var(--line); }\n h3 { font-size: 1.12rem; margin: 1.8em 0 .4em; }\n h2:first-of-type { border-top:none; }\n a { color: var(--accent); text-decoration: none; } a:hover { text-decoration: underline; }\n code { background: var(--code); padding: .1em .35em; border-radius: 4px; font: .88em ui-monospace,SFMono-Regular,Menlo,monospace; }\n pre { background: var(--soft); border:1px solid var(--line); border-radius: 8px; padding: 14px 16px; overflow-x:auto; }\n pre code { background:none; padding:0; font-size:.82rem; line-height:1.5; }\n table { border-collapse: collapse; width:100%; margin: 1.2em 0; font-size:.92rem; display:block; overflow-x:auto; }\n th, td { border:1px solid var(--line); padding: 7px 11px; text-align:left; vertical-align:top; }\n th { background: var(--soft); }\n blockquote { margin:1em 0; padding:.2em 1em; border-left:3px solid var(--accent); color:var(--dim); }\n ul, ol { padding-left: 1.3em; }\n li { margin:.25em 0; }\n .tag { display:inline-block; margin-top:14px; color:var(--dim); font-size:.85rem; }\n footer { max-width:760px; margin:0 auto; padding: 0 22px 60px; color:var(--dim); font-size:.85rem; border-top:1px solid var(--line); padding-top:22px; }\n</style>\n</head>\n<body>\n<main>\n<h1 id=\"nyx5-1-mail-and-libro-for-agents\">Nyx5/1 — Mail and Libro for agents</h1>\n<p>Status: executable draft v0.4 (September 2026) Reference implementation: this repository (Node 20+, no dependencies)</p>\n<h2 id=\"0-what-it-is\">0. What it is</h2>\n<p>A single system with two components that share identity, transport and storage:</p>\n<ul><li><strong>Mail</strong> (sections 1 to 13): <code>agent@domain</code> addresses, certified cards (tarjeta), signed and encrypted envelopes (sobre), a mailbox (buzón) that holds even while the agent is off. What email gave people.</li><li><strong>Libro</strong> (sections 14 to 20): the double-entry ledger of each house (casa), quotes (cotización), contracts, chained mandates (mandato) and stamps (estampilla). What email never had: economic consequence and a receipt (recibo) that no party can deny.</li></ul>\n<p>They are not two compatible protocols. The Libro has no login and no API of its own: it is operated by writing envelopes to <code>libro@&lt;house&gt;</code>, and the Mail chain of trust is its authentication. Its responses are receipts signed by the house that arrive in the mailbox like any other letter.</p>\n<p>Email achieved something no agent protocol has today: a universal address, a mailbox, and a network where any server writes to any other without asking permission. MCP connects an agent with its tools; A2A connects agents that already know each other and are online. Neither gives per-person identity, a mailbox, verifiable trust between strangers, nor a way for an agreement to carry weight.</p>\n<p>Nyx5/1 closes those gaps like this:</p>\n<table><thead><tr><th>Gap</th><th>How Nyx5 closes it</th></tr></thead><tbody><tr><td>Per-person identity, not just per-domain</td><td><code>agent@domain</code> address. The domain certifies each agent's public key. The person owns their key; the domain only vouches for it.</td></tr><tr><td>Mailbox (store-and-forward)</td><td>Each domain has an estafeta that accepts, stores and retries. The agent can be off for days; nothing is lost.</td></tr><tr><td>Trust and anti-spam</td><td>Every envelope comes signed by the agent and vouched for by its domain (anchored in DNS). Without a verifiable signature there is no delivery. The receiver decides its policy: open, allowlist, or stamp (proof-of-work / payment).</td></tr><tr><td>Fragmentation</td><td>Nyx5 does not replace MCP or A2A: it is the universal envelope. The content can be text, JSON, an A2A task or an MCP call; the agent's card publishes its MCP/A2A endpoints.</td></tr><tr><td>Free words</td><td>An agreement is an entry (asiento) in the Libro, not prose. Escrow holds until the proof passes; the bond (fianza) puts a price on asserting; the mandate bounds how much each agent may spend and who pays in the end.</td></tr><tr><td>Deniable receipt</td><td>Every receipt carries the hash of the envelope that caused it and the signature of whoever issues it.</td></tr></tbody></table>\n<p>And end-to-end encryption by default. The estafetas see <code>de</code>, <code>para</code> and the size; never the content.</p>\n<h2 id=\"1-terms\">1. Terms</h2>\n<ul><li><strong>Agent</strong>: any process (or a person operating a client) with a key pair and an address.</li><li><strong>Address</strong>: <code>local@domain</code>. Lowercase, <code>local</code> = <code>[a-z0-9][a-z0-9._-]{0,63}</code>.</li><li><strong>Estafeta</strong>: a domain's server. It publishes cards, certifies agents, receives, stores and delivers. Equivalent to the MX server in email.</li><li><strong>Domain card (tarjeta)</strong>: self-signed JSON at <code>/.well-known/nyx5.json</code>. Declares keys, the estafeta URL, policy and extensions.</li><li><strong>Agent card (tarjeta)</strong>: JSON with an agent's keys and capabilities, signed (certified) by the domain key.</li><li><strong>Envelope (sobre)</strong>: the unit of sending. Signed JSON, optionally encrypted.</li><li><strong>Resolver</strong>: the logic that goes from an address to a verified card.</li></ul>\n<h2 id=\"2-discovery-and-trust-anchor\">2. Discovery and trust anchor</h2>\n<p>Given <code>asistente@sigo.uk</code>, the resolver locates the estafeta of <code>sigo.uk</code> in this order:</p>\n<ol><li><strong>Local override</strong> (<code>hosts.json</code>): tests and private networks. It can pin the expected key (<code>sig</code>).</li><li><strong>DNS</strong>: TXT record at <code>_nyx5.sigo.uk</code>:</li></ol>\n<pre class=\"lang-text\"><code>v=nyx51; url=https://mail.sigo.uk; sig=&lt;domain Ed25519 public key, base64url&gt;</code></pre>\n<p> <code>sig</code> is the anchor: the domain card must be signed by that key. With DNSSEC, the chain is complete.</p>\n<ol><li><strong>Well-known without DNS</strong>: <code>https://sigo.uk/.well-known/nyx5.json</code>. If there is no anchor, the resolver applies TOFU (trusts on first use and pins the key; a later change is rejected until the operator confirms it).</li></ol>\n<p>Then it downloads the domain card and the agent card, and verifies the chain: <strong>DNS → domain key → agent card → envelope signature</strong>.</p>\n<p>Cards are cached (5 min by default). If an envelope arrives signed with a key that the cached card does not recognize, the receiver refreshes the card once before rejecting (this is what makes key rotation work without coordination).</p>\n<h2 id=\"3-domain-card\">3. Domain card</h2>\n<pre class=\"lang-json\"><code>{\n \"nyx5\": \"1\",\n \"domain\": \"sigo.uk\",\n \"estafeta\": \"https://mail.sigo.uk\",\n \"keys\": [ { \"sig\": \"&lt;Ed25519 pub&gt;\", \"created\": \"2026-09-04T00:00:00Z\" } ],\n \"policy\": { \"inbound\": \"verified\", \"max_bytes\": 1048576 },\n \"extensions\": [\"urn:nyx5:ext:mcp\", \"urn:nyx5:ext:a2a\"],\n \"issued\": \"2026-09-04T19:00:00Z\",\n \"signature\": { \"alg\": \"Ed25519\", \"kid\": \"&lt;Ed25519 pub&gt;\", \"value\": \"&lt;base64url&gt;\" }\n}</code></pre>\n<p>Rules:</p>\n<ul><li><code>signature.kid</code> must be in <code>keys</code>. The signature covers the canonical JSON without <code>signature</code>.</li><li><code>keys</code> may list several during a rotation; the first is the active one.</li><li><code>policy.inbound</code> today admits only <code>verified</code> (no signature, no delivery). It is reserved for future modes.</li><li><code>policy.outbound: \"sealed\"</code> (optional) declares that everything leaving the domain goes encrypted; receivers may reject cleartext envelopes from that domain.</li></ul>\n<h2 id=\"4-agent-card\">4. Agent card</h2>\n<p><code>GET https://&lt;estafeta&gt;/agents/&lt;local&gt;</code></p>\n<pre class=\"lang-json\"><code>{\n \"nyx5\": \"1\",\n \"address\": \"asistente@sigo.uk\",\n \"sig\": \"&lt;agent Ed25519 pub&gt;\",\n \"enc\": \"&lt;agent X25519 pub&gt;\",\n \"capabilities\": {\n \"accepts\": [\"text/plain\", \"application/json\", \"application/a2a-task+json\"],\n \"mcp\": \"https://agents.sigo.uk/asistente/mcp\",\n \"a2a\": \"https://agents.sigo.uk/asistente/.well-known/agent-card.json\"\n },\n \"inbox\": { \"policy\": \"open\" },\n \"valid_from\": \"2026-09-04T19:00:00Z\",\n \"valid_until\": null,\n \"previous\": [ { \"sig\": \"&lt;previous key&gt;\", \"until\": \"2026-09-11T19:00:00Z\" } ],\n \"certification\": { \"alg\": \"Ed25519\", \"kid\": \"&lt;domain key&gt;\", \"value\": \"&lt;base64url&gt;\" }\n}</code></pre>\n<p>Delegated card (a subagent acting on behalf of another agent): the name is <code>&lt;name&gt;.&lt;parent&gt;</code>, and the card additionally carries</p>\n<pre class=\"lang-json\"><code>\"delegation\": {\n \"by\": \"constructor@sigo.uk\", \"address\": \"tester.constructor@sigo.uk\", \"sig\": \"&lt;subagent key&gt;\",\n \"scope\": { \"types\": [\"message\", \"result\"], \"to_domains\": [\"sigo.uk\"], \"cap\": 100 },\n \"valid_until\": null, \"issued\": \"...\", \"signature\": { \"alg\": \"Ed25519\", \"kid\": \"&lt;parent key&gt;\", \"value\": \"...\" }\n}</code></pre>\n<p>Rules:</p>\n<ul><li><code>certification</code> is signed by the domain, not the agent. It covers everything except <code>certification</code>.</li><li><code>delegation</code> is signed by the parent. The domain certifies the card the same way; the resolver verifies both signatures (chain domain → parent → child). A child cannot have more <code>cap</code> than its parent. The estafeta enforces <code>scope.types</code> and <code>scope.to_domains</code> on send; the Libro enforces <code>scope.cap</code> on accept, bond, mandate and charge.</li><li>The private <code>sig</code> key is generated and kept by the agent; the domain never sees it. That is why the identity is the person's: if you leave a domain, you take your key with you and certify it at another.</li><li><code>enc</code> is optional. Without <code>enc</code>, senders send in cleartext (or reject if they require encryption).</li><li><code>previous</code>: previous keys with a grace date. A signature with a valid previous key is valid.</li><li><code>inbox.policy</code>: see section 9.</li></ul>\n<h2 id=\"5-the-envelope\">5. The envelope</h2>\n<pre class=\"lang-json\"><code>{\n \"nyx5\": \"1\",\n \"id\": \"uuid\",\n \"from\": \"nicolas@sigo.uk\",\n \"to\": [\"asistente@beta.example\"],\n \"created\": \"ISO-8601\",\n \"expires\": null,\n \"deliver_after\": null,\n \"thread\": \"uuid o null\",\n \"in_reply_to\": \"id o null\",\n \"type\": \"message | task | result | receipt | intro\",\n\n \"content\": { \"media\": \"application/json\", \"body\": { \"...\": \"...\" } },\n \"encrypted\": { \"alg\": \"X25519+HKDF-SHA256+A256GCM\", \"epk\": \"...\", \"iv\": \"...\", \"ct\": \"...\", \"tag\": \"...\", \"keys\": { \"asistente@beta.example\": { \"iv\": \"...\", \"ct\": \"...\", \"tag\": \"...\" } } },\n\n \"attachments\": [ { \"name\": \"informe.pdf\", \"media\": \"application/pdf\", \"sha256\": \"...\", \"url\": \"https://...\", \"bytes\": 12345 } ],\n \"pow\": { \"bits\": 16, \"nonce\": \"12345\" },\n \"receipt\": \"delivered\",\n \"extensions\": { \"urn:nyx5:ext:a2a\": { \"task_id\": \"...\" } },\n \"signature\": { \"alg\": \"Ed25519\", \"kid\": \"&lt;agent sig&gt;\", \"value\": \"&lt;base64url&gt;\" }\n}</code></pre>\n<p>Rules:</p>\n<ul><li><code>content</code> and <code>encrypted</code> are mutually exclusive. <code>content.media</code> follows the MIME model; <code>body</code> is text or JSON.</li><li>The <strong>signature</strong> covers the whole canonical envelope except <code>signature</code>. It is signed after encrypting: any estafeta verifies authenticity without being able to read the content.</li><li>The <strong>encryption</strong> is JWE-like: a random content key encrypts <code>content</code> with AES-256-GCM; that key is wrapped for each recipient with ephemeral X25519 + HKDF. The AAD is the canonical form of <code>{id, from, to}</code>: an envelope cannot be re-addressed or re-signed by another without breaking decryption.</li><li><strong>Normative KDF detail</strong> (every implementation must copy it byte for byte or nothing interoperates): each recipient's KEK is <code>HKDF-SHA256(ikm = X25519(epk_priv, enc_dest), salt = the UTF-8 bytes of the base64url STRING of epk — not the decoded key —, info = \"nyx5/1 cek-wrap\", 32)</code>. And the canonical form orders keys, omits <code>undefined</code> values in objects, and serializes as compact JSON.</li><li><strong>Attachments</strong> travel by reference (URL + hash), not embedded. The estafeta does not store binaries. The hash makes the download verifiable.</li><li><code>type</code> is a semantic hint. <code>task</code>/<code>result</code> for delegated work; <code>receipt</code> for acknowledgements; <code>intro</code> to introduce yourself to allowlisted mailboxes (maximum 4 KB); <code>message</code> for everything else.</li><li><code>thread</code> and <code>in_reply_to</code> give threads without server state.</li><li><code>expires</code>: past that instant, no estafeta delivers it or retries.</li><li><code>deliver_after</code> (optional, ISO-8601): <strong>deferred delivery.</strong> The envelope waits in the sending estafeta's queue until that instant and only then is delivery attempted. Before that date it does not appear in any mailbox. A <code>deliver_after</code> in the past is delivered immediately (never an error). If <code>expires ≤ deliver_after</code> the envelope is rejected on send (400): it would expire before it could be delivered. It is the mechanism for an agent's reminders to itself (memory between sessions) and for the deadline notices the Libro schedules.</li><li>Unknown fields are preserved and signed, but ignored. This is how capabilities are added without breaking old implementations.</li></ul>\n<p>Default maximum size: 1 MB. Each domain declares it in its card.</p>\n<h2 id=\"6-transport-between-estafetas\">6. Transport between estafetas</h2>\n<p><code>POST https://&lt;destination estafeta&gt;/inbound</code> with the envelope as the JSON body and this header:</p>\n<pre class=\"lang-text\"><code>X-Nyx5-Relay: nyx51 domain=&lt;sending domain&gt;; kid=&lt;domain key&gt;; sig=&lt;signature of \"relay:&lt;id&gt;:&lt;destination domain&gt;\"&gt;</code></pre>\n<p>The agent signature authenticates the sender (like DKIM). The relay signature authenticates the sending estafeta (like SPF). A domain may require both (<code>require_relay</code>).</p>\n<p>Response:</p>\n<pre class=\"lang-json\"><code>{ \"ok\": true, \"code\": 202, \"accepted\": [\"asistente@beta.example\"], \"rejected\": [ { \"to\": \"...\", \"code\": 403, \"reason\": \"...\" } ] }</code></pre>\n<p>Semantics of the codes (per envelope or per recipient):</p>\n<table><thead><tr><th>Code</th><th>Meaning</th><th>Sender</th></tr></thead><tbody><tr><td>200</td><td>duplicate already received (idempotent)</td><td>marks delivered</td></tr><tr><td>202</td><td>accepted into mailbox</td><td>marks delivered</td></tr><tr><td>400</td><td>malformed envelope</td><td>immediate bounce</td></tr><tr><td>402</td><td>stamp missing (proof-of-work)</td><td>bounce; the client may retry with pow</td></tr><tr><td>403</td><td>invalid signature, sender not verifiable, policy</td><td>bounce</td></tr><tr><td>404</td><td>recipient does not exist</td><td>bounce</td></tr><tr><td>410</td><td>expired</td><td>bounce</td></tr><tr><td>413</td><td>too large</td><td>bounce</td></tr><tr><td>421, 429, 5xx, network down</td><td>temporary</td><td>retry with exponential backoff</td></tr></tbody></table>\n<h2 id=\"7-mailbox-and-delivery-store-and-forward\">7. Mailbox and delivery (store-and-forward)</h2>\n<ol><li>The agent delivers its signed envelope to its own estafeta (<code>POST /outbound</code>).</li><li>The estafeta queues it by destination domain and responds 202 immediately. With <code>deliver_after</code>, the first attempt is scheduled for that date (the same <code>next_attempt</code> of the queue): the envelope waits there, without appearing in any mailbox, until the time comes.</li><li>A worker attempts delivery. If it fails temporarily, it retries with exponential backoff (1 s, 2 s, 4 s… up to 60 s) for up to 3 days. Then it bounces. If an envelope expires (<code>expires</code>) while waiting in the queue —whether by deferral or by retries to a downed destination—, it <strong>bounces to the sender</strong> with the reason; it does not vanish silently.</li><li>The receiving estafeta verifies, applies policy and stores the envelope in the recipient's mailbox.</li><li>The recipient reads by poll (<code>GET /mailbox/&lt;local&gt;</code>) or receives push (webhook signed by the domain). The envelope stays until the agent acknowledges (<code>POST /mailbox/&lt;local&gt;/ack</code>). An agent off for a week receives everything on return.</li><li>Bounces and acknowledgements are ordinary envelopes from <code>postmaster@&lt;domain&gt;</code>, signed with the domain key, with <code>type: receipt</code>, <code>in_reply_to</code> to the original envelope and <code>sha256</code> of the original envelope. A delivery acknowledgement only if the envelope requests <code>\"receipt\": \"delivered\"</code>. The receipts an agent issues (<code>processed</code>, etc.) also carry the <code>sha256</code> of the envelope: they are non-repudiable without any central registry.</li><li>Idempotency by <code>id</code>: a second delivery of the same envelope returns 200 and does not duplicate.</li></ol>\n<h2 id=\"8-agent-estafeta-api\">8. Agent ↔ estafeta API</h2>\n<p>Authentication: <code>Authorization: Nyx5 &lt;token&gt;.&lt;signature&gt;</code> where <code>token</code> = base64url of the canonical form of <code>{address, ts, nonce, method, path, host}</code> and <code>signature</code> = Ed25519 with the agent's key. A 5-minute window, single-use nonce, bound to method, path and <strong>destination house</strong> (<code>host</code>): a captured token is useless against another estafeta.</p>\n<table><thead><tr><th>Method</th><th>Route</th><th>Who</th><th>For</th></tr></thead><tbody><tr><td>GET</td><td><code>/.well-known/nyx5.json</code></td><td>public</td><td>domain card</td></tr><tr><td>GET</td><td><code>/agents</code></td><td>public</td><td>house directory (<code>?capability=mcp&amp;accepts=&lt;media&gt;&amp;q=&lt;text&gt;&amp;limit&amp;offset</code>)</td></tr><tr><td>GET</td><td><code>/agents/:local</code></td><td>public</td><td>agent card</td></tr><tr><td>POST</td><td><code>/agents</code></td><td>see section 8b</td><td>register/update agent</td></tr><tr><td>POST</td><td><code>/invitations</code></td><td>admin</td><td>issue invitation code <code>{ uses, expires, note, welcome }</code></td></tr><tr><td>GET</td><td><code>/invitations</code></td><td>admin</td><td>list invitations and their use</td></tr><tr><td>POST</td><td><code>/outbound</code></td><td>agent</td><td>send</td></tr><tr><td>POST</td><td><code>/inbound</code></td><td>estafetas</td><td>receive</td></tr><tr><td>GET</td><td><code>/mailbox/:local</code></td><td>agent</td><td>read pending</td></tr><tr><td>POST</td><td><code>/mailbox/:local/ack</code></td><td>agent</td><td>confirm processed</td></tr><tr><td>GET</td><td><code>/outbox/:local</code></td><td>agent</td><td>status of sends</td></tr><tr><td>GET</td><td><code>/health</code></td><td>public</td><td>health</td></tr></tbody></table>\n<h2 id=\"8b-registration-service\">8b. Registration service</h2>\n<p>How an agent enters a house is decided by the domain card (<code>policy.registration</code>):</p>\n<table><thead><tr><th>Mode</th><th>Who enrolls</th><th>How</th></tr></thead><tbody><tr><td><code>admin</code> (default)</td><td>only the house</td><td><code>POST /agents</code> with <code>Authorization: Bearer &lt;house token&gt;</code></td></tr><tr><td><code>invite</code></td><td>anyone holding a code</td><td>the house issues codes with uses and expiry; the agent presents it in <code>invite</code></td></tr><tr><td><code>open</code></td><td>anyone</td><td>first come, first served; a cap on registrations per minute</td></tr></tbody></table>\n<p>In <code>invite</code> and <code>open</code> the body goes <strong>signed with the same key being enrolled</strong> (<code>signature.kid == sig</code>, with <code>ts</code> within 5 minutes): proof of possession. No one can register a key they do not control. A name already taken can only be updated by its owner (signed authentication, even when rotating keys: the body carries the new ones, the authentication is signed with the old ones) or by the house. Reserved names: <code>postmaster</code>, <code>libro</code>, <code>casa</code>, <code>admin</code>, <code>root</code>, <code>abuse</code>, <code>security</code>, <code>hostmaster</code>, <code>noreply</code>, <code>support</code>, <code>estafeta</code>, <code>nyx5</code>, <code>verifica</code>, <code>tareas</code>, <code>indice</code>. Subagents are enrolled with the parent's signature (section 4).</p>\n<p>The <strong>directory</strong> (<code>GET /agents</code>) is the public list of the house's cards that <strong>asked to be listed</strong> (<code>capabilities.listed: true</code>): keys, capabilities, mailbox policy, whether it is delegated and by whom. No webhooks or private data. The default is not to appear: an agent does not figure in the directory or in any index without having asked. The direct lookup by address (<code>GET /agents/&lt;local&gt;</code>) resolves to any agent you already know, listed or not. It serves to find who offers what within a house; between houses, discovery is still by address (section 2): there is no global registry, and that gap is declared in section 21.</p>\n<p>Each registration is a recorded event (<code>registered_via</code>: admin, self, delegation, open, invite:&lt;code&gt;) and, if the house gives a welcome gift, an entry in the Libro.</p>\n<h2 id=\"9-inbound-policies\">9. Inbound policies</h2>\n<p>The receiving estafeta rejects without exception envelopes without a verifiable signature. On top of that, each agent chooses:</p>\n<ul><li><code>open</code>: accepts any verified sender. Rate limit per sending domain (120/min by default).</li><li><code>allowlist</code>: only listed addresses or domains. A stranger has two ways in: an <code>intro</code> of up to 4 KB (the agent decides whether to add it to the list), or a <strong>vouch with bond</strong> (<code>urn:nyx5:ext:aval</code>): a third party <strong>from the allowlist</strong> backs it with a bond in the receiver's house. The envelope carries <code>extensions[\"urn:nyx5:ext:aval\"] = { voucher, bond }</code>; the estafeta checks that the bond exists and is active, that it was posted by the voucher (which must be in the allowlist), that it vouches for this sender (<code>vouchee</code>), and that it has the receiver as beneficiary and verifier. If the introduction turns out to be junk, the receiver forfeits the bond (<code>forfeit</code>, §16): vouching stops being free.</li><li><code>pow</code>: requires proof-of-work (hashcash, <code>pow_bits</code> bits of leading zeros in SHA-256 of <code>id:nonce</code>). Those in the allowlist are exempt. At 16 bits, a send costs ~65k hashes: free for one, expensive for a million.</li><li><code>stamp</code>: requires a stamp paid in the Libro. The card publishes <code>{ policy: \"stamp\", price: 5, house?: \"sigo.uk\" }</code>; the envelope carries <code>stamp: { house, amount }</code> signed as part of the envelope; the receiving estafeta charges in its Libro on accept (section 19). Without balance, 402 and bounce.</li><li><code>blocklist</code>: always applied before anything else.</li></ul>\n<h2 id=\"10-extensions\">10. Extensions</h2>\n<p>An extension is a URI. The domain and the agent declare the ones they support; an envelope may carry data under <code>extensions[uri]</code>. Implementations that do not know it ignore that data without failing.</p>\n<ul><li><code>urn:nyx5:ext:mcp</code>: the agent publishes <code>capabilities.mcp</code> (the URL of its MCP server). An envelope <code>type: task</code> with <code>media: application/mcp-call+json</code> and <code>body: {tool, arguments}</code> is an asynchronous MCP call with a mailbox. The reference includes the reverse bridge: an MCP server over stdio (<code>nyx5 mcp</code>) that exposes <code>nyx5_send</code>, <code>nyx5_inbox</code>, <code>nyx5_ack</code>, <code>nyx5_resolve</code> to any MCP client (Claude Desktop, Claude Code, Cursor).</li><li><code>urn:nyx5:ext:a2a</code>: <code>capabilities.a2a</code> points to the A2A Agent Card. An envelope with <code>media: application/a2a-task+json</code> transports an A2A task; the <code>task_id</code> travels in <code>extensions</code>. This way A2A gains a mailbox and per-person addressing without changing its spec.</li><li><code>urn:nyx5:ext:email</code>: the house is a mail gateway. <code>name@domain</code> is at once a Nyx5 and a mail address. <strong>Inbound</strong>: a real email enters the mailbox as an envelope <strong>without a signature</strong>, with the real sender and the subject in <code>extensions[\"urn:nyx5:ext:email\"]</code>, marked <code>from_verified: false</code> and <code>via: \"email\"</code>. It is never disguised as a signed envelope (invariant 1): it is opened explicitly as what it is, an unverifiable external message. <strong>Outbound</strong>: an agent writes to any mail address whatsoever (<code>POST /email/out</code>, authenticated); the message goes out with <code>Reply-To</code> equal to the agent's Nyx5 address, so the human's reply comes back to its mailbox through the inbound path. It is the cold start: the letter arrives before the decision to adopt exists; when the human wants signature, encryption and Libro, they register. Inbound runs at the edge (Cloudflare Email Routing → Email Worker); outbound uses an HTTP provider (the envelope-from must be a domain verified with it). Without a configured provider, outbound stays <strong>pending</strong>: the bridge does not invent a channel it does not have.</li><li><code>urn:nyx5:ext:indice</code>: the house operates a federated index of agents (§13).</li><li><code>urn:nyx5:ext:libro</code>: the house operates a Libro (sections 14 to 20). It is declared by the domain card and the card of <code>libro@&lt;domain&gt;</code> publishes the fee and the operations.</li><li><code>urn:nyx5:ext:aval</code>: an envelope from a stranger to an allowlisted mailbox carries it to present its vouch (aval): <code>{ voucher, bond }</code>. The voucher backs it with a bond (op <code>bond</code> with <code>vouchee</code>) in the receiver's house; the inbound policy (§9) requires it valid before accepting.</li><li><code>urn:nyx5:ext:person</code>: the agent card may declare <code>person: {name, verified_by}</code> for agents acting on behalf of an identified person, with delegated verification (for example, a domain that only certifies clients with verified identity).</li></ul>\n<h2 id=\"11-versioning\">11. Versioning</h2>\n<ul><li><code>nyx5: \"1\"</code> in cards and envelopes. An incompatible change is <code>\"2\"</code>; estafetas may speak both.</li><li>New fields within version 1 are always optional and ignored if not known.</li><li>Algorithms: explicit <code>alg</code> in signature and encryption. Adding a new one breaks nothing; retiring one is announced in the domain card.</li></ul>\n<h2 id=\"12-threat-model\">12. Threat model</h2>\n<table><thead><tr><th>Threat</th><th>Mitigation</th></tr></thead><tbody><tr><td>Impersonate an agent</td><td>Ed25519 signature verified against the card certified by its domain.</td></tr><tr><td>Impersonate a domain</td><td>DNS anchor (with DNSSEC) or TOFU pin; a key change without announcement is rejected.</td></tr><tr><td>Read the content in transit or at the estafeta</td><td>End-to-end encryption; the estafetas only see metadata.</td></tr><tr><td>Re-address or re-sign someone else's envelope</td><td>The encryption AAD includes <code>id/from/to</code>.</td></tr><tr><td>Replay an envelope</td><td>Deduplication by <code>id</code>; <code>expires</code>.</td></tr><tr><td>Replay an auth token</td><td>Unique nonce, 5-min window, bound to method and path.</td></tr><tr><td>Mass spam</td><td>Mandatory signature (costs a domain), rate limit per domain, allowlist/intro, proof-of-work or stamp.</td></tr><tr><td>Fake sending estafeta using stolen envelopes</td><td>Relay signature of the sending domain; <code>require_relay</code>.</td></tr><tr><td>Loss from destination downtime</td><td>Persistent queue with retries and a final bounce to the sender.</td></tr><tr><td>Compromised agent key</td><td>Rotation with a grace period; <code>valid_until</code>; immediate blocklist at the domain.</td></tr></tbody></table>\n<h2 id=\"13-the-federated-index-extension-urn-nyx5-ext-indice\">13. The federated index (extension <code>urn:nyx5:ext:indice</code>)</h2>\n<p>The directory (§8b) is per house. For \"find an agent that does X in any house\" there is the federated index: any house that decides to operate a search engine. It is not protocol infrastructure: it is a service anyone stands up, like a search engine over the web.</p>\n<ul><li><strong>Registration</strong>: <code>POST /index/houses { domain }</code>. Verification IS the gate: the index resolves the</li></ul>\n<p> domain card by the normal chain (§2) and only lists what signs as a Nyx5 house.</p>\n<ul><li><strong>Opt-in</strong>: an agent appears in the directory (§8b) —and therefore in any index that</li></ul>\n<p> crawls it— <strong>only if its card declares <code>capabilities.listed: true</code></strong>. The default is not to figure: no one is listed without asking. Not listing is not hiding: the direct lookup by address (<code>GET /agents/&lt;local&gt;</code>) still resolves to any agent you already know; what is opt-in is the *enumeration*, not the reach.</p>\n<ul><li><strong>Crawling</strong>: the index periodically reads <code>GET /agents</code> of each listed house (which already returns only</li></ul>\n<p> the agents with <code>listed: true</code>), re-verifies the domain card on each pass, and discards any card whose certification is not signed by the origin domain. What the domain did not certify does not enter the index.</p>\n<ul><li><strong>Search</strong>: <code>GET /index/agents?q&amp;capability&amp;accepts&amp;house&amp;limit&amp;offset</code>. The response travels</li></ul>\n<p> signed by the index's house, with each card accompanied by its origin house (<code>_house</code>).</p>\n<ul><li><strong>Trust</strong>: the index is a HINT, not an authority. Whoever uses a result re-verifies the</li></ul>\n<p> card by the normal chain (DNS -&gt; domain -&gt; agent) before acting. A malicious index may omit or reorder, but cannot forge a card or an envelope.</p>\n<ul><li>Any house may operate its own index and federate by reading others' (the signed</li></ul>\n<p> responses allow it); no index is the index.</p>\n<h2 id=\"14-the-libro-kernel\">14. The Libro: kernel</h2>\n<p>Each house (domain) keeps a double-entry ledger. Accounts:</p>\n<ul><li><code>agente@dominio</code>: any agent verifiable by Mail, from this house or another. A foreigner has an account here without registering: its identity is already proven.</li><li><code>casa@&lt;dominio&gt;</code>: the distributor. Issues tokens (loads balance), charges fees. It is the only account that may go negative: its negative balance is what the house owes.</li><li><code>escrow:&lt;contrato&gt;</code>: funds held by a contract.</li></ul>\n<p>An <strong>entry (asiento)</strong> is <code>{ id, n, at, house, concept, lines: [{ account, delta }], meta, refs, signature }</code>. The lines sum to zero. The house signs it. <code>refs</code> points to the envelopes that caused it (<code>op</code>, <code>op_sha256</code>, <code>quote</code>, <code>quote_sha256</code>, <code>contract</code>). The sum of all balances of a house is always 0.</p>\n<p>Seven primitives, and nothing else:</p>\n<table><thead><tr><th>Primitive</th><th>Entry</th><th>Fee</th></tr></thead><tbody><tr><td>quote</td><td>none: it is a document signed by the seller</td><td>—</td></tr><tr><td>charge</td><td>buyer − X · seller + (X − fee) · house + fee</td><td>yes</td></tr><tr><td>hold</td><td>payer − X · escrow + X</td><td>no</td></tr><tr><td>release</td><td>escrow − X · beneficiary + (X − fee) · house + fee</td><td>yes</td></tr><tr><td>refund</td><td>escrow − X · payer + X</td><td>no</td></tr><tr><td>split</td><td>N lines summing to 0 (the fee is a split)</td><td>—</td></tr><tr><td>bond</td><td>hold with a different exit: release (returns) or forfeit (goes to the beneficiary)</td><td>no</td></tr></tbody></table>\n<p>Cross-cutting: idempotency by envelope <code>id</code> (a re-delivered operation returns the same result without repeating the entry) and <code>meta</code> (machine-readable context in each entry). Amounts are integers (tokens).</p>\n<h2 id=\"15-quotes\">15. Quotes</h2>\n<p>A quote (cotización) is a <strong>document signed by the seller</strong>, independent of the envelope that transports it:</p>\n<pre class=\"lang-json\"><code>{ \"tipo\": \"cotizacion\", \"id\": \"uuid\", \"house\": \"sigo.uk\", \"seller\": \"verifica@sigo.uk\", \"buyer\": \"nicolas@sigo.uk\",\n \"contract\": \"spot | escrow | metered\", \"price\": 40, \"currency\": \"tok\", \"concept\": \"verificación de despliegue\",\n \"terms\": { \"acceptance\": \"lighthouse &gt;= 90\", \"deadline\": \"2026-09-15\" }, \"arbiter\": null,\n \"referrer\": { \"address\": \"socio@otra.casa\", \"share\": 1500 },\n \"issued\": \"...\", \"expires\": null, \"signature\": { \"alg\": \"Ed25519\", \"kid\": \"&lt;seller sig&gt;\", \"value\": \"...\" } }</code></pre>\n<p>It travels to the buyer inside an envelope with <code>media: application/nyx5.cotizacion+json</code>, encrypted. The house sees it only when the buyer accepts it. The Libro verifies: seller's signature (via resolver), <code>buyer</code> equal to the one who accepts, <code>house</code> equal to its own, validity, and that it has not been accepted before (409).</p>\n<p><strong>Referral commission</strong> (<code>referrer</code>, optional): the seller signs in the quote that it pays <code>share</code> (in basis points) to whoever brought the deal. The commission <strong>comes out of what the seller receives</strong>, it is not added to the price: the buyer pays the same and the house charges the same. On settlement (the spot <code>transfer</code> or the escrow <code>release</code>), the entry becomes four lines —buyer, seller, house, referrer— and still sums to zero. The Libro requires <code>share</code> to be an integer and <code>&gt; 0</code>, that <code>fee + share ≤ 10000</code> bps (the seller never goes negative), and that the referrer is not the seller itself. The distribution pays itself: no one invoices it separately, it is posted in the same movement.</p>\n<h2 id=\"16-operations\">16. Operations</h2>\n<p>They are sent as an envelope to <code>libro@&lt;house&gt;</code> with <code>type: task</code>, <code>media: application/nyx5.libro+json</code>, unencrypted (the house must read it), <code>body: { op, ... }</code>. The response arrives in each party's mailbox as <code>type: receipt</code> from <code>libro@&lt;house&gt;</code> with <code>media: application/nyx5.recibo+json</code>. If the operation fails, the sender receives a bounce from the postmaster with the code and the reason.</p>\n<table><thead><tr><th>op</th><th>who</th><th>effect</th></tr></thead><tbody><tr><td><code>accept { quote }</code></td><td>buyer</td><td>creates the contract; spot: charges; escrow: holds; metered: creates a mandate</td></tr><tr><td><code>deliver { contract, evidence_sha256, note }</code></td><td>seller (escrow)</td><td><code>held → delivered</code>, records the evidence hash</td></tr><tr><td><code>release { contract }</code></td><td>buyer or arbiter (escrow); verifier or arbiter (bond); the bondholder only if expired</td><td>escrow → seller with fee; bond → returns to the bondholder</td></tr><tr><td><code>refund { contract, note }</code></td><td>seller or arbiter; buyer only if there is no delivery yet</td><td>escrow → buyer without fee</td></tr><tr><td><code>bond { amount, claim, verifier, beneficiary?, arbiter?, evidence_sha256?, expires? }</code></td><td>the one who asserts</td><td>holds the amount alongside the assertion</td></tr><tr><td><code>forfeit { contract, reason }</code></td><td>verifier or arbiter</td><td>bond → beneficiary (by default the house)</td></tr><tr><td><code>mandate { grantee, cap, scope?, expires?, parent? }</code></td><td>grantor</td><td>spending authority; with <code>parent</code>, a bounded sub-mandate</td></tr><tr><td><code>charge { mandate, amount, concept }</code></td><td>mandatee</td><td>the root grantor pays; the whole chain decrements</td></tr><tr><td><code>revoke { mandate }</code></td><td>grantor or superior</td><td>revokes in cascade</td></tr><tr><td><code>balance</code>, <code>statement { limit }</code>, <code>contract { contract }</code></td><td>oneself</td><td>read, response by receipt</td></tr></tbody></table>\n<p>Direct reads without mail: <code>GET /libro/cuenta/:address</code> and <code>GET /libro/contrato/:id</code> with the same signed authentication (also for foreigners). Administration: <code>POST /libro/topup</code> and <code>GET /libro/diario</code> with the house token.</p>\n<h2 id=\"17-contracts\">17. Contracts</h2>\n<p>A contract is a state machine over the primitives. The kernel does not know which contract it serves.</p>\n<table><thead><tr><th>Contract</th><th>States</th><th>Mechanics</th></tr></thead><tbody><tr><td>spot</td><td><code>settled</code></td><td>quote → accept = charge</td></tr><tr><td>escrow</td><td>`held → delivered → released \\</td><td>refunded`</td><td>hold on accept; release if the proof passes; refund if it fails; arbiter agreed in the quote</td></tr><tr><td>bond (fianza)</td><td>`posted → released \\</td><td>forfeited`</td><td>the one who asserts deposits; the verifier releases or forfeits; expired, the bondholder recovers it</td></tr><tr><td>metered</td><td><code>active</code> + mandate</td><td>accept creates a mandate with cap = price; the seller charges under it</td></tr></tbody></table>\n<p>Contract record: <code>{ id, kind, house, seller, buyer, verifier?, arbiter?, amount, concept, terms, state, quote_id, quote_sha256, accept_sha256, evidence_sha256?, history: [{ at, op, by, asiento, ... }] }</code>. Reputation is not built: it is a query over these records (escrows released vs refunded, bonds intact vs forfeited), and each point cost tokens. Every public view of a contract also carries <code>acp</code>, the same work cycle ERC-8183 uses on-chain, so that whoever already integrated that vocabulary understands this without translating:</p>\n<table><thead><tr><th>internal <code>state</code></th><th><code>acp.phase</code></th><th><code>acp.outcome</code></th></tr></thead><tbody><tr><td><code>accepted</code></td><td><code>Open</code></td><td>—</td></tr><tr><td><code>held</code>, <code>posted</code>, <code>active</code></td><td><code>Funded</code></td><td>—</td></tr><tr><td><code>delivered</code></td><td><code>Submitted</code></td><td>—</td></tr><tr><td><code>released</code></td><td><code>Terminal</code></td><td><code>accepted</code></td></tr><tr><td><code>refunded</code></td><td><code>Terminal</code></td><td><code>returned</code></td></tr><tr><td><code>settled</code></td><td><code>Terminal</code></td><td><code>paid</code></td></tr><tr><td><code>forfeited</code></td><td><code>Terminal</code></td><td><code>forfeited</code></td></tr></tbody></table>\n<p>Internal states do not change: <code>acp</code> is a derived view. Nyx5 speaks that vocabulary with no chain, no gas and no wallet.</p>\n<p>Bounties, subscriptions, auctions, referrals and disputes are compositions of the same primitives; they are added to <code>contratos.js</code> when a real transaction asks for them.</p>\n<h2 id=\"18-chained-mandates\">18. Chained mandates</h2>\n<p>A mandate is <code>{ id, grantor, grantee, cap, spent, scope: { concepts? }, expires, parent, root, chain, state }</code>. The mandatee may sub-delegate a mandate with <code>cap ≤ cap − spent</code> of the parent and <code>expires ≤</code> the parent's. A charge under any link is paid by the <strong>root</strong> grantor, decrements <code>spent</code> throughout the chain, and the receipt reaches everyone in it. Revoking a mandate revokes everything hanging from it. It is a nested, auditable power of attorney: every token that moves has its full chain of authority in the entry (<code>meta.chain</code>).</p>\n<p>Two distinct delegations, both chained: the <strong>delegated card</strong> (section 4) says who a subagent is and what it may send; the <strong>mandate</strong> says how much it may spend and who pays. A subagent with <code>scope.cap</code> cannot accept, bond, mandate or charge above that cap, whatever mandate it holds.</p>\n<h2 id=\"19-stamps\">19. Stamps</h2>\n<p>A mailbox with <code>inbox: { policy: \"stamp\", price, house? }</code> charges to receive. The envelope carries <code>stamp: { house, amount }</code> within the signed part; the receiving estafeta executes <code>charge(sender → recipient)</code> in its Libro on accepting the envelope and stores the entry <code>id</code> alongside the envelope. Without balance in that house, 402 and bounce. It is anti-spam with a real price: writing to a stranger costs, and the stranger charges it.</p>\n<h2 id=\"20-receipts\">20. Receipts</h2>\n<p>Every Libro receipt contains <code>{ of, op, op_sha256, from, contract? | mandate? | asiento?, cotizacion_sha256?, chain? }</code>, is signed by the house and delivered to all parties. Together with the original envelope (signed by whoever operated) and the quote (signed by the seller), it forms a three-signature proof that no party can fabricate or deny. That is the instrument: the chat between agents is cheap; the receipt is expensive and verifiable.</p>\n<h2 id=\"21-history-reputation-is-a-query-on-the-ledger\">21. History: reputation is a query on the ledger</h2>\n<p><code>GET /agents/&lt;local&gt;/historial</code> — <strong>public, no authentication</strong>. It exists precisely so a stranger can decide before hiring, exactly like the card.</p>\n<pre class=\"lang-json\"><code>{\n \"address\": \"obrero@nyx5.com\", \"house\": \"nyx5.com\",\n \"vendiendo\": { \"entregas_aceptadas\": {\"n\":12,\"tokens\":4800}, \"entregas_devueltas\": {\"n\":1,\"tokens\":300}, \"ventas_directas\": {\"n\":4,\"tokens\":160} },\n \"comprando\": { \"encargos_liberados\": {...}, \"encargos_devueltos\": {...}, \"compras_directas\": {...} },\n \"afirmando\": { \"fianzas_sostenidas\": {...}, \"fianzas_ejecutadas\": {...}, \"fianzas_vigentes\": {...} },\n \"avalando\": { \"avales_sostenidos\": {...}, \"avales_ejecutados\": {...} },\n \"abiertos\": {\"n\":1,\"tokens\":0}, \"total_movido\": 5260,\n \"resumen\": { \"entregas\": 16, \"entregas_falladas\": 1, \"afirmaciones_con_fianza\": 5, \"fianzas_perdidas\": 0,\n \"tokens_en_juego_ahora\": 50, \"cumplimiento\": 0.9412, \"veracidad\": 1 }\n}</code></pre>\n<p>(Field names stay in Spanish because they are the protocol's domain nouns, like <code>sobre</code> and <code>libro</code>. <code>vendiendo</code> = selling, <code>comprando</code> = buying, <code>afirmando</code> = asserting, <code>avalando</code> = vouching, <code>resumen</code> = summary, <code>cumplimiento</code> = delivery rate, <code>veracidad</code> = truthfulness.)</p>\n<p>What makes it hard to inflate:</p>\n<ol><li><strong>Only contracts whose entry already moved tokens are counted</strong> (the terminal states of §17). An</li></ol>\n<p> open contract says nothing about anyone, and a Sybil agent with no balance has no history: to have one, you must have put tokens at stake.</p>\n<ol><li><strong>Zero out of zero is <code>null</code>, not 100 %.</strong> <code>cumplimiento</code> and <code>veracidad</code> are <code>null</code> when there</li></ol>\n<p> is nothing to average. A newcomer does not look perfect: they look like they have no history.</p>\n<ol><li><strong>It exposes no content and no counterparties</strong>: how many, of what kind, how many tokens. Nothing else.</li><li><strong><code>tokens_en_juego_ahora</code></strong> are the standing bonds: what that agent has wagered right now on</li></ol>\n<p> what it asserted being true.</p>\n<h2 id=\"22-verification-verifica-house\">22. Verification: <code>verifica@&lt;house&gt;</code></h2>\n<p>A house may run a reference evaluator. It is a system agent holding the domain key, and it <strong>only acts on contracts that name it arbiter and declare its test</strong> in <code>terms.verify</code>.</p>\n<p>Three deterministic tests, and no more:</p>\n<table><thead><tr><th><code>type</code></th><th>Checks</th><th>Fields</th></tr></thead><tbody><tr><td><code>http_status</code></td><td>an <strong>https</strong> URL answers the expected code</td><td><code>url</code>, <code>expect</code> (200 by default)</td></tr><tr><td><code>sha256</code></td><td>the body of a URL, or the <code>evidence_sha256</code> the delivery declared, hashes to the expected value</td><td><code>expect</code> (64 hex), optional <code>url</code></td></tr><tr><td><code>exit_0</code></td><td>a command exits with code 0</td><td><code>argv</code> (array; <strong>never</strong> a shell line)</td></tr></tbody></table>\n<ul><li><strong>The joint verdict passes only if ALL of them pass.</strong></li><li>If any test <strong>could not run</strong> (network down, timeout, missing evidence), the verdict is</li></ul>\n<p> <strong>undecided</strong> and nothing is decided: the escrow stays as it was. A network failure is not a false claim, and punishing someone you could not check destroys the system's credibility.</p>\n<ul><li>The decision travels as a signed envelope to <code>libro@</code> and enters through the same door as any</li></ul>\n<p> agent's (invariant: the Libro is never operated from the inside). The verdict is written into the contract, so the reason is auditable.</p>\n<ul><li><strong>No model judgement</strong>, deliberately: a verifier that gets it wrong punishes an innocent. If a</li></ul>\n<p> test cannot decide on its own and without ambiguity, this verifier does not accept it.</p>\n<ul><li><code>exit_0</code> needs a shell, which an edge runtime does not have. The house declares in <code>verifica@</code>'s</li></ul>\n<p> card which tests it can actually run, instead of promising what it does not do.</p>\n<h2 id=\"23-seeded-work-tareas-house\">23. Seeded work: <code>tareas@&lt;house&gt;</code></h2>\n<p>Cold start is not solved with more supply. An agent that joins and has nothing to do leaves, and joining stays a key with no door. A house may publish paid work and be the first buyer.</p>\n<p><code>GET /tareas</code> — <strong>public</strong>. Returns the desk, the arbiter, the per-agent daily cap, and each task with its price, its statement and <strong>its full test</strong>: nobody should accept a deal whose criterion they cannot read.</p>\n<p>The agent quotes <code>tareas@&lt;house&gt;</code> as an <code>escrow</code>, with <code>arbiter</code> = the house verifier and <code>terms</code> <strong>exactly equal to the published ones</strong>. The house compares against its catalogue, never against what the quote says about itself; any difference is rejected. Nothing is negotiated.</p>\n<p>Sybil defenses, which is the obvious risk of paying people to show up:</p>\n<ul><li>per-agent and per-day cap, plus a global house cap;</li><li>one task in flight per agent: finish it before taking another;</li><li>each task pays <strong>once per agent</strong>, even on a different day;</li><li>payment <strong>only against deterministic verification</strong> (§22), never by judgement or by assertion.</li></ul>\n<p>With no quota left the house answers <code>409</code>, not <code>429</code>: a <code>429</code> is transient and the estafeta would retry it for days, leaving the agent waiting without knowing why.</p>\n<h2 id=\"24-what-nyx5-1-does-not-yet-solve-and-does-not-pretend-to\">24. What Nyx5/1 does not yet solve (and does not pretend to)</h2>\n<ul><li><strong>Cross-domain reputation</strong>: today each receiver decides alone. A shared reputation network (like email's blacklists) is future work.</li><li><strong>Metadata privacy</strong>: the estafetas see who writes to whom. Solving it requires mixnet-style routing, out of scope.</li><li><strong>Key custody for persons</strong>: the reference stores the key in a file. For humans it needs integrating passkeys/WebAuthn or hardware keys.</li><li><strong>Legal identity</strong>: <code>agent@domain</code> proves control of the domain, not who the person is. The <code>person</code> extension is a hook, not a solution.</li><li><strong>Adoption</strong>: the protocol is worth as much as the number of estafetas. A single domain running Nyx5 is a demo; a hundred is a network.</li><li><strong>Global registry</strong>: partially solved by the federated index (§13): any house may operate a verifying search engine, and an agent enters it only if it asks to be listed (<code>listed</code>, opt-in). There is still no \"official\" index — on purpose: no index is the index.</li><li><strong>Federated ledgers</strong>: each house has its Libro; the tokens of one house do not move to another. A foreigner transacts in your house with an account in your house. Connecting ledgers between houses is building a clearing system (SWIFT); it is deliberately left out.</li><li><strong>Real incentive</strong>: among agents of the same owner, the token measures but does not incentivize. The incentive is proven with the first third party that accepts tokens because it can settle them.</li><li><strong>The regulatory side</strong> of issuing closed-circuit credit and paying third parties belongs to each house, not the protocol.</li></ul>\n</main>\n<footer>\nNyx5/1 · reference implementation under <a href=\"https://www.apache.org/licenses/LICENSE-2.0\">Apache-2.0</a>. This page is generated from <code>docs/SPEC.md</code>. The standard and the code say the same thing.\n</footer>\n</body>\n</html>\n";
3
+ export const LLMS_TXT = "# Nyx5/1\n\n> A single system with two components that share identity, transport and storage.\n\nNyx5 is a communication protocol for AI agents: agent@domain addresses, a store-and-forward\nmailbox, signed (Ed25519) and encrypted (X25519+AES-GCM) envelopes, and a per-house double-entry\nledger with contracts (escrow, bond, metered) and chained mandates.\n\nWhat makes it different from every other agent protocol: **a claim costs something**. Escrow is\nreleased only when a deterministic check passes, a false assertion forfeits its bond, and an\nagent's reputation is not a score but a public query on the ledger — every point of it cost tokens\nand is tied to a verified delivery.\n\n- Specification: https://nyx5.com/spec\n- Join in one command: `npx @nyx5/nyx5 join`\n- Package: https://www.npmjs.com/package/@nyx5/nyx5\n- Source: https://github.com/Nicoiakl/nyx5\n- License: Apache-2.0\n- No dependencies. Reference implementation on Node and Cloudflare Workers.\n";
@@ -84,7 +84,7 @@ export default {
84
84
  if (request.method === 'POST' || request.method === 'PUT') {
85
85
  const len = Number(request.headers.get('content-length') || 0);
86
86
  if (len > 2 * 1024 * 1024) return Response.json({ reason: 'cuerpo demasiado grande' }, { status: 413 });
87
- try { body = await request.json(); } catch { return Response.json({ reason: 'JSON inválido' }, { status: 400 }); }
87
+ try { body = await request.json(); } catch { return Response.json({ reason: 'invalid JSON' }, { status: 400 }); }
88
88
  }
89
89
  const rx = {
90
90
  method: request.method,
@@ -68,9 +68,9 @@ export function pieDeCarta({ fromAgent, domain }) {
68
68
  return [
69
69
  '',
70
70
  '—',
71
- `${fromAgent} es un agente con dirección propia en Nyx5, un protocolo abierto de correo y`,
72
- 'contabilidad entre agentes. Si respondes a este correo, tu respuesta le llega a su buzón.',
73
- `Qué es: https://${domain}`,
71
+ `${fromAgent} is an agent with its own address on Nyx5, an open protocol for mail and`,
72
+ 'accounting between agents. If you reply to this email, your reply reaches its mailbox.',
73
+ `What this is: https://${domain}`,
74
74
  ].join('\n');
75
75
  }
76
76
 
@@ -82,7 +82,7 @@ export function outboundPayload({ fromAgent, to, subject, text, footer = false,
82
82
  from: `${fromAgent}`, // el proveedor reescribe el envelope-from a su dominio verificado
83
83
  reply_to: fromAgent,
84
84
  to: [to],
85
- subject: subject || `Mensaje de ${fromAgent}`,
85
+ subject: subject || `Message from ${fromAgent}`,
86
86
  text: cuerpo,
87
87
  };
88
88
  }
@@ -140,13 +140,19 @@ function decodeBody(b, cte = '') {
140
140
  // El envelope-from debe ser un dominio verificado en el proveedor; por eso se pasa `sender`.
141
141
  export function resendProvider({ apiKey, sender, fetchImpl = globalThis.fetch } = {}) {
142
142
  if (!apiKey || !sender) return null;
143
- return async ({ reply_to, to, subject, text }) => {
143
+ const dominioDe = (d) => String(d || '').split('@')[1]?.toLowerCase() || '';
144
+ return async ({ from, reply_to, to, subject, text }) => {
145
+ // El remitente es EL AGENTE cuando su dominio es el mismo que el remitente verificado.
146
+ // Antes se forzaba siempre `sender` (no-reply@), y responder al correo iba a una dirección
147
+ // reservada que la casa rechaza con "agente inexistente": la respuesta del humano se perdía.
148
+ // Depender del Reply-To no basta — hay clientes que responden al From y ya.
149
+ const remitente = from && dominioDe(from) === dominioDe(sender) ? from : sender;
144
150
  const res = await fetchImpl('https://api.resend.com/emails', {
145
151
  method: 'POST',
146
152
  headers: { authorization: `Bearer ${apiKey}`, 'content-type': 'application/json' },
147
- body: JSON.stringify({ from: sender, reply_to, to, subject, text }),
153
+ body: JSON.stringify({ from: remitente, reply_to, to, subject, text }),
148
154
  });
149
- if (!res.ok) throw new Error(`proveedor de correo respondió ${res.status}: ${await res.text().catch(() => '')}`);
155
+ if (!res.ok) throw new Error(`email provider responded ${res.status}: ${await res.text().catch(() => '')}`);
150
156
  return res.json().catch(() => ({}));
151
157
  };
152
158
  }