# Building a Self-Hosted Crypto Payment Gateway: Architecture, Security, and SDK Design
If you've ever integrated a crypto payment gateway like BitPay or Coinbase Commerce, you know the drill: redirect the customer, wait for block confirmations, listen for webhooks. It works. But there's a cost — not just the 0.5–1% per transaction, but the loss of control over your keys, your uptime, and your customer experience.
I spent the last few months building [XPayLabs](https://github.com/yan253319066/XPayLabs), a self-hosted alternative with open-source SDKs and tools. In this article, I want to walk through the three technical pillars that make it work: **non-custodial key management**, **multi-chain transaction detection**, and **API/SDK design for developer experience**.
---
## 1. Non-Custodial Key Management with HD Wallets
The first question anyone asks about a self-hosted payment gateway is: "Where are the private keys?"
The answer matters because it determines who can touch your funds. In a custodial model, the gateway provider generates and holds the keys. You trust them not to lose them, freeze them, or go out of business with your money.
In a non-custodial model, the keys never leave your infrastructure. Here's how we approached it.
### BIP-44 HD Wallet Derivation
Rather than generating a new random wallet for every invoice (imagine managing millions of private keys), we use **hierarchical deterministic (HD) wallet derivation** per BIP-44. A single master seed generates an unlimited tree of child addresses:
m / purpose' / coin_type' / account' / change / address_index
For a TRON USDT deposit address, the derivation path looks like:
m / 44' / 195' / 0' / 0 / 0
The coin_type index follows SLIP-44: `195` for TRON, `60` for Ethereum and EVM chains, `9006` for SUI. This means you back up **one seed phrase**, and every address ever generated is recoverable.
### Key Generation Flow
When the gateway starts up, it:
1. Reads the master seed from the environment variable (`XPAY_MASTER_SEED`)
2. Derives a master private key using BIP-39 + BIP-32
3. On each invoice creation, derives the next unused address from the chain-specific path
4. Stores the derivation index (not the private key) in the database
5. Monitors all derived addresses for incoming transactions
The critical security property is that the gateway only stores derivation paths in the database — never the master seed, never private keys in plaintext. If the database is breached, an attacker learns which addresses belong to you but cannot derive the corresponding private keys.
### Why This Matters
Most hosted gateways generate addresses server-side and manage keys themselves. With HD wallets, you get:
- **Deterministic recovery**: Lose your server? Restore from seed + blockchain history.
- **Key separation**: Each invoice gets its own address. Customers cannot see each other's on-chain activity.
- **Single backup**: One seed phrase, not one per address.
---
## 2. Multi-Chain Transaction Detection
A crypto payment gateway's core job is to detect when a customer sends funds. Simple in theory, complex in practice when you support 8+ blockchains with different block times, finality guarantees, and API semantics.
### Detection Strategies by Chain
We use a hybrid approach: **mempool-level detection where possible, block polling everywhere else**.
| Chain | Detection Method | Typical Latency | Confirmations Needed |
|-------|-----------------|-----------------|---------------------|
| TRON (TRC20) | TRON Grid API polling | 1–6 seconds | 1–2 |
| Ethereum (ERC20) | JSON-RPC `eth_getLogs` | 12–15 seconds | 12–30 |
| BNB Chain (BEP20) | JSON-RPC polling | 3–5 seconds | 15–30 |
| Polygon | JSON-RPC polling | 2–4 seconds | 30–100 |
| Arbitrum | JSON-RPC polling | 1–3 seconds | 10–20 |
| SUI | SUI RPC polling | 2–5 seconds | 5–10 |
### The Scanner Architecture
Each chain runs as an independent scanner that polls its node/RPC provider at a configurable interval. The scanner:
1. **Fetches the latest block** (or ledger version for SUI)
2. **Extracts all `Transfer` events** involving tracked addresses
3. **Matches events to pending invoices** by address + amount
4. **Updates invoice status** and enqueues a webhook delivery
typescript
// Simplified scanner loop — each chain runs one of these
async function scannerLoop(chain: ChainAdapter, store: Store) {
let lastBlock = await chain.getLatestBlock();
setInterval(async () => {
const currentBlock = await chain.getLatestBlock();
if (currentBlock <= lastBlock) return;
const pendingAddresses = await store.getPendingAddresses(chain.name);
const txs = await chain.scanTransactions(pendingAddresses, lastBlock + 1, currentBlock);
for (const tx of txs) {
await store.markDetected(tx);
}
lastBlock = currentBlock;
}, chain.pollIntervalMs);
}
### Confirmation Strategy
Different assets need different confirmation counts. The risk model is straightforward:
- **Small payments (<$100)**: 1 confirmation is usually safe. The cost of a reorg attack exceeds the payment value.
- **Large payments ($100–$10,000)**: Wait for chain-specific recommended confirmations (e.g., 12 for Ethereum, 30 for BSC).
- **Whale-sized (>$10,000)**: Consider additional safeguards like waiting for "safe" confirmation counts or requiring multiple confirmations across separate monitoring nodes.
We expose this as a configurable `minConfirmations` parameter per chain so merchants can dial the risk/reward balance themselves.
---
## 3. API & SDK Design: Stripe-Inspired, Crypto-Native
The third pillar is developer experience. If the API is painful, nobody will use it, no matter how secure the key management is.
### Request Signing with HMAC-SHA256
Instead of simple bearer tokens, every API request is signed using an HMAC-SHA256 envelope:
json
{
"sign": "a1b2c3d4e5f6...",
"timestamp": 1717000000,
"nonce": "7c9e5a24-1d4f-4b3a-8e7d-3f2c6b1a9e5d",
"data": { ... }
}
The signature is computed over the `data` object serialized as `data={key=value,...}` with `nonce` and `timestamp` appended via `&`. For example: `data={amount=100.00,chain=tron,symbol=USDT}&nonce=...×tamp=...`. This is deterministic across all SDKs. The server validates:
- **Signature match**: Proves the request came from someone holding the API secret.
- **Timestamp within 30 seconds**: Prevents replay attacks.
- **Nonce uniqueness**: Ensures each request is executed at most once.
### SDK Design Patterns
We maintain first-party SDKs in **Node.js/TypeScript** and **Java/Spring Boot**, with identical API surfaces. Here's the philosophy:
**Node.js SDK:**
typescript
import { XPay } from '@xpaylabs/node-sdk';
const xpay = new XPay({
apiKey: process.env.XPAY_API_KEY,
apiSecret: process.env.XPAY_API_SECRET,
baseUrl: 'https://payments.example.com'
});
// Create a collection (receive funds)
const order = await xpay.createCollection({
amount: '100.00',
symbol: 'USDT',
chain: 'tron',
orderId: 'order_12345'
});
// Verify incoming webhook
const event = xpay.parseWebhook(payload, signature);
**Java SDK:**
java
XPay xpay = new XPay(XPayConfig.builder()
.apiKey(apiKey)
.apiSecret(apiSecret)
.baseUrl(gatewayUrl)
.build());
CreateCollectionRequest request = CreateCollectionRequest.builder()
.amount("100.00")
.symbol("USDT")
.chain("tron")
.orderId("order_12345")
.build();
ApiResponse response = xpay.createCollection(request);
CollectionData result = response.getData();
Both SDKs handle:
- Request signing and nonce generation
- Response parsing with typed models
- Webhook signature verification
- Error handling with structured exceptions
### Webhook System
Webhooks are the backbone of payment notification. Our system uses:
1. **HMAC-SHA256 signing**: Each webhook payload includes an `X-Webhook-Signature` header.
2. **Queue-backed delivery**: Failed deliveries retry with exponential backoff (1s → 5s → 30s → 5min → 30min → 2h → 6h → 24h).
3. **Idempotency keys**: Each event includes an `id` for deduplication.
typescript
// Webhook verification in Node.js
app.post('/webhooks/xpay', express.raw({type: 'application/json'}), (req, res) => {
const event = xpay.parseWebhook(req.body, req.headers['x-webhook-signature']);
switch (event.type) {
case 'ORDER_SUCCESS':
await fulfillOrder(event.data.orderId);
break;
case 'ORDER_FAILED':
await notifyCustomer(event.data.orderId);
break;
}
res.status(200).json({ received: true });
});
---
## Putting It Together: The Cost Case
Here's what the economics look like for a merchant doing $100,000/month in crypto volume:
| Item | BitPay | Coinbase Commerce | Self-Hosted (XPayLabs) |
|------|--------|-------------------|----------------------|
| Transaction fees | $1,000/mo | $1,000/mo | $0 |
| Monthly platform fee | $30–300/mo | $0 | $0 |
| Server + RPC costs | $0 | $0 | ~$30–50/mo |
| **Annual cost** | **$12,360–15,600** | **$12,000** | **~$360–600** |
The trade-off is operational responsibility: you manage the server, the database backups, the security patches. But for any merchant doing meaningful volume, the savings are orders of magnitude — and you never give up custody of your keys.
---
## Final Thoughts
Building a self-hosted crypto payment gateway is a rewarding architectural challenge. The three pillars I covered — HD wallet key management, multi-chain transaction detection, and clean API/SDK design — form the foundation of a system that can rival hosted solutions in reliability while exceeding them in cost efficiency and sovereignty.
If you're evaluating whether to build or adopt a self-hosted solution, start with these questions:
- **Can you generate and protect a seed phrase offline?** If yes, the key management problem is solved.
- **Which chains does your user base actually use?** 80%+ of stablecoin volume is on TRON and a handful of EVM chains — you probably don't need all 20.
- **Do you have the DevOps bandwidth?** Docker Compose makes deployment straightforward, but you still need to monitor, back up, and patch.
The SDKs (Node.js and Java), checkout UI, and documentation are open-source under MIT at [github.com/yan253319066/XPayLabs](https://github.com/yan253319066/XPayLabs). The core gateway engine is source-available under the XPay Enterprise License — deploy it via Docker to keep full control of your keys.
Find the SDKs on [npm](https://www.npmjs.com/package/@xpaylabs/node-sdk) and [Maven Central](https://central.sonatype.com/artifact/io.xpay/xpay-java-sdk).
Top comments (0)