Skip to content

Protocol

Overview

rail0 is a single immutable smart contract that implements the full authorize → capture → refund lifecycle for stablecoin payments. Buyer (payer) and merchant (payee) transact directly: there is no owner, no admin, no fee and no operator in between, and anyone can deploy it or call it.

rail0 protocol flow: payer authorizes or charges into the rail0 escrow, the payee captures, and refund / void / release return funds to the payer rail0 protocol flow: payer authorizes or charges into the rail0 escrow, the payee captures, and refund / void / release return funds to the payer rail0 protocol flow: payer authorizes or charges into the rail0 escrow, the payee captures, and refund / void / release return funds to the payer rail0 protocol flow: payer authorizes or charges into the rail0 escrow, the payee captures, and refund / void / release return funds to the payer
  • One signature, no allowance. The buyer signs an EIP-3009 ReceiveWithAuthorization off-chain; the merchant submits the transaction and pays gas. With a plain ERC-20, a contract can only move your tokens after you grant it an allowance with approve: a separate transaction, with its own gas, that leaves a standing permission open. An EIP-3009 signature authorizes one specific transfer and nothing else, so there is no allowance to grant, no setup transaction, and nothing left behind to revoke.
  • No custody outside the escrow window. The contract holds funds only while a payment is authorized and not yet captured, and routes every captured token to the merchant in full.
  • Time-based recourse only. There is no arbitration layer. Funds return to the buyer through void, release after expiry, or the merchant’s refund.

Every payment is defined by terms the buyer and merchant agree on up front, including two deadlines:

  • Until authorizationExpiry, the merchant can capture the escrowed funds — partially or in full, across one or more calls — or, while nothing has been captured yet, void the hold.
  • After authorizationExpiry, either party can release whatever is left in escrow back to the buyer.
  • Until refundExpiry, the merchant can refund any captured amount, and the buyer can raise a dispute.

refundExpiry must be at least one day after authorizationExpiry, so every payment keeps a usable refund window. authorizationExpiry is also the deadline for opening the payment: rail0 pins it as the buyer signature’s validBefore.

OperationSubmitted byWhenEffect
authorizepayee, with the payer’s signaturebefore authorizationExpiryMoves amount from the buyer into escrow.
chargepayee, with the payer’s signaturebefore authorizationExpiryPays amount straight to the merchant — no hold.
capturepayeebefore authorizationExpiryMoves escrowed funds to the merchant. Partial captures allowed.
voidpayeewhile nothing is capturedReturns the whole escrow to the buyer.
releasepayer or payeeafter authorizationExpiryReturns the remaining escrow to the buyer.
refundpayee, with its own signaturebefore refundExpiryPulls funds from the merchant’s wallet back to the buyer.
dispute / closeDisputepayerdispute before refundExpiryRaises or withdraws a dispute flag. Moves no funds.

Every fund-moving operation is submitted by the merchant, except release, which either party may submit — the funds go to the buyer either way.

A payment’s terms are committed on its first call and passed, unchanged, on every call after it. The contract stores only their EIP-712 hash and rejects any call whose struct does not match (PaymentMismatch).

FieldTypeMeaning
payeraddressBuyer: source of the funds and signer of the EIP-3009 authorization.
payeeaddressMerchant: submits the operations and receives captured funds.
tokenaddressThe stablecoin. Must be on the deployment’s allowlist.
amountuint120Exactly what the buyer commits to pay.
authorizationExpiryuint48End of the capture window; release opens at this time.
refundExpiryuint48End of the refund and dispute window.

Each payment is identified by a caller-chosen bytes32 paymentId. The contract enforces that an id is never reused, but does not generate it — use a collision-resistant scheme.

Per payment the contract keeps one packed storage slot:

  • capturableAmount — funds held in escrow.
  • refundableAmount — funds already paid to the merchant that can still be refunded.
  • disputed — the buyer’s dispute flag. It has no effect on funds.

capture moves value from the first bucket to the second; refund drains the second. Read them with getPaymentState(paymentId).

rail0 introduces no signing format of its own. The buyer signs the token’s standard EIP-3009 ReceiveWithAuthorization, with a nonce rail0 derives from the operation:

authorizeNonce = keccak256(keccak256("RAIL0.AUTHORIZE"), paymentId, configHash)
chargeNonce = keccak256(keccak256("RAIL0.CHARGE"), paymentId, configHash)

configHash is the payment’s EIP-712 hash, so the nonce binds the signature to the exact terms: change any field and the recovered signer no longer matches, and the token rejects it. The two prefixes keep an authorize signature from being replayed as a charge. Refunds work the same way in reverse — the merchant signs, with a nonce that also encodes the current balances, so each partial refund gets a distinct one.

Because the receive variant requires the caller to be the recipient, a signature lifted from the mempool cannot be spent against the token directly — only through rail0.

rail0 runs on any chain that meets two requirements:

  • An EVM at the Cancun fork or later. The contract is Solidity 0.8.31 targeting Cancun: it uses MCOPY and, for its reentrancy lock, transient storage (TLOAD / TSTORE).
  • Stablecoins that implement EIP-3009. Every token a deployment accepts must expose receiveWithAuthorization; authorize, charge and refund all spend a signature through it.

It is at its best on stablecoin-native chains with sub-second finality, where the merchant pays gas in the asset being settled and checkout confirms at once; on any other EVM chain gas is paid in that chain’s native token. A live network’s name links to the rail0 contract on its block explorer.

ChainTestnetMainnetStablecoins
Arbitrum Sepolia Live Mainnet Planned USDC
Arc Testnet Live — USDC, EURC
Avalanche Fuji Live Mainnet Planned USDC, EURC*
Base Sepolia Live Mainnet Planned USDC, EURC*
Celo Sepolia Live Mainnet Planned USDC, USD₮
Ethereum Sepolia Live Mainnet Planned USDC, PYUSD
Monad Testnet Live Mainnet Planned USDC
Optimism Sepolia Live Mainnet Planned USDC
Plasma Testnet Planned — USDT0
Polygon Amoy Live Mainnet Planned USDC
Sei Testnet Live Mainnet Planned USDC
Sonic Testnet Live Mainnet Planned USDC
Unichain Sepolia Live Mainnet Planned USDC
World Chain Sepolia Live Mainnet Planned USDC, EURC*
Tempo Awaiting EIP-3009 TIP-20

* Mainnet only.

Each deployment is constructed with a fixed list of accepted tokens; adding a stablecoin means a new deployment. The contract version is part of the EIP-712 domain the buyer signs over, so a signature made for one version does not verify against another. Pin the address and version per chain: most testnets above share an address, but that is a coincidence of the deployer’s nonces, not a CREATE2 guarantee.