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:

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:

  1. 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.
  2. 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.
  3. 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:

SituationBehavior
Relay B unreachableRetry with exponential backoff (30 s doubling, capped at 1 h)
Keeps failingDead-lettered after 5 attempts or 24 h in queue
Big batchChunked — 64 posts / 256 KiB per connection, each chunk covered by the same signed authorization
Second message arrivesMerged into the queue as its own signed group; already-queued posts are never invalidated
Same post delivered twiceDeduplicated by ID — the re-transfer is acknowledged as dup, a no-op
Relay restartsThe 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

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.

← back to under.town