/**
* 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}`));