Lesson 23 — Payment-rail engineering — the L1 category when settlement is the product
Question
Build a payment rail you'd run for Stripe or Meta. Concretely — in chainspec, executor, precompiles, indexer — what's different from an L1 you'd run for traders?
Most engineers, after L22, can read tempoxyz/tempo as "Reth + customizations." This lesson asks the prior question: what are the customizations FOR? Once you can name the four category shifts and verify each in source, you can read any payment-rail L1 — Tempo, the next one after Tempo — by inspection, not by tour guide.
Principle (minimum model)
A payment-rail L1 makes four shifts vs general-purpose:
- Settlement assurance > throughput. Fast, deterministic finality with an explicit "final" signal, plus defined recovery paths (who can reclaim or claw back funds, and under what rules). Throughput matters only after these. (Card networks have chargebacks; what does an L1 offer instead?)
- Fee abstraction > fee bidding. Fees are predictable and denominated in the money being moved, and someone other than the sender can pay them. On Tempo there is no native gas token: gas is paid in USD-denominated TIP-20 stablecoins, and a third party can sponsor fees with a second signature on the transaction.
- Identity hooks > pseudonymity. Compliance rules (allowlists, blocklists, freezes) are enforced by the protocol on every transfer, not left to each token contract. On Tempo this is the TIP-403 policy registry: the chain enforces address-level policies; deciding who is on a list (KYC) stays with the issuer, off-chain.
- Chain-agnostic surface > chain-specific UX. The payment protocol is payment-method-agnostic; the chain is one implementation. MPP (the Machine Payments Protocol) puts Tempo, Stripe, cards, Lightning, Solana and others behind the same HTTP 402 interface (
tempoxyz/mpp-specs/specs/methods/is where each one plugs in).
Sources for Tempo claims in this lesson: tempoxyz/tempo at commit aaf3418 (2026-10-28) and the protocol docs at tempo.xyz/developers/docs/protocol (the old docs.tempo.xyz URLs redirect there).
Three data points to test the category (so this is a category, not a Tempo lesson):
- Tempo — an L1 "designed specifically for stablecoin payments" (repo README), built on the Reth SDK with Simplex consensus via Commonware. Tempo co-authors MPP with Stripe.
- Hyperliquid — USDC is bridged in and trading settles on Hyperliquid's own L1.
- Base + Coinbase products — a general-purpose OP Stack L2, with USDC payments, wallets and on-ramps layered on top.
Tempo puts most of the four shifts into the L1 itself. Whether, and where, the other two stacks provide each shift is what Drill 4 asks you to work out from their docs. Same shifts, different placement is the hypothesis to test: if you can locate the four shifts somewhere in a stack, you've found a payment rail.
1. Setting the question right
Look at L22's tempoxyz/reth data point: 0 commits ahead, 2686 behind upstream (checked 2026-10; the fork is archived, and Tempo's Cargo.toml pins paradigmxyz/reth by git rev). They didn't fork; they composed. That answers "how" but not "why." Why customize at all? What does a payment rail need that general-purpose Reth doesn't already provide?
The cheap answer — "regulatory compliance" — is incomplete. Regulatory compliance is one shift (identity hooks). The other three are technical patterns that exist whether or not a regulator is looking. A self-custodial USDC settlement rail with zero KYC still needs settlement assurance, fee abstraction, and chain-agnostic surface to be a payment rail rather than just an L1.
🛑 Predict. Before reading the four shifts in detail, write down: what would happen if you tried to use vanilla Reth (no customizations) as the L1 for Stripe? Three concrete failures, not "it wouldn't work."
2. Shift 1 — Settlement assurance > throughput
What "settled" means on a general-purpose L1: the tx is in a block, plus some number of confirmations or a finalized tag that trails the head (on Ethereum mainnet, by minutes). No chargeback, no dispute window.
What a payment rail needs, and what Tempo actually ships:
-
Fast deterministic finality. Tempo uses Simplex consensus via Commonware (
crates/consensus); the README claims "sub-second finality in normal conditions," and the MPP Tempo charge spec (draft-tempo-charge-00) describes a transfer as "included in block with immediate finality (~500ms)". A merchant doesn't have to pick a confirmation count. -
An explicit finality signal. Besides the standard
eth_*RPC, the node exposes aconsensus_*namespace —consensus_getFinalization,consensus_getLatest, and a finalized-block subscription (crates/node/src/rpc/consensus/mod.rs). "Final" is something you query, not something you infer. -
Recovery paths instead of chargebacks. Tempo has no generic reversal or dispute-window primitive, and no "settlement attestation" precompile. What it has are narrower, rule-bound mechanisms:
- Issuer controls on TIP-20 tokens:
pause,burnBlocked(burn from an address the transfer policy blocks), andburnAt(burn from any unprotected address, gated byBURN_AT_ROLE; TIP-1006, T12). - Receiver-side bounce: under TIP-1028, a transfer blocked by the receiver's own receive policy is redirected to the
ReceivePolicyGuardprecompile, which records a receipt the originator (or a recovery address chosen by the receiver) can later claim. - Payment channels: the
TIP20ChannelReserveprecompile (TIP-1034) hasopen/settle/topUp/requestClose/close/withdraw, so a payer can recover unsettled deposit after a close request.
A consumer chargeback (a card-network dispute) has no on-chain equivalent here; if you need one, it lives in the application or the issuer's policy, not in the chain.
- Issuer controls on TIP-20 tokens:
Where this lands in a Reth-based node: the consensus layer (crates/consensus, built on Commonware rather than anything in Reth), a custom RPC namespace, and precompiles in crates/precompiles/ dispatched from the EVM.
🛑 Predict (hypothetical exercise — not a Tempo feature). Suppose you did want a protocol-level "mutual-consent reversal" precompile on a Reth-based chain. Which layer registers it (the EVM/precompile set your executor builds), what state would it need, and why would it change the meaning of "final" for every other contract? If you can't answer the first part, re-read L20 (Custom Executor).
3. Shift 2 — Fee abstraction > fee bidding
General-purpose chains: the user signs a tx, pays gas in the native asset, and the fee is a bid in a public mempool. Sponsorship is bolted on (ERC-4337 paymasters, or EIP-7702 delegation plus a relayer — the building course's EIP-7702 sponsor lab (build-7702-sponsor) covers this end to end).
Tempo moves this into the protocol (fee spec):
- No native token; gas in stablecoins. Fees are paid in a USD-denominated TIP-20 token. The fee token is resolved in order: the transaction's
fee_tokenfield, the fee payer's account preference on the FeeManager precompile (setUserToken), the TIP-20 token itself when the call istransfer/transferWithMemo, the Stablecoin DEXtokenInfor certain swaps, and finally a pathUSD fallback (from T13: pathUSD, then OUSD). - Fee AMM. Validators choose their own preferred token (
setValidatorToken). If the user's token differs, the Fee AMM swaps after execution at a fixed rate of 0.9970 per fee swap; the 0.3% goes to liquidity providers. Both live in the FeeManager precompile at0xfeec000000000000000000000000000000000000(crates/precompiles/src/tip_fee_manager/). - Max fee up front, refund after. The protocol deducts
max_feefrom the fee payer before execution and refunds(gas_limit - gas_used) * gas_priceafter. - Native sponsorship. The Tempo Transaction (type
0x76, TIP-0001) has an optionalfee_payer_signature. The sender signs with the fee token left blank; the sponsor picks the fee token and signs separately. No paymaster contract. - Reserved blockspace. Payment lanes: a
general_gas_limitheader field caps gas for non-payment transactions, so payments keep capacity during congestion. TIP-1045 defines what counts as a payment (every call must match an allow-list such as TIP-20transfer/transferWithMemo; empty access list; no authorization lists).
The transaction type that carries these fields, abridged from crates/primitives/src/transaction/tempo_transaction.rs at aaf3418 (attributes and most doc comments removed):
pub struct TempoTransaction {
pub chain_id: ChainId,
/// Optional fee token preference (`None` means no preference)
pub fee_token: Option<Address>,
pub max_priority_fee_per_gas: u128,
pub max_fee_per_gas: u128,
pub gas_limit: u64,
/// Calls to be executed atomically
pub calls: Vec<Call>,
pub access_list: AccessList,
/// Nonce key for 2D nonce system
pub nonce_key: U256,
pub nonce: u64,
/// Optional fee payer signature for sponsored transactions (secp256k1 only)
pub fee_payer_signature: Option<Signature>,
pub valid_before: Option<NonZeroU64>,
pub valid_after: Option<NonZeroU64>,
pub key_authorization: Option<SignedKeyAuthorization>,
pub tempo_authorization_list: Vec<TempoSignedAuthorization>,
}
Batched calls, parallel nonce lanes (nonce_key), validity windows and sponsorship are all transaction fields, not wallet conventions.
One layer up is MPP HTTP-402, where the payment itself becomes the HTTP authentication exchange. From tempoxyz/mpp-specs:
Client → GET /resource
Server → 402 Payment Required + WWW-Authenticate: Payment ...
Client → fulfills payment
Client → GET /resource + Authorization: Payment <credential>
specs/core defines the HTTP semantics; specs/intents defines abstract payment patterns (charge and subscription there; the README also lists session, which the Tempo method specifies); specs/methods defines how each network fulfills an intent. The Tempo charge method has a feePayer flag: when true, the server adds the fee-payer signature, so the paying client never holds a fee balance.
The crucial part: MPP is co-authored by Tempo and Stripe (mpp-specs README). The protocol abstracts whether settlement happens on Tempo, through Stripe, on a card network, or elsewhere. A server can offer several challenges (one per method or intent), and the client chooses which one to answer.
🔍 Find in repo. Open
tempoxyz/mpp-specs/specs/intents/. Name the intent types and explain each in one sentence. Then openspecs/methods/— how many concrete payment methods are specified, and what's the shape of a method spec?
4. Shift 3 — Identity hooks > pseudonymity
General-purpose chains: addresses are anonymous by default; KYC happens at the off-ramp (exchange or on-ramp provider), and any on-chain restrictions are whatever each token contract implements for itself.
Payment rails: compliance is a protocol concern. On Tempo the hook is the TIP-403 Policy Registry precompile at 0x403C000000000000000000000000000000000000 (spec, crates/precompiles/src/tip403_registry/):
- Policy types:
WHITELIST,BLACKLIST, andCOMPOUND(separate sub-policies for senders, recipients and mint recipients; TIP-1015). Policy ID 0 always rejects; policy ID 1 always allows. - Shared across tokens: a TIP-20 token points at a policy via
transferPolicyId(). One policy can govern many tokens, so an issuer updates a list once and it applies everywhere. - Enforced on every transfer: TIP-20 transfers call
isAuthorizedSender()for the source andisAuthorizedRecipient()for the destination; mints check the mint-recipient policy. - Receiver-side policies: TIP-1028 lets an address restrict which tokens and senders it accepts (see the
ReceivePolicyGuardbounce in Shift 1).
What TIP-403 does not do: it stores no identity attestations and performs no KYC. The spec cites KYC/AML as motivation, but the mechanism is address lists. Who goes on a list is the issuer's off-chain decision.
The policy also travels: Tempo Zones (private chains anchored to Tempo, now developed in the zones/ directory of the Tempo monorepo) mirror TIP-403 policies from Tempo, so a freeze on Tempo applies inside the Zone in its next block.
Reading test: open crates/precompiles/src/tip403_registry/ and crates/precompiles/src/tip20/. Find where a TIP-20 transfer consults the registry. Which part of a Reth-based node makes that call possible on every transfer — the transaction pool, the EVM's precompile set, or the payload builder?
5. Shift 4 — Chain-agnostic surface > chain-specific UX
The biggest shift, and the one most engineers miss: the payment rail's public interface is the payment protocol, not the chain.
A user, an agent or a merchant interacts with MPP. They get a 402 listing one or more payment challenges, fulfill one, and retry with a credential. Whether the money moved on Tempo, through Stripe, or over Lightning is a property of the method they chose.
What this means architecturally:
- The chain is one method among several.
specs/methods/currently has folders for card, evm, hedera, lightning, nearintents, solana, stellar, stripe, tempo, usdc and xrpl. - The chain's UX (gas, nonces, RPC, bridging) is hidden behind the method spec and SDKs.
- Methods can be added without changing the merchant's HTTP integration.
Compare to a general-purpose L1: the chain is the interface. RPCs, wallets, gas estimation, block explorers — the user is on the chain.
This is the "Stripe of crypto" thesis in code, not in marketing: Stripe's value isn't being a payment processor; it's making the underlying payment method an implementation detail. MPP does the same for chains.
🛑 Predict. If MPP's payment-method abstraction is correct, what's the relationship between a payment rail's revenue and the chain's transaction volume? Sketch this in two sentences before continuing.
(One answer: weak coupling. MPP volume can grow without on-chain volume growing, because payments may settle through other methods. That is the inverse of an L1's usual economics: the protocol isn't betting on any one chain being the dominant settlement method.)
6. Worked example — one settled payment, end to end
Trace a hypothetical $0.05 API-access payment via MPP, settled on Tempo with the Tempo charge method in its default pull mode (flow from draft-tempo-charge-00; values illustrative):
- Client → Server.
GET /api/v1/premium-feature(an agent buying premium API access). - Server → Client.
HTTP/1.1 402 Payment Requiredwith a challenge such asWWW-Authenticate: Payment id="…", realm="api.example.com", method="tempo", intent="charge", expires="…", request="<base64url JSON>". The request names the amount, the TIP-20 token, the recipient, and herefeePayer: true. - Client chooses. If the server offered several challenges, the client picks one it can satisfy; here, Tempo.
- Client signs, does not broadcast. It builds a Tempo Transaction (
0x76) whose call is TIP-20transfer(ortransferWithMemoif the request carries a memo), withfee_payer_signatureas a0x00placeholder andfee_tokenempty, and sends it back as atype="transaction"credential:Authorization: Payment <credential>. - Server sponsors and broadcasts. It picks a USD TIP-20 fee token, adds its fee-payer signature, and submits with
eth_sendRawTxSync. - Tempo executes. The FeeManager collects the max fee from the server in its fee token; the TIP-20 transfer checks TIP-403 sender and recipient authorization; the transaction qualifies for the payment lane under TIP-1045; the block finalizes (~500 ms per the spec).
- Server → Client.
200 OKwith aPayment-Receiptheader whosereferenceis the transaction hash. - Merchant books it. Reconciliation uses the receipt, the memo if one was used, and (optionally) an indexer such as
tempoxyz/tidx, which indexes Tempo chain data into PostgreSQL + ClickHouse.
In push mode the client broadcasts itself and returns a type="hash" credential; the server then checks the receipt with eth_getTransactionReceipt. Push mode cannot be combined with feePayer: true.
Every step maps to something you can open:
| Step | Where it lives |
|---|---|
| 2, 4, 7 — challenge, credential, receipt headers | specs/core/draft-httpauth-payment-01.md |
| 2–7 — Tempo request schema, pull/push, fee payer | specs/methods/tempo/draft-tempo-charge-00.md |
4 — 0x76 transaction | crates/primitives/src/transaction/tempo_transaction.rs; TIP-0001 |
| 6 — fee collection + Fee AMM | crates/precompiles/src/tip_fee_manager/ |
| 6 — TIP-403 checks | crates/precompiles/src/tip403_registry/, crates/precompiles/src/tip20/ |
| 6 — payment-lane classification | TIP-1045 |
| 6 — finality | crates/consensus/; consensus_getFinalization in crates/node/src/rpc/consensus/mod.rs |
| 8 — indexing | tempoxyz/tidx (read in the building course's indexer lab) |
No step is magic. Every step is in public source. The lesson of the table: a payment rail is the composition of these surfaces, not any one of them alone.
7. Anti-fluency — why "just use a fast L1" doesn't close the gap
A reasonable engineer hearing the four shifts might think: most of these are application-layer concerns. Use a fast L1 for throughput, add a paymaster contract for fee abstraction, use Circle's CCTP for cross-chain. Why does a payment rail need to be a category at all?
Compare what a fast general-purpose L1 plus bolt-ons gives you with what Tempo puts in the protocol:
- Settlement assurance: partly available. Plenty of fast L1s have quick finality. What a bolt-on can't give you is blockspace reserved for payments when the chain is busy — Tempo's payment lane is a consensus rule (
general_gas_limit), not an app-level priority fee. - Fee abstraction: works, with friction. Paymasters work, but the sponsor still needs the native gas token, and every chain's sponsorship semantics differ. Tempo has no native token, and sponsorship is a transaction field.
- Identity hooks: fragmented. Token contracts can (and do) ship their own blocklists, but each token implements them separately. TIP-403 gives one registry, shared by many tokens, enforced on every TIP-20 transfer, and mirrored into Zones.
- Chain-agnostic surface: not a chain property. This one doesn't require a special chain at all — MPP already specifies a
solanamethod next totempo. It lives above the chain.
So the claim is narrower than "only a purpose-built L1 can do payments": three of the four shifts are cheaper to provide in the protocol, and the fourth sits above any chain. The payment-rail L1 bundles the first three so applications don't each rebuild them.
The same shape as "why not just use Linux for embedded?" The answer isn't that Linux can't do it; it's that retrofitting real-time guarantees onto a general-purpose system can cost more than building them in. It's an analogy, not a benchmark.
Drill
Choose 2 of 4. Each is ~30 minutes of source reading; output is a short written deliverable.
- Find Tempo's settlement surfaces. In
tempoxyz/tempo, locate (a) theconsensus_*RPC trait, (b) theReceivePolicyGuardprecompile (TIP-1028), and (c) theclose/withdrawpath in theTIP20ChannelReserveprecompile (TIP-1034). Write 5 lines mapping each to a file path, then one paragraph on why none of them is a chargeback. - Read all intent types in MPP. Open
tempoxyz/mpp-specs/specs/intents/and the Tempo session spec inspecs/methods/tempo/. Write one sentence per intent explaining what it does and how it differs from a single chain tx. Then write one paragraph on why MPP defines these in the protocol rather than leaving them to applications. - Trace a TIP-403 check. Starting from TIP-20
transferincrates/precompiles/src/tip20/, follow the call intocrates/precompiles/src/tip403_registry/. Write down (a) the policy lookup, (b) the sender and recipient checks, (c) where the TIP-1028 receive policy is consulted, and (d) which part of the node registers these precompiles with the EVM. - Map a non-Tempo stack to the four shifts. Pick Hyperliquid, Base + Coinbase products, or Solana Pay. For each shift, write 2–3 sentences locating where it happens in the stack (or noting that it doesn't), citing the project's own docs. Compare your map to Tempo's. The deliverable is one page.
Pass criteria
- Name the four shifts in one sentence each, from memory, in any order.
- For each shift, name Tempo's real mechanism (Simplex finality +
consensus_*RPC; stablecoin gas + FeeManager +fee_payer_signature+ payment lanes; TIP-403; MPP methods) and the node layer it lives in (consensus / RPC / precompile / transaction type / above the chain). - State, in one sentence, what Tempo does not provide (no generic reversal / chargeback primitive, no identity attestations on-chain).
- Articulate, in 2–3 sentences, why "just use a fast L1 + paymaster" doesn't reproduce a payment rail — and which shift doesn't need the chain at all.
- For the worked example, name the file path or spec for at least 5 of the 8 steps without looking at the table.
If you can't hit these without re-reading, the category hasn't fully landed yet — revisit section 1 and the worked example.
📺 Further reading
- L13 (this course) — EVM privacy / Tempo Zones — the privacy slot in the same stack
- L22 (this course) — alphanet / Tempo / MegaETH case study — the source deep-read this lesson abstracts from
- The Building tier's
tidxindexer lab — indexing Tempo chain data for merchants - The Building tier's HTTP 402 / MPP machine-payments lab — the practical lab complement
tempoxyz/mpp-specs— the protocol; co-authored by Tempo and Stripetempoxyz/tempo— node, precompiles, TIPs, and (since the archive oftempoxyz/zones) Zones- Tempo protocol docs — TIP-20, TIP-403, fees, Fee AMM, payment lanes
Summary (3 lines)
- Four category shifts make an L1 a payment rail: settlement assurance > throughput, fee abstraction > fee bidding, identity hooks > pseudonymity, chain-agnostic surface > chain-specific UX.
- On Tempo they map to real components: Simplex finality + a
consensus_*RPC; stablecoin gas, FeeManager/Fee AMM,fee_payer_signatureand payment lanes; the TIP-403 policy registry; and MPP (co-authored with Stripe) above the chain. Tempo has no generic reversal primitive; recovery is issuer-, receiver- or channel-scoped. - The same shifts can be looked for in other stacks — same shifts, different placement. A payment rail is identifiable by where the four shifts live, not by which company runs it.