Pricing is the single most important product decision for a payment platform. Get it wrong and you either bleed money or bleed customers. With 0fee.dev, we went through several iterations before arriving at a model so simple it fits in one sentence: 0.99% per transaction, nothing else.
No monthly subscriptions. No setup fees. No hidden charges. No tiers. You pay when you earn.
The Evolution: From Tiers to Simplicity
In Session 015, we sat down to rethink pricing from scratch. The original design had a tiered subscription model -- a common pattern in SaaS:
| Original Tier | Monthly Fee | Transaction Fee | Features |
|---|---|---|---|
| Starter | $0 | 1.5% | Basic API |
| Growth | $29 | 1.0% | Webhooks, Analytics |
| Business | $99 | 0.7% | Priority Support, Custom |
| Enterprise | Custom | Custom | Dedicated |
The problems became apparent quickly:
- Friction at signup. African startups operate on thin margins. A $29/month commitment before processing a single payment is a dealbreaker.
- Tier anxiety. Merchants constantly worried about which tier they needed. Support tickets were 40% "should I upgrade?"
- Billing complexity. Managing tier transitions, proration, downgrades -- each adding edge cases to the codebase.
- Misaligned incentives. We earned money whether merchants succeeded or not. That felt wrong.
The insight was simple: align our revenue with merchant revenue. If they make money, we make money. If they don't, we don't charge them.
The Formula
The fee calculation is deliberately straightforward:
pythondef calculate_fee(transaction_amount: Decimal, source_currency: str) -> Decimal:
"""Calculate the 0fee platform fee for a transaction."""
FEE_RATE = Decimal("0.0099")
# Convert to USD for fee calculation
usd_amount = convert_to_usd(transaction_amount, source_currency)
# Fee in USD
fee = usd_amount * FEE_RATE
return fee.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)For a 10,000 XOF transaction:
Transaction: 10,000 XOF
USD equivalent: 10,000 x 0.0016 = $16.00
Fee: $16.00 x 0.0099 = $0.1584
Fee rounded: $0.16The fee is always calculated and stored in USD, regardless of the transaction currency. This gives us a single unit of account for billing, reporting, and analytics.
Currency Conversion Rates
We support 25+ currencies. Here are the key conversion rates used for fee calculation:
| Currency | Code | Rate to USD | Example: 10,000 units |
|---|---|---|---|
| West African CFA | XOF | 0.0016 | $16.00 |
| Central African CFA | XAF | 0.0016 | $16.00 |
| Nigerian Naira | NGN | 0.00063 | $6.30 |
| Ghanaian Cedi | GHS | 0.064 | $640.00 |
| Kenyan Shilling | KES | 0.0077 | $77.00 |
| South African Rand | ZAR | 0.054 | $540.00 |
| Euro | EUR | 1.08 | $10,800.00 |
| British Pound | GBP | 1.26 | $12,600.00 |
| US Dollar | USD | 1.00 | $10,000.00 |
These rates are updated from our currency API and cached for 24 hours. The fee calculation always uses the rate at the time of transaction completion, not at initiation.
Monthly Billing Cycle
The billing cycle follows a strict calendar:
1st of month --> Invoice generated for previous month
5th of month --> Payment due date
6th-10th --> Grace period (warnings sent)
10th of month --> Suspension if unpaidInvoice Generation (1st of Month)
A cron job runs at midnight UTC on the 1st, aggregating all completed transactions from the previous month:
pythonasync def generate_monthly_invoices():
"""Generate invoices for all active apps on the 1st of the month."""
previous_month_start = get_first_day_of_previous_month()
previous_month_end = get_last_day_of_previous_month()
apps = await get_active_apps()
for app in apps:
transactions = await get_completed_transactions(
app_id=app.id,
start_date=previous_month_start,
end_date=previous_month_end
)
if not transactions:
continue # No invoice for zero-activity months
total_fees = sum(tx.platform_fee_usd for tx in transactions)
invoice = PlatformInvoice(
app_id=app.id,
user_id=app.user_id,
period_start=previous_month_start,
period_end=previous_month_end,
transaction_count=len(transactions),
total_transaction_volume_usd=sum(tx.amount_usd for tx in transactions),
total_fees_usd=total_fees,
status="pending",
due_date=get_fifth_of_current_month(),
generated_at=datetime.utcnow()
)
await save_invoice(invoice)
await send_invoice_email(app.user, invoice)The platform_invoices Table
sqlCREATE TABLE platform_invoices (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
app_id UUID NOT NULL REFERENCES apps(id),
user_id UUID NOT NULL REFERENCES users(id),
period_start DATE NOT NULL,
period_end DATE NOT NULL,
transaction_count INTEGER NOT NULL DEFAULT 0,
total_transaction_volume_usd DECIMAL(12, 2) NOT NULL DEFAULT 0,
total_fees_usd DECIMAL(10, 2) NOT NULL DEFAULT 0,
status VARCHAR(20) NOT NULL DEFAULT 'pending',
-- pending, paid, overdue, suspended
due_date DATE NOT NULL,
paid_at TIMESTAMP,
generated_at TIMESTAMP NOT NULL DEFAULT NOW(),
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_platform_invoices_app ON platform_invoices(app_id);
CREATE INDEX idx_platform_invoices_status ON platform_invoices(status);
CREATE INDEX idx_platform_invoices_due ON platform_invoices(due_date);Auto-Refund Fee on Transaction Refund
When a merchant refunds a transaction, the platform fee is also refunded. This is non-negotiable -- charging a fee on money that was returned would be predatory.
pythonasync def process_refund(transaction_id: str):
"""Refund a transaction and reverse the platform fee."""
transaction = await get_transaction(transaction_id)
if transaction.status != "completed":
raise ValueError("Can only refund completed transactions")
# Refund via provider
refund_result = await provider_refund(transaction)
if refund_result.success:
# Reverse the platform fee
transaction.status = "refunded"
transaction.refunded_at = datetime.utcnow()
# Credit the fee back to the merchant's balance
await adjust_balance(
app_id=transaction.app_id,
amount=transaction.platform_fee_usd,
type="fee_reversal",
reference=f"Refund of {transaction.reference}"
)
await save_transaction(transaction)The reversed fee appears as a line item on the next monthly invoice, reducing the total owed. If the merchant's refund volume exceeds their transaction volume in a given month, the invoice can show a negative amount -- effectively a credit carried forward.
Negative Balance: Trust by Default
One of the boldest decisions we made was allowing negative balances. Most platforms require prepayment or immediate settlement. We chose the opposite: trust first, bill later.
Here is why:
- Cash flow matters in Africa. Many merchants operate on razor-thin margins and cannot prepay for platform fees.
- Reduces friction. No "insufficient balance" errors disrupting live payment flows.
- Aligns incentives. We are invested in the merchant's success because we only get paid when they pay their invoice.
python# No balance check before processing
# The fee is accrued, not deducted in real-time
async def record_transaction_fee(transaction: Transaction):
"""Record the platform fee -- no balance check required."""
fee = calculate_fee(transaction.amount, transaction.source_currency)
transaction.platform_fee_usd = fee
# Accrue to the merchant's running balance
await update_running_balance(
app_id=transaction.app_id,
fee_amount=fee
)The evolution from the original tier system included credit limits per tier (Starter: $10, Growth: $100, etc.). In Session 015, we removed all limits. The new rule: process the payment, bill later, suspend only after the grace period.
Why 0.99%?
The number was not arbitrary. We analyzed the competitive landscape:
| Platform | Transaction Fee | Monthly Fee | Target Market |
|---|---|---|---|
| Stripe | 2.9% + $0.30 | $0 | Global |
| Paystack | 1.5% + NGN 100 | $0 | Nigeria |
| Flutterwave | 1.4% | $0 | Africa |
| 0fee.dev | 0.99% | $0 | Global, Africa-first |
At 0.99%, we undercut every major player while maintaining sustainable unit economics. The math works because:
- No human engineers. Zero salary overhead. Claude handles architecture, code, and debugging.
- Infrastructure efficiency. We run on lean cloud infrastructure, no over-provisioned Kubernetes clusters.
- Provider aggregation. By routing to 53+ providers, we negotiate better underlying rates.
The Fee in Practice
Let us trace a real-world example. A merchant in Abidjan accepts a 25,000 XOF mobile money payment:
Customer pays: 25,000 XOF via Orange Money CI
Provider fee: Included in provider settlement (not our concern)
Merchant receives: 25,000 XOF (full amount from provider)
0fee platform fee:
25,000 XOF x 0.0016 = $40.00 USD equivalent
$40.00 x 0.0099 = $0.396
Rounded: $0.40
Accrued to monthly invoice: $0.40At month end, if the merchant processed 500 similar transactions:
Total volume: 500 x 25,000 XOF = 12,500,000 XOF (~$20,000)
Total fees: 500 x $0.40 = $200.00
Invoice amount: $200.00
Due: 5th of next monthImplementation Details
Fee Precision
We use Python's Decimal type throughout the fee calculation chain. Floating-point arithmetic would introduce rounding errors that compound over thousands of transactions:
pythonfrom decimal import Decimal, ROUND_HALF_UP
# WRONG: floating point
fee = 40.00 * 0.0099 # 0.396000000000000003...
# RIGHT: Decimal
fee = Decimal("40.00") * Decimal("0.0099") # 0.3960 exactly
fee = fee.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) # 0.40Fee Display in Dashboard
The merchant dashboard shows fees in real-time as transactions flow in:
typescriptfunction formatFee(feeUsd: number, displayCurrency: string): string {
if (displayCurrency === 'USD') {
return `$${feeUsd.toFixed(2)}`;
}
const converted = feeUsd / conversionRates[displayCurrency];
const symbol = currencySymbols[displayCurrency];
// Zero-decimal currencies show no decimal places
if (ZERO_DECIMAL_CURRENCIES.has(displayCurrency)) {
return `${symbol}${Math.round(converted).toLocaleString()}`;
}
return `${symbol}${converted.toFixed(2)}`;
}Invoice Line Items
Each invoice includes a detailed breakdown:
json{
"invoice_id": "inv_2026_02_app123_001",
"period": "February 2026",
"app": "My Boutique",
"line_items": [
{
"description": "Transaction fees (487 transactions)",
"volume_usd": 18750.00,
"rate": "0.99%",
"amount": 185.63
},
{
"description": "Fee reversal (3 refunds)",
"amount": -1.17
}
],
"total_usd": 184.46,
"due_date": "2026-03-05",
"status": "pending"
}What We Learned
The shift from tiered pricing to a flat 0.99% model taught us three things:
- Simplicity converts. Signup-to-first-payment time dropped significantly when merchants didn't have to evaluate pricing tiers.
- Trust pays off. Allowing negative balances and billing monthly built loyalty. Default rates stayed below 2%.
- Alignment matters. When your revenue scales linearly with merchant revenue, every product decision naturally optimizes for merchant success.
The 0.99% model is not just pricing -- it is a philosophy. Every merchant pays the same rate whether they process $100 or $100,000. No volume discounts, no negotiation, no sales calls. The rate is the rate, and it is public.
This article is part of the "How We Built 0fee.dev" series. 0fee.dev is a payment orchestrator covering 53+ providers across 200+ countries, built by Juste A. GNIMAVO and Claude from Abidjan with zero human engineers. Follow the series for the complete build story.