Stamps
A recipient can charge strangers to reach them. Three rules make the economics hold up, and all three are enforced on chain rather than by the interface.
Gas already gives mail a floor price, which makes mass spam expensive. Stamps go further: a recipient sets a price that unknown senders must escrow to reach them. Answer and it goes back. Ignore it and it is yours.
The three rules
These are not interface conventions. Each is enforced by the contract, which matters because a rule that lives in the frontend is a rule an attacker skips by calling the contract directly.
Answering is always free
Replying to a message somebody paid to send you never costs a stamp. Without this, a recipient could bait you and raise their price before you replied.
Time never refunds a spammer
After thirty days the escrow belongs to the recipient. If waiting returned the money, a patient spammer would simply wait.
You cannot take it and shut the door
Blocking a sender returns their escrow. Keeping the money and the block at once is refused on chain.
Escrow lifecycle
Per message, not per pair
Every message has its own Stamp record keyed by
mailId. An early revision aggregated escrow per sender-recipient
pair, and the consequence was severe: replying once to a spammer who had sent
fifty stamped messages refunded the entire batch. Per-message escrow makes each
payment settle on its own terms.
struct Stamp {
address sender; // who paid it
uint96 amount; // wei escrowed for this one message
address recipient; // who it was paid to reach
uint64 stampedAt;
StampStatus status; // None, Held, Refunded, Claimed, Reclaimed
}
Who mails free
| Sender | Required stamp |
|---|---|
| Recipient has no price set | Zero. An open inbox is the default. |
| On the recipient's allowlist, within its window | Zero. Allowlist entries are time-bounded. |
| Answering a live stamp the recipient paid to reach them | Zero, by rule one. |
| Anyone else | The recipient's current stampPrice. |
| Blocked by the recipient | Cannot send at any price. Reverts with Blocked. |
An early fix for rule one made replies free only when answering a
stamped message, and reverted with NoSuchStamp when
replying to ordinary free mail. That broke the default path for every user.
The second audit caught it; the reply path now handles both cases.
Settlement paths
Each has a batch form that skips rows it cannot settle rather than reverting
the whole call, and emits BatchSettled(caller, count, total) so the
caller can see what actually moved.
Payouts cannot be used as a weapon
A naive implementation pushes value with a plain transfer, which lets a
contract recipient revert on receipt and permanently block the counterparty's
refunds and replies. bMail sends with a fixed PAYOUT_GAS budget of
60,000, guards it against the available gas, and credits the amount internally if
the transfer fails.
// the failure path never blocks the protocol
credits[to] += amount;
emit PaymentCredited(to, amount);
// the owner pulls later via withdrawCredits()
Bounds
Whether the protocol takes a fee on claims, whether a slice is burned, and whether the default stamp price should be non-zero are deliberately left unset. They are business calls, not defects. See the roadmap.