Architecture
Two contracts, one browser client, no server. Why the split exists, what each component is allowed to know, and why messages live in logs rather than storage.
The system is deliberately small. There are two contracts, a static client, and nothing else. There is no backend, no database, no indexer that must stay online for the product to function, and no operator key that can move a user's name or read a user's mail.
Component responsibilities
| Component | Owns | Can never |
|---|---|---|
MailRegistry |
Names, wallet bindings, published X25519 public keys, key versions, name epochs, reservations | Read mail, or reassign a name without the owner acting |
MailBox |
Message emission, stamp escrow, allow and block lists, credit balances | Decrypt an envelope, or hold a message hostage to a payment |
| Browser client | Key derivation, sealing, opening, log scanning, all plaintext | Persist a private key anywhere |
| Chain | Ordering, timestamps, permanence, public verifiability | Learn message contents |
Why identity and transport are separate contracts
Splitting them keeps each one auditable on its own terms and lets transport
be replaced without touching identity. MailBox holds an immutable
reference to the registry and calls exactly one method on it,
isRegistered(address). That narrow interface is the entire coupling
between the two.
interface IRegistry {
function isRegistered(address wallet) external view returns (bool);
}
The practical consequence: a future MailBox with stealth
addressing or a different escrow model can be deployed alongside the current one
and share the same identities. Names and keys do not have to migrate.
The storage decision
This is the choice the whole cost model rests on. Contract storage costs 20,000 gas for each 32-byte slot written. Event log data costs roughly 8 gas per byte. For a payload measured in kilobytes the difference is three orders of magnitude.
What is stored on chain
Only the small, structural facts live in storage. Everything large lives in logs.
| In contract storage | Why it must be |
|---|---|
ownerOfName, accounts | Resolution must be callable by other contracts and by view calls |
keyVersionOf, keyHistory | Old mail must stay decryptable after a rotation |
nameEpoch | Clients need to detect that a name changed hands |
stamps, credits | Escrow is money; it needs authoritative state |
stampPrice, allowedUntil, blockedSender | Enforced at send time, so the contract must read them |
| Message bodies are not in this list, and never will be. | |
Two chains, one identity
The design targets BSC and opBNB together. The mailbox key derivation
deliberately omits chainId from its EIP-712 domain and uses a fixed
salt for domain separation instead, so one signature yields the same mailbox
identity on both networks.
A name registered on BSC does not automatically exist on opBNB. Whether the registry is mirrored, bridged, or made canonical on one chain is one of the launch-gating decisions recorded on the roadmap.
Client architecture
The app is deliberately dependency-light and ships no build output. It is
served under a strict Content Security Policy with connect-src 'none',
which is possible because all chain communication goes through the injected
wallet provider by message passing rather than through page-originated network
requests.
Cryptography
HKDF derivation, sealing, opening, peer-key screening, payload validation. Holds the only plaintext in the system.
Chain access
Provider setup, network switching, contract calls, and log filtering by topic.
Interface
Views, composition, list rendering. Never touches key material directly.
Deployment
Per-network chain IDs, contract addresses, explorers. The only file that changes between deployments.
Failure modes by design
| If this fails | What still works |
|---|---|
| The website goes offline | Everything. Any client that speaks the contract ABI can read and send. |
| The project is abandoned | Names remain owned, mail remains readable, contracts keep running. |
| An RPC provider blocks you | Point the wallet at any other node. |
| A payout transfer reverts | The amount is credited for the owner to pull later; the message was already emitted. |
| You lose your wallet | You lose the mailbox. This is stated plainly rather than papered over. |