Build Your Own Crypto Payment Gateway with XPayLabs (xpay): Architecture, SDKs, and Production Deployment
Accepting crypto payments doesn't have to mean paying 1% to BitPay or trusting Coinbase Commerce with your private keys. With modern tools, you can self-host a production-grade gateway that supports USDT, USDC, DAI, and BUSD across TRON, Ethereum, BNB Chain, Polygon, Arbitrum, Optimism, Avalanche, Base, and SUI — with zero transaction fees.
This article breaks down the architecture, SDK integration patterns, and production deployment of XPayLabs (often shortened to xpay), a self-hosted gateway with open-source SDKs and tools.
Architecture Overview
┌─────────────────────────────────────────┐
│ Docker Compose Stack │
│ ┌────────┐ ┌────────┐ ┌──────────┐ │
│ │ MySQL │ │ Redis │ │ Gateway │ │
│ │ │ │ │ │ (Java 17 │ │
│ │ │ │ │ │ Spring) │ │
│ └────────┘ └────────┘ └────┬─────┘ │
│ │ │
│ ┌─────────────────────────────┘ │
│ │ ┌──────────────┐ ┌──────────────┐ │
│ │ │ Scanner: │ │ Scanner: │ │
│ │ │ TRON │ │ EVM Chains │ │
│ │ └──────────────┘ └──────────────┘ │
│ │ ┌──────────────┐ ┌──────────────┐ │
│ │ │ Scanner: │ │ Webhook │ │
│ │ │ SUI │ │ Dispatcher │ │
│ │ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────┘
The gateway runs as multiple services in a Docker Compose stack:
- Gateway API (Spring Boot 3.4+): REST API for order creation, status queries, and balance checks
- Blockchain Scanners: Concurrent per-chain workers that monitor mempool and blocks
- Webhook Dispatcher: Queue-backed delivery with exponential backoff
- MySQL: Persistent storage for orders, addresses, and transactions
- Redis: Caching, job queues, and nonce deduplication
1. HD Wallet Key Management
The gateway uses BIP-39 + BIP-44 hierarchical deterministic wallet derivation. One master seed generates every deposit address.
How It Works
// Conceptual example — HD wallet derivation
import { generateMnemonic, mnemonicToSeedSync } from 'bip39';
import { HDKey } from 'micro-bip32';
// Step 1: Generate or load a mnemonic (12 words)
const mnemonic = generateMnemonic(); // or load from env
// Step 2: Derive master key from seed
const seed = mnemonicToSeedSync(mnemonic);
const masterKey = HDKey.fromMasterSeed(seed);
// Step 3: Derive child key for a specific chain + index
// Path: m / 44' / coin_type' / 0' / 0 / address_index
// TRON coin_type = 195, EVM = 60, SUI = 9006
const childKey = masterKey.derive(`m/44'/195'/0'/0/${addressIndex}`);
const depositAddress = childKey.publicKey; // → TXYZ1234...
Security Model
- The master seed is injected via
XPAY_MASTER_SEEDenvironment variable — never stored in the database - Only derivation paths (integers) are stored in MySQL
- If the database is compromised, an attacker sees which addresses belong to you but cannot derive private keys
- The seed should be generated offline and backed up physically
2. Multi-Chain Transaction Detection
Each blockchain scanner runs as an independent worker with chain-specific logic:
// ChainAdapter interface — implemented per blockchain
interface ChainAdapter {
generateAddress(derivationPath: string): string;
scanTransactions(
addresses: string[],
fromBlock: number,
toBlock: number
): Promise<DetectedTransaction[]>;
getConfirmationCount(txHash: string): Promise<number>;
getLatestBlock(): Promise<number>;
}
TRON (TRC20) Scanner
TRON accounts for the majority of stablecoin volume globally. The scanner:
- Polls TRON Grid API or a local node every 3 seconds
- Uses
getTransactionsToThisfor tracked addresses - Filters by USDT/USDC contract address
- Matches amounts with pending invoices (with small tolerance for gas deductions)
// TRON scanner pseudocode
async function scanTron(pendingAddresses: string[]) {
const txs = await tronGrid.getTransactionsToThis(pendingAddresses, {
onlyConfirmed: false, // mempool detection
minTimestamp: Date.now() - 120_000 // last 2 minutes
});
for (const tx of txs) {
if (tx.contractType === 'Transfer' &&
tx.contractData.contractAddress === USDT_CONTRACT) {
await processDetection('tron', tx);
}
}
}
EVM Scanner (Ethereum, BSC, Polygon, Arbitrum, etc.)
For EVM chains, we use eth_getLogs with the Transfer event signature:
event Transfer(address indexed from, address indexed to, uint256 value);
// EVM scanner pseudocode
async function scanEvm(chain: string, rpc: string, pendingAddresses: string[]) {
const latestBlock = await getBlockNumber(rpc);
const fromBlock = latestBlock - 200; // look back 200 blocks
const logs = await ethGetLogs(rpc, {
fromBlock: toHex(fromBlock),
toBlock: toHex(latestBlock),
address: USDT_CONTRACTS[chain],
topics: [TRANSFER_EVENT_SIG, null, pendingAddresses.map(toPaddedHex)]
});
for (const log of logs) {
const tx = decodeTransferLog(log);
await processDetection(chain, tx);
}
}
3. SDK Integration Patterns
XPayLabs (xpay) provides first-party SDKs in Node.js/TypeScript and Java with identical API surfaces.
Node.js SDK Installation
npm install @xpaylabs/node-sdk
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 Payment)
const collection = await xpay.createCollection({
amount: '100.00',
symbol: 'USDT',
chain: 'tron',
orderId: 'order_20260601_001',
});
// collection.address: deposit address for customer
// collection.orderId: unique gateway order ID
// collection.expireAt: ISO timestamp after which the order expires
Create a Payout (Send Payment)
const payout = await xpay.createPayout({
amount: '50.00',
symbol: 'USDT',
chain: 'tron',
receiveAddress: 'TXYZ...recipient...',
orderId: 'payout_20260601_001'
});
Webhook Verification
import express from 'express';
const app = express();
app.post('/webhooks/xpay', express.raw({ type: 'application/json' }), (req, res) => {
try {
const event = xpay.parseWebhook(
req.body,
req.headers['x-webhook-signature'] as string
);
switch (event.type) {
case 'ORDER_SUCCESS':
console.log(`Payment received: ${event.data.orderId}`);
// Fulfill order, update database, send email
break;
case 'ORDER_FAILED':
console.log(`Payment failed: ${event.data.orderId}`);
// Notify customer, release inventory
break;
}
res.status(200).json({ received: true });
} catch (err) {
res.status(401).json({ error: 'Invalid signature' });
}
});
Java/Spring Boot SDK
<dependency>
<groupId>io.xpay</groupId>
<artifactId>xpay-java-sdk</artifactId>
<version>0.1.0</version>
</dependency>
@Configuration
public class PaymentConfig {
@Bean
public XPay xPay(@Value("${xpay.api-key}") String apiKey,
@Value("${xpay.api-secret}") String apiSecret,
@Value("${xpay.base-url}") String baseUrl) {
return new XPay(XPayConfig.builder()
.apiKey(apiKey)
.apiSecret(apiSecret)
.baseUrl(baseUrl)
.build());
}
}
@Service
public class PaymentService {
private final XPay xpay;
public PaymentService(XPay xpay) {
this.xpay = xpay;
}
public String createPayment(String orderId, BigDecimal amount) {
CreateCollectionRequest request = CreateCollectionRequest.builder()
.amount(amount.toPlainString())
.symbol("USDT")
.chain("tron")
.orderId(orderId)
.build();
ApiResponse<CollectionData> response = xpay.createCollection(request);
return response.getData().getAddress();
}
}
4. HMAC-SHA256 Request Signing (Under the Hood)
One of the most important security features is how API requests are signed. Instead of sending a bearer token, every request includes a cryptographic signature:
function generateSignature(
params: Record<string, string>,
data: Record<string, string>,
secret: string
): string {
// 1. Serialize data as data={key=value,...} sorted by key
const dataStr = 'data={' + Object.keys(data)
.sort()
.map(k => `${k}=${data[k]}`)
.join(',') + '}';
// 2. Append nonce and timestamp
const allParams = { ...params, data: dataStr };
const sorted = Object.keys(allParams)
.sort()
.map(k => `${k}=${allParams[k]}`)
.join('&');
// 3. HMAC-SHA256
return crypto
.createHmac('sha256', secret)
.update(sorted)
.digest('hex');
}
// Usage
const data = { amount: '100.00', symbol: 'USDT', chain: 'tron' };
const params = { timestamp: Date.now().toString(), nonce: crypto.randomUUID() };
};
const sign = generateSignature(params, apiSecret);
// POST /v1/order/createCollection
// Body: { sign, timestamp: params.timestamp, nonce: params.nonce, data: {...} }
The gateway validates:
- Signature: Recomputes HMAC on the server side
- Timestamp: Rejects if more than 30 seconds old
- Nonce: Rejects if already seen (Redis-backed deduplication)
5. Docker Compose Production Deployment
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MYSQL_DATABASE: xpay_gateway
volumes:
- mysql_data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
redis:
image: redis:7-alpine
command: redis-server --requirepass ${REDIS_PASSWORD}
healthcheck:
test: ["CMD", "redis-cli", "ping"]
gateway:
image: ghcr.io/xpaylabs/gateway:latest
ports:
- "8080:8080"
environment:
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/xpay_gateway
SPRING_DATASOURCE_USERNAME: xpay
SPRING_DATASOURCE_PASSWORD: ${DB_PASSWORD}
SPRING_REDIS_PASSWORD: ${REDIS_PASSWORD}
XPAY_MASTER_SEED: ${XPAY_MASTER_SEED}
XPAY_WEBHOOK_SECRET: ${XPAY_WEBHOOK_SECRET}
XPAY_API_KEY: ${XPAY_API_KEY}
XPAY_API_SECRET: ${XPAY_API_SECRET}
XPAY_CHAINS: tron,eth,bsc,polygon,arbitrum,base
depends_on:
mysql: { condition: service_healthy }
redis: { condition: service_healthy }
volumes:
mysql_data:
redis_data:
Environment Variables
| Variable | Description |
|---|---|
XPAY_MASTER_SEED |
BIP-39 mnemonic (generate offline!) |
XPAY_API_KEY |
Public API identifier |
XPAY_API_SECRET |
HMAC signing secret |
XPAY_WEBHOOK_SECRET |
HMAC secret for webhook payloads |
XPAY_CHAINS |
Comma-separated chain names to activate |
Startup
# One command to deploy
docker compose up -d
# Verify
curl http://localhost:8080/v1/symbol/supportSymbols
Cost Comparison
| BitPay | Coinbase Commerce | Self-Hosted | |
|---|---|---|---|
| Setup fee | $0–$300 | $0 | $0 (Docker) |
| Per-transaction | 0.5–1% | 1% | $0 (gas only) |
| Monthly (100k volume) | $1,000+ | $1,000+ | ~$30–50 server |
| Key custody | Theirs | Theirs | Yours |
| White-label | Premium | No | Yes |
| Annual savings vs BitPay | — | $360–$3,600 | $11,500+ |
Summary
Self-hosting a crypto payment gateway is more accessible than most developers think. The key components are:
- HD wallet derivation for deterministic, scalable address generation
- Per-chain scanners with chain-specific detection logic
- HMAC-signed API with SDKs in Node.js and Java
- Docker Compose deployment for operational simplicity
xpay SDKs (Node.js and Java), checkout UI, and documentation are open-source under MIT at github.com/yan253319066/XPayLabs. The core gateway engine is source-available under the XPay Enterprise License.
SDK packages:
- npm (Node.js)
- Maven Central (Java)
If you're processing $5,000+/month in crypto, the self-hosted approach will save you significant fees — and you'll never have to trust a third party with your keys.
Top comments (1)
Great insights! One thing many businesses overlook when choosing a cryptocurrency exchange development company is long-term scalability. Beyond launch-ready features, it's important to evaluate the company's ability to provide ongoing support, security updates, liquidity solutions, and compliance integrations as regulations evolve. We, Dappfort focus on building secure, high-performance crypto exchanges with advanced trading features, strong security architecture, and post-launch technical assistance, helping startups and enterprises launch confidently in the competitive crypto market. The right development partner should grow alongside your business, not just deliver a product.