If you ship software in 2026 and you have international customers, you've probably been asked the same question I have: "can we just pay you in USDC?". Stablecoins now move more value on-chain than Visa moves on-card, settle in seconds, and cost cents. The ecosystem has matured, the regulatory picture is clearer, and the integration story for developers is finally — finally — sane.
This guide is the integration playbook I wish I'd had two years ago. We'll cover the moving parts of accepting stablecoin payments end-to-end, the trade-offs between rolling-your-own and using a gateway, and a working integration in under 50 lines of code.
The five things every stablecoin integration has to solve#
Whether you build it yourself or use a provider, every stablecoin checkout has to handle:
- Address generation. A unique deposit address per payment intent so you can attribute incoming funds.
- Mempool / chain watching. A reliable way to detect when funds arrive and how many confirmations they have.
- Confirmation policy. When is a payment "good enough" to release the goods? This varies wildly by chain.
- Webhooks. A signed, idempotent push so your application can react without polling.
- Custody and withdrawal. Where do funds end up, who controls the keys, and how do you sweep them to your treasury?
Almost every "I rolled my own" horror story is one of these five going sideways under load. None of them are interesting business logic — but all of them are existential if they break.
EVM vs non-EVM: what actually differs#
On EVM chains (Ethereum, Polygon, BNB Chain, Arbitrum), USDC and USDT are
ERC-20 contracts. You're listening for Transfer(address,address,uint256)
events on a known contract address.
On Solana, USDC is an SPL token; you watch for tokenAccount balance
changes on a per-payment associated token account.
On Tron — still the cheapest place in the world to send USDT — you watch
TRC-20 Transfer events with a slightly different RPC shape.
Here's the simplified mental model:
| Chain | Token standard | Typical confirmations | Per-tx cost |
|---|---|---|---|
| Ethereum | ERC-20 | 12 | $0.50–$5 |
| Polygon | ERC-20 | 64 | <$0.01 |
| BNB Chain | BEP-20 | 15 | <$0.10 |
| Solana | SPL | 32 (finalized) | <$0.001 |
| Tron | TRC-20 | 19 | $0–$1 |
Per-transaction cost is passed to the user, not to you, but it's the single biggest factor in whether your customer actually completes the payment. If you're charging $20 for a SaaS plan, USDT-on-Tron or USDC-on-Solana will have dramatically better conversion than USDC-on- Ethereum mainnet.
Build vs buy: the honest comparison#
If you're a 50-person team with a dedicated payments squad, building this yourself is reasonable. You'll need:
- A multi-region archive node per chain (or a relationship with three RPC providers and a hot-failover strategy).
- An HD-wallet derivation service with hardware-backed key custody.
- A mempool / blocks worker per chain, plus a re-org-safe confirmation state machine.
- A signed webhook system with an idempotent retry queue and a dead- letter store.
- An audit log that survives subpoenas.
- 24/7 on-call coverage.
That's a real engineering investment — call it 2–3 engineers for 6 months to v1, then ongoing.
If you'd rather ship in an afternoon and treat crypto rails like you'd treat Stripe, a developer-first gateway like BchainPay handles all five items above and gives you back a single REST API.
The integration: 47 lines of TypeScript#
Here's the entire happy-path integration with BchainPay. We'll create a payment intent, render a checkout, and react to the webhook.
1. Create a payment intent#
import { z } from 'zod';
const Body = z.object({ orderId: z.string(), amountUsd: z.number() });
export async function POST(req: Request) {
const { orderId, amountUsd } = Body.parse(await req.json());
const r = await fetch('https://api.bchainpay.com/v1/payment-intents', {
method: 'POST',
headers: {
'authorization': `Bearer ${process.env.BCHAINPAY_KEY!}`,
'content-type': 'application/json',
'idempotency-key': orderId,
},
body: JSON.stringify({
amount: { value: amountUsd, currency: 'USD' },
accept: ['USDC.polygon', 'USDC.solana', 'USDT.tron'],
metadata: { orderId },
success_url: `https://example.com/orders/${orderId}/thanks`,
}),
});
return Response.json(await r.json());
}The key detail: the idempotency-key header is your shield against
double-charging if the request is retried. Use a value derived from your
order ID — never a random UUID generated per request.
2. Render the hosted checkout#
The response includes a hosted_url you can redirect to, or a
deposit_addresses map you can render yourself. For 99% of cases
the hosted page is what you want — it shows network selection, a QR
code, a countdown to expiry, and adapts to mobile wallets.
3. React to the webhook#
import { createHmac, timingSafeEqual } from 'node:crypto';
const SECRET = process.env.BCHAINPAY_WEBHOOK_SECRET!;
export async function POST(req: Request) {
const ts = req.headers.get('x-bchainpay-timestamp')!;
const sig = req.headers.get('x-bchainpay-signature')!;
const raw = await req.text();
const expected = createHmac('sha256', SECRET)
.update(`${ts}.${raw}`)
.digest('hex');
if (!timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
return new Response('bad signature', { status: 401 });
}
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) {
return new Response('stale', { status: 401 });
}
const event = JSON.parse(raw);
if (event.type === 'payment_intent.succeeded') {
await markOrderPaid(event.data.metadata.orderId);
}
return new Response('ok');
}That's it. Three checks (signature, timestamp window, dedup-by-event-id) and your application is safe against replays, forgeries and stale deliveries. We have a whole post on hardening webhooks if you want to go deeper.
What about chargebacks and refunds?#
Crypto transactions are irreversible — that's the headline feature for
merchants and the headline footnote for buyers. Refunds, however, are a
first-class operation: BchainPay exposes a POST /v1/refunds endpoint
that initiates an on-chain return to the original sender. There's no
chargeback mechanism because there's no card network behind the scenes.
For dispute-prone categories (digital goods with intangible delivery, high-value services), consider holding funds in escrow for 24–48 hours before sweeping to your treasury. The dashboard supports per-product release rules.
Compliance: what you actually need#
For most SaaS / e-commerce use cases in 2026:
- Sanctions screening on incoming addresses (we do this for you).
- KYC of your own business (one-time, during onboarding).
- Tax-reporting export (CSV for your accountant; we provide).
Custodial wallet activity in your name does not by itself trigger MSB / VASP licensing in most jurisdictions, as long as you don't act as a user-facing exchange. If you're not sure, we have a one-pager you can forward to your counsel — email hello@bchainpay.com.
Where to go from here#
If you want to play with this in five minutes, sandbox API keys are free and don't require a card. The Quickstart walks through the above with copy-pasteable curl, and the Postman collection lets you skip straight to the API surface.
Stablecoins are no longer "the future of payments" — for the merchants shipping them today, they're just payments. The infrastructure has caught up. Now the only question is what you build on top of it.