Core concepts
The mental model behind 0fra — platforms, merchants, fees, and ledger.
Three layers, three wallets
Every payment touches exactly three parties on-chain. Each is a distinct wallet address.
| Layer | Who | Wallet variable | Cut |
|---|---|---|---|
| L0 | 0fra (us) | service_wallet | service_fee_bps (set by 0fra) |
| L1 | Your platform | platform_wallet | platform_fee_bps (set by you) |
| L2 | Sub-merchant | merchant_wallet | Whatever's left, minus their reserve |
bps(basis points) is 1/10000.100 bps = 1%,1000 bps = 10%.
The split formula
const serviceFee = gross * serviceFeeBps / 10_000;
const platformFee = gross * platformFeeBps / 10_000;
const merchantNet = gross - serviceFee - platformFee;
const reserveHold = merchantNet * reserveBps / 10_000;
const merchantAvailable = merchantNet - reserveHold;The on-chain PaymentRouter.payOrder() runs the same arithmetic and reverts if the values you submit don't match the registries. You can't tamper with the math.
Reserves
A reserve is a percentage of every payment held back from the merchant for a configurable period — used to cover refunds without bouncing tx through wallets.
- Set per-merchant on creation (
reserve_bps), with a platform-wide default - Reserves accrue in a 0fra-controlled vault, indexed by
(merchant_id, token, chain_id) - Refund flow draws from the reserve first; if insufficient, the platform absorbs and rolls a negative balance for the merchant
Identity
| Term | Stripe analog | 0fra equivalent |
|---|---|---|
| Account | acct_… | platform.id (uuid) |
| Connected acct | Account | merchant.id (uuid) |
| Payment intent | pi_… | order.id (uuid) |
| Charge | ch_… | payment_attempt (per tx_hash) |
| Checkout sess. | cs_… | cs_live_… token |
| API key | sk_live_… | Same. Plus pk_live_… publishable. |
| Webhook event | evt_… | Webhook delivery (per attempt) |
How payments actually move
sequenceDiagram
actor Buyer
participant Wallet as Buyer wallet
participant Router as PaymentRouter
participant 0fra as 0fra treasury
participant Plat as Platform wallet
participant Merch as Merchant wallet
participant Vault as ReserveVault
Buyer->>Wallet: approve USDC
Buyer->>Router: payOrder(params, signature)
Router->>Router: verify EIP-712 + registries + amount
Router->>0fra: transfer service fee
Router->>Plat: transfer platform fee
Router->>Merch: transfer merchant net (minus reserve)
Router->>Vault: hold reserve
Router-->>Buyer: emit OrderPaidThe dotted-line on the right represents a single atomic transaction. Either everything happens or nothing does.
Account state machine
order:
pending_payment ──► confirmed ──► refunded / partially_refunded
└─► expired
merchant:
pending_review ──► active ──► frozen
invitation:
pending ──► completed
└─► expiredWhat 0fra keeps off-chain
- A ledger of every dollar that passed through (
ledger_entriestable) - The idempotency cache so retries are safe
- Webhook delivery queue with HMAC signatures and exponential retries
- API keys (only their SHA-256 hash) and session cookies for the dashboard
What's never off-chain: the actual transfer. That always lives on Base.