December 10, 2025. Still the same day as Session 001 and Session 002. By this point, 0fee.dev had a complete backend, 7 payment providers, a SolidJS dashboard, a checkout widget, Celery background tasks, and SDKs in TypeScript and Python. Session 003 added a Stripe-inspired marketing website, 5 new SDKs covering Go, Ruby, PHP, Java, and C#, a full Docker production stack with 7 services, and a webhook delivery service. Approximately 7,650 lines of code.
The Marketing Website
The marketing website needed to accomplish one thing: convince a developer visiting 0fee.dev for the first time that this is a serious, professional payment platform worth integrating. The design benchmark was Stripe's marketing site -- clean, developer-focused, with interactive code examples and clear pricing.
Component Architecture
The website was built with SolidJS (matching the dashboard stack) and TailwindCSS:
website/
├── package.json
├── vite.config.ts
├── tailwind.config.js
├── index.html
├── Dockerfile
├── nginx.conf
└── src/
├── index.tsx
├── App.tsx
├── styles/
│ └── globals.css
├── components/
│ ├── Navbar.tsx # Mega-menu navigation
│ ├── Footer.tsx # Links + social
│ ├── Hero.tsx # Animated hero with stats
│ ├── CodeExample.tsx # Interactive code tabs
│ ├── Features.tsx # 8-feature grid
│ ├── CountryMap.tsx # 21-country selector
│ ├── Pricing.tsx # 3-tier pricing
│ ├── Testimonials.tsx # Customer quotes
│ └── CTA.tsx # Call-to-action
└── pages/
├── Home.tsx # Landing page
├── Products.tsx # Product overview
├── Pricing.tsx # Detailed pricing
├── Docs.tsx # Documentation
├── About.tsx # Team + mission
├── Contact.tsx # Contact form
├── Login.tsx # OTP login
└── Register.tsx # RegistrationThe Hero Section
The hero needed to communicate three things in under 5 seconds: what 0fee.dev does, how many countries it covers, and that integration is simple. The implementation used animated stat counters and a single code snippet:
tsxfunction Hero() {
return (
<section class="relative overflow-hidden bg-gradient-to-b from-gray-900 to-gray-800 text-white">
<div class="max-w-7xl mx-auto px-4 py-24 sm:py-32">
<div class="text-center">
<h1 class="text-5xl sm:text-7xl font-bold tracking-tight">
One API.{" "}
<span class="text-blue-400">Every payment.</span>
</h1>
<p class="mt-6 text-xl text-gray-300 max-w-3xl mx-auto">
Accept mobile money, cards, and wallets across
200+ countries with a single integration.
Built for Africa, ready for the world.
</p>
<div class="mt-12 grid grid-cols-3 gap-8 max-w-2xl mx-auto">
<StatCounter value={53} suffix="+" label="Providers" />
<StatCounter value={200} suffix="+" label="Countries" />
<StatCounter value={7} label="SDKs" />
</div>
</div>
</div>
</section>
);
}Interactive Code Examples
The CodeExample component shows how to integrate 0fee.dev in four languages, with syntax-highlighted tabs that switch between TypeScript, Python, Go, and cURL:
tsxfunction CodeExample() {
const [activeTab, setActiveTab] = createSignal("typescript");
const examples = {
typescript: `import { ZeroFee } from "zerofee";
const zf = new ZeroFee("sk_live_...");
const payment = await zf.payments.create({
amount: 5000,
payment_method: "PAYIN_ORANGE_CI",
customer: { phone: "+2250709757296" },
});`,
python: `from zerofee import ZeroFee
zf = ZeroFee(api_key="sk_live_...")
payment = zf.payments.create(
amount=5000,
payment_method="PAYIN_ORANGE_CI",
customer={"phone": "+2250709757296"},
)`,
go: `client := zerofee.New("sk_live_...")
payment, err := client.Payments.Create(&zerofee.PaymentParams{
Amount: 5000,
PaymentMethod: "PAYIN_ORANGE_CI",
Customer: &zerofee.Customer{Phone: "+2250709757296"},
})`,
curl: `curl -X POST https://api.0fee.dev/v1/payments \\
-H "Authorization: Bearer sk_live_..." \\
-H "Content-Type: application/json" \\
-d '{
"amount": 5000,
"payment_method": "PAYIN_ORANGE_CI",
"customer": {"phone": "+2250709757296"}
}'`,
};
return (
<section class="py-24 bg-gray-950">
<div class="max-w-4xl mx-auto px-4">
<h2 class="text-3xl font-bold text-white text-center mb-12">
Start accepting payments in minutes
</h2>
<div class="rounded-xl overflow-hidden border border-gray-800">
<div class="flex border-b border-gray-800 bg-gray-900">
<For each={Object.keys(examples)}>
{(lang) => (
<button
onClick={() => setActiveTab(lang)}
class={`px-4 py-3 text-sm font-medium ${
activeTab() === lang
? "text-blue-400 border-b-2 border-blue-400"
: "text-gray-400"
}`}
>
{lang}
</button>
)}
</For>
</div>
<pre class="p-6 bg-gray-950 text-gray-300 overflow-x-auto">
<code>{examples[activeTab()]}</code>
</pre>
</div>
</div>
</section>
);
}Country Map
The CountryMap component is an interactive selector showing which countries 0fee.dev supports. Clicking a country reveals which payment methods are available there:
tsxfunction CountryMap() {
const [selected, setSelected] = createSignal<string | null>(null);
const countries = [
{ code: "CI", name: "Ivory Coast", methods: ["Orange", "MTN", "Wave", "Moov"] },
{ code: "SN", name: "Senegal", methods: ["Orange", "Wave", "Free"] },
{ code: "GH", name: "Ghana", methods: ["MTN", "Vodafone", "AirtelTigo"] },
{ code: "KE", name: "Kenya", methods: ["M-Pesa", "Airtel"] },
{ code: "NG", name: "Nigeria", methods: ["Card", "Bank Transfer"] },
// ... 16 more countries
];
return (
<section class="py-24">
<h2 class="text-3xl font-bold text-center mb-12">
Payments across Africa and beyond
</h2>
<div class="grid grid-cols-3 sm:grid-cols-5 md:grid-cols-7 gap-4 max-w-5xl mx-auto">
<For each={countries}>
{(country) => (
<button
onClick={() => setSelected(country.code)}
class={`p-4 rounded-lg text-center transition-all ${
selected() === country.code
? "bg-blue-600 text-white scale-105"
: "bg-gray-100 hover:bg-gray-200"
}`}
>
<span class="text-2xl">{countryFlag(country.code)}</span>
<span class="block text-xs mt-1">{country.name}</span>
</button>
)}
</For>
</div>
<Show when={selected()}>
<div class="mt-8 p-6 bg-gray-50 rounded-xl max-w-2xl mx-auto">
<h3 class="font-semibold mb-4">
Available methods in {getCountryName(selected()!)}
</h3>
<div class="flex flex-wrap gap-2">
<For each={getMethodsForCountry(selected()!)}>
{(method) => (
<span class="px-3 py-1 bg-white rounded-full text-sm border">
{method}
</span>
)}
</For>
</div>
</div>
</Show>
</section>
);
}Design Choices
The marketing site used several design patterns from modern developer-focused products:
- Dark hero, light content. The hero section uses a dark gradient to create visual impact, while feature and pricing sections use light backgrounds for readability.
- Glassmorphism effects. Semi-transparent backgrounds with blur effects on navigation and floating elements.
- Mobile-first responsive design. Every component adapts from single-column mobile to multi-column desktop.
- Dark/light mode toggle. Persisted in localStorage, because developer tools should respect system preferences.
Five New SDKs
Session 003 added SDKs in Go, Ruby, PHP, Java, and C#, bringing the total to 7 languages. Each SDK followed the same API design: a client initialized with an API key, resource-based methods (payments, apps, checkout), and consistent error handling.
Go SDK
gopackage main
import "github.com/0feedev/zerofee-go"
func main() {
client := zerofee.New("sk_live_your_key_here")
payment, err := client.Payments.Create(&zerofee.PaymentParams{
Amount: 5000,
PaymentMethod: "PAYIN_ORANGE_CI",
Customer: &zerofee.Customer{
Phone: "+2250709757296",
},
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Payment ID: %s\n", payment.ID)
fmt.Printf("Status: %s\n", payment.Status)
}The Go SDK used the standard net/http client with struct-based parameters. Error handling followed Go conventions: every method returns (result, error).
Ruby SDK
rubyrequire "zerofee"
ZeroFee.api_key = "sk_live_your_key_here"
payment = ZeroFee::Payment.create(
amount: 5000,
payment_method: "PAYIN_ORANGE_CI",
customer: { phone: "+2250709757296" }
)
puts payment.id # "txn_abc123..."
puts payment.status # "pending"Built on Faraday for HTTP with a gem structure ready for RubyGems publishing.
PHP SDK
php<?php
require_once 'vendor/autoload.php';
\ZeroFee\ZeroFee::setApiKey('sk_live_your_key_here');
$payment = \ZeroFee\Payment::create([
'amount' => 5000,
'payment_method' => 'PAYIN_ORANGE_CI',
'customer' => ['phone' => '+2250709757296'],
]);
echo $payment->id; // "txn_abc123..."
echo $payment->status; // "pending"The PHP SDK used Guzzle for HTTP requests and followed Stripe's PHP SDK pattern with static class methods. Composer package with PSR-4 autoloading.
Java SDK
javaimport dev.zerofee.ZeroFee;
import dev.zerofee.model.Payment;
import dev.zerofee.model.PaymentParams;
import dev.zerofee.model.Customer;
public class Example {
public static void main(String[] args) {
ZeroFee zf = new ZeroFee("sk_live_your_key_here");
Payment payment = zf.payments().create(
new PaymentParams.Builder()
.amount(5000)
.paymentMethod("PAYIN_ORANGE_CI")
.customer(new Customer.Builder()
.phone("+2250709757296")
.build())
.build()
);
System.out.println(payment.getId());
System.out.println(payment.getStatus());
}
}The Java SDK used OkHttp with Gson serialization and the builder pattern for request parameters. Gradle build with Kotlin DSL.
C# SDK
csharpusing ZeroFee;
var client = new ZeroFeeClient("sk_live_your_key_here");
var payment = await client.Payments.CreateAsync(new PaymentParams
{
Amount = 5000,
PaymentMethod = "PAYIN_ORANGE_CI",
Customer = new Customer
{
Phone = "+2250709757296"
}
});
Console.WriteLine(payment.Id); // "txn_abc123..."
Console.WriteLine(payment.Status); // "pending"The C# SDK targeted .NET 8.0 with System.Text.Json serialization and async/await throughout.
SDK Line Counts
| SDK | Language | Approximate Lines | HTTP Client |
|---|---|---|---|
| Go | Go | ~800 | net/http |
| Ruby | Ruby | ~400 | Faraday |
| PHP | PHP | ~500 | Guzzle |
| Java | Java | ~1,200 | OkHttp + Gson |
| C# | C# | ~600 | HttpClient |
Java is notably larger because of the language's verbosity -- builder patterns, separate model classes, and explicit type declarations add significant line count compared to dynamic languages.
Docker Configuration
The Docker stack defines 7 services that constitute the complete 0fee.dev deployment:
yaml# docker-compose.yml
version: "3.8"
services:
api:
build:
context: .
dockerfile: Dockerfile
ports:
- "8000:8000"
environment:
- DATABASE_PATH=/data/0fee.db
- CACHE_URL=redis://dragonfly:6379
volumes:
- api-data:/data
depends_on:
- dragonfly
worker:
build:
context: .
dockerfile: Dockerfile
command: celery -A tasks.celery_app worker -l info
environment:
- CACHE_URL=redis://dragonfly:6379
depends_on:
- dragonfly
- api
scheduler:
build:
context: .
dockerfile: Dockerfile
command: celery -A tasks.celery_app beat -l info
environment:
- CACHE_URL=redis://dragonfly:6379
depends_on:
- dragonfly
- api
dragonfly:
image: docker.dragonflydb.io/dragonflydb/dragonfly
ports:
- "6379:6379"
volumes:
- dragonfly-data:/data
dashboard:
build:
context: ./frontend
dockerfile: Dockerfile
ports:
- "3000:80"
website:
build:
context: ./website
dockerfile: Dockerfile
ports:
- "3001:80"
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- api
- dashboard
- website
profiles:
- production
volumes:
api-data:
dragonfly-data:Service Responsibilities
| Service | Role | Notes |
|---|---|---|
api | FastAPI backend | Main application server |
worker | Celery worker | Processes webhook retries, reconciliation |
scheduler | Celery beat | Triggers scheduled tasks |
dragonfly | Cache + message broker | Sessions, rate limiting, Celery broker |
dashboard | Merchant dashboard | SolidJS app served by nginx |
website | Marketing website | SolidJS app served by nginx |
nginx | Reverse proxy | Routes traffic, SSL termination (production only) |
The nginx service uses a production profile, meaning it only starts when explicitly requested with docker compose --profile production up. During development, services are accessed directly on their individual ports.
Webhook Delivery Service
The last component built in Session 003 was backend/services/webhook_delivery.py -- the service responsible for delivering payment events to merchant endpoints.
Key features:
- HMAC-SHA256 signature generation for every delivery, allowing merchants to verify authenticity.
- Exponential backoff retries with 5 attempts.
- Delivery logging recording every attempt, response code, and response time.
- Queue-based async processing via Celery integration.
- Timeout handling with a 30-second maximum wait per delivery attempt.
Event types covered:
payment.created
payment.completed
payment.failed
payment.expired
payment.cancelled
refund.created
refund.completed
refund.failed
checkout.completed
checkout.expiredSession 003 By the Numbers
| Metric | Value |
|---|---|
| Marketing website components | 9 |
| Marketing website pages | 8 |
| New SDKs | 5 (Go, Ruby, PHP, Java, C#) |
| Total SDKs | 7 |
| Docker services | 7 |
| Lines of code added | ~7,650 |
| Time elapsed | One session |
Three sessions completed on December 10, 2025. The first day of development produced a payment orchestration platform with a backend, 7 providers, a dashboard, a checkout widget, background tasks, 7 SDKs in 7 languages, a marketing website, Docker deployment, and a webhook delivery service. Session 004 would add the CLI tool, hosted checkout pages, and comprehensive API documentation.
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.