ChainGate lets merchants accept cryptocurrency payments directly into their own wallet, with no Stripe, no Coinbase Commerce, no third-party custody, and none of the 1-1.5% fees those gateways charge. It’s an Express.js API that issues a unique deposit address per invoice, watches four EVM chains for incoming funds, and notifies your app the moment a payment confirms.
Receives money, can’t spend it
The server only ever holds an extended public key (xpub). It derives a fresh deposit address for every invoice but is cryptographically incapable of signing a transaction or moving funds, so your private keys never leave your hardware wallet.
Tech Stack
Node.js · TypeScript · Express.js · ethers.js · Prisma · PostgreSQL · Docker · Jest
How It Works
A merchant creates an invoice; the API returns a one-time deposit address derived from the merchant’s xpub. The customer pays it, the block scanner detects the transaction, and once it reaches the chain’s confirmation threshold a signed webhook fires back to the merchant’s app.
curl -X POST https://api.example.com/api/v1/invoices \
-H "Content-Type: application/json" \
-H "X-API-Key: your-api-key" \
-d '{ "chainId": "ethereum", "amount": "0.05", "currency": "ETH", "expiresIn": 3600 }'{
"id": "uuid",
"status": "created",
"chainId": "ethereum",
"depositAddress": "0x…",
"amount": "0.05",
"currency": "ETH",
"expiresAt": "2026-01-01T01:00:00.000Z"
}Key Features
- Self-custody address derivation from an xpub, so the server can receive funds but never spend them
- Parallel block scanner across all four chains that confirms payments within one block
- Direct-to-wallet settlement with no gateway fees
- HMAC-signed webhooks with automatic retries
- Full invoice lifecycle tracking, including expired, overpaid, and underpaid states
- Production setup with Zod validation, Prisma, PostgreSQL, Swagger docs, and Docker
Supported Chains
| Chain | Chain ID | Confirmations | Block Time |
|---|---|---|---|
| Ethereum | 1 | 12 | ~12s |
| Polygon | 137 | 30 | ~2s |
| BSC | 56 | 15 | ~3s |
| Arbitrum | 42161 | 1 | ~250ms |
Each chain activates only when its RPC URL is configured, so merchants run exactly the networks they need.
Verifying Webhooks
Every webhook is signed so the merchant can confirm it really came from ChainGate:
const crypto = require("crypto");
function verifyWebhook(body, secret, signature) {
const expected = crypto.createHmac("sha256", secret).update(body).digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(signature, "hex"),
);
}