ADR-0017: The revenue model: fees are kept, rent is reclaimed, and the fee path is SOL only

The devnet chain earns nothing by design and pays for every block.

Status: accepted. Dated 2026-09-05.

2026-09-05. Status: ACCEPTED — the owner accepted this ADR and ADR-0018 on 2026-09-05 ("same like Arbitrum and Optimism": gas in the base asset, the sequencer keeps the fee, a governance token). §6.1 is implemented (the fee is kept from a per-chain activation height, fees::fee_kept_from); §6.2–6.7 are open. Numbers are the node's own measurements (fees.rs, getSettlement) on 2026-09-05.

1. The problem, in numbers

The devnet chain earns nothing by design and pays for every block.

itemlamportssource
user fee per transaction5,000, burnedwire::FLAT_FEE, charge_fee
settlement per block: root record rent2,042,160blockCost.rootRent (beta, 5,080 lamports per byte)
settlement per block: batch record rent1,229,360blockCost.batchRent
settlement per block: signature fees20,0004 transactions
settlement per block: bond100,000, returned on finalityPROPOSER_BOND
sunk per block3,291,520blockCost.sunk

Rent is 164× the fees at beta's rent schedule (204× at alpha's), and it is sunk because nothing closes a root or batch record: the commitment chain is the chain. At the flat fee a block needs about 820 transactions to pay for its own settlement — and because the fee is burned, reaching 820 changes nothing for the operator, who pays the full cost either way. On mainnet, at one block a second, that is on the order of 300 SOL a day.

Two facts make this tractable. Blocks are produced only when there is work, so an idle chain costs nothing. And the cost is rent, which is a function of bytes and not of congestion, so it is predictable and, unlike fees, recoverable.

2. Decision

  1. The fee path is SOL, only SOL, and stays that way. Gas is bridged SOL. There is no fee token, no fee discount in a token, no token bid in any auction. This tightens the 2026-09-02 decision, which had reserved a Starknet-style discount; the owner withdrew that on 2026-09-05. (ADR-0018 says what the token is for instead.)
  2. The fee is kept, not burned. It is credited on the L2 to the sequencer identity the genesis declares, and leaves through the bridge like any balance. A burn is what an L1 does with its base fee because there is nobody to pay; a rollup's sequencer pays real L1 costs, and every rollup that covers them — Arbitrum, Optimism, Base — keeps the fee. Burning while paying rent is the one combination that cannot stand.
  3. The fee is composed, not flat, once the machinery to compose it exists: a data share (bytes published), a settlement share (rent per transaction at the block's fill), and a margin. Until then the flat fee stays and the subsidy is disclosed, as the site does today.
  4. Rent is reclaimed. Finalized root records older than a retention window are closed and their rent refunded; DA batch records are closed after a longer retention, with the data archived off-chain first. This turns the dominant cost from sunk into float.
  5. Ordering is sold, in SOL. A priority-fee auction — already on the site's roadmap — pays the sequencer for the one thing it has that costs it nothing.
  6. The bridge charges nothing on the canonical path; the fast exit earns the spread, in kind. A deposit or a slow withdrawal carries only Solana's own transaction fee, as the canonical Arbitrum and Optimism bridges do — a fee at the door is the wrong place to earn what is a tiny amount at this stage, and never in the token, which would force a swap before a user's first transaction. Whoever wants their asset before the 48-hour window pays a fast-exit provider a spread in the asset withdrawn; the operator earns as a provider or through a share of that spread. Any future fee on SPL-token bridging is taken in the token bridged, capped, and governable (ADR-0018 §2a).
  7. Services are revenue that does not wait for volume: keyed RPC and explorer APIs, indexing, and running chains for others with the deploy tooling and driver that already exist.

3. What is SOL and what is the token — the whole map

The owner's question was whether "all other use cases" use the token. Not quite: anything that is a fee or a collateral securing user funds is SOL; anything that is a reward, incentive, membership stake or vote is the token. The reason for the line is ADR-0013's: a guarantee that weakens exactly when it is needed is not a guarantee, and a token's price falls exactly when a slashing event is in the news.

use caseassetwhy
gas, priority bidsSOLfee path; users hold it, wallets understand it, no oracle, no price exposure against L1 costs
canonical bridgeno protocol feeSolana's transaction fee only; the door is the wrong place to earn
fast-exit spread, any SPL bridging feethe asset bridged, in kinda user bridging a token may hold nothing else; every canonical bridge that charges does so in kind
proposer bond, challenger bond, sequencer receipt bond (ADR-0012)SOLcollateral that secures user funds or pays the party who caught a fraud; must not be worth less on the day it is slashed
rewards to proposers, verifiers, challengerstokenpayment for settlement work that held; the token's first job (ADR-0018)
staked sequencer set membershiptokena liveness and ordering commitment, slashable for double-signing; the receipt bond that protects users stays SOL
fast-exit liquidity incentivestokenproviders front SOL and earn their spread in SOL; the token bootstraps depth
governance of non-security parameterstokenfee margins, LP fee caps, treasury spend, grants — never the window, the air gap, the dispute authority, which are code
treasurySOL firstrevenue is SOL; the treasury may hold and buy the token, which is policy, not protocol

4. Alternatives considered

  • Keep burning, sweep the vault's excess on L1. Burned fees shrink the L2 supply, so the vault holds excess SOL equal to everything ever burned; a portal instruction could release it. Rejected for now: the portal would need the burned total committed in every root to bound the sweep, which is a root-record change and a new invariant, for the same money that crediting the sequencer delivers with the machinery the chain already has (a two-leaf step and the bridge).
  • A fee token or a discount in the token. Withdrawn by the owner on 2026-09-05; the reasoning in TOKEN-IDEA.md stands.
  • Settling every N blocks with one root. A real saving, but a root today commits one block's trace (ADR-0011); a multi-block root changes what a dispute is about. A cheaper first step with the same effect is a longer block interval, which is a node parameter.

5. Consequences

  • The fee step changes shape: from one leaf (payer − fee) to two (payer − fee, sequencer + fee). On the sparse tree that is the transfer class's shape, so the verifier is a small variant of what exists; the node's Recorder::fee and charge_fee change with it, and the harness pairs FA/FB and EA/EB gain the two-leaf form. It moves transactions_root for every block, so it lands with a redeploy of the game and, because the first block after it differs, at a block boundary the operator names.
  • Closing finalized roots means a withdrawal can no longer be proven against an arbitrary old root, only against one inside the retention window. Leaves never expire, and every later root contains every earlier leaf, so a holder proves against any retained final root; the portal needs no change beyond accepting that. Closing batch records is a data- availability decision: the retention must exceed the challenge window by a margin, and an archive must hold the bytes before the record closes.
  • The sequencer key holds revenue on the L2 until swept through the bridge. That is an operational hot-key exposure; the sweep should be frequent and the destination a cold key on L1.
  • Every number here is disclosed on the fees page, which already carries the cost model. The mainnet fee decision the site calls open becomes: composed fee, kept, with the subsidy period disclosed while the flat fee remains.

6. Implementation order

Each step is independently shippable and gated by the harness.

  1. ~~Keep the fee~~ — done 2026-09-05. Node: charge_fee credits the genesis sequencer's L2 account from the chain's activation height (fees::fee_kept_from: beta from block 100, alpha never, new chains from block 1); Recorder::fee records Fee { payer, recipient, fee } with recipient: None before the height, so old blocks replay and their fee steps stay defensible; Witness::Fee carries both leaves. Game: a new one_step_verify_witnessed_fee_paid_sparse beside the burn verifier, re-rooting two paths (the payer updated or deleted, the sequencer updated or inserted; paying oneself moves nothing). Harness: EA/EB gained the paid fee, to an existing sequencer and one created by an emptying fee, honest and lying. The explorer distinguishes burned from kept. One redeploy of the game, and the node before beta's block 100.
  2. Block interval — node parameter; measure the fill and the cost per transaction before and after (fees.rs already reports both).
  3. ~~Reclaim root rent~~ — built 2026-09-07. SCC: close_finalized_root(n), permissionless, every lamport to the root's recorded proposer (the destination is pinned, so the sender only pays the fee), allowed when the root is FINAL and past its retention. Portal: unchanged. Node: the finalizer closes as it finalizes, through the same rule (solieum_core::checks::close_retention_ok) before it sends, so a root inside its retention costs a read, never a refused transaction; getSettlement reports closed and closerNote, and solieum-node finalize runs one pass. Gated by the harness against the rebuilt program: refusals inside the retention (slots, count, wrong destination), and a 1,000-root chain closing root 1 with its rent returned to the proposer, to the lamport. Implementation note, a strengthening of the rule above: retention is the root count and a slot floor, RETENTION_ROOTS = 1,000 and RETENTION_SLOTS = 6,480,000 (thirty days at the 400 ms target), both counted from the slot the root's window closed. Closing a root ends the ability to prove new withdrawals against it (the portal reads the root PDA; already-proven withdrawals are unaffected), so the retention is a proving deadline — and a count alone is a deadline that shrinks with block cadence: a thousand roots is six weeks at the beta chain's pace and under two hours at a five-second block. The floor is what a user can rely on whatever the cadence; the count is what keeps a halted chain, which finalizes nothing, from closing anything while nobody could prove. Both numbers are the operator's to change; they live in one place. Not yet deployed: the beta SCC is still the 2026-09-05 build, and its oldest root has 14 newer final roots of the 1,000 it needs, so nothing there is closable for weeks in any case.
  4. ~~Reclaim batch rent~~ — built 2026-09-10, in the order this step demands. The archive came first (028138a): closing a batch destroys frame_hash, the commitment published chunks fold to, so a verifier could not begin on that block without somewhere else to read it. solieum-node verify --archive DATADIR reads a block's bytes from a datadir's blocks.jsonl — which the node already writes — when the cluster no longer carries them, whether because the batch was closed or because a pruned endpoint dropped the chunk transactions, which is already true of public devnet. The archive is not trusted: its bytes are decompressed, decoded, their deposits matched against inbox entries read under quorum, and replayed to a state root that must equal the root account's on chain. Only the cheap frame-hash pre-fold is lost. A byte-level failure from the archive is reported as unverifiable rather than as a divergence, because without the published bytes nothing says which of the two lied. Then close_batch (ad6e6a9): permissionless, no signer, every lamport to the feed's poster, sealed batches only — an unsealed one is abandon_batch's business — and the sequence number is never freed, so a closed batch cannot be reopened and the published history cannot be rewritten. sealed_at_slot is appended to the account, so every existing field keeps its offset and a batch sealed by an older build simply keeps its rent. Implementation note: the retention is BATCH_RETENTION_SLOTS = 3,240,000 slots, fifteen days at the 400 ms target, counted from the seal. It sits between two bounds a test enforces: many times longer than the 432,000-slot challenge window, so no batch closes while its block is disputable, and shorter than the root's retention, so batches close before the roots archived bytes are replayed against. That margin is also why the rule needs no cross-program read of the root's status — a coupling that would have made a batch uncloseable the moment §6.3 closed its root. Not deployed: beta's DA program is the 2026-09-05 build, and its fifteen batches predate sealed_at_slot.
  5. Composed fee — computed 2026-09-10 (f3efee3, 36fe70f), charged by nothing. fees::compose builds the three shares this step names — a data share from the bytes published, a settlement share from the block's fill, a margin from fees::Schedule — and solieum-node fees prints them beside the flat fee, with --margin-bps and --fee-floor to price a proposal. Both default to zero, which is the absence of a published price rather than a price, because this step requires disclosure before charging. It is not ready to be charged, and the arithmetic says why. The settlement share is a division by the fill, so it is largest exactly when a block is emptiest: on beta's block shape one transaction is billed 4,487,240 lamports and eight hundred are billed 6,066. Corrected 2026-09-10 (751c0d4): the earlier figures here priced every block size at a one-transaction payload, which understated a full block by 66× once the rent is reclaimed. The payload is compressed — 223 bytes per transaction at one, 89 at fifteen — and fees::measured_payload_bytes now carries the measured curve. That correction surfaced a second thing, which §6.5 has to answer before it is charged: cost per transaction is not monotonic in the fill. Filling from three transactions to four loses the single-signature settle, and the bytes saved do not pay for the three extra signatures until much later. Post-reclaim, per transaction: 5,000 at one, 1,667 at three, 5,000 at four, 463 at eight hundred. A fee derived from cost alone would therefore RISE as a block fills across that boundary, which is the argument for the schedule's floor doing real work rather than being a formality. And the rent decides whether it is sane at all — the same one-transaction block composes to exactly 5,000, the flat fee to the lamport, once §6.3 and §6.4 return the rent. So the flat fee is already correctly priced for a combined block on a chain that reclaims, and this step is a successor to it only after those two are deployed, on a chain busy enough that the fill is not one. Charging it before either would bill a single user for a whole block.
  6. Priority-fee auction — a sealed first-slot auction per block, bids in SOL to the sequencer; honoured ordering; disclosed. Not started, and not a pure implementation task: as written it collides with two things already shipped (surveyed 2026-09-10). The reading that keeps all of it is the ADR's own word first-slot: reserve position 0 of each batch for the auction and assign 1..n at arrival as now. Every non-bidding transaction keeps its arrival guarantee and its receipt; exactly one slot per block is sold; the published rule becomes "first-come, first-served after a reserved first slot", republished before anything is charged. The rule is built to that reading (a005984, node/src/auction.rs), wired to nothing. Three choices the collision forces: first price, because the auctioneer is the sequencer and is the party being paid, so a second-price clear would let it set the price from a fabricated runner-up only it can see; commit then reveal, bound to the batch, so it cannot read the field and bid a lamport over, nor can a losing bid be carried to the next auction; and the reveal is the transaction, in hand before the batch opens, which is what protects the receipts — a slot reserved for a winner who never showed would leave position 0 a permanent gap, and every receipt in that batch would fail against the published batch. Ties break on the commitment hash, not arrival order, so the rule stays a deterministic function of published data. An auction nobody bid in reduces to today's chain exactly, which is what makes the module safe to carry unwired. Not fixed by it, and said in the module: commit–reveal hides amounts, it does not compel inclusion — the sequencer can still ignore a reveal. Making that provable is forced inclusion's job (ADR-0009) and is not wired to this. What is still the owner's: turning it on changes invariant 1 and the published ordering rule, so it needs an activation height and the site restated before it earns a lamport.
    • The sequencer signs a position at arrival. Sequencer::accept assigns position = pending.len() and signs the receipt before it can know what arrives next — the crate's invariant 1, "index == assigned position". An auction needs the order to depend on bids that have not all arrived yet, so the two cannot both hold for the same slot.
    • The bond is sized against reordering. ADR-0012 states the rule as "the bond must exceed what a sequencer could gain by reordering", and equivocation on a (batch, position) is slashable on chain — demonstrated with a real slash. Selling a slot makes some reordering the sequencer's paid job, so what the bond secures has to be restated, not just resized.
    • And first-come is a published promise. The site says so in three places, including "Today's rule is first-come, first-served by admission, and it is checkable" and "signs a receipt fixing each position before executing anything". Changing the rule is a disclosure before it is a feature.
  7. Fast-exit share — the rule built 2026-09-10 (f472668), charged by nothing. fastexit::split_fill divides one filled withdrawal three ways — user, LP, protocol — summing to the amount exactly. The operator's share comes out of the spread the LP quoted; a governance-set bridging fee is added on top and is wholly the protocol's, since the LP never quoted it. Both default to zero, which is what the beta demonstration ran. Every figure is lamports of the withdrawal, so "in the asset withdrawn, never in the token" holds by construction rather than by promise. The cap this step asks the code to enforce did not exist: spread_bps is a u16 with nothing below 10,000 stopping it, so a configuration could have taken half of an exit. MAX_TOTAL_DEDUCTION_BPS is 500 and bounds the total, so a bridging fee cannot smuggle the sum past it. A guard rail rather than a price — honest spreads are single or low double-digit basis points and beta ran at 50 — and a constant rather than a config field, so the owner can lower it and the code will not raise it. Five per cent is the implementation's number and the owner's to change. What is still missing is the thing that would charge it: there is no provider program, and the beta demonstration deliberately needed none (../notes/testnet-fast-exit-demo.md). So this step is complete as a rule and not started as a mechanism.

Services (RPC, explorer, indexing, hosting) need no protocol change and can start whenever the operator wants them to.

7. What this does not decide

Whether the operator accepts a disclosed subsidy on mainnet for a period, and how long. The cost model is published; either choice can be stated honestly. And nothing here is legal or tax advice: keeping fees is revenue, and revenue has consequences that depend on where the operator sits.