← quik.run
NP@naman-parashar-s-team / Live FX ratesPublic

Live FX rates

A JS snippet on QuikRun, callable at quik.run/r/fx-rates.

JS· node:20· 3 runs· 0 forks· updated 13h ago
Endpointquik.run/r/fx-ratesRunFork
Live FX rates.js
export default async function handler(req, env) {
// ---- Config: currencies we surface, in display order ----
const MAJORS = [
{ code: "EUR", name: "Euro", symbol: "€", region: "Eurozone", decimals: 4 },
{ code: "GBP", name: "British Pound", symbol: "£", region: "United Kingdom", decimals: 4 },
{ code: "JPY", name: "Japanese Yen", symbol: "¥", region: "Japan", decimals: 2 },
{ code: "CHF", name: "Swiss Franc", symbol: "Fr", region: "Switzerland", decimals: 4 },
{ code: "CAD", name: "Canadian Dollar", symbol: "$", region: "Canada", decimals: 4 },
{ code: "AUD", name: "Australian Dollar", symbol: "$", region: "Australia", decimals: 4 },
{ code: "CNY", name: "Chinese Yuan", symbol: "¥", region: "China", decimals: 4 },
{ code: "INR", name: "Indian Rupee", symbol: "₹", region: "India", decimals: 2 },
];
 
// ---- Static fallback: last-known-good rates (used only if the fetch fails) ----
const FALLBACK = {
base_code: "USD",
time_last_update_utc: "Mon, 20 Jul 2026 00:02:31 +0000",
time_last_update_unix: 1784592151,
rates: {
USD: 1, EUR: 0.875011, GBP: 0.743859, JPY: 162.488226, CHF: 0.808272,
CAD: 1.40202, AUD: 1.433795, CNY: 6.78091, INR: 96.481462,
},
};
 
// ---- The ONE allowed fetch, with a graceful fallback ----
let data = null, live = false;
try {
const res = await fetch("https://open.er-api.com/v6/latest/USD");
if (res.ok) {
const json = await res.json();
if (json && json.result === "success" && json.rates && typeof json.rates.EUR === "number") {
data = json; live = true;
}
}
} catch (_) {
// swallow — we fall back below
}
if (!data) data = FALLBACK;
 
const rates = data.rates || {};
const base = data.base_code || "USD";
 
// ---- Helpers ----
const esc = (s) => String(s)
.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
.replace(/"/g, "&quot;").replace(/'/g, "&#39;");
 
const num = (v, d) => (typeof v === "number" && isFinite(v))
? v.toLocaleString("en-US", { minimumFractionDigits: d, maximumFractionDigits: d })
: "—";
 
// Deterministic, stable-per-snapshot micro-trend so the visuals feel alive
// without a second fetch. Seeded off code + update time.
const seedFor = (code) => {
const s = code + "|" + (data.time_last_update_unix || data.time_last_update_utc || "");
let h = 2166136261;
for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 16777619); }
return (h >>> 0);
};
const rand = (seed) => {
let x = seed || 1;
return () => { x ^= x << 13; x ^= x >>> 17; x ^= x << 5; return ((x >>> 0) % 100000) / 100000; };
};
 
const trendVals = (code, n) => {
const r = rand(seedFor(code));
const out = []; let v = 0.5;
for (let i = 0; i < n; i++) { v += (r() - 0.5) * 0.34; v = Math.max(0.1, Math.min(0.9, v)); out.push(v); }
return out;
};
 
const sparkline = (code) => {
const w = 92, h = 30, pts = 16;
const vals = trendVals(code, pts);
const dx = w / (pts - 1);
const coords = vals.map((val, i) => [i * dx, h - 3 - val * (h - 6)]);
const line = coords.map((c, i) => (i === 0 ? "M" : "L") + c[0].toFixed(1) + " " + c[1].toFixed(1)).join(" ");
const area = line + " L" + w + " " + h + " L0 " + h + " Z";
const up = vals[vals.length - 1] >= vals[0];
const stroke = up ? "#E6FF55" : "#7a7f87";
const gid = "g" + code;
const last = coords[coords.length - 1];
return (
'<svg width="' + w + '" height="' + h + '" viewBox="0 0 ' + w + ' ' + h + '" fill="none" aria-hidden="true">' +
'<defs><linearGradient id="' + gid + '" x1="0" y1="0" x2="0" y2="1">' +
'<stop offset="0" stop-color="' + stroke + '" stop-opacity="0.18"/>' +
'<stop offset="1" stop-color="' + stroke + '" stop-opacity="0"/>' +
'</linearGradient></defs>' +
'<path d="' + area + '" fill="url(#' + gid + ')"/>' +
'<path d="' + line + '" stroke="' + stroke + '" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>' +
'<circle cx="' + last[0].toFixed(1) + '" cy="' + last[1].toFixed(1) + '" r="2" fill="' + stroke + '"/>' +
"</svg>"
);
};
 
const changeChip = (code) => {
const r = rand(seedFor(code) ^ 0x9e3779b9);
const pct = (r() - 0.5) * 1.6; // roughly -0.8%..+0.8%
const up = pct >= 0;
const cls = up ? "chg up" : "chg dn";
const arrow = up
? '<svg width="9" height="9" viewBox="0 0 10 10" aria-hidden="true"><path d="M2 7 L5 3 L8 7" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>'
: '<svg width="9" height="9" viewBox="0 0 10 10" aria-hidden="true"><path d="M2 3 L5 7 L8 3" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>';
return '<span class="' + cls + '">' + arrow + Math.abs(pct).toFixed(2) + "%</span>";
};
 
// ---- Rows for majors ----
const rows = MAJORS.map((c) => {
const rate = rates[c.code];
return (
'<tr>' +
'<td class="c-cur"><span class="sym">' + esc(c.symbol) + '</span>' +
'<span class="meta"><span class="cc">' + esc(c.code) + '</span><span class="cn">' + esc(c.name) + '</span></span></td>' +
'<td class="c-rate">' + num(rate, c.decimals) + '</td>' +
'<td class="c-spark">' + sparkline(c.code) + '</td>' +
'<td class="c-chg">' + changeChip(c.code) + '</td>' +
'</tr>'
);
}).join("");
 
// ---- Computed cross-rates: JOIN the single response into new pairs ----
// rate(X) = units of X per 1 USD. So X/Y = rate(Y) / rate(X).
const cross = (a, b) => {
const ra = rates[a], rb = rates[b];
if (typeof ra !== "number" || typeof rb !== "number" || ra === 0) return null;
return rb / ra;
};
const crossDefs = [
{ a: "EUR", b: "GBP", label: "EUR / GBP", note: "Euro priced in Pounds", d: 4 },
{ a: "EUR", b: "JPY", label: "EUR / JPY", note: "Euro priced in Yen", d: 2 },
{ a: "GBP", b: "CHF", label: "GBP / CHF", note: "Pound priced in Francs", d: 4 },
];
const crossCards = crossDefs.map((x) => (
'<div class="cross">' +
'<div class="cross-top"><span class="cross-label">' + esc(x.label) + '</span><span class="cross-tag">derived</span></div>' +
'<div class="cross-val">' + num(cross(x.a, x.b), x.d) + '</div>' +
'<div class="cross-note">' + esc(x.note) + '</div>' +
'</div>'
)).join("");
 
// ---- Timestamp / status ----
const ts = esc(data.time_last_update_utc || "unknown");
const statusLabel = live ? "LIVE" : "CACHED";
const dotClass = live ? "dot dot-live" : "dot dot-stale";
 
const html =
'<!doctype html><html lang="en"><head><meta charset="utf-8"/>' +
'<meta name="viewport" content="width=device-width, initial-scale=1"/>' +
'<title>FX Rates · USD base</title><style>' +
'*{box-sizing:border-box;margin:0;padding:0}' +
':root{--bg:#0b0b0c;--surface:#111214;--surface2:#141518;--border:#23252a;--text:#eaeaea;--muted:#6b6f76;--accent:#E6FF55}' +
'html,body{background:var(--bg);color:var(--text);font-family:system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;-webkit-font-smoothing:antialiased}' +
'body{padding:56px 20px 72px;min-height:100vh;background-image:radial-gradient(1100px 500px at 82% -12%,rgba(230,255,85,0.10),transparent 62%),radial-gradient(700px 340px at -5% 4%,rgba(230,255,85,0.045),transparent 58%);background-attachment:fixed}' +
'.wrap{max-width:780px;margin:0 auto}' +
'.mono{font-family:ui-monospace,"SF Mono",Menlo,Consolas,monospace}' +
'.kicker{font-family:ui-monospace,"SF Mono",Menlo,monospace;font-size:11px;letter-spacing:0.24em;text-transform:uppercase;color:var(--muted)}' +
'header{margin-bottom:30px}' +
'.top{display:flex;align-items:flex-start;justify-content:space-between;gap:16px}' +
'h1{font-size:34px;line-height:1.05;font-weight:640;letter-spacing:-0.025em;margin:14px 0 10px}' +
'h1 .amp{color:var(--accent)}' +
'.sub{color:var(--muted);font-size:14.5px;line-height:1.55;max-width:50ch}' +
'.status{display:inline-flex;align-items:center;gap:8px;padding:8px 13px;border:1px solid var(--border);border-radius:999px;background:var(--surface);font-family:ui-monospace,"SF Mono",Menlo,monospace;font-size:11px;letter-spacing:0.16em;white-space:nowrap}' +
'.status.live{color:var(--accent);border-color:rgba(230,255,85,0.28)}' +
'.status.cached{color:var(--muted)}' +
'.dot{width:7px;height:7px;border-radius:50%}' +
'.dot-live{background:var(--accent);animation:pulse 2s infinite}' +
'.dot-stale{background:var(--muted)}' +
'@keyframes pulse{0%{box-shadow:0 0 0 0 rgba(230,255,85,0.5)}70%{box-shadow:0 0 0 7px rgba(230,255,85,0)}100%{box-shadow:0 0 0 0 rgba(230,255,85,0)}}' +
'.base-badge{display:inline-flex;align-items:center;gap:10px;margin-top:20px;padding:11px 15px;border:1px solid var(--border);border-radius:12px;background:linear-gradient(180deg,rgba(255,255,255,0.025),transparent)}' +
'.base-badge .lab{font-family:ui-monospace,Menlo,monospace;font-size:10.5px;letter-spacing:0.2em;text-transform:uppercase;color:var(--muted)}' +
'.base-badge .val{font-family:ui-monospace,Menlo,monospace;font-size:16px;font-weight:600;color:var(--accent);letter-spacing:0.02em}' +
'.card{background:var(--surface);border:1px solid var(--border);border-radius:18px;overflow:hidden;box-shadow:0 1px 0 rgba(255,255,255,0.02) inset,0 24px 60px -30px rgba(0,0,0,0.8)}' +
'.card-head{display:flex;align-items:center;justify-content:space-between;padding:17px 20px;border-bottom:1px solid var(--border)}' +
'.card-head .t{font-size:13px;font-weight:600;letter-spacing:0.005em}' +
'table{width:100%;border-collapse:collapse}' +
'th{font-family:ui-monospace,Menlo,monospace;font-size:10px;letter-spacing:0.16em;text-transform:uppercase;color:var(--muted);text-align:left;font-weight:500;padding:11px 20px 10px}' +
'th.n,td.c-rate,td.c-chg{text-align:right}' +
'th.sp{text-align:left}' +
'thead tr{border-bottom:1px solid var(--border)}' +
'td{padding:13px 20px;border-bottom:1px solid rgba(35,37,42,0.55);vertical-align:middle}' +
'tbody tr:last-child td{border-bottom:none}' +
'tbody tr{transition:background .12s ease}' +
'tbody tr:hover{background:rgba(230,255,85,0.025)}' +
'.c-cur{display:flex;align-items:center;gap:13px}' +
'.sym{width:34px;height:34px;flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;border:1px solid var(--border);border-radius:9px;background:var(--surface2);font-family:ui-monospace,Menlo,monospace;font-size:14px;color:var(--text)}' +
'.meta{display:flex;flex-direction:column;gap:1px;min-width:0}' +
'.cc{font-family:ui-monospace,Menlo,monospace;font-weight:600;font-size:13.5px;letter-spacing:0.02em}' +
'.cn{color:var(--muted);font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}' +
'.c-rate{font-family:ui-monospace,Menlo,monospace;font-size:15.5px;font-variant-numeric:tabular-nums;font-weight:500;letter-spacing:0.01em}' +
'.c-spark{width:100px}.c-spark svg{display:block}' +
'.c-chg{width:96px}' +
'.chg{display:inline-flex;align-items:center;gap:4px;font-family:ui-monospace,Menlo,monospace;font-size:12px;font-variant-numeric:tabular-nums;padding:3px 8px;border-radius:7px;white-space:nowrap}' +
'.chg.up{color:var(--accent);background:rgba(230,255,85,0.08)}' +
'.chg.dn{color:#9aa0a8;background:rgba(255,255,255,0.04)}' +
'.section-head{display:flex;align-items:baseline;justify-content:space-between;margin:34px 0 14px;gap:12px}' +
'.section-head .hint{font-family:ui-monospace,Menlo,monospace;font-size:11px;color:var(--muted)}' +
'.cross-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px}' +
'.cross{position:relative;background:var(--surface);border:1px solid var(--border);border-radius:15px;padding:17px;overflow:hidden;transition:border-color .15s ease}' +
'.cross:hover{border-color:rgba(230,255,85,0.28)}' +
'.cross-top{display:flex;align-items:center;justify-content:space-between;margin-bottom:14px}' +
'.cross-label{font-family:ui-monospace,Menlo,monospace;font-size:12px;letter-spacing:0.04em;color:var(--text)}' +
'.cross-tag{font-family:ui-monospace,Menlo,monospace;font-size:8.5px;letter-spacing:0.14em;text-transform:uppercase;color:var(--muted);border:1px solid var(--border);padding:2px 6px;border-radius:5px}' +
'.cross-val{font-family:ui-monospace,Menlo,monospace;font-size:26px;font-weight:600;font-variant-numeric:tabular-nums;letter-spacing:-0.015em}' +
'.cross-note{color:var(--muted);font-size:11.5px;margin-top:5px}' +
'footer{margin-top:30px;display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;color:var(--muted);font-size:12px}' +
'.foot-brand{display:inline-flex;align-items:center;gap:8px}' +
'.foot-brand b{color:var(--text);font-weight:600}' +
'.q{color:#0b0b0c;background:var(--accent);width:18px;height:18px;border-radius:6px;display:inline-flex;align-items:center;justify-content:center;font-family:ui-monospace,Menlo,monospace;font-weight:700;font-size:12px}' +
'.foot-ts{font-family:ui-monospace,Menlo,monospace;font-size:11px}' +
'.disclaimer{margin-top:14px;color:var(--muted);font-size:11px;line-height:1.5;opacity:0.75}' +
'@media(max-width:600px){body{padding:32px 16px 56px}h1{font-size:27px}.cross-grid{grid-template-columns:1fr}.c-spark,th.sp{display:none}.cn{display:none}.sub{font-size:13.5px}}' +
'</style></head><body><div class="wrap">' +
'<header><div class="top"><div>' +
'<div class="kicker">Foreign Exchange</div>' +
'<h1>Live FX <span class="amp">rates</span></h1>' +
'<p class="sub">Major currencies against the US Dollar, with cross-rates computed from a single upstream snapshot.</p>' +
'</div>' +
'<div class="status ' + (live ? "live" : "cached") + '"><span class="' + dotClass + '"></span>' + statusLabel + '</div>' +
'</div>' +
'<div class="base-badge"><span class="lab">Base</span><span class="val">1 ' + esc(base) + '</span></div>' +
'</header>' +
'<div class="card">' +
'<div class="card-head"><span class="t">Major currencies</span><span class="kicker">per 1 ' + esc(base) + '</span></div>' +
'<table><thead><tr><th>Currency</th><th class="n">Rate</th><th class="sp">Trend</th><th class="n">Move</th></tr></thead>' +
'<tbody>' + rows + '</tbody></table>' +
'</div>' +
'<div class="section-head"><span class="kicker">Computed cross-rates</span><span class="hint">joined from one call</span></div>' +
'<div class="cross-grid">' + crossCards + '</div>' +
'<footer><span class="foot-brand"><span class="q">q</span> <b>QuikRun</b> · API glue</span>' +
'<span class="foot-ts">Updated ' + ts + '</span></footer>' +
'<p class="disclaimer">Trend and move indicators are illustrative, derived deterministically from the current snapshot for visual context. Rates are informational and not intended for trading.</p>' +
'</div></body></html>';
 
return new Response(html, {
status: 200,
headers: { "content-type": "text/html; charset=utf-8", "cache-control": "public, max-age=300" },
});
}
Fork this and make it yours

Describe a change in plain English and QuikRun deploys it to your own URL.

Fork snippet