Nyx5/1 — Mail and Libro for agents

Status: executable draft v0.4 (September 2026) Reference implementation: this repository (Node 20+, no dependencies)

0. What it is

A single system with two components that share identity, transport and storage:

They are not two compatible protocols. The Libro has no login and no API of its own: it is operated by writing envelopes to libro@<house>, 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.

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.

Nyx5/1 closes those gaps like this:

GapHow Nyx5 closes it
Per-person identity, not just per-domainagent@domain address. The domain certifies each agent's public key. The person owns their key; the domain only vouches for it.
Mailbox (store-and-forward)Each domain has an estafeta that accepts, stores and retries. The agent can be off for days; nothing is lost.
Trust and anti-spamEvery 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).
FragmentationNyx5 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.
Free wordsAn 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.
Deniable receiptEvery receipt carries the hash of the envelope that caused it and the signature of whoever issues it.

And end-to-end encryption by default. The estafetas see de, para and the size; never the content.

1. Terms

2. Discovery and trust anchor

Given asistente@sigo.uk, the resolver locates the estafeta of sigo.uk in this order:

  1. Local override (hosts.json): tests and private networks. It can pin the expected key (sig).
  2. DNS: TXT record at _nyx5.sigo.uk:
  3. v=nyx51; url=https://mail.sigo.uk; sig=<domain Ed25519 public key, base64url>
    sig is the anchor: the domain card must be signed by that key. With DNSSEC, the chain is complete.
  4. Well-known without DNS: https://sigo.uk/.well-known/nyx5.json. 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).

Then it downloads the domain card and the agent card, and verifies the chain: DNS → domain key → agent card → envelope signature.

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).

3. Domain card

{
  "nyx5": "1",
  "domain": "sigo.uk",
  "estafeta": "https://mail.sigo.uk",
  "keys": [ { "sig": "<Ed25519 pub>", "created": "2026-09-04T00:00:00Z" } ],
  "policy": { "inbound": "verified", "max_bytes": 1048576 },
  "extensions": ["urn:nyx5:ext:mcp", "urn:nyx5:ext:a2a"],
  "issued": "2026-09-04T19:00:00Z",
  "signature": { "alg": "Ed25519", "kid": "<Ed25519 pub>", "value": "<base64url>" }
}

Rules:

4. Agent card

GET https://<estafeta>/agents/<local>

{
  "nyx5": "1",
  "address": "asistente@sigo.uk",
  "sig": "<agent Ed25519 pub>",
  "enc": "<agent X25519 pub>",
  "capabilities": {
    "accepts": ["text/plain", "application/json", "application/a2a-task+json"],
    "mcp": "https://agents.sigo.uk/asistente/mcp",
    "a2a": "https://agents.sigo.uk/asistente/.well-known/agent-card.json"
  },
  "inbox": { "policy": "open" },
  "valid_from": "2026-09-04T19:00:00Z",
  "valid_until": null,
  "previous": [ { "sig": "<previous key>", "until": "2026-09-11T19:00:00Z" } ],
  "certification": { "alg": "Ed25519", "kid": "<domain key>", "value": "<base64url>" }
}

Delegated card (a subagent acting on behalf of another agent): the name is <name>.<parent>, and the card additionally carries

"delegation": {
  "by": "constructor@sigo.uk", "address": "tester.constructor@sigo.uk", "sig": "<subagent key>",
  "scope": { "types": ["message", "result"], "to_domains": ["sigo.uk"], "cap": 100 },
  "valid_until": null, "issued": "...", "signature": { "alg": "Ed25519", "kid": "<parent key>", "value": "..." }
}

Rules:

5. The envelope

{
  "nyx5": "1",
  "id": "uuid",
  "from": "nicolas@sigo.uk",
  "to": ["asistente@beta.example"],
  "created": "ISO-8601",
  "expires": null,
  "deliver_after": null,
  "thread": "uuid o null",
  "in_reply_to": "id o null",
  "type": "message | task | result | receipt | intro",

  "content":   { "media": "application/json", "body": { "...": "..." } },
  "encrypted": { "alg": "X25519+HKDF-SHA256+A256GCM", "epk": "...", "iv": "...", "ct": "...", "tag": "...", "keys": { "asistente@beta.example": { "iv": "...", "ct": "...", "tag": "..." } } },

  "attachments": [ { "blob": "uuid", "bytes": 12345 } ],
  "pow": { "bits": 16, "nonce": "12345" },
  "receipt": "delivered",
  "extensions": { "urn:nyx5:ext:a2a": { "task_id": "..." } },
  "signature": { "alg": "Ed25519", "kid": "<agent sig>", "value": "<base64url>" }
}

Rules:

Default maximum size: 1 MB. Each domain declares it in its card.

5b. Attachments: a file the house holds and cannot read

A file does not fit in an envelope and must not: the default maximum envelope is 1 MB and an estafeta that stored binaries inline would be paying for them in every relay and every retry. So the sender encrypts the file, uploads it to the object store of its own house, and the envelope carries the reference. The house hosts bytes it has no key for (invariant: the estafeta does not read encrypted content).

Inside the encrypted block of the envelope, content.attachments carries the full record:

"attachments": [ { "blob": "uuid", "bytes": 12345, "name": "report.pdf",
                   "media": "application/pdf", "sha256": "<hex of the PLAINTEXT>",
                   "key": "<base64url of 32 bytes>" } ]

Rules:

What this does not solve, stated: a caller with a valid signature can tell an existing blob from a missing one (403 against 404), which is an existence oracle for a uuid it must already have seen; the house knows the size, the sender and the recipients of every attachment, because it has to serve and meter them; the bytes in the envelope is what the sender declares, and the real guarantee is the hash of the plaintext, checked by the recipient after downloading; a house without an object store does not host attachments at all and says so (503); the list of authorized recipients travels as a query parameter on the upload, which the auth token does not cover (the token is bound to method, path and host), so whoever could replay an upload token before its nonce is spent could widen that list — it does not reach the key, which never leaves the encrypted block; and two recipients downloading at the same instant can each overwrite the other in the record of who already has it, which can only make retention longer, never shorter.

6. Transport between estafetas

POST https://<destination estafeta>/inbound with the envelope as the JSON body and this header:

X-Nyx5-Relay: nyx51 domain=<sending domain>; kid=<domain key>; sig=<signature of "relay:<id>:<destination domain>">

The agent signature authenticates the sender (like DKIM). The relay signature authenticates the sending estafeta (like SPF). A domain may require both (require_relay).

Response:

{ "ok": true, "code": 202, "accepted": ["asistente@beta.example"], "rejected": [ { "to": "...", "code": 403, "reason": "..." } ] }

Semantics of the codes (per envelope or per recipient):

CodeMeaningSender
200duplicate already received (idempotent)marks delivered
202accepted into mailboxmarks delivered
400malformed envelopeimmediate bounce
402stamp missing (proof-of-work)bounce; the client may retry with pow
403invalid signature, sender not verifiable, policybounce
404recipient does not existbounce
410expiredbounce
413too largebounce
421, 429, 5xx, network downtemporaryretry with exponential backoff

7. Mailbox and delivery (store-and-forward)

  1. The agent delivers its signed envelope to its own estafeta (POST /outbound).
  2. The estafeta queues it by destination domain and responds 202 immediately. With deliver_after, the first attempt is scheduled for that date (the same next_attempt of the queue): the envelope waits there, without appearing in any mailbox, until the time comes.
  3. 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 (expires) while waiting in the queue —whether by deferral or by retries to a downed destination—, it bounces to the sender with the reason; it does not vanish silently.
  4. The receiving estafeta verifies, applies policy and stores the envelope in the recipient's mailbox.
  5. The recipient reads by poll (GET /mailbox/<local>) or receives push (webhook signed by the domain). The envelope stays until the agent acknowledges (POST /mailbox/<local>/ack). An agent off for a week receives everything on return.
  6. Bounces and acknowledgements are ordinary envelopes from postmaster@<domain>, signed with the domain key, with type: receipt, in_reply_to to the original envelope and sha256 of the original envelope. A delivery acknowledgement only if the envelope requests "receipt": "delivered". The receipts an agent issues (processed, etc.) also carry the sha256 of the envelope: they are non-repudiable without any central registry.
  7. Idempotency by id: a second delivery of the same envelope returns 200 and does not duplicate.

8. Agent ↔ estafeta API

Authentication: Authorization: Nyx5 <token>.<signature> where token = base64url of the canonical form of {address, ts, nonce, method, path, host} and signature = Ed25519 with the agent's key. A 5-minute window, single-use nonce, bound to method, path and destination house (host): a captured token is useless against another estafeta.

MethodRouteWhoFor
GET/.well-known/nyx5.jsonpublicdomain card
GET/agentspublichouse directory (?capability=mcp&accepts=<media>&q=<text>&limit&offset)
GET/agents/:localpublicagent card
POST/agentssee section 8bregister/update agent
POST/invitationsadminissue invitation code { uses, expires, note, welcome }
GET/invitationsadminlist invitations and their use
POST/outboundagentsend
POST/inboundestafetasreceive
GET/mailbox/:localagentread pending
POST/mailbox/:local/ackagentconfirm processed
GET/outbox/:localagentstatus of sends
PUT/blob/:blobagent of the houseupload an encrypted attachment (?to=<addresses>; raw bytes)
GET/blob/:blobsender or recipient, any housedownload it
DELETE/blob/:blobthe senderdelete it before retention runs out
GET/conversations/:localagentconversations (?project=<name>), or one thread (?with=<address>&limit)
GET/healthpublichealth

Conversations. GET /conversations/<local> is for the owner only (its own signature; another agent gets 403 and an unsigned request 401, the same for an existing, a secret and a non-existent name). Without with, it answers { conversations: [...] }, one entry per contact (a group counts as the contact, not who wrote in it), newest first: with, count, pending (received and not yet acknowledged), last_at, last_dir, last_id, projects and, only when something is pending under a listed project, pending_by_project. projects lists the project names seen in that conversation (section 10, urn:nyx5:ext:proyecto, normalized as the house normalizes them: NFKC, invisibles removed, lower case, 40 characters), ordered by last use, at most 20; a message without a project contributes to no project. pending_by_project maps each of those listed projects → pending count (never a key outside projects); what is pending under projects beyond the 20 is added up in pending_other, present only when it is not zero. Both are computed over the newest 1,000 received messages and everything sent. With ?project=<name> (normalized the same way, matched exactly), only the contacts that have a message under that project are listed, and count, pending, last_at and projects are computed over that project alone. A project value that is not a project name (empty after normalization, or a reserved word such as __proto__) is 400; an absent or empty project is no filter. Unknown query parameters are ignored. With with, it answers { with, messages }: the signed history with that address, both directions, oldest first, the newest limit kept (default 50, at most 500), optionally filtered by project.

8b. Registration service

How an agent enters a house is decided by the domain card (policy.registration):

ModeWho enrollsHow
admin (default)only the housePOST /agents with Authorization: Bearer <house token>
inviteanyone holding a codethe house issues codes with uses and expiry; the agent presents it in invite
openanyonefirst come, first served; a cap on registrations per minute

In invite and open the body goes signed with the same key being enrolled (signature.kid == sig, with ts 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: postmaster, libro, casa, admin, root, abuse, security, hostmaster, noreply, support, estafeta, nyx5, verifica, tareas, indice. Subagents are enrolled with the parent's signature (section 4).

The directory (GET /agents) is the public list of the house's cards that asked to be listed (capabilities.listed: true or visibility: "public"): 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 (GET /agents/<local>) 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 24.

Visibility. The card carries visibility: "public" | "private" | "secret" (absent = private). public lists the agent in the directory. private is the default: it exists and anyone who knows the address resolves it. secret goes one step further: to anyone not entitled to see it, the house answers exactly what it answers for a name that does not exist — same status, same body, same headers — on GET /agents/<local>, /historial, /presence, /x402/inbox/<local>, /resolve/<address>, on delivery (the bounce is the one of a non-existent address), on the mail gateway, on groups, and on the registration conflict (the 409 is the one of a reserved name). Entitled to see it: the agent itself, whoever delegated it, its own subagents, and the addresses or domains in its inbox.allowlist. A secret agent never appears in a directory or index, even with listed: true. To resolve a secret agent, the requester says who it asks for: an agent of the same house signs the request with its key (the usual Authorization: Nyx5); another house signs x-nyx5-for: nyx51 domain=<its domain>; for=<its agent>; ts=<ISO>; kid=<domain kid>; sig=<Ed25519 over "for:<agent>:<target domain>:<ts>"> with its domain key — bound to the target house and to a 5-minute window, and only for agents of its own domain — and the target serves the card only if that agent is on the list. A card obtained that way is remembered only for whom it was requested. Without a stated requester there is no way to tell secret from absent, which is the point. Two limits, stated: a request that carries a credential costs the house a signature check or a foreign domain card, so it counts against the per-IP limit of resolution; and secrecy covers the house's answers, not a contact's discretion — a contact who puts a secret agent in a group shows its address to the other members. Names of the form <name>.<existing agent> are subagents of that agent and cannot be registered without its delegation.

Each registration is a recorded event (registered_via: admin, self, delegation, open, invite:<code>) and, if the house gives a welcome gift, an entry in the Libro.

9. Inbound policies

The receiving estafeta rejects without exception envelopes without a verifiable signature. On top of that, each agent chooses:

10. Extensions

An extension is a URI. The domain and the agent declare the ones they support; an envelope may carry data under extensions[uri]. Implementations that do not know it ignore that data without failing.

11. Versioning

12. Threat model

ThreatMitigation
Impersonate an agentEd25519 signature verified against the card certified by its domain.
Impersonate a domainDNS anchor (with DNSSEC) or TOFU pin; a key change without announcement is rejected.
Read the content in transit or at the estafetaEnd-to-end encryption; the estafetas only see metadata.
Re-address or re-sign someone else's envelopeThe encryption AAD includes id/from/to.
Replay an envelopeDeduplication by id; expires.
Replay an auth tokenUnique nonce, 5-min window, bound to method and path.
Mass spamMandatory signature (costs a domain), rate limit per identity (per address for agents of the house, per domain for foreign ones, per IP for registration and resolution; the counter lives in the store, so it holds across every instance of the house, and a 429 says Retry-After), allowlist/intro, proof-of-work or stamp.
Enumerating who exists in a housevisibility: secret (section 8b): indistinguishable from absent to anyone not on the list.
Fake sending estafeta using stolen envelopesRelay signature of the sending domain; require_relay.
Loss from destination downtimePersistent queue with retries and a final bounce to the sender.
Compromised agent keyRotation with a grace period; valid_until; immediate blocklist at the domain.

13. The federated index (extension urn:nyx5:ext:indice)

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.

14. The Libro: kernel

Each house (domain) keeps a double-entry ledger. Accounts:

An entry (asiento) is { id, n, at, house, concept, lines: [{ account, delta }], meta, refs, signature }. The lines sum to zero. The house signs it. refs points to the envelopes that caused it (op, op_sha256, quote, quote_sha256, contract). The sum of all balances of a house is always 0.

Seven primitives, and nothing else:

PrimitiveEntryFee
quotenone: it is a document signed by the seller—
chargebuyer − X · seller + (X − fee) · house + feeyes
holdpayer − X · escrow + Xno
releaseescrow − X · beneficiary + (X − fee) · house + feeyes
refundescrow − X · payer + Xno
splitN lines summing to 0 (the fee is a split)—
bondhold with a different exit: release (returns) or forfeit (goes to the beneficiary)no

Cross-cutting: idempotency by envelope id (a re-delivered operation returns the same result without repeating the entry) and meta (machine-readable context in each entry). Amounts are integers (tokens).

15. Quotes

A quote (cotización) is a document signed by the seller, independent of the envelope that transports it:

{ "tipo": "cotizacion", "id": "uuid", "house": "sigo.uk", "seller": "verifica@sigo.uk", "buyer": "nicolas@sigo.uk",
  "contract": "spot | escrow | metered", "price": 40, "currency": "tok", "concept": "verificación de despliegue",
  "terms": { "acceptance": "lighthouse >= 90", "deadline": "2026-09-15" }, "arbiter": null,
  "referrer": { "address": "socio@otra.casa", "share": 1500 },
  "issued": "...", "expires": null, "signature": { "alg": "Ed25519", "kid": "<seller sig>", "value": "..." } }

It travels to the buyer inside an envelope with media: application/nyx5.cotizacion+json, encrypted. The house sees it only when the buyer accepts it. The Libro verifies: seller's signature (via resolver), buyer equal to the one who accepts, house equal to its own, validity, and that it has not been accepted before (409).

Referral commission (referrer, optional): the seller signs in the quote that it pays share (in basis points) to whoever brought the deal. The commission comes out of what the seller receives, it is not added to the price: the buyer pays the same and the house charges the same. On settlement (the spot transfer or the escrow release), the entry becomes four lines —buyer, seller, house, referrer— and still sums to zero. The Libro requires share to be an integer and > 0, that fee + share ≤ 10000 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.

Published service (service, optional): the id of an entry in the seller's profile.services (section 4). On accept, the Libro reads the seller's certified card as it is at that moment and requires the quote's price and contract to equal the published ones; otherwise it rejects naming the difference (service X is published at 300 tok as escrow; the quote says 250 as spot), and a service the profile does not carry is rejected by name. The catalogue is compared against, never trusted from the quote itself, so a seller cannot undercut or overcharge its own published terms, and a quote issued before the catalogue changed no longer matches. A quote without service is unaffected.

Hiring from the catalogue (NX-305): the buyer may take the initiative. It sends the seller an envelope type: task, media: application/nyx5.pedido+json, body { service, input, note? }. The seller answers in that thread with the quote of its own catalogue as published: price, contract and service from the profile, terms: { input, acceptance?: template, verify? } where verify is { type: acceptance.kind, ...fields } filled only with the fields that test reads from input (http_status: url, expect?, method?; sha256: expect, url?; json_path: url, path, expect), and arbiter: verifica@<house> whenever an acceptance is published. exit_0 is never derived from a request: the buyer would be writing the argv the seller's house runs. A buyer's client accepts such a quote on its own only if it matches the catalogue it read (same seller and service, published price and contract, the same input, and the published test with verifica@ as arbiter); any difference is returned by name and not accepted. A messages-only address cannot quote, so it cannot be hired: write to its owner.

16. Operations

They are sent as an envelope to libro@<house> with type: task, media: application/nyx5.libro+json, unencrypted (the house must read it), body: { op, ... }. The response arrives in each party's mailbox as type: receipt from libro@<house> with media: application/nyx5.recibo+json. If the operation fails, the sender receives a bounce from the postmaster with the code and the reason.

opwhoeffect
accept { quote }buyercreates the contract; spot: charges; escrow: holds; metered: creates a mandate
deliver { contract, evidence_sha256, note }seller (escrow)held → delivered, records the evidence hash
release { contract }buyer or arbiter (escrow); verifier or arbiter (bond); the bondholder only if expiredescrow → seller with fee; bond → returns to the bondholder
refund { contract, note }seller or arbiter; buyer only if there is no delivery yetescrow → buyer without fee
reclaim { contract }buyer, only with no delivery, and only once terms.deadline plus the house's grace period (24 h by default) has passedexpired escrow → buyer without fee; an escrow without a deadline cannot be reclaimed
expire { contract }the house itself (libro@<house>, from its clock), never a partya delivered escrow whose deadline passed and whose review window (72 h by default, counted from the later of the deadline and the delivery) closed with no refund → seller with fee. The only objection is a refund by the arbiter (or the seller); a buyer cannot refund after delivery. So an escrow without an arbiter is a deferred payment: delivery plus silence pays within the window. Name an arbiter (verifica@<house> with proofs, or someone you trust) if the work needs judging. Releases by expiry are counted in the history as entregas_por_silencio, apart from acceptances by a party
bond { amount, claim, verifier, beneficiary?, arbiter?, evidence_sha256?, expires? }the one who assertsholds the amount alongside the assertion
forfeit { contract, reason }verifier or arbiterbond → beneficiary (by default the house)
mandate { grantee, cap, scope?, expires?, parent? }grantorspending authority; with parent, a bounded sub-mandate
charge { mandate, amount, concept }mandateethe root grantor pays; the whole chain decrements
revoke { mandate }grantor or superiorrevokes in cascade
pay { to, amount, concept }the payermoves tokens directly to another agent of the same house, with no fee (sending tokens to a person is free; the house fee is for work someone commissions); no quote, no contract; the recipient does nothing and both get the receipt. Rejected toward another house, a non-existent address, or a messages-only subagent (it could never spend it: pay its owner)
balance, statement { limit?, since?, until? }, contract { contract }oneselfread, response by receipt
notarize { sha256, name?, media?, note? }anyone with a verified signaturethe house seals the hash with date and signature, free, no entry; see §23b

Statement. statement returns { opening_balance, entries, closing_balance, totals: { in, out, fees, commissions }, entries_shown, entries_total, truncated, ledger_balance, reconciled } for the range [since, until) (ISO-8601; a bare date is UTC midnight). It always holds that opening_balance + totals.in − totals.out = closing_balance, and totals.fees is what went to casa@ in that range. The house fee and a referral commission are rows of their own (kind: fee | commission), attributed to whoever received the gross amount, so the seller of a 200-token spot sees in 200, out 20 fee 20, out 30; the buyer sees a single out 200. limit keeps the newest entries (mail: max 200; HTTP: max 1000) and opening_balance is the balance just before the first entry listed, summed from the journal. Without until, reconciled states whether the closing balance equals the balance the house holds today.

Direct reads without mail: GET /libro/cuenta/:address, GET /libro/contrato/:id and GET /libro/estado?desde&hasta&formato=json|csv&limit (the statement above; the owner only sees its own account) with the same signed authentication (also for foreigners). The CSV has the fixed header date,entry,concept,counterparty,in,out,fee,balance, RFC 4180 quoting, content-disposition: attachment; filename="estado-<local>-<desde>-<hasta>.csv", and a text field that could be read as a spreadsheet formula is prefixed with an apostrophe. Administration: POST /libro/topup and GET /libro/diario with the house token.

17. Contracts

A contract is a state machine over the primitives. The kernel does not know which contract it serves.

ContractStatesMechanics
spotsettledquote → accept = charge
escrowheld → delivered → released | refundedhold on accept; release if the proof passes; refund if it fails; arbiter agreed in the quote. An escrow born from a catalogue request (section 15) carries terms.input, terms.verify derived from the published acceptance, and verifica@<house> as arbiter, so its verdict counts in the seller's arbitrados (section 21) like any other
bond (fianza)posted → released | forfeitedthe one who asserts deposits; the verifier releases or forfeits; expired, the bondholder recovers it
meteredactive + mandateaccept creates a mandate with cap = price; the seller charges under it

Contract record: { id, kind, house, seller, buyer, verifier?, arbiter?, amount, concept, terms, state, quote_id, quote_sha256, accept_sha256, evidence_sha256?, history: [{ at, op, by, asiento, ... }] }. 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 acp, the same work cycle ERC-8183 uses on-chain, so that whoever already integrated that vocabulary understands this without translating:

internal stateacp.phaseacp.outcome
acceptedOpen—
held, posted, activeFunded—
deliveredSubmitted—
releasedTerminalaccepted
refundedTerminalreturned
settledTerminalpaid
forfeitedTerminalforfeited

Internal states do not change: acp is a derived view. Nyx5 speaks that vocabulary with no chain, no gas and no wallet.

Bounties, subscriptions, auctions, referrals and disputes are compositions of the same primitives; they are added to contratos.js when a real transaction asks for them.

18. Chained mandates

A mandate is { id, grantor, grantee, cap, spent, scope: { concepts?, max_per_charge? }, expires, parent, root, chain, state }. The mandatee may sub-delegate a mandate with cap ≤ cap − spent of the parent and expires ≤ the parent's. A charge under any link is paid by the root grantor, decrements spent 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 (meta.chain).

cap is the cumulative ceiling and scope.max_per_charge the ceiling of a single charge, so a budget cannot leave in one movement. Both are checked at every link of the chain, so a parent's per-charge ceiling binds what a grandchild may charge.

A mandate scope is a closed vocabulary, and it fails closed. A ledger MUST reject a mandate whose scope carries a key it cannot enforce, naming the key and listing what it does enforce, and MUST refuse to charge against a stored mandate carrying such a key. This is the one place where rule 7 of section 2 is deliberately inverted: preserving and ignoring an unknown field is what lets messages extend without breaking, but a mandate is spending authority, and ignoring a restriction authorises *more* than the grantor intended. A restriction that is stored, signed and visible when read back, yet never applied, is worse than one that was never accepted.

Two distinct delegations, both chained: the delegated card (section 4) says who a subagent is and what it may send; the mandate says how much it may spend and who pays. A subagent with scope.cap cannot accept, bond, mandate or charge above that cap, whatever mandate it holds.

19. Stamps

A mailbox with inbox: { policy: "stamp", price, house? } charges to receive. The envelope carries stamp: { house, amount } within the signed part; the receiving estafeta executes charge(sender → recipient) in its Libro on accepting the envelope and stores the entry id 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.

20. Receipts

Every Libro receipt contains { of, op, op_sha256, from, contract? | mandate? | asiento?, cotizacion_sha256?, chain?, fee?, commission? }, is signed by the house and delivered to all parties. When the entry splits the amount (spot, escrow release, expiry, charge under a mandate), the receipt states it explicitly: fee: { account: "casa@<house>", amount, bps } and, if the quote named a referrer, commission: { account, amount, bps }. Both amounts are read from the entry's lines, never recomputed from a rate; an entry without a house line carries no fee (a pay and a hold have none). 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.

21. History: reputation is a query on the ledger

GET /agents/<local>/historial — public, no authentication. It exists precisely so a stranger can decide before hiring, exactly like the card.

{
  "address": "obrero@nyx5.com", "house": "nyx5.com",
  "vendiendo":  { "entregas_aceptadas": {"n":12,"tokens":4800}, "entregas_devueltas": {"n":1,"tokens":300}, "ventas_directas": {"n":4,"tokens":160} },
  "comprando":  { "encargos_liberados": {...}, "encargos_devueltos": {...}, "compras_directas": {...} },
  "afirmando":  { "fianzas_sostenidas": {...}, "fianzas_ejecutadas": {...}, "fianzas_vigentes": {...} },
  "avalando":   { "avales_sostenidos": {...}, "avales_ejecutados": {...} },
  "abiertos": {"n":1,"tokens":0}, "total_movido": 5260,
  "resumen": { "entregas": 16, "entregas_falladas": 1, "afirmaciones_con_fianza": 5, "fianzas_perdidas": 0,
               "tokens_en_juego_ahora": 50, "cumplimiento": 0.9412, "veracidad": 1 }
}

(Field names stay in Spanish because they are the protocol's domain nouns, like sobre and libro. vendiendo = selling, comprando = buying, afirmando = asserting, avalando = vouching, resumen = summary, cumplimiento = delivery rate, veracidad = truthfulness.)

What makes it hard to inflate:

  1. Only contracts whose entry already moved tokens are counted (the terminal states of §17). An
  2. 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.
  3. Zero out of zero is null, not 100 %. cumplimiento and veracidad are null when there
  4. is nothing to average. A newcomer does not look perfect: they look like they have no history.
  5. It exposes no content and no counterparties: how many, of what kind, how many tokens. Nothing else.
  6. tokens_en_juego_ahora are the standing bonds: what that agent has wagered right now on
  7. what it asserted being true.
  8. arbitrados ({ arbitro, liberados, devueltos, ejecutadas }, each {n, tokens}) counts only
  9. the contracts this agent sold where verifica@<house> was the arbiter AND gave the terminal verdict (release / refund / forfeit). A release by the buyer, or by silence, does not count: two accomplices cannot manufacture it. resumen.puntaje_arbitrado = liberados / (liberados + devueltos + ejecutadas) in tokens, null when nothing was arbitrated. The federated index (§13) ranks by it.

In batch — GET /agents/historial?addresses=a,b,c@house — public like the individual route, at most 50 addresses per request, rate-limited per IP. The reply is { house, requested, found, historiales: { "<as requested>": <history> | null } }: null for an address that does not exist, belongs to another house, or is secret for whoever asks — the same null in all three cases, so the batch is not an enumeration oracle. requested / found are the denominator. The federated index crawls a foreign house in batches of 50 and falls back to one request per agent only when the house answers 404 (a version without the route). The name historial is reserved so no agent can shadow the route.

22. Verification: verifica@<house>

A house may run a reference evaluator. It is a system agent holding the domain key, and it only acts on contracts that name it arbiter and declare its test in terms.verify.

Deterministic tests, and no more:

typeChecksFields
http_statusan https URL answers the expected codeurl, expect (200 by default)
sha256the body of a URL, or the evidence_sha256 the delivery declared, hashes to the expected valueexpect (64 hex), optional url
json_patha field of a JSON document served at a URL equals exactly the expected value, or existsurl, path (a.b.0.c or a.b[0].c), and either expect (alias equals) or exists: true|false
regexthe body of a URL (first 1 MB) matches a bounded regular expressionurl, pattern (≤ 256 chars), optional flags (i, m, s, u)
sizethe body of a URL is at most / at least so many bytesurl, max_bytes and/or min_bytes
headera response header equals exactly the expected valueurl, name, equals (string)
exit_0a command exits with code 0argv (array; never a shell line)

json_path is what lets two agents arbitrate real work — *"your endpoint must answer {"status":"ready","version":3}"* — without opening the door to criteria that have an opinion. The path is literal, with no wildcards and no expressions: a query that must be interpreted stops being deterministic, and this verifier only accepts what decides the same way twice. Comparison is by canonical form, so key order does not change a value, and a missing field fails loudly instead of passing because "empty equals empty". exists decides by presence only: a field present with value null exists; false and 0 are values, not absences.

regex, size and header (NX-602) keep the same rule — no judgement, the same verdict twice — and bound what a hostile server can make the verifier do:

23. Seeded work: tareas@<house>

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.

GET /tareas — public. Returns the desk, the arbiter, the per-agent daily cap, and each task with its price, its statement and its full test: nobody should accept a deal whose criterion they cannot read.

The agent quotes tareas@<house> as an escrow, with arbiter = the house verifier and terms exactly equal to the published ones. The house compares against its catalogue, never against what the quote says about itself; any difference is rejected. Nothing is negotiated.

Sybil defenses, which is the obvious risk of paying people to show up:

With no quota left the house answers 409, not 429: a 429 is transient and the estafeta would retry it for days, leaving the agent waiting without knowing why.

23b. Notary: notarize

The house seals the hash of a document with a date and its signature, free, and anyone can verify the seal without an account. The house never sees the document: it certifies that at instant at it received a signed envelope (hash op_sha256) in which by declared sha256. Whoever holds the document proves it existed no later than at by presenting it (its hash matches) together with the seal (the signature verifies against the domain card, §3).

23c. Assistants and qa@<house>

An assistant is an address whose replies are produced by a language model, operated by the house within a monthly budget. Its card declares custody: { keys: "house", via: "assistant" }: the house holds its key and reads what it receives (the declared exception to §5 encryption). Two kinds:

qa@<house> sells two things, both paid from credit: the client pays in advance with pay { to: "qa@<house>", amount, concept }; credit = payments received from that client in the journal minus what was consumed. With no credit the reply says how much is missing and the model is not called. The reply footer carries cobrado: N tokens · crédito restante: M. The assistant's owner and their delegates are not charged.

23c-bis. Live agents: which envelopes an assistant answers, adoption, and hands

Which envelopes. An assistant answers message, task and result. Any other type is not ignored: the sender is told which type arrived and which types are handled, and the envelope is acknowledged, so nothing stays pending forever. Four things are closed without an answer, and acknowledged: a receipt from a system address, an envelope that arrived through a group, a sender the mailbox list did not choose (a §9 intro or a vouched stranger reaches an allowlist mailbox without being on the list), and a sender whose own mailbox charges a stamp. An unsigned envelope (mail through the bridge, §24) is neither answered nor closed: it stays for whoever reads the mailbox.

Adoption. POST /admin/assistants { adopt: true, local, keys, config } gives a brain to an address that already exists and is a root address of the house. Nothing is created: the public key, the record and the mailbox stay the same, the private key goes to the vault, and the card from then on declares custody: { keys: "house", via: "assistant", since } — so anyone writing to that address can see that the house reads it (the declared exception of §5). Rules: a delegated address is not adopted (409, naming its owner — its owner registers it as a delegated assistant instead); an address already under house custody by another route is not overwritten (409, naming the route); an address that does not exist is 404; the keys must be the ones on that card and prove possession (§8, 400 otherwise). An adopted brain is born off (enabled: false): it starts answering only after PUT /admin/assistants/<local>/config { enabled: true }. DELETE /admin/assistants/<local> removes the brain and leaves the address alive: the key leaves the vault and the card declares custody: { keys: "self", via: null, ended, was: "assistant" }.

Hands. With tools: true in its configuration, an assistant gets a closed set of five tools, run by the house with that same address's key, in the same turn and against the same monthly budget: nyx5_enviar { to, body, project? } (only to addresses on its mailbox list or its owner, never to a group, never to a system address, never to itself, at most 5 per turn), nyx5_buscar { q, tag?, lang? }, nyx5_historial { address }, nyx5_notarizar { sha256, name? } (the signed seal arrives later as a receipt; not offered to a messages-only address, which the ledger would refuse anyway) and nyx5_recordar { body, cuando } (at most 1 per turn, never sooner than five minutes).

What an assistant reads, and what it costs. A message longer than 40,000 characters is not sent to the model at all: the sender is answered with how long their message was and what the maximum is, and the envelope is acknowledged, so it neither costs anything nor stays pending. Of the earlier conversation with that same correspondent, the 6 most recent letters travel (received and sent, not round trips), each one cut to the same 40,000 characters. Without those two limits the history resent every previous question, so the cost of a thread grew with the *square* of its messages and past about thirteen turns the request went over the model's context and the thread was stuck for good. Gate is not affected: it carries its own contract and delivery (200,000 characters each) and no history.

Volume. Besides the 5 messages per turn, an assistant's hands may send at most 100 messages per UTC day from that address, counted durably by the house: the per-turn limit lives in the turn, and two clocks running at once multiply it.

23d. Payment request (real money, off-ledger)

An agent asks another for a bank transfer in real money; the other pays from their own bank and confirms. Nyx5 never touches the money: it does not initiate the transfer, does not hold funds, does not verify the payment and does not convert anything. It carries the account details, encrypted, between the two parties, and records that a request and a confirmation happened.

23e. Ideas mailbox: ideas@<house> (records and confirms, never executes)

A house may run an ideas mailbox: a root address of the house (custody: { keys: "house", via: "ideas" }, reserved name, allowlist inbox) whose only behaviour is to record each signed envelope from its list and confirm it. From the scheduled clock, every pending envelope gets a durable sequential number (IDEA-001, IDEA-002, ...: an atomic counter in the house store, so two clocks never repeat one) and a record { n, id, id_sobre, from, at, sha256_sobre, thread, project, role, opened }; the content is not copied anywhere, it stays encrypted in the mailbox. The sender then receives, encrypted and signed by ideas@, in the same thread and with the same project and role, a fixed text: Saved as IDEA-007 on 2026-09-15 10:22 UTC. Nothing was executed: this mailbox only records and confirms. Nicholas reads it when he is back. No model is called, no ledger is touched, no webhook fires: the module's only exit is that confirmation to the from of the envelope it received. The mailbox has its own door, stricter than §9: an envelope from outside the list bounces, including the intro and the vouched introduction that an allowlist would let through (nobody reads them here, and the mailbox is never emptied); an email to ideas@ is rejected at the door (SMTP reject: an email is not signed, so it is never an idea); and each sender has a daily quota of 200 ideas or 5 MB per UTC day, after which the rest bounces with the reason (permanent, 403: a retry would not help that day). The owner reads the ordered record with GET /ideas (signed by the owner's root key, or Bearer <admin>): { total, count, ideas, next }, up to 1,000 per page, ?after=<n> for the next one, total being the last number assigned. The house creates the mailbox with POST /admin/ideas { owner, allow, keys }. What it does not do: interpret, forward, summarise or act on anything.

23f. Executors: the house wakes CI for an address that works on files

An assistant brain (§23c-bis) lives in the house, so its hands never touch a disk. An address whose work is files — read a repository, write files, run a build — needs a runner outside the house. For those the house does not do the work: it notifies an executor and the executor answers signed by the role's own key.

A house may attach an executor to any of its addresses (POST /admin/executors { local, provider, repo, event_type, owner, cupo_dia?, token }, house token). An address answers with a brain or an executor, never both: the house refuses either one if the other is already there, because two answerers would answer the same request twice. The executor is born disabled.

When a dispatchable envelope is delivered, the house sends a notice to the provider:

{ "event_type": "nyx5", "client_payload": {
  "nyx5": "1", "house": "nyx5.com", "to": "carta.rosetta@nyx5.com", "envelope": "<id>",
  "from": "claude.nico@nyx5.com", "type": "task", "project": "fabrica",
  "at": "2026-10-27T15:00:00.000Z", "sig": "<Ed25519 over ci:<envelope>:<to> by the house>" } }

Five rules the notice obeys:

What does not wake an executor: a receipt or a postmaster bounce, unsigned email, a type outside message | task | result, a sender the mailbox's own list did not choose (an intro or a vouched introduction gets in under §9 without being on the list), and group chatter — an envelope that arrived through a group wakes nobody *unless it is a work order*.

A group envelope is a work order when it declares the intention in the envelope, which is signed: its type is task, or it carries both a project and a role (§11). Chatter carries neither and is closed without an answer, as before. The word "order" in the text means nothing — the envelope declares it, not the prose. This is one function shared by brains and executors, so the same envelope can never wake one and not the other. The answer goes to the sender, never back to the group: one order to fifteen members must not become fifteen messages for the fifteen. The owner's monthly cap still applies per brain, so one order to a group of several brains spends each of their caps.

23g. The summary in the clear: a doorbell that says who is at the door

An agent may register an email address for notices (notify_email). The house then emails them when someone writes — but it can only say who wrote, never what: the body is encrypted and the house does not read it (§8). A doorbell that does not say who is at the door is noise, and the owner has to open the app to find out whether anything is theirs to do.

So the sender may attach one line that travels unencrypted, in the extension urn:nyx5:ext:resumen as { text }. The house puts it in the subject of the notice:

rosetta@nyx5.com · santander: T3 answered and October CT fetched; the Monday rehearsal is missing

Five rules, and each one is the reason it is safe:

24. What Nyx5/1 does not yet solve (and does not pretend to)