Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Economics

Where the SOL goes: every lamport transfer in the protocol, traced from the on-chain programs (mail_program/src/processor/), and what each participant class pays and collects. Amounts marked ≈ are rent-exemption minimums — a refundable, one-time deposit every Solana account needs to exist, sized to the account’s byte length rather than to any price or value it represents (see Closing accounts for what rent is and how to reclaim it) — and vary with account size at current rent parameters. This is a completely separate quantity from postage: postage is a price the recipient chooses; rent is a storage deposit nobody chooses and everybody gets back.

The stamp lifecycle

A stamp is prepaid postage for one email from one sender (“from” address) to one recipient wallet. Its value cycles through three accounts, then settles across four destinations:

The stamp lifecycle: a buyer funds a frombox via AddStamps, SendMail moves one stamp’s value onto a parked message account, and DeleteMail settles that value to the sender, delete signer, domain authority, and recipient.

  1. Buying (AddStamps / CreateFrombox): anyone may fund the frombox for a (recipient, from) pair. Each stamp costs the frombox’s required_postage plus a two-signature fee surcharge (STAMP_FEE_SURCHARGE_LAMPORTS = 10 000 = 2 × the 5 000-lamport base fee); the surcharge prefunds both settlement signature refunds (SendMail and DeleteMail). The whole amount transfers into the frombox account. A new frombox inherits required_postage from the recipient mailbox’s default_postage; only the recipient may change it afterwards (UpdateFrombox requires the recipient’s signature). Price changes do not revalue stamps already bought — value amortizes over the remaining stamp count. Third-party purchases additionally pay the per-stamp protocol fee straight to the postoffice (see “The per-stamp protocol fee” below); purchases paid by the recipient wallet itself are fee-free.
  2. Sending (SendMail): the signer must be the sender named in the email (or the active domain authority for the recipient’s domain — the MX operator’s wallet, for relayed mail). The signer pays the transaction fee and fronts the message account’s rent-exemption (≈0.003–0.006 SOL depending on address/cid sizes — a deposit sized to the account’s byte length, not to the postage price; see the note above) as a new deposit, separate from anything paid at the Buying step. One proportional share of the frombox’s value — (balance − frombox rent) / stamps — moves onto the message account alongside that rent, and one stamp burns. A frombox whose share rounds to zero still sends (the stamp count is the gate; the value is what settles later).
  3. Settling (DeleteMail): only the sender or the recipient may delete. The message balance is split, in order: the sender recovers the message rent + one signature fee, the delete signer recovers this transaction’s signature fee (both prefunded by the surcharge), the recipient’s domain authority collects the operator share (OPERATOR_SHARE_BPS = 10% of the remaining postage), and the recipient collects the rest.1 Postage only settles on delete; until then it sits parked on message accounts.

Try it yourself: a worked example

The script below re-derives the three steps above as plain arithmetic — paste it into a browser console or run it with node (no dependencies, no network access, nothing on-chain). Change postageLamports, messageRentLamports, or thirdPartyBuyer at the top to try a different scenario.

Money enters this example at two different points, not one: the buyer’s purchase at Buying, and the sender’s rent deposit at Sending (the same wallet, if the buyer is also the sender — but still two separate transactions). That’s why the settlement totals add up to more than buyerPays alone: the conservation check sums both inputs (buyerPays + messageRentLamports) against everything paid out, and only that combined total should balance.

Note: the default postageLamports below is priced at roughly what a single US first-class postage stamp costs (about $0.82 as of this writing), converted to lamports at the SOL/USD rate on the date this page was last updated ($81.16/SOL, 2026-07-06) — a deliberate nod to the stamp analogy, not a protocol default. Change it to see how the split scales.

// SithBit stamp economics calculator -- a hypothetical worked example.
// Pure JavaScript, no dependencies: paste into a browser console, or run
// with `node stamp-calculator.js`. All amounts are in lamports
// (1 SOL = 1,000,000,000 lamports).

// Protocol constants (from mail_model::constants)
const SIGNATURE_FEE_LAMPORTS = 5_000;
const STAMP_FEE_SURCHARGE_LAMPORTS = 10_000;   // 2 x SIGNATURE_FEE_LAMPORTS
const POSTOFFICE_STAMP_FEE_LAMPORTS = 100_000; // waived if the buyer is the recipient
const OPERATOR_SHARE_BPS = 1_000;              // 10% of postage, to the domain authority
const POSTAGE_ROUNDING_LAMPORTS = 1_000;       // recipient payout rounds down to this

// --- Inputs: change these to try a different scenario ---
const postageLamports = 10_000_000;    // ~0.01 SOL -- priced at ~$0.82, a US first-class stamp
const messageRentLamports = 4_000_000; // ~0.004 SOL -- rent-exemption sized to account bytes, independent of postage
const thirdPartyBuyer = true;          // false = the recipient self-funds (fee waived)
const solUsdPrice = 81.16;             // SOL/USD rate used for the $ comments below (2026-07-06)

function sol(lamports) {
  return (lamports / 1_000_000_000).toFixed(9) + " SOL";
}

function usd(lamports) {
  return "$" + ((lamports / 1_000_000_000) * solUsdPrice).toFixed(2);
}

// 1. Buying: AddStamps / CreateFrombox
const stampValue = postageLamports + STAMP_FEE_SURCHARGE_LAMPORTS;
const protocolFee = thirdPartyBuyer ? POSTOFFICE_STAMP_FEE_LAMPORTS : 0;
const buyerPays = stampValue + protocolFee;

console.log("== Buying: AddStamps / CreateFrombox ==");
console.log(`Buyer pays:          ${buyerPays} lamports (${sol(buyerPays)})`);
console.log(`  // ≈ ${usd(buyerPays)}`);
console.log(`  -> frombox value:  ${stampValue} lamports (${sol(stampValue)})`);
console.log(`  -> protocol fee:   ${protocolFee} lamports (${sol(protocolFee)})${thirdPartyBuyer ? " to the postoffice" : " (waived)"}`);

// 2. Sending: SendMail
const messageBalance = messageRentLamports + stampValue;
console.log("\n== Sending: SendMail ==");
console.log(`Sender fronts message rent: ${messageRentLamports} lamports (${sol(messageRentLamports)})`);
console.log(`  // a new deposit -- separate from what the buyer paid above`);
console.log(`One stamp's value moves onto the message account: ${stampValue} lamports (${sol(stampValue)})`);
console.log(`Message account now holds: ${messageBalance} lamports (${sol(messageBalance)})`);

// 3. Settling: DeleteMail
const senderShare = messageRentLamports + SIGNATURE_FEE_LAMPORTS;
const deleteSignerShare = SIGNATURE_FEE_LAMPORTS;
const remainingPostage = messageBalance - senderShare - deleteSignerShare; // == postageLamports
const operatorShare = Math.floor((remainingPostage * OPERATOR_SHARE_BPS) / 10_000);
const recipientRaw = remainingPostage - operatorShare;
const recipientShare = Math.floor(recipientRaw / POSTAGE_ROUNDING_LAMPORTS) * POSTAGE_ROUNDING_LAMPORTS;
const postofficeResidue = recipientRaw - recipientShare;

console.log("\n== Settling: DeleteMail ==");
console.log(`Sender recovers:        ${senderShare} lamports (${sol(senderShare)})  [rent + 1 signature fee]`);
console.log(`  // ≈ ${usd(senderShare)}`);
console.log(`Delete signer recovers: ${deleteSignerShare} lamports (${sol(deleteSignerShare)})  [1 signature fee]`);
console.log(`  // ≈ ${usd(deleteSignerShare)}`);
console.log(`Domain authority earns: ${operatorShare} lamports (${sol(operatorShare)})  [10% operator share]`);
console.log(`  // ≈ ${usd(operatorShare)}`);
console.log(`Recipient collects:     ${recipientShare} lamports (${sol(recipientShare)})  [the rest, rounded down]`);
console.log(`  // ≈ ${usd(recipientShare)}`);

// 4. Conservation check
const totalIn = buyerPays + messageRentLamports;
const totalOut = senderShare + deleteSignerShare + operatorShare + recipientShare + protocolFee + postofficeResidue;
console.log("\n== Conservation check ==");
console.log("  // totalIn = buyerPays + messageRentLamports: money entered at TWO steps, not one");
console.log(`Total in:  ${totalIn} lamports`);
console.log(`Total out: ${totalOut} lamports`);
console.log(totalIn === totalOut ? "Every lamport is accounted for." : "Mismatch -- check the math!");

Example output for the defaults above (a stranger buying one first-class-stamp-priced stamp, with the recipient later deleting the message to collect payment):

== Buying: AddStamps / CreateFrombox ==
Buyer pays:          10110000 lamports (0.010110000 SOL)
  // ≈ $0.82
  -> frombox value:  10010000 lamports (0.010010000 SOL)
  -> protocol fee:   100000 lamports (0.000100000 SOL) to the postoffice

== Sending: SendMail ==
Sender fronts message rent: 4000000 lamports (0.004000000 SOL)
  // a new deposit -- separate from what the buyer paid above
One stamp's value moves onto the message account: 10010000 lamports (0.010010000 SOL)
Message account now holds: 14010000 lamports (0.014010000 SOL)

== Settling: DeleteMail ==
Sender recovers:        4005000 lamports (0.004005000 SOL)  [rent + 1 signature fee]
  // ≈ $0.33
Delete signer recovers: 5000 lamports (0.000005000 SOL)  [1 signature fee]
  // ≈ $0.00
Domain authority earns: 1000000 lamports (0.001000000 SOL)  [10% operator share]
  // ≈ $0.08
Recipient collects:     9000000 lamports (0.009000000 SOL)  [the rest, rounded down]
  // ≈ $0.73

== Conservation check ==
  // totalIn = buyerPays + messageRentLamports: money entered at TWO steps, not one
Total in:  14110000 lamports
Total out: 14110000 lamports
Every lamport is accounted for.

Per-participant flows

Recipient (mailbox owner)

PaysCollects
Mailbox rent ≈0.0012 SOL (once, at CreateMailbox)Stamp value of every received-then-deleted mail
Encryption-key account rent ≈0.005 SOL (once)
Frombox funding for correspondents they allow-listThat same funding back, as mail arrives and is deleted

The recipient is the protocol’s revenue side: strangers pay required_postage per message, and pricing is the recipient’s spam control (the CLI defaults new mailboxes to 1 SOL per stamp — unknown senders are priced out until the recipient lowers the price for them). When the recipient funds a correspondent’s frombox themselves, the value round-trips back minus transaction fees — allow-listing costs only fees. Accumulates SOL in proportion to mail from senders who paid their own postage; net of their one-time rents otherwise.

Sender (wallet-holding)

Pays postage (the recipient’s price) plus the fee surcharge per stamp, and fronts the message rent per send. On delete, rent and one signature fee come back. Net cost per mail ≈ required_postage — the intended price of communication. Spends SOL by design.

MX operator (runs sithbitd + the gRPC gateway)

For relayed mail, the operator’s domain-authority wallet is the on-chain “sender”: it fronts the message rent and transaction fee per inbound delivery, and recovers rent plus the surcharge-funded signature fee at DeleteMail. On-chain revenue: the operator share — every settled message for a mailbox on the operator’s active domain pays the authority 10% of the postage (OPERATOR_SHARE_BPS). At scale this turns relaying into a revenue stream rather than a pure cost. Off-chain costs remain (IPFS pinning, servers, RPC); the share offsets them on-chain.

The per-stamp protocol fee

The postoffice’s primary revenue: a flat fee per purchased stamp, collected at purchase time and transferred directly to the postoffice PDA.

The per-stamp protocol fee: a buyer’s AddStamps/CreateFrombox purchase checks whether the fee payer is the recipient wallet — if so the fee is waived (owner prefunding a sender’s frombox, free like Gmail); otherwise it’s a third-party purchase and the fee times the stamp count goes to the postoffice.

  • Amount: stamp_fee_lamports from the postoffice account (default POSTOFFICE_STAMP_FEE_LAMPORTS = 100 000 = 0.0001 SOL), charged per stamp in the purchase.
  • Waiver: the fee is skipped iff the purchase’s fee payer is the recipient wallet (fee_payer == to_account). The payer is a required signer, so the check is unforgeable. This keeps the adoption path free: a mailbox owner who prices a correspondent’s frombox low and prefunds its stamps pays no protocol fee, and the recipient’s settlement payouts are untouched — mailbox owners perceive zero cost. Gift purchases by anyone else pay the fee.
  • Governance: the postmaster tunes the fee with SetStampFee (sithbit postmaster set-stamp-fee <LAMPORTS>), hard-capped on-chain at MAX_POSTOFFICE_STAMP_FEE_LAMPORTS (1 000 000) so a compromised postmaster key cannot price-gouge purchasers. There is no on-chain read instruction — account state is world-readable; anyone can query the current fee with the ungated sithbit postoffice fee.
  • Layout migration: postoffice accounts created before the fee field (32-byte layout) still work everywhere — readers parse them with a versioned load that applies the default fee, and the writers (SetStampFee, TransferPostmaster) grow the account in place (resize
    • rent top-up from the signing postmaster) on their next run.

Domain authorities

CreateDomain takes an explicit payer — either the postmaster or the domain authority may fund the domain account’s rent plus the flat DOMAIN_AUTHORIZATION_FEE_LAMPORTS (0.01 SOL) paid to the postoffice. Either way the postmaster must sign: domain ownership is proven off-chain (the authority’s pubkey in the domain’s DNS TXT record, checked by the postmaster’s trusted agent), so on-chain authorization is always the postmaster’s — authority-pays is a co-signed two-signature transaction, never authority-alone. The domain records its rent_payer, and CloseDomain (postmaster-signed) refunds the rent to whoever paid. When the postmaster sponsors a domain itself, the fee is a wash — it lands in the postoffice the postmaster can sweep. Deactivation remains a separate, reversible toggle. In exchange, the authority earns the 10% operator share on every settled message for its domain.

Alias holders

An alias costs its own account rent plus the flat ALIAS_FEE_LAMPORTS (0.01 SOL) paid to the mail program’s postoffice — squatting a name now has a price. Names remain globally unique, first-come, and never expire. CloseAlias lets the holder (the wallet the alias points at) reclaim the rent; the fee is not refunded.

Where SOL parks or strands

  • Message accounts hold rent + stamp value until someone deletes. Both sides have an incentive to delete (sender: rent; recipient: postage), but nothing forces it.
  • Every other account class now has a close path: CloseFrombox (the recipient reclaims rent plus any residual stamp value — stamps never used), CloseMailbox and CloseKey (owner reclaims rent), CloseAlias (holder reclaims rent), CloseDomain (rent back to the recorded payer), and WithdrawPostoffice (the postmaster sweeps accumulated revenue). A closed mailbox’s undelivered message accounts stay open and settle individually via DeleteMail; recreating the mailbox restarts message ids at zero, so new sends fail until those old message accounts are deleted — an availability nuisance, never a loss of funds.

Constants worth knowing

Protocol economics are consensus constants in mail_model::constants: SIGNATURE_FEE_LAMPORTS (5 000, the real base fee — replacing the deprecated 10 000-lamport fee-calculator default that overcharged buyers ~2×), STAMP_FEE_SURCHARGE_LAMPORTS (2 signatures), DEFAULT_POSTAGE_LAMPORTS (1 SOL), OPERATOR_SHARE_BPS (1 000 = 10%), DOMAIN_AUTHORIZATION_FEE_LAMPORTS (10 000 000 — charged by CreateDomain), ALIAS_FEE_LAMPORTS (10 000 000 — charged by CreateAlias), POSTOFFICE_STAMP_FEE_LAMPORTS (100 000 — the default per-stamp protocol fee charged by AddStamps/CreateFrombox on third-party purchases), and MAX_POSTOFFICE_STAMP_FEE_LAMPORTS (1 000 000 — the hard cap on the postmaster-tunable fee).

On-chain CreateMailbox applies the 1-SOL DEFAULT_POSTAGE_LAMPORTS spam-pricing floor when the instruction omits a price — a raw-instruction caller must opt into a cheaper (or free) mailbox explicitly; the safety margin is no longer client-side only.

Rent hygiene (implemented)

The follow-ups the original analysis recommended are now protocol behavior (see the “Economics” record in HANDOFF.md):

  • Domain economicsCreateDomain takes an explicit payer (postmaster or authority; postmaster always signs), charges DOMAIN_AUTHORIZATION_FEE_LAMPORTS to the postoffice, and records the rent_payer so CloseDomain can refund the rent to whoever funded it.
  • Close instructionsCloseFrombox (recipient reclaims residue + rent), CloseMailbox/CloseKey (owner reclaims rent), CloseAlias (holder reclaims rent), and WithdrawPostoffice (the postmaster sweeps the postoffice balance above its rent-exempt minimum — without which the accumulated protocol fee revenue would be unspendable).
  • Alias fee — a flat ALIAS_FEE_LAMPORTS charge on alias creation, CPI-transferred to the mail program’s postoffice, pricing out squatting. Waived when the payer is the postmaster (the fee would round-trip into the postoffice they sweep) — see bulk alias reservation.

Money is only half the picture: for what each participant must trust — the MX operator’s sender authentication, the singleton postmaster, public message metadata, and frombox custody (including that CloseFrombox returns third-party-funded stamps to the recipient, not the buyer) — see Trust assumptions and threat model.


  1. The recipient’s payout is rounded down to the nearest POSTAGE_ROUNDING_LAMPORTS (1 000 lamports); the sub-quantum remainder — at most 999 lamports per message — accrues to the postoffice. This is below the smallest price increment anyone quotes and isn’t something senders, recipients, or operators need to think about.