The Relay Mesh, Explained
How paid subscriptions and signed contacts move your encrypted mail across Undertown — even when nobody is online.
1. The offline problem
Every messaging system has to answer one awkward question: what happens when you hit send and the recipient isn't there? Centralized services answer it with an inbox — a server owned by a company that stores your messages, reads their metadata, and can be subpoenaed, breached, or shut down.
Undertown answers it differently. Instead of one company's server, there is a mesh of independent relays — ordinary peers running the same software as you. A relay holds your mail while you're offline, passes it along, and forgets it. But a relay that stores anything for you is a trust problem of its own. Who runs it? Why would they? What stops them from lying about where your messages go?
Our answer has three parts: signed contacts (identity nobody can forge), paid subscriptions (relays with skin in the game, coordinated by a smart contract), and a paranoid delivery protocol that verifies every hop cryptographically. This post walks through all three.
2. The Contact: a signed business card
In Undertown you have no username, no email, no phone number. Your identity is a
Contact — a small document that says who you are (a peer ID
derived from your Ed25519 key), how to reach you (a direct address, a relay
address, or both), how to encrypt to you (an X25519 key), and — since the
subscription system landed — which paid relay networks you belong to.
What makes it more than a JSON blob is the signature. The contact is serialized into a deterministic canonical message and signed by your wallet key — the same keccak256/ECDSA scheme Ethereum uses:
I create Undertown Contact for wallet 0xf39f… peer_id:12D3KooW… name:Alice ed25519:9d61b19d… x25519:37a7c4e0… direct:/ip4/1.2.3.4/tcp/9000 relay:/ip4/relay.example/tcp/9000 subscriptions:31337:0x5FbDB231…,1:0x8464135c… created:1760000000 expires:1771000000
Anyone who receives this document can reconstruct that exact string, recover the signer's address from the 65-byte signature, and check it matches the claimed wallet. No certificate authority, no server-side account — your identity is a statement your own wallet key makes about itself. Contacts expire (four months by default) and every meaningful change — a new relay address, a new subscription — requires a fresh signature.
The subscriptions: line is the newest part, and it's the bridge to the rest of
this post. Each entry names a smart contract, on a specific chain, that governs one of your
paid relay memberships. Because the list lives inside the signed document, nobody
can claim you belong to a network you never joined without invalidating your signature.
3. Paying for postage: the subscription contracts
Running a relay costs bandwidth and disk. Undertown makes that cost explicit instead of hiding it in someone's terms of service. Two contract types do the accounting:
- The Subscription contract is the network's registry. It tracks, per user, which relay they've paid and until when. It also lists the relays allowed to operate — relays register themselves here and pay their own registration fee. Admins can block bad actors; a price feed converts everything from USD to native currency.
-
The RelayNode contract is each relay's personal record: its owner, its
per-period fee, whether it accepts new users, and a
contactCode— a pointer to the relay's own signed Contact, published on-chain so clients can discover its network addresses without anyone running a phonebook.
Fees are set in USD and quoted at send time. If the contract uses a Chainlink price feed,
the client divides through the feed's 8-decimal answer; if the operator pinned a manual
price, it multiplies instead. The client's quoting code mirrors the contract's arithmetic
branch-for-branch — with checked math that overflows the same way Solidity does
— and every payable operation is simulated with eth_call before any gas
is spent, so you see "Insufficient payment" in a dialog instead of in a block
explorer.
From the app's Relays & Subscriptions settings tab, joining a network is
three clicks: browse the relays registered on a chain, pick one, confirm the quoted fee.
Renewals extend your expiry; removals release your slot. AI agents can do the same over
the built-in MCP server (subscription.quote_add, subscription.add,
relay.get_info, …).
4. Life of a message
Suppose Alice sends a direct message to Bob, who is offline. Stripping away the encryption layers (every payload below is X25519-encrypted ciphertext before it leaves her device), the journey looks like this:
The interesting parts are steps 2 and 3. How does Relay A even know Bob uses Relay B? And why should Relay B believe a connection claiming to carry Alice's mail?
5. Finding the right relay — the registry, not the contact book
Relay A resolves Bob's relay by asking the blockchain, not any user's address
book: the Subscription contract answers getSubscriptions(Bob), the returned
RelayNode's contactCode yields Bob's signed contact entry for that relay, and
the signature is verified before the multiaddrs inside are trusted. User contact files are
never consulted for routing — there's an executable test proving that a contact book
stuffed with plausible addresses still resolves to nothing. The registry is the single
source of truth, and it's one nobody can rewrite.
6. The three-factor handshake
Before one relay hands another a batch of ciphertext, the receiving relay runs a challenge protocol with three independent factors. Fail any of them and the connection is dropped:
- The sender's signature. Alice's client signed a routing authorization at push time — a document listing the exact posts (by ID and clock), the two relays, both users, and an expiry about an hour out. Relay B verifies it recovers to Alice's wallet and that the transfer's contents are a subset of what she signed. An expired or mismatched batch is dead on arrival.
- The blockchain's word. Relay B checks on-chain that Relay A is a registered relay, that Alice actually subscribes to Relay A, and — critically — that Bob subscribes to Relay B itself, using Relay B's own tracked contracts. A relay can't be tricked into holding mail for a network it doesn't serve, and contract-confusion games fail by construction.
-
The operator challenge. Relay B hands Relay A a fresh 32-byte nonce and
demands it be signed with the private key of Relay A's contract owner. Relay B
recovers the signature against a live
owner()read from Relay A's RelayNode contract — so even a stolen client identity can't impersonate a relay without the operator's key, and an ownership change mid-handshake is caught immediately.
Clients earn their relay's trust the same way, in miniature: to register, a client answers a challenge nonce signed with the wallet key, and the relay confirms the subscription on-chain before adding the client to its verified set. Verifications are short-lived (ten minutes) and re-earned by re-handshake. Five failures in a sliding ten-minute window and the source is rate-limited. One transfer per connection — relays dial each other for a single delivery, then hang up.
7. Failure is the normal case
Offline delivery means relays fail constantly: peers vanish, chains lag, transfers get cut mid-flight. The routing queue is built for it:
| Situation | Behavior |
|---|---|
| Relay B unreachable | Retry with exponential backoff (30 s doubling, capped at 1 h) |
| Keeps failing | Dead-lettered after 5 attempts or 24 h in queue |
| Big batch | Chunked — 64 posts / 256 KiB per connection, each chunk covered by the same signed authorization |
| Second message arrives | Merged into the queue as its own signed group; already-queued posts are never invalidated |
| Same post delivered twice | Deduplicated by ID — the re-transfer is acknowledged as dup, a no-op |
| Relay restarts | The queue is in-memory only; your client re-pushes, re-signs, and the cycle repeats |
Every failure is silent to the sender until it matters: your message sits encrypted in a queue, retries on a schedule, and the only thing that ever gives up is the relay — after your signature has long expired. Nothing is delivered without a valid, un-expired, per-batch authorization.
8. What a relay can't do
- Read your mail. Relays only ever hold
enc:-prefixed ciphertext; decryption keys never leave the endpoints. Live end-to-end tests assert the relay's database contains zero plaintext rows. - Redirect your mail. Destinations come from the on-chain registry only, and each batch must be covered by the sender's own signature over those exact posts.
- Impersonate a relay. Registration and the operator challenge both anchor to keys whose ownership is provable on-chain.
- Accumulate dossiers. Queues are transient, contacts are never used for routing, and a relay's database holds ciphertext plus routing clocks — nothing else.
The honest caveat: a relay operator always sees metadata — encrypted blob sizes, timing, which subscription contracts you use. Undertown's threat model removes content and identity from the relay's reach; it does not pretend relays are blind to the fact that traffic exists. Picking a relay you trust, or running your own, is a feature — that's why there's a market.
9. Try it
In the app, open Settings → Relays & Subscriptions: browse the relays registered on a chain, subscribe with a quoted fee, and track the contract so your contacts (and any AI agents you connect via MCP) discover the network automatically. Operators deploy their own RelayNode contract, set a fee, and register — the mesh grows one operator at a time.
Under the hood this is all ordinary Rust: libp2p/QUIC for transport, alloy for the chain, and a lot of paranoia in between. The contracts are open source, and so is the discipline — every protocol claim in this post is pinned by an executable test, including the ones about what relays cannot do.