Live mid-market rates
Rates come from public mid-market feeds and are cross-calculated through a USD base, so any of the 166 currencies can quote against any other.
166 × 165 pairsA drop-in converter widget for money-transfer products. Type an amount and the rate, the fee and the recipient's total resolve in the same frame — no spinners, no round-trip per keystroke, no framework.
Every corridor your product needs
Currencies, with flags and names
Runtime dependencies
Minified widget payload
Network call per session, not per keystroke
Not a toy converter. This is the quoting surface of a remittance product — the part customers stare at before they trust you with their money.
Rates come from public mid-market feeds and are cross-calculated through a USD base, so any of the 166 currencies can quote against any other.
166 × 165 pairsType what you want to send, or type what the recipient must receive. The other field solves for it instantly, with the spread applied in the correct direction.
bidirectionalPeople know "Philippines", not "PHP". Every currency is indexed against the countries that use it, so both spellings land on the same result.
country indexVector flags for 161 currencies, lazy-loaded from a CDN, with a typographic monogram fallback for supranational codes like XOF and XDR.
SVG · lazyThe margin, the amount actually converted and the guaranteed rate are printed above the button — the transparency pattern that wins fintech trust.
0.48% spreadTwo independent rate providers, a six-hour local cache and a bundled offline snapshot. If every feed is down, the widget still quotes and says so.
3-tier failoverOne data-currencies attribute restricts the picker to the corridors you
are licensed for. Leave it off and you get all 166.
The pickers are proper comboboxes: arrow keys to move, Enter to pick, Escape to dismiss, with labelled inputs and live ARIA state throughout.
combobox · listboxIt is one <div> and two script tags. No bundler, no npm install, no
React version to satisfy — WordPress, Webflow, Laravel or Next.js all the same.
Most transfer tools hide their profit inside a worse exchange rate, so the customer can never work out what they paid. This widget does the opposite: it quotes the real mid-market rate, then shows the margin as a separate line item.
Illustrative comparison. The 0.48% column is this widget's configured spread; the bank column uses a 2.5% markup, a figure commonly published in retail FX research.
Copy the widget block and two script tags into your page. There is no build step and nothing to install — it renders the moment the parser reaches it.
List the currencies you are licensed to serve in data-currencies, and set
the opening pair on the two currency buttons. That is the whole configuration.
The quote button is yours: hand the send amount, receive amount and locked rate to your onboarding flow, your CRM, or a checkout — whatever comes next.
Keystrokes are parsed and normalised locally — no network on the typing path.
Provider → cache → offline snapshot. The first tier that answers wins.
The 0.48% margin is applied and broken out as its own visible line.
Both amounts, the guaranteed rate and the savings estimate paint together.
The whole widget is vanilla JavaScript in an IIFE — nothing leaks into the global
scope but a single FXConverter handle for you to drive it from.
<!-- 1. drop the widget wherever the quote belongs -->
<div class="widget_container" id="widget_container"
data-currencies="USD,EUR,GBP,CAD,AUD,BDT,INR,PKR,PHP,NGN">
<!-- markup from snippet.html -->
</div>
<!-- 2. two scripts, no bundler, no npm install -->
<script src="assets/js/currency-data.js"></script>
<script src="assets/js/converter.js"></script>
/* Offer every currency the rate feed returns — omit the attribute. */
<div class="widget_container"> … </div>
/* …or restrict it to the corridors you are licensed for. */
<div class="widget_container"
data-currencies="CAD,BDT,INR,PKR,PHP"> … </div>
/* The opening pair lives on the two currency buttons. */
<div class="input_holder_right" data-selected_currency="CAD">
<div class="input_holder_right" data-selected_currency="BDT">
/* Every extra is optional. Ship without them and the widget
falls back to the original 2022 layout, untouched. */
<button data-swap> // swap the pair
<b data-fee> // margin line
<b data-quoted-rate> // locked rate line
<span data-freshness> // live / cached / offline badge
// The commercial numbers live in one object — change them here.
var CONFIG = {
spread: 0.0048, // 0.48% margin off the send amount
inverse_spread: 0.00482315, // exact inverse, for reverse quotes
bank_markup: 0.025, // benchmark for the savings estimate
rate_decimals: 4,
cache_ttl: 6 * 60 * 60 * 1000,
popular: ["USD", "EUR", "GBP", "CAD", "BDT"]
};
// Forward quote
receive = send * (1 - CONFIG.spread) * rate;
// Reverse quote — the inverse spread keeps both directions consistent
send = receive * (1 + CONFIG.inverse_spread) / rate;
// Providers are tried in order. The widget never renders broken.
var providers = [
{ name: "exchangerate-api", url: "https://open.er-api.com/v6/latest/USD" },
{ name: "currency-api", url: "https://cdn.jsdelivr.net/…/usd.json" }
];
// live provider → localStorage cache → bundled snapshot
FXConverter.engine.load().then(function (table) {
console.log(table.source); // "live" | "cache" | "offline"
console.log(table.provider); // which feed answered
});
// Any pair cross-calculates through the USD base
FXConverter.engine.rate("CAD", "BDT"); // => 88.12…
The original build fetched two rate tables on every input event. The engine now resolves one USD-based table per session and cross-calculates locally.
The class names, data-role hooks and pricing maths from the 2022 deliverable
are untouched, so the original markup still runs on the new engine.
Everything is wrapped in an IIFE. The only export is window.FXConverter,
which exposes the rate engine and the pricing config.
Try it the way your customers will — search "Philippines", "taka" or "NGN" and watch the same index the widget's picker uses do the work.
One of the first serious front-end builds I took on as a freelancer — and still the one I reach for when a client asks what production-grade vanilla JavaScript looks like.
data-* attributes so the client could reconfigure it themselves.
Front-end engineer · fintech & product interfaces
Quote widgets, pricing tables, onboarding flows, dashboards — the interfaces that carry the money and the doubt. I care about the parts users feel but never name: the field that recalculates before you finish typing, the dropdown that finds "Philippines" when you meant PHP, the state that still renders when the API is down.
Public mid-market feeds — exchangerate-api first, a jsDelivr-hosted currency API as the backup. Both are keyless and CORS-enabled, so there is no secret to leak in front-end code. Swapping in a paid or in-house feed means editing one array.
The widget falls back to a six-hour local cache, and then to a rate snapshot bundled with the file itself. The badge above the fields changes from "live" to "cached" or "offline snapshot" so nobody is quoted a stale number without being told.
Yes — they are three numbers at the top of converter.js: the spread, its
inverse for reverse quoting, and the bank markup used for the savings line. Nothing
else in the file needs to change.
All three. It is a plain <div> plus two script tags with no global
pollution and no framework assumptions. In React, render the markup and let the
script initialise it on mount — there is no virtual-DOM conflict because the widget
owns only its own subtree.
Put a comma-separated list in data-currencies on the widget container.
Anything outside that list disappears from both pickers and from search. Leave the
attribute off entirely and the widget offers every currency the feed returns.
No. FXKit is the front-end quoting surface only — a portfolio demonstration of the interface layer. It moves no money, holds no funds and is not a licensed payments provider. The rates shown are indicative and are not an offer to transact.
That is what this page is for. Quote widgets, pricing calculators, onboarding flows, dashboards — in vanilla JavaScript when the constraint calls for it, or React and TypeScript when the product is bigger. Tell me what the screen has to do and I will come back with an approach.
Send me the brief — even a rough one. You will get a straight answer about scope, approach and whether I am the right person for it.