Back to 0fee
0fee

The 0.99% Fee Model: Zero Monthly Fees

How 0fee.dev pricing works: 0.99% per transaction, zero subscriptions, monthly invoicing, multi-currency conversion. By Juste A. Gnimavo and Claude.

Thales & Claude | March 25, 2026 9 min 0fee
pricingbillingbusiness-model

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 TierMonthly FeeTransaction FeeFeatures
Starter$01.5%Basic API
Growth$291.0%Webhooks, Analytics
Business$990.7%Priority Support, Custom
EnterpriseCustomCustomDedicated

The problems became apparent quickly:

  1. Friction at signup. African startups operate on thin margins. A $29/month commitment before processing a single payment is a dealbreaker.
  2. Tier anxiety. Merchants constantly worried about which tier they needed. Support tickets were 40% "should I upgrade?"
  3. Billing complexity. Managing tier transitions, proration, downgrades -- each adding edge cases to the codebase.
  4. 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.16

The 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:

CurrencyCodeRate to USDExample: 10,000 units
West African CFAXOF0.0016$16.00
Central African CFAXAF0.0016$16.00
Nigerian NairaNGN0.00063$6.30
Ghanaian CediGHS0.064$640.00
Kenyan ShillingKES0.0077$77.00
South African RandZAR0.054$540.00
EuroEUR1.08$10,800.00
British PoundGBP1.26$12,600.00
US DollarUSD1.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 unpaid

Invoice 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:

  1. Cash flow matters in Africa. Many merchants operate on razor-thin margins and cannot prepay for platform fees.
  2. Reduces friction. No "insufficient balance" errors disrupting live payment flows.
  3. 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:

PlatformTransaction FeeMonthly FeeTarget Market
Stripe2.9% + $0.30$0Global
Paystack1.5% + NGN 100$0Nigeria
Flutterwave1.4%$0Africa
0fee.dev0.99%$0Global, Africa-first

At 0.99%, we undercut every major player while maintaining sustainable unit economics. The math works because:

  1. No human engineers. Zero salary overhead. Claude handles architecture, code, and debugging.
  2. Infrastructure efficiency. We run on lean cloud infrastructure, no over-provisioned Kubernetes clusters.
  3. 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.40

At 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 month

Implementation 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.40

Fee 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:

  1. Simplicity converts. Signup-to-first-payment time dropped significantly when merchants didn't have to evaluate pricing tiers.
  2. Trust pays off. Allowing negative balances and billing monthly built loyalty. Default rates stayed below 2%.
  3. 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.

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles