Developing Crypto Invoicing
Crypto invoicing is not "accepting payment in crypto". This is creating a system where a customer receives an invoice for a specific fiat amount (or crypto amount), pays it in one of the supported currencies, and the vendor receives confirmation with correctly matched invoice. The main engineering problem is volatility: if a customer sees an invoice for $500 and ETH moves 3% while they transfer, you need to decide whose problem that is and how to handle it.
Invoice Pricing Models
Fixed crypto amount: invoice for 0.5 ETH. Customer pays exactly 0.5 ETH. No volatility — fiat volatility is entirely on vendor. Suitable for crypto-native B2B settlements.
Fixed fiat amount with lock-in: invoice for $500, system converts to crypto amount at creation time and fixes it for 15–30 minutes. Customer must pay the fixed crypto amount within window. If window expires — recalculate exchange rate. This is the most common model.
Floating with tolerance: accept payment in range ±1–2% of expected amount. Minor discrepancies from network fees or insignificant price movement don't block payment. Underpayment policy: credit at paid amount or require top-up — configurable by merchant.
System Architecture
Invoice Lifecycle
DRAFT → PENDING_PAYMENT (address assigned, timer started)
→ PARTIALLY_PAID (partial amount received)
→ PAID (full amount received, awaiting confirmations)
→ CONFIRMED (N confirmations)
→ EXPIRED (expired without payment)
→ OVERPAID (received more — requires manual decision)
interface Invoice {
id: string;
merchantId: string;
fiatAmount: Decimal;
fiatCurrency: 'USD' | 'EUR' | 'GBP';
cryptoAmount: Decimal;
cryptoCurrency: 'ETH' | 'USDT' | 'USDC' | 'BTC';
depositAddress: string;
exchangeRateLockedAt: Date;
expiresAt: Date;
status: InvoiceStatus;
paidAmount: Decimal;
txHashes: string[];
}
Address Generation
For each invoice — unique address derived from HD wallet xpub. This allows unambiguous matching of incoming payment to invoice without memo/tags:
function deriveInvoiceAddress(
xpub: string,
invoiceIndex: number,
network: Network
): string {
const node = HDNodeWallet.fromExtendedKey(xpub);
// path: m/44'/60'/0'/0/{invoiceIndex} for EVM
return node.deriveChild(invoiceIndex).address;
}
For Bitcoin — native SegWit (bech32) addresses via BIP84 derivation. For TRON USDT — same logic, separate xpub for TRC-20 namespace.
Monitoring Incoming Payments
EVM networks: subscribe via WebSocket eth_subscribe("logs") to Transfer events for ERC-20 tokens, filter by active deposit address list. For native ETH — monitor via eth_subscribe("newHeads") + eth_getTransactionReceipt.
const monitorERC20Transfers = async (
activeAddresses: Set<string>,
provider: WebSocketProvider
) => {
const filter = {
topics: [
ethers.id("Transfer(address,address,uint256)"),
null,
[...activeAddresses].map(addr => ethers.zeroPadValue(addr, 32))
]
};
provider.on(filter, async (log) => {
const invoiceAddress = ethers.getAddress('0x' + log.topics[2].slice(26));
const amount = BigInt(log.data);
await handleIncomingPayment(invoiceAddress, amount, log.transactionHash);
});
};
Multi-Currency and Exchange Rates
For inbound rate conversion — price aggregation from multiple sources with anomaly protection:
class PriceAggregator:
SOURCES = ['binance', 'coinbase', 'kraken']
MAX_DEVIATION_PCT = 1.0 # if one source deviates > 1% from median
async def get_price(self, base: str, quote: str) -> Decimal:
prices = await asyncio.gather(*[
self.fetch_price(source, base, quote)
for source in self.SOURCES
])
valid_prices = [p for p in prices if p is not None]
median = statistics.median(valid_prices)
# Filter anomalous values
filtered = [
p for p in valid_prices
if abs(p - median) / median * 100 < self.MAX_DEVIATION_PCT
]
return Decimal(str(statistics.mean(filtered)))
Webhooks and Merchant Integration
Merchants receive invoice status notifications via signed webhooks:
function signWebhookPayload(payload: object, secret: string): string {
const body = JSON.stringify(payload);
const timestamp = Math.floor(Date.now() / 1000);
const signature = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${body}`)
.digest('hex');
return `t=${timestamp},v1=${signature}`;
}
Recipient verifies signature and timestamp (replay attack protection — reject events older than 5 minutes). Same scheme as Stripe.
Retry policy for failed deliveries: exponential backoff (1min → 5min → 30min → 2h → 24h), after 5 failed attempts — alert in admin panel.
Accounting and Reporting
For B2B applications, automatic generation of:
- PDF invoice with fiat amount, crypto amount, exchange rate at creation, txHash
- CSV export of transactions with fiat equivalent for accountant
- Accounting API integration (Xero, QuickBooks) via their REST API
Fiat equivalent for tax reporting is calculated at transaction confirmation rate — fixed and stored with invoice unchanged.
Stack and Deployment
- Backend: Node.js/TypeScript or Go, REST API + WebSocket for statuses
- Queue: BullMQ (Redis) for confirmation workers and webhook delivery
- DB: PostgreSQL (invoices, transactions) + Redis (price cache, active addresses)
- Nodes: Alchemy/QuickNode with failover or own nodes for high volumes
MVP with ETH, USDT (ERC-20), USDC support and basic merchant portal — 3–4 weeks. Full system with multi-currency, reporting, and partner APIs — 8–10 weeks.







