HERMAI.IO / A GRAINWORK PROJECT Local alpha

HERMAI

An agent that can't spam you.

Hermai gives an AI agent a signed permit: who is accountable for it, how many messages it can send, to whom, and until when. The rail checks the permit and charges for the send before the message exists, so a message can only reach you if the permit allowed it.

A herma was the stone the Greeks set at a property line. It marked the limit, and it protected whoever passed it.

For recipients

You're getting more agent mail every week, and no way to sort it.

An agent's message looks exactly like a person's, or exactly like spam, and you find out which one it was afterward. You have filters, rate limits and allowlists, and none of them tell you the one thing that matters: what ceiling the thing that just wrote to you is operating under. So the choice collapses to blocking broadly, which costs real customers and partners, or absorbing it.

On Hermai a message only exists because a permit allowed it and a unit of postage paid for it. The arithmetic ran before it was sent, so you're not left guessing at intent. Today that check happens at the rail, not in your hands, and the limits section says exactly what that costs you.

Accountable

One key signed it, and it pays

The permit is signed with the operator's own key, resolved from the directory, not from whoever hands it to you, and it's that operator's postage that gets spent. Binding a key to a legal name is enrollment admission, and that isn't built.

Mechanism

An Ed25519 signature over a canonical-JSON payload under the domain separator postage.agent.warrant.v1, using the same primitive as recipient-issued consent. The verifier resolves the operator's root key by identity from the directory; a key supplied alongside the permit is never trusted.

Verification fails closed. A permit whose issuing operator holds no durable record of it is treated as revoked, so possession of a copy grants nothing. Expiry is mandatory and capped at 90 days, which bounds the damage from a lost operator key.

Enforced

The ceiling can't be exceeded

The send counter is written in the same database transaction as the payment, not in a policy check that runs first. No crash, retry or race gets a fifth message out of a permit for four.

Mechanism

The meter increment is emitted by the same durable event as the postage hold and committed through one multi-stream executeMany inside a single SQLite BEGIN IMMEDIATE. There is no compensating write and no second transaction to lose, so a process killed between the two is not a reachable state.

Retries are exactly-once by construction: a replayed send returns the first response and consumes nothing further. The window is rolling and computed from durable spend timestamps, so a burst at 23:59 plus one at 00:01 cannot double a daily limit.

Proven by crash injection at every hook on the send path, and separately by a full close and reopen of the database mid-flight, asserting meter and wallet agree.
Conserved

More agents doesn't mean more mail

An agent holds no postage of its own and has no account with the issuer. Spin up a thousand and there's still one wallet, still the same size. That's measured by test, not asserted.

Mechanism

An agent account is never given a principal at the allowance issuer. Not a principal with a zero balance: no principal at all, because the issuer accrues eligibility from the moment a principal is opened regardless of what was ever issued to it. There is exactly one code path that opens one, and it is guarded on account kind.

Agents spend from the operator's wallet through that operator's principal, so conservation is enforced per grant exactly as it is for a human sender. Every unit an agent spends is a unit the operator no longer has.

Proven by opening fifty agents under one operator, simulating a month of accrual, and asserting the issuer's state is identical to a devnet that never had an agent.
Revocable

Shut off one agent, keep the company

Consent is issued per agent, signed by you, and revocable at any time. You keep the vendor relationship and lose the bot.

Mechanism

Consent binds the agent's own principal, not its operator's, so an agent cannot inherit permission you gave the company or a sibling agent. Revocation is a durable flag on your side that verification consults every time.

Revoking an operator's permit is separate and stronger. In the same transaction as the revocation it stops the next send everywhere and disables the agent's API key, so a credential cannot outlive its authority. A credential also dies on ordinary expiry, not only on revocation: an expired permit means the agent's key stops authenticating at all, not just for sending. A running batch is then cancelled and its unspent postage returns. A message that holds postage but has not reached custody is stopped immediately, and its unit comes back when the reservation window closes, not instantly. A message already handed to custody proceeds to its own terminal outcome and cannot be recalled.

The refusal

A refusal that tells the agent exactly when to come back.

This agent's operator gave it three sends a day. Here is the fourth, from a running system, abridged. The real object also carries a budget footer and a six-field details block; npm run v2:agent-demo prints all of it.

ok: false
code: "WARRANT_WINDOW_EXHAUSTED"
message: "This agent has used its whole send
  allowance for the current window. The
  warrant allows 3 per window and 3 are in
  the current window. The next unit frees at
  2026-08-29T04:40:07.911Z. 3 remain on the
  lifetime budget. Waiting is what clears
  this one."
remedies: ["wait_until",
  "ask_operator_to_raise_limit"]
Permit issued3
First message2
Second message1
Third message0
Fourth attemptrefused

A rate limit says go away. This says come back at 4:40, and here's who could raise it. It's built so a retry loop has somewhere to go instead of a wall.

The refusal contract

Refusals are typed so a caller can branch without parsing prose. 429 with a Retry-After for a window ceiling. 409 for a ceiling time will never clear, such as a spent lifetime budget or an empty operator wallet. 403 for an authority answer: expired, revoked, missing, or a recipient the permit doesn't cover. The status is a coarse signal; the body carries the distinction that matters.

Details carry the exact numbers, including the permit's own expiry, so a caller can tell a wait that ends in a unit from a wait that ends in a dead permit. When the next unit would free after the permit expires, the refusal stops offering the wait and says a new permit is the only thing that clears it.

For agent builders

Your agent is about to get blocked by default.

On 15 September Cloudflare starts blocking AI crawlers by default for new sites and every free customer, and is building a payment path for AI access. Inboxes have the same problem and fewer options, so they'll follow.

Reputation won't save you. It's slow, it's owned by whoever runs it, and a brand new agent has none of it by definition. A permit is faster than a reputation, and it's portable. You write your own limits, the rail enforces them so your word isn't part of the transaction, and the recipient verifies in one step.

You set the number. Three a day or three hundred. The point was never that the number is small. The point is that the number is real.

Be refused by a real permit in ten minutes. Hermai ships a dependency-free MCP server, so an agent on your own machine can hold a permit, spend it, and hit the ceiling:

npm run v2:agent-demo        # the whole loop, including the refusal
HERMAI_API_KEY=<key> npm run v2:mcp -- \
  --api-url http://127.0.0.1:8787 --account <agentAccountId>

The permit

The terms a recipient can read.

One signed document. The operator's root key signs it, at most one is live per agent, and reissuing revokes its predecessor.

postage.agent.warrant.v1, signed payload
scope
"agent-send-authority"
operatorIdentityId
who signed, and whose postage pays
agentIdentityId
one agent, named
limits.unitsPerWindow
how many sends per window
limits.windowMs
the window, rolling, never a calendar day
limits.totalUnits
a lifetime ceiling no waiting clears
limits.maxRecipientsPerSend
the widest a single batch may be
recipientPolicy
an allowlist, the recipient's own consent, or both
expiresAtMs
mandatory and bounded; permits die on their own
redelegable
false. An agent can never issue one.

Limits

What Hermai doesn't claim.

Better you hear this from us than find it yourself.

  • A permit bounds a delegated agent and the blast radius of a leaked agent key. It does not bound a determined operator who opens more accounts. Closing that is enrollment admission at the gateway, and it isn't built yet.
  • Postage buys admission, never attention. Nothing here improves the odds of being read, and a recipient can never see what a sender spent.
  • A receipt proves a device signed for the ciphertext. It is not evidence that anyone read anything.
  • Revoking a permit stops the next message and returns unspent postage. A message already handed to custody can't be recalled.
  • There's no token, no yield, no payout, and nothing to buy that moves anyone up a queue.
  • The permit doesn't travel with the message yet. The rail checks it at send time; nothing on the wire lets you verify it for yourself after the fact. A message carries its sender's identity, not its operator's permit. The wire profile that would let you check one directly is named and not built, and until it is, the recipient's assurance is admission rather than inspection.
  • Hermai is its own transport with its own directory, not a filter in front of the inbox you already have. Both sides run it, and there's no SMTP bridge today.

Status

A working local alpha. Not a hosted service yet.

Hermai runs today as a durable local devnet: an allowance issuer, a postage ledger that only ever sees the message as an opaque commitment, four relay roles that must agree three of four before a message is admitted, and a dependency-free MCP server, so an agent on your own machine can hold a permit and be refused by it.

Test suite
281 passing
Dependencies
zero
Networking
loopback only
Hosted gateway
not yet

Three things land before any of it is hosted, and they're named rather than hand waved: per-account authentication instead of one development token, a key custody split so no server holds your identity keys, and enrollment admission so an account isn't a free mint. The rail gets hosted. The mailbox stays yours.

If you're on the receiving end of more agent mail than you can sort, or you're building an agent that's about to get blocked, we want to hear which one you are: [email protected].

For auditors

The whole mechanism, if you're the one who has to check it.

If you're the engineer or security reviewer being asked to let agent mail through, everything above was the summary. This is the detail.

How a message is actually admitted

Roles are separated and separately persisted: a directory of key-derived identities, an allowance issuer, a postage ledger, a custody coordinator, four independently keyed relay roles, and the client's own store.

The sender encrypts to every active recipient device, then signs a holder authorization binding one grant to one exact envelope commitment, storage class and lease deadline. The ledger reserves the grant, relays persist the ciphertext and sign a deterministic custody intent, and three of four votes are required to admit. The ledger then irreversibly commits the grant, and three custody votes make the envelope visible to the recipient's devices.

The recipient writes its local archive before its signed receipt enters a durable outbox, so a crash cannot acknowledge a message that was never stored. Delivery and expiry are mutually exclusive through a write-once coordinator proposal plus a durable per-relay lock: exactly one terminal outcome is ever certified.

What the ledger and issuer can see

The postage ledger receives a grant, a holder signature, a spend sequence and an opaque envelope commitment. It does not receive the sender, the recipient, or the ciphertext, and the permit's recipient rules are deliberately never given to it. That separation is why recipient ceilings are enforced client-side and postage ceilings are enforced where scarcity is provable.

A recipient cannot see what a sender spent. The requests projection is narrowed to the fields the client declares, so postage detail does not reach the receiving side at all.

What is proven, and by what

281 tests, zero npm dependencies in the protocol implementation, and a coverage run at 97.5% of lines. Beyond the happy paths: seeded property fuzzing over the canonical encoding and the envelope security properties, an injected-crash matrix over every fault hook, and restart tests that close and reopen every database mid-flight.

Two independent adversarial reviews have been run against this code by models that did not write it, with every finding either fixed and pinned by a regression test or recorded with its reasoning. Conservation held: no probe exceeded a ceiling anywhere. Two authority defects were found, a credential outliving its permit's expiry and an interrupted revocation revived by a reissue, and both are fixed and pinned. The rest were postage being stranded rather than ceilings being broken.

What would make this untrue

The four relay roles run in one process today. Their signatures model the protocol's evidence correctly, but one host is not four independent fault domains, and hosting does not change that. Nothing here is Byzantine consensus and no part of the page should be read as claiming it.

The local development vault stores account keys in plaintext and the development gateway uses a single bearer token. Neither is fit to host, which is why the hosted split keeps the mailbox and its keys on your side and hosts only the admission rail.

A tombstone records a certified local terminal decision. It is not proof that any operator erased their copy, and no assertion in this system is described as proof of deletion.