How SaaS Founders Are Using Crypto Payments to Reach Global Customers
How SaaS Founders Are Using Crypto Payments to Reach Global Customers
The indie SaaS playbook is straightforward: build something useful, put it on Stripe, grow. But this playbook breaks for a significant portion of the world's developers and their customers.
Stripe is unavailable or severely restricted in over 100 countries. Paddle is even more selective. PayPal has notorious account freeze problems. If you're building for a global audience — or if you're a founder based outside the US/EU — traditional payment infrastructure actively limits your growth.
Crypto payments are how an increasing number of SaaS founders are working around this.
The Global Payment Problem
Let's be specific about what "payment restrictions" actually means in practice:
Geographic Availability
Stripe's full product is available in about 46 countries. Paddle supports around 200 countries for buyers, but founder accounts are restricted to fewer. If you're a developer in Vietnam, Egypt, Nigeria, or Pakistan — or if your primary customers are there — you're locked out of the default SaaS payment stack.
Category Restrictions
Even in supported countries, certain product categories trigger review or outright rejection. AI tools, content moderation services, cryptocurrency-adjacent products, and several other categories face enhanced scrutiny. Many legitimate SaaS founders have had their Stripe accounts terminated mid-operation.
Account Stability
Stripe and PayPal reserve the right to hold funds, freeze accounts, or terminate relationships with limited explanation. For a bootstrapped SaaS where every dollar matters, a two-week hold is a crisis.
What Crypto Payments Actually Solve
Crypto payments solve the infrastructure problem, not the product problem. They work for SaaS when:
Your customers already own crypto — developers, technical founders, traders, Web3 users, and increasingly mainstream tech-savvy consumers
Transaction values are meaningful — the economics work better at $50+ per transaction where fee savings are noticeable
You're selling digital goods — no physical delivery means chargebacks (which don't exist in crypto anyway) aren't a concern
Geographic reach matters — if you're targeting Southeast Asia, Eastern Europe, Latin America, or the Middle East, a significant portion of potential customers may find crypto easier than cards
For USDT specifically: it's a dollar-pegged stablecoin, so there's no price volatility. Accepting 100 USDT means receiving $100, always.
The SaaS Subscription Model with Crypto
The honest challenge: crypto has no native recurring billing. Stripe subscriptions automatically charge a card every month. Crypto requires the customer to take action each period.
There are two common patterns:
Pattern 1: Manual Renewal (Simple)
Customers pay upfront for a period (monthly or annual) and receive an email reminder before expiry to renew.
Pros: Simple to implement. No custody complexity. Cons: Higher churn at renewal (requires customer action). More operational overhead.
This is how most crypto SaaS currently works. It's not elegant, but it functions.
Pattern 2: Credit System (Better UX)
Customers top up a credit balance. Your system deducts daily or monthly credits. When balance runs low, notify to top up.
Pros: Better retention. One payment covers multiple periods. Cons: More complex accounting. Need to handle balance edge cases.
For most indie SaaS, Pattern 1 is the right starting point. Build Pattern 2 once you have enough crypto-paying customers to warrant the complexity.
Complete Integration: ChainPay + Node.js/Express
Here's a complete example of a SaaS subscription flow using ChainPay.
Setup
npm install express
# No SDK needed — ChainPay is a REST API
// config.js
module.exports = {
chainpayKey: process.env.CHAINPAY_API_KEY, // sk_live_...
chainpayWebhookSecret: process.env.CHAINPAY_WEBHOOK_SECRET,
appUrl: process.env.APP_URL
};
Create a Subscription Payment
// routes/billing.js
const express = require('express');
const router = express.Router();
const config = require('../config');
// POST /billing/subscribe
// Called when user clicks "Subscribe with USDT"
router.post('/subscribe', requireAuth, async (req, res) => {
const { plan, currency = 'USDT', chain = 'trc20' } = req.body;
const user = req.user;
const PLAN_PRICES = {
monthly: '29.00',
annual: '290.00' // ~17% discount vs monthly
};
const amount = PLAN_PRICES[plan];
if (!amount) return res.status(400).json({ error: 'Invalid plan' });
// Create ChainPay order
const order = await fetch('https://chainpay.pro/api/v1/orders', {
method: 'POST',
headers: {
'Authorization': `Bearer ${config.chainpayKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
amount,
currency,
chain,
externalId: `sub_${user.id}_${plan}_${Date.now()}`,
metadata: {
userId: user.id,
plan,
userEmail: user.email
}
})
}).then(r => r.json());
if (order.error) {
return res.status(500).json({ error: 'Failed to create payment' });
}
// Save pending order to DB
await db.orders.create({
orderId: order.orderId,
userId: user.id,
plan,
amount,
status: 'pending',
expiresAt: order.expiresAt
});
// Return checkout URL
res.json({
checkoutUrl: order.checkoutUrl,
orderId: order.orderId,
expiresAt: order.expiresAt
});
});
module.exports = router;
Handle Payment Webhooks
// routes/webhooks.js
const crypto = require('crypto');
const express = require('express');
const router = express.Router();
const config = require('../config');
// POST /webhooks/chainpay
// Must be raw body — do NOT use express.json() for this route
router.post('/chainpay',
express.raw({ type: 'application/json' }),
async (req, res) => {
// 1. Verify signature
const sig = req.headers['x-chainpay-signature'];
const expected = 'sha256=' + crypto
.createHmac('sha256', config.chainpayWebhookSecret)
.update(req.body)
.digest('hex');
if (sig !== expected) {
console.warn('Invalid ChainPay signature');
return res.status(401).end();
}
const event = JSON.parse(req.body.toString());
console.log(`ChainPay event: ${event.event} for ${event.orderId}`);
if (event.event === 'payment.completed') {
await handlePaymentCompleted(event);
} else if (event.event === 'payment.expired') {
await handlePaymentExpired(event);
}
// Always return 200 quickly — processing happens async
res.status(200).end();
}
);
async function handlePaymentCompleted(event) {
const { orderId, externalId, metadata, netAmount } = event;
// Look up the pending order
const order = await db.orders.findOne({ orderId });
if (!order) {
console.error(`Order not found: ${orderId}`);
return;
}
if (order.status !== 'pending') {
// Idempotency: webhook may fire more than once
console.log(`Order ${orderId} already processed`);
return;
}
// Calculate subscription period
const now = new Date();
const expiresAt = order.plan === 'annual'
? new Date(now.getFullYear() + 1, now.getMonth(), now.getDate())
: new Date(now.getFullYear(), now.getMonth() + 1, now.getDate());
// Activate subscription
await db.transaction(async (trx) => {
await trx.orders.update(
{ orderId },
{ status: 'completed', completedAt: now }
);
await trx.subscriptions.upsert({
userId: order.userId,
plan: order.plan,
status: 'active',
activatedAt: now,
expiresAt,
lastOrderId: orderId
});
});
// Send confirmation email
await sendEmail({
to: metadata.userEmail,
subject: 'Subscription Activated',
text: `Your ${order.plan} subscription is active until ${expiresAt.toDateString()}.`
});
console.log(`Subscription activated for user ${order.userId} until ${expiresAt}`);
}
async function handlePaymentExpired(event) {
const order = await db.orders.findOne({ orderId: event.orderId });
if (order && order.status === 'pending') {
await db.orders.update({ orderId: event.orderId }, { status: 'expired' });
}
}
module.exports = router;
Subscription Status Check
// middleware/requireSubscription.js
module.exports = async function requireSubscription(req, res, next) {
const sub = await db.subscriptions.findOne({
userId: req.user.id,
status: 'active',
expiresAt: { $gt: new Date() }
});
if (!sub) {
return res.status(402).json({
error: 'Subscription required',
checkoutUrl: `${process.env.APP_URL}/billing`
});
}
req.subscription = sub;
next();
};
7-Day Renewal Reminder
// jobs/subscriptionReminders.js
// Run daily via cron
async function sendRenewalReminders() {
const soon = new Date();
soon.setDate(soon.getDate() + 7);
const expiringSubs = await db.subscriptions.find({
status: 'active',
expiresAt: { $lt: soon, $gt: new Date() },
reminderSentAt: null
});
for (const sub of expiringSubs) {
const user = await db.users.findById(sub.userId);
await sendEmail({
to: user.email,
subject: 'Your subscription expires in 7 days',
html: `<p>Renew at <a href="${process.env.APP_URL}/billing">your billing page</a>.</p>`
});
await db.subscriptions.update(
{ id: sub.id },
{ reminderSentAt: new Date() }
);
}
}
Pricing Strategy for Crypto SaaS
USD pricing, crypto settlement. Set your prices in USD. ChainPay fetches real-time exchange rates and calculates the exact crypto amount at order creation. Customers pay the USDT equivalent of your USD price.
Offer a crypto discount. Some SaaS founders offer a 5-10% discount for crypto payment to incentivize adoption. The reasoning: no chargeback risk, lower processing fees, no payment processor fees. The economics support it.
Annual plans work better in crypto. The "pay once, covered for a year" model reduces the renewal friction inherent in non-recurring crypto. Consider pricing annual plans more aggressively.
Getting Started
- Register at chainpay.pro
- Generate an API key in Settings
- Configure your USDT TRC20 payout wallet
- Implement the two routes above (~1 hour)
- Add a "Pay with USDT" button to your pricing page
- Test with the sandbox:
POST /api/v1/sandbox/simulate-payment
Start with USDT TRC20 — it's the fastest (1 minute), cheapest (under $1 in fees), and most widely supported stablecoin. Add BTC and ETH as options once the basic flow is working.