/** * Remit — Node.js backend * Connects your frontend to Currencycloud for real FX rates and transfers. * * Setup: * npm install express axios cors dotenv * Create a .env file (see below), then: node remit-server.js * * .env file contents: * CC_LOGIN_ID=your_currencycloud_login_id * CC_API_KEY=your_currencycloud_api_key * CC_ENV=demo # use 'demo' for sandbox, 'live' for production * PORT=3001 */ require('dotenv').config(); const express = require('express'); const axios = require('axios'); const cors = require('cors'); const app = express(); app.use(express.json()); app.use(cors({ origin: 'https://your-website.com' })); // replace with your domain // ─── Currencycloud base URLs ─────────────────────────────────────────────── const CC_BASE = process.env.CC_ENV === 'live' ? 'https://api.currencycloud.com' : 'https://devapi.currencycloud.com'; // ─── Auth token cache ────────────────────────────────────────────────────── // Currencycloud tokens expire after 30 minutes — we cache and reuse them. let ccToken = null; let ccTokenTime = 0; async function getCCToken() { const AGE_MS = 25 * 60 * 1000; // refresh every 25 min (token lasts 30) if (ccToken && Date.now() - ccTokenTime < AGE_MS) return ccToken; const res = await axios.post(`${CC_BASE}/v2/authenticate/api`, { login_id: process.env.CC_LOGIN_ID, api_key: process.env.CC_API_KEY, }); ccToken = res.data.auth_token; ccTokenTime = Date.now(); return ccToken; } function ccHeaders(token) { return { 'X-Auth-Token': token, 'Content-Type': 'application/json', }; } // ─── GET /api/rates ──────────────────────────────────────────────────────── // Returns live exchange rates from Currencycloud. // Query params: base (e.g. GBP), currencies (comma-separated, e.g. EUR,USD,INR) app.get('/api/rates', async (req, res) => { try { const token = await getCCToken(); const base = req.query.base || 'GBP'; const currency = req.query.currencies || 'EUR,USD,INR,PHP,NGN,MXN,PKR,BDT,GHS'; const { data } = await axios.get(`${CC_BASE}/v2/rates/find`, { headers: ccHeaders(token), params: { client_sell_currency: base, client_buy_currency: currency, // Currencycloud accepts comma-separated }, }); // Normalise to { EUR: 1.172, USD: 1.268, ... } const rates = {}; (data.rates || []).forEach(r => { rates[r.client_buy_currency] = parseFloat(r.client_rate); }); res.json({ base, rates }); } catch (err) { console.error('Rates error:', err.response?.data || err.message); res.status(502).json({ error: 'Could not fetch rates' }); } }); // ─── POST /api/transfer ──────────────────────────────────────────────────── // Submits a real transfer via Currencycloud. // Body: { sellCurrency, buyCurrency, amount, recipientName, accountNumber, reason } app.post('/api/transfer', async (req, res) => { const { sellCurrency, buyCurrency, amount, recipientName, accountNumber, reason, userId, // your internal user ID — link to your KYC-verified user } = req.body; // Basic server-side validation if (!sellCurrency || !buyCurrency || !amount || amount < 1) { return res.status(400).json({ error: 'Invalid transfer details' }); } if (!recipientName || !accountNumber) { return res.status(400).json({ error: 'Recipient details required' }); } try { const token = await getCCToken(); // Step 1: Create a conversion (lock in the rate) const convRes = await axios.post( `${CC_BASE}/v2/conversions/create`, { sell_currency: sellCurrency, buy_currency: buyCurrency, fixed_side: 'sell', amount: amount.toString(), reason: reason || 'Personal transfer', term_agreement: true, }, { headers: ccHeaders(token) } ); const conversion = convRes.data; // Step 2: Create the payment const payRes = await axios.post( `${CC_BASE}/v2/payments/create`, { currency: buyCurrency, beneficiary_id: accountNumber, // In production: use a stored Currencycloud beneficiary ID amount: conversion.client_buy_amount, reason: reason || 'Personal transfer', reference: `REMIT-${Date.now()}`, conversion_id: conversion.id, payment_type: 'regular', }, { headers: ccHeaders(token) } ); const payment = payRes.data; res.json({ success: true, reference: payment.reference, status: payment.status, amount_sent: conversion.client_sell_amount, amount_recv: conversion.client_buy_amount, rate: conversion.client_rate, settles_at: conversion.settlement_date, }); } catch (err) { console.error('Transfer error:', err.response?.data || err.message); res.status(502).json({ error: 'Transfer failed', detail: err.response?.data?.error_messages }); } }); // ─── GET /api/history ───────────────────────────────────────────────────── // Returns a user's past transfers from Currencycloud. // In production, filter by your internal userId to scope results. app.get('/api/history', async (req, res) => { try { const token = await getCCToken(); const { data } = await axios.get(`${CC_BASE}/v2/payments/find`, { headers: ccHeaders(token), params: { per_page: 25, order: 'created_at', order_asc_desc: 'desc' }, }); const transfers = (data.payments || []).map(p => ({ id: p.id, reference: p.reference, currency: p.currency, amount: p.amount, status: p.status, created_at: p.created_at, reason: p.reason, })); res.json({ transfers }); } catch (err) { console.error('History error:', err.response?.data || err.message); res.status(502).json({ error: 'Could not load history' }); } }); // ─── POST /api/recipients ───────────────────────────────────────────────── // Creates a Currencycloud beneficiary (recipient) so you can pay them later. app.post('/api/recipients', async (req, res) => { const { name, accountNumber, bankCountry, currency } = req.body; if (!name || !accountNumber || !bankCountry || !currency) { return res.status(400).json({ error: 'Missing recipient details' }); } try { const token = await getCCToken(); const { data } = await axios.post( `${CC_BASE}/v2/beneficiaries/create`, { name, bank_account_holder_name: name, bank_country: bankCountry, currency, account_number: accountNumber, }, { headers: ccHeaders(token) } ); res.json({ success: true, beneficiary_id: data.id, name: data.name }); } catch (err) { console.error('Recipient error:', err.response?.data || err.message); res.status(502).json({ error: 'Could not save recipient', detail: err.response?.data?.error_messages }); } }); // ─── Health check ────────────────────────────────────────────────────────── app.get('/health', (_, res) => res.json({ ok: true })); // ─── Start ───────────────────────────────────────────────────────────────── const PORT = process.env.PORT || 3001; app.listen(PORT, () => console.log(`Remit backend running on port ${PORT}`));