Development of Crypto-to-Fiat Card Conversion System
User holds USDC, wants to pay with a card at a regular store. At the moment of card payment: USDC is debited from crypto balance, converted to USD/EUR, and card processing proceeds as normal fiat payment. From the terminal's perspective — regular Visa/Mastercard transaction. This is not theory, it's working architecture behind products like Crypto.com Card, Binance Card, Coinbase Card.
Building such a system from scratch means solving several independent engineering tasks: card issuance, real-time conversion, BIN sponsorship and licensing, compliance. Each is a separate workstream.
Architectural Components
BIN Sponsorship and Card Issuance
BIN (Bank Identification Number) — first 6-8 digits of card, determine issuer. Without own banking license you work through BIN sponsor: licensed bank-issuer that issues cards under its BIN, but by your logic. You are the program manager.
Main BIN sponsors and card issuing platforms:
| Provider | Networks | Regions | API |
|---|---|---|---|
| Marqeta | Visa, Mastercard | US, EU, UK | REST + Webhooks |
| Galileo | Visa | US, LATAM | REST |
| Stripe Issuing | Visa, Mastercard | US, EU | REST |
| Moorwand | Mastercard | EU/EEA | REST |
| Railsbank (Railsr) | Visa, Mastercard | EU, UK | REST |
Marqeta — most common in crypto-card projects (Coinbase Card uses Marqeta). Just-In-Time (JIT) funding — key feature: Marqeta calls your webhook at authorization moment, you confirm or decline transaction with real-time conversion.
Just-In-Time Funding: Real-Time Logic
JIT — heart of architecture. Scheme:
Card terminal → Visa/MC network → Marqeta → your JIT webhook (< 2 sec) →
[you convert crypto → fiat] → respond approve/decline → Marqeta → terminal
JIT webhook response time: strictly less than 2 seconds. This is hardware timeout of card networks. Missed — transaction auto-declines.
// JIT webhook handler
import { FastifyRequest, FastifyReply } from 'fastify';
import { MarqetaJITPayload } from './types';
export async function handleJITFunding(
req: FastifyRequest<{ Body: MarqetaJITPayload }>,
reply: FastifyReply,
) {
const startTime = Date.now();
const { transaction, card_token } = req.body;
try {
// 1. User identification by card_token
const user = await userService.findByCardToken(card_token);
if (!user) return reply.send({ result: 'DECLINED', memo: 'USER_NOT_FOUND' });
// 2. Get amount in fiat currency
const { currency_code, amount } = transaction;
// 3. Calculate crypto amount for deduction
const cryptoAmount = await pricingService.fiatToCrypto({
fiatAmount: amount,
fiatCurrency: currency_code,
cryptoCurrency: user.primaryAsset, // USDC, BTC, ETH
});
// 4. Check balance
const balance = await walletService.getBalance(user.id, user.primaryAsset);
if (balance < cryptoAmount.amountWithFee) {
return reply.send({ result: 'DECLINED', memo: 'INSUFFICIENT_FUNDS' });
}
// 5. Reserve crypto (hold, don't deduct yet)
const holdId = await walletService.createHold({
userId: user.id,
asset: user.primaryAsset,
amount: cryptoAmount.amountWithFee,
expiresAt: new Date(Date.now() + 30 * 60 * 1000), // 30 min
transactionRef: transaction.token,
});
// Check we're within timeout
if (Date.now() - startTime > 1500) {
await walletService.releaseHold(holdId);
return reply.send({ result: 'DECLINED', memo: 'TIMEOUT' });
}
return reply.send({
result: 'APPROVED',
funding: { amount, currency_code },
});
} catch (error) {
logger.error({ error, transaction }, 'JIT funding error');
return reply.send({ result: 'DECLINED', memo: 'INTERNAL_ERROR' });
}
}
Hold mechanism is critical. Card authorization and actual deduction (clearing) are different events. Up to 5 days can pass between them (especially for offline transactions). Hold reserves crypto balance, real conversion happens at clearing.
Crypto to Fiat Conversion
At clearing moment you need to actually convert cryptocurrency to fiat to top up card account. Two approaches:
Pre-funded fiat float. You hold fiat balance on BIN sponsor's account. At clearing — deduct from float, in parallel convert crypto and top up float. Advantage: decoupling from conversion speed. Disadvantage: need significant working capital (float).
Real-time liquidation. At clearing immediately sell crypto via CEX or liquidity provider. Money goes directly to card account.
// Clearing handler (called webhook from Marqeta at settlement)
export async function handleClearing(clearingEvent: MarqetaClearingEvent) {
const { original_jit_funding_token, clearing_amount, currency_code } = clearingEvent;
// Find original hold by JIT token
const hold = await holdRepository.findByTransactionRef(original_jit_funding_token);
// Calculate final crypto amount (rate may have changed)
const finalCryptoAmount = await pricingService.fiatToCrypto({
fiatAmount: clearing_amount,
fiatCurrency: currency_code,
cryptoCurrency: hold.asset,
slippage: 0.005, // 0.5% slippage tolerance
});
// Execute conversion through liquidity provider
const conversion = await liquidityService.convert({
fromAsset: hold.asset,
toFiat: currency_code,
amount: finalCryptoAmount.amount,
destinationAccount: BIN_SPONSOR_ACCOUNT_ID,
});
// Final deduction from user's crypto balance
await walletService.finalizeConversion({
userId: hold.userId,
holdId: hold.id,
cryptoAmount: finalCryptoAmount.amount,
fiatAmount: clearing_amount,
conversionRate: conversion.rate,
fee: finalCryptoAmount.fee,
});
// User notification
await notificationService.sendPushNotification(hold.userId, {
type: 'card_payment_settled',
amount: clearing_amount,
currency: currency_code,
cryptoSpent: finalCryptoAmount.amount,
cryptoAsset: hold.asset,
merchantName: clearingEvent.merchant_name,
});
}
Liquidity Providers and FX
For automatic crypto to fiat conversion:
CEX via API: Binance, Coinbase Prime, Kraken. Suitable for medium volumes. Risk: API latency, exchange might be unavailable. Need failover to second provider.
OTC / Prime Brokers: Galaxy Digital, FalconX, Cumberland. For large volumes ($100k+) offer better rates, work via API or RFQ (Request for Quote). Minimum deal usually $10k–$50k — not suitable for retail transactions directly, but good for float top-up.
Embedded liquidity: integration with Fireblocks Settlement Network or B2C2. Programmatic access, fixed spreads, enterprise SLA.
// Abstraction for multi-provider liquidity
interface LiquidityProvider {
getQuote(params: QuoteParams): Promise<Quote>;
executeConversion(quoteId: string): Promise<ConversionResult>;
getBalance(currency: string): Promise<Decimal>;
}
class LiquidityRouter implements LiquidityProvider {
private providers: LiquidityProvider[];
async getQuote(params: QuoteParams): Promise<Quote> {
// Request quotes from all providers in parallel
const quotes = await Promise.allSettled(
this.providers.map(p => p.getQuote(params))
);
// Select best quote (by rate accounting for fee)
const validQuotes = quotes
.filter(q => q.status === 'fulfilled')
.map(q => (q as PromiseFulfilledResult<Quote>).value);
return validQuotes.sort((a, b) => b.netRate - a.netRate)[0];
}
}
Compliance and KYC/AML
Mandatory Components
KYC (Know Your Customer). Mandatory for card product. Minimum: identity verification (passport/ID), proof of address, OFAC screening. Providers: Sumsub, Jumio, Onfido. Verification levels affect limits (basic: $500/day, enhanced: $10,000/day).
Transaction monitoring. AML requirements: monitoring suspicious patterns, structuring detection (splitting into small amounts), unusual merchant categories. Providers: Chainalysis, Elliptic for crypto side; ComplyAdvantage for fiat side.
Licensing. In EU: EMI license (Electronic Money Institution) or partnership with licensed EMI. In US: Money Transmitter Licence in each state, or partnership with licensed issuer. License obtainment: 6–18 months, $100k–$500k. Alternative: operating under BIN sponsor's license (faster, but more limited).
Tax Implications for Users
Crypto conversion at card payment is taxable event in many jurisdictions (US, UK, most EU). System should:
// Generate tax-lot records for each conversion
interface TaxLot {
userId: string;
asset: string;
acquiredDate: Date;
acquiredCostBasis: Decimal; // purchase price in fiat
disposedDate: Date;
disposalProceeds: Decimal; // transaction amount by card
gainLoss: Decimal; // profit/loss
transactionRef: string;
}
Providing tax report (1099 in US, similar in EU) — not optional for licensed operations.
Card Mechanics: Physical and Virtual
Virtual cards — issued instantly via Marqeta API, used for Apple Pay / Google Pay. Priority for fast MVP.
Physical cards — need card personalisation bureau (Matica, Entrust). Production time: 5–14 days. Delivery tracking integration.
// Marqeta: creating virtual card
const card = await marqeta.cards.create({
user_token: marqetaUserId,
card_product_token: CARD_PRODUCT_TOKEN,
fulfillment: { shipping: { method: 'GROUND' } }, // for physical
});
// Provisioning to Apple Pay / Google Pay
const tokenizationData = await marqeta.digitalWallets.provisionApplePay({
card_token: card.token,
provisioning_payload: applePayProvisioningRequest,
certificates: [...],
});
Freeze/unfreeze — user should instantly freeze card via app. Marqeta API: cards/{token}/transitions with state: SUSPENDED.
Development Phases and Timeline
| Phase | Content | Timeline |
|---|---|---|
| Setup & licensing | BIN sponsor choice, legal structure, KYC provider | 4–8 weeks |
| Core wallet | Crypto wallet, balances, holds | 3–4 weeks |
| JIT funding | Webhook, pricing engine, hold mechanism | 3–4 weeks |
| Liquidity | FX integration, clearing conversion | 2–3 weeks |
| Card management | Virtual cards, Marqeta integration | 3–4 weeks |
| Compliance | AML monitoring, tax reporting | 3–4 weeks |
| Physical cards | Production, delivery | 2–3 weeks |
| Testing & launch | End-to-end, UAT, soft launch | 3–4 weeks |
Realistic timeline from zero to working product with virtual cards: 6–9 months. Main blockers — legal/compliance onboarding with BIN sponsor and KYC provider, not development.







