Integration of Exchanger with Payment Systems
A crypto-exchanger without fiat payment methods is a crypto-to-crypto platform. Adding fiat payments (bank transfer, cards, e-wallets) expands the audience and increases transaction volumes. The main challenge is that each payment provider has its own KYC requirements, limits, and API.
Types of Payment Integrations
Bank Cards (Visa/Mastercard)
For accepting cards through a PSP (Payment Service Provider): Stripe, Adyen, Checkout.com. Direct connection to Visa/Mastercard requires PCI DSS certification—complex and expensive. PSPs handle compliance for you.
import Stripe from 'stripe';
const stripe = new Stripe(STRIPE_SECRET_KEY, { apiVersion: '2023-10-16' });
class CardPaymentProvider {
async createPaymentIntent(amount: number, currency: string, metadata: object) {
// amount in minimal currency units (cents for USD)
const intent = await stripe.paymentIntents.create({
amount: Math.round(amount * 100),
currency: currency.toLowerCase(),
automatic_payment_methods: { enabled: true },
metadata: {
order_id: metadata.orderId,
user_id: metadata.userId,
},
});
return {
clientSecret: intent.client_secret,
intentId: intent.id,
};
}
async handleWebhook(rawBody: Buffer, signature: string) {
const event = stripe.webhooks.constructEvent(
rawBody,
signature,
STRIPE_WEBHOOK_SECRET
);
switch (event.type) {
case 'payment_intent.succeeded':
const intent = event.data.object;
await this.onPaymentSuccess(intent.metadata.order_id, intent.amount / 100);
break;
case 'payment_intent.payment_failed':
await this.onPaymentFailed(event.data.object.metadata.order_id);
break;
}
}
}
Bank Transfers (SEPA, SWIFT)
For European users—SEPA Credit Transfer. For international—SWIFT. Integration through Banking API providers: Wise Business API, Modulr, Railsr.
class WiseProvider:
BASE_URL = 'https://api.wise.com'
def __init__(self, api_token: str, profile_id: str):
self.token = api_token
self.profile_id = profile_id
self.headers = {'Authorization': f'Bearer {api_token}'}
def create_quote(self, source_currency: str, target_currency: str, amount: float) -> dict:
"""Get a conversion quote"""
response = requests.post(f"{self.BASE_URL}/v3/profiles/{self.profile_id}/quotes",
headers=self.headers,
json={
'sourceCurrency': source_currency,
'targetCurrency': target_currency,
'sourceAmount': amount,
}
)
return response.json()
def create_recipient(self, name: str, iban: str, country: str) -> dict:
"""Add a payment recipient"""
return requests.post(f"{self.BASE_URL}/v1/accounts",
headers=self.headers,
json={
'currency': 'EUR',
'type': 'iban',
'profile': self.profile_id,
'accountHolderName': name,
'details': {'iban': iban},
}
).json()
def create_transfer(self, quote_id: str, recipient_id: str, reference: str) -> dict:
"""Create a transfer"""
return requests.post(f"{self.BASE_URL}/v1/transfers",
headers=self.headers,
json={
'targetAccount': recipient_id,
'quoteUuid': quote_id,
'customerTransactionId': reference,
'details': {'reference': reference},
}
).json()
E-wallets
QIWI, YooMoney (for RU market), PayPal, Skrill, Neteller:
class YooMoneyProvider:
def __init__(self, token: str, client_id: str):
self.token = token
self.client_id = client_id
def create_payment(self, amount: Decimal, order_id: str, return_url: str) -> str:
"""Create a payment form, return redirect URL"""
response = requests.post('https://yoomoney.ru/quickpay/confirm', data={
'receiver': RECEIVER_WALLET,
'quickpay-form': 'button',
'targets': f'Order #{order_id}',
'paymentType': 'AC', # bank card
'sum': float(amount),
'label': order_id,
'successURL': return_url,
})
return response.url
def check_payment(self, label: str) -> Optional[PaymentInfo]:
"""Check if payment has been received"""
response = requests.post('https://yoomoney.ru/api/operation-history',
headers={'Authorization': f'Bearer {self.token}'},
data={'label': label, 'type': 'deposition'}
)
operations = response.json().get('operations', [])
if operations and operations[0]['status'] == 'success':
return PaymentInfo(
amount=Decimal(str(operations[0]['amount'])),
datetime=operations[0]['datetime'],
operation_id=operations[0]['operation_id'],
)
return None
Provider Abstraction
To support multiple providers, you need a unified abstraction:
from abc import ABC, abstractmethod
class PaymentProvider(ABC):
@abstractmethod
async def create_payment(self, order: ExchangeOrder) -> PaymentLink:
"""Create a payment link/form"""
pass
@abstractmethod
async def check_payment_status(self, payment_id: str) -> PaymentStatus:
"""Check payment status"""
pass
@abstractmethod
async def refund(self, payment_id: str, amount: Decimal) -> bool:
"""Refund funds"""
pass
class PaymentRouter:
def __init__(self, providers: dict[str, PaymentProvider]):
self.providers = providers
async def create_payment(self, order: ExchangeOrder, method: str) -> PaymentLink:
provider = self.providers.get(method)
if not provider:
raise ValueError(f"Payment method not supported: {method}")
# Check method limits
limits = PAYMENT_LIMITS[method]
if order.amount < limits.min or order.amount > limits.max:
raise ValueError(f"Amount out of range for {method}")
return await provider.create_payment(order)
Limits and KYC Thresholds
PAYMENT_LIMITS = {
'card': PaymentLimits(
min=Decimal('10'),
max_no_kyc=Decimal('500'), # up to 500 EUR without KYC
max_basic_kyc=Decimal('2000'),
max_full_kyc=Decimal('50000'),
daily_no_kyc=Decimal('1000'),
),
'bank_transfer': PaymentLimits(
min=Decimal('50'),
max_no_kyc=Decimal('0'), # bank transfer always requires KYC
max_basic_kyc=Decimal('5000'),
max_full_kyc=Decimal('100000'),
),
'yoomoney': PaymentLimits(
min=Decimal('100'),
max_no_kyc=Decimal('15000'), # in rubles
max_basic_kyc=Decimal('100000'),
),
}
def get_required_kyc_tier(method: str, amount: Decimal) -> KYCTier:
limits = PAYMENT_LIMITS[method]
if amount <= limits.max_no_kyc:
return KYCTier.NONE
elif amount <= limits.max_basic_kyc:
return KYCTier.BASIC
else:
return KYCTier.FULL
Monitoring and Reconciliation
-- Daily reconciliation: volumes by payment method
SELECT
payment_method,
COUNT(*) AS transactions,
SUM(amount) AS total_volume,
SUM(CASE WHEN status = 'completed' THEN amount ELSE 0 END) AS completed_volume,
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) AS failed_count,
SUM(CASE WHEN status = 'refunded' THEN amount ELSE 0 END) AS refunded_volume
FROM payments
WHERE created_at::date = CURRENT_DATE - INTERVAL '1 day'
GROUP BY payment_method;
Integration Timeline
| Provider | Integration Time |
|---|---|
| Stripe (cards) | 1–2 weeks |
| Wise (SEPA/SWIFT) | 2–3 weeks |
| QIWI/YooMoney | 1–2 weeks |
| PayPal | 1–2 weeks |
| Each additional | 1 week |
Full multi-provider system with abstraction, KYC limits, and reconciliation: 6–8 weeks.







