diff --git a/mintlify/docs.json b/mintlify/docs.json index 97723499..b1068719 100644 --- a/mintlify/docs.json +++ b/mintlify/docs.json @@ -56,6 +56,7 @@ "platform-overview/core-concepts/account-model", "platform-overview/core-concepts/transaction-lifecycle", "platform-overview/core-concepts/currencies-and-rails", + "fx-rates", "platform-overview/configuration" ] } diff --git a/mintlify/fx-rates.mdx b/mintlify/fx-rates.mdx new file mode 100644 index 00000000..2a6d57aa --- /dev/null +++ b/mintlify/fx-rates.mdx @@ -0,0 +1,1035 @@ +--- +title: "FX Rates" +description: "Compare Grid's FX rates against major providers across 40+ corridors" +icon: "/images/icons/globe.svg" +"og:image": "/images/og/og-fx-rates.png" +mode: "wide" +--- + +export const CURRENCIES = [ + { code: 'USD', name: 'US Dollar', region: 'Americas', usdMid: 1, gridSpread: 0, wiseSpread: 0 }, + { code: 'CAD', name: 'Canadian Dollar', region: 'Americas', usdMid: 1.362, gridSpread: 0.35, wiseSpread: 0.55 }, + { code: 'MXN', name: 'Mexican Peso', region: 'Americas', usdMid: 17.45, gridSpread: 0.5, wiseSpread: 0.95 }, + { code: 'BRL', name: 'Brazilian Real', region: 'Americas', usdMid: 5.05, gridSpread: 0.55, wiseSpread: 1.00 }, + { code: 'CRC', name: 'Costa Rican Colón', region: 'Americas', usdMid: 512, gridSpread: 0.8, wiseSpread: 1.60 }, + { code: 'EUR', name: 'Euro', region: 'Europe', usdMid: 0.921, gridSpread: 0.25, wiseSpread: 0.55 }, + { code: 'GBP', name: 'British Pound', region: 'Europe', usdMid: 0.791, gridSpread: 0.25, wiseSpread: 0.55 }, + { code: 'CHF', name: 'Swiss Franc', region: 'Europe', usdMid: 0.881, gridSpread: 0.3, wiseSpread: 0.55 }, + { code: 'DKK', name: 'Danish Krone', region: 'Europe', usdMid: 6.87, gridSpread: 0.3, wiseSpread: 0.55 }, + { code: 'SEK', name: 'Swedish Krona', region: 'Europe', usdMid: 10.45, gridSpread: 0.4, wiseSpread: 0.90 }, + { code: 'NOK', name: 'Norwegian Krone', region: 'Europe', usdMid: 10.55, gridSpread: 0.4, wiseSpread: 0.90 }, + { code: 'CZK', name: 'Czech Koruna', region: 'Europe', usdMid: 23.15, gridSpread: 0.45, wiseSpread: 0.95 }, + { code: 'HUF', name: 'Hungarian Forint', region: 'Europe', usdMid: 362, gridSpread: 0.5, wiseSpread: 1.00 }, + { code: 'PLN', name: 'Polish Zloty', region: 'Europe', usdMid: 4.02, gridSpread: 0.45, wiseSpread: 0.90 }, + { code: 'RON', name: 'Romanian Leu', region: 'Europe', usdMid: 4.58, gridSpread: 0.5, wiseSpread: 1.00 }, + { code: 'BGN', name: 'Bulgarian Lev', region: 'Europe', usdMid: 1.80, gridSpread: 0.45, wiseSpread: 0.95 }, + { code: 'ISK', name: 'Icelandic Króna', region: 'Europe', usdMid: 138, gridSpread: 0.8, wiseSpread: 1.50 }, + { code: 'NGN', name: 'Nigerian Naira', region: 'Africa', usdMid: 1550, gridSpread: 0.8, wiseSpread: 2.50 }, + { code: 'KES', name: 'Kenyan Shilling', region: 'Africa', usdMid: 129.5, gridSpread: 0.7, wiseSpread: 2.00 }, + { code: 'ZAR', name: 'South African Rand', region: 'Africa', usdMid: 18.5, gridSpread: 0.5, wiseSpread: 0.95 }, + { code: 'GHS', name: 'Ghanaian Cedi', region: 'Africa', usdMid: 14.8, gridSpread: 0.9, wiseSpread: 2.50 }, + { code: 'UGX', name: 'Ugandan Shilling', region: 'Africa', usdMid: 3750, gridSpread: 0.9, wiseSpread: 2.80 }, + { code: 'TZS', name: 'Tanzanian Shilling', region: 'Africa', usdMid: 2530, gridSpread: 0.9, wiseSpread: 2.50 }, + { code: 'ZMW', name: 'Zambian Kwacha', region: 'Africa', usdMid: 26.5, gridSpread: 1.0, wiseSpread: 2.80 }, + { code: 'MWK', name: 'Malawian Kwacha', region: 'Africa', usdMid: 1730, gridSpread: 1.1, wiseSpread: 3.00 }, + { code: 'XOF', name: 'West African CFA', region: 'Africa', usdMid: 605, gridSpread: 0.8, wiseSpread: 2.00 }, + { code: 'XAF', name: 'Central African CFA', region: 'Africa', usdMid: 605, gridSpread: 0.85, wiseSpread: 2.00 }, + { code: 'CDF', name: 'Congolese Franc', region: 'Africa', usdMid: 2780, gridSpread: 1.2, wiseSpread: 3.00 }, + { code: 'BWP', name: 'Botswana Pula', region: 'Africa', usdMid: 13.6, gridSpread: 0.9, wiseSpread: 2.00 }, + { code: 'INR', name: 'Indian Rupee', region: 'Asia-Pacific', usdMid: 83.5, gridSpread: 0.45, wiseSpread: 0.90 }, + { code: 'PHP', name: 'Philippine Peso', region: 'Asia-Pacific', usdMid: 56.2, gridSpread: 0.5, wiseSpread: 1.00 }, + { code: 'IDR', name: 'Indonesian Rupiah', region: 'Asia-Pacific', usdMid: 15650, gridSpread: 0.7, wiseSpread: 1.10 }, + { code: 'SGD', name: 'Singapore Dollar', region: 'Asia-Pacific', usdMid: 1.34, gridSpread: 0.3, wiseSpread: 0.55 }, + { code: 'HKD', name: 'Hong Kong Dollar', region: 'Asia-Pacific', usdMid: 7.81, gridSpread: 0.2, wiseSpread: 0.50 }, + { code: 'CNY', name: 'Chinese Yuan', region: 'Asia-Pacific', usdMid: 7.24, gridSpread: 0.6, wiseSpread: 1.10 }, + { code: 'KRW', name: 'South Korean Won', region: 'Asia-Pacific', usdMid: 1325, gridSpread: 0.45, wiseSpread: 0.90 }, + { code: 'THB', name: 'Thai Baht', region: 'Asia-Pacific', usdMid: 35.2, gridSpread: 0.5, wiseSpread: 0.95 }, + { code: 'VND', name: 'Vietnamese Dong', region: 'Asia-Pacific', usdMid: 25200, gridSpread: 0.8, wiseSpread: 1.10 }, + { code: 'MYR', name: 'Malaysian Ringgit', region: 'Asia-Pacific', usdMid: 4.65, gridSpread: 0.5, wiseSpread: 1.00 }, + { code: 'LKR', name: 'Sri Lankan Rupee', region: 'Asia-Pacific', usdMid: 298, gridSpread: 0.9, wiseSpread: 1.80 }, +]; + +export const CURRENCY_TO_COUNTRY = { + USD: 'us', CAD: 'ca', MXN: 'mx', BRL: 'br', CRC: 'cr', + EUR: 'eu', GBP: 'gb', CHF: 'ch', DKK: 'dk', SEK: 'se', + NOK: 'no', CZK: 'cz', HUF: 'hu', PLN: 'pl', RON: 'ro', + BGN: 'bg', ISK: 'is', NGN: 'ng', KES: 'ke', ZAR: 'za', + GHS: 'gh', UGX: 'ug', TZS: 'tz', ZMW: 'zm', MWK: 'mw', + XOF: 'sn', XAF: 'cm', CDF: 'cd', BWP: 'bw', + INR: 'in', PHP: 'ph', IDR: 'id', SGD: 'sg', HKD: 'hk', + CNY: 'cn', KRW: 'kr', THB: 'th', VND: 'vn', MYR: 'my', LKR: 'lk', +}; + +export const flagUrl = (code) => + 'https://hatscripts.github.io/circle-flags/flags/' + (CURRENCY_TO_COUNTRY[code] || code.slice(0, 2).toLowerCase()) + '.svg'; + +export const SOURCE_CODES = ['USD', 'EUR', 'GBP', 'CAD', 'SGD', 'HKD']; + +export const SOURCE_OPTIONS = SOURCE_CODES.map(code => { + const c = CURRENCIES.find(x => x.code === code); + return { value: code, label: code + ' \u2014 ' + c.name }; +}); + +export const formatNumber = (num, decimals) => { + if (typeof num !== 'number' || !isFinite(num)) return '\u2014'; + return num.toLocaleString('en-US', { + minimumFractionDigits: decimals, + maximumFractionDigits: decimals, + }); +}; + +export const getRateDecimals = (rate) => { + if (rate >= 1000) return 0; + if (rate >= 100) return 2; + if (rate >= 1) return 3; + return 4; +}; + +export const getReceiveDecimals = (rate) => { + if (rate >= 100) return 0; + return 2; +}; + +export const WISE_API_BASE = 'https://api.transferwise.com/v4/comparisons/'; + +export const CACHE_TTL = 30 * 60 * 1000; +export const STALE_TTL = 2 * 60 * 60 * 1000; +export const BATCH_SIZE = 10; + +export const getStripeFxBps = (source, dest) => { + const majorPairs = ['USD-EUR','USD-GBP','EUR-USD','GBP-USD','EUR-GBP','GBP-EUR']; + if (majorPairs.includes(source + '-' + dest)) return 50; + if (source === 'USD') return 100; + return 200; +}; + +export const STRIPE_CB_BPS = { + USD: 25, GBP: 25, EUR: 25, CAD: 25, MXN: 25, CHF: 25, + NOK: 25, SEK: 25, CZK: 25, HUF: 25, BGN: 25, ISK: 25, + DKK: 50, HKD: 50, IDR: 50, PLN: 50, SGD: 50, ZAR: 50, THB: 50, + INR: 75, KES: 75, RON: 75, +}; + +export const STRIPE_FIXED_FEE = 1.50; + +export const getStripeTotalBps = (source, dest, sendAmount) => { + const fxBps = getStripeFxBps(source, dest); + const cbBps = STRIPE_CB_BPS[dest] || 100; + const fixedBps = sendAmount > 0 ? Math.round(STRIPE_FIXED_FEE / sendAmount * 10000) : 0; + return fxBps + cbBps + fixedBps; +}; + +export const AIRWALLEX_MAJORS = ['USD','EUR','GBP','JPY','CHF','AUD','NZD','CAD','HKD','SGD','CNY']; +export const getAirwallexBps = (dest) => AIRWALLEX_MAJORS.includes(dest) ? 50 : 100; + +export const bucketAmount = (amt) => { + if (amt < 1000) return Math.max(100, Math.round(amt / 100) * 100); + if (amt < 10000) return Math.round(amt / 500) * 500; + return Math.round(amt / 1000) * 1000; +}; + +export const fetchBatch = (items, batchSize, fetchFn) => { + const results = []; + let i = 0; + const next = () => { + if (i >= items.length) return Promise.resolve(results); + const batch = items.slice(i, i + batchSize); + i += batchSize; + return Promise.allSettled(batch.map(fetchFn)).then(batchResults => { + results.push(...batchResults); + return next(); + }); + }; + return next(); +}; + +export const processWiseResults = (results, targets, sendAmount) => { + const newData = {}; + const providerFreq = {}; + let anySuccess = false; + results.forEach((result, idx) => { + const val = result.status === 'fulfilled' ? result.value : null; + if (!val || !val.data) return; + const target = val.target || targets[idx]; + const data = val.data; + if (!data.providers || !Array.isArray(data.providers)) return; + anySuccess = true; + let midRate = null; + data.providers.forEach(p => { + if (p.quotes && p.quotes.length > 0 && p.quotes[0].isConsideredMidMarketRate) { + midRate = p.quotes[0].rate; + } + }); + const midReceive = midRate && typeof midRate === 'number' ? sendAmount * midRate : null; + const providers = []; + data.providers.forEach(p => { + if (!p.quotes || p.quotes.length === 0 || !p.name) return; + const q = p.quotes[0]; + if (typeof q.receivedAmount === 'number') { + providers.push({ + name: p.name, + receivedAmount: q.receivedAmount, + fee: typeof q.fee === 'number' ? q.fee : 0, + type: p.type || 'unknown', + }); + if (p.name !== 'Wise') { + providerFreq[p.name] = (providerFreq[p.name] || 0) + 1; + } + } + }); + newData[target] = { providers: providers, midReceive: midReceive }; + }); + const sorted = Object.entries(providerFreq) + .sort((a, b) => b[1] - a[1]) + .slice(0, 2) + .map(entry => entry[0]); + return { data: newData, providers: sorted, anySuccess: anySuccess }; +}; + +export const formatAmountInput = (num) => { + if (!num && num !== 0) return ''; + return Number(num).toLocaleString('en-US'); +}; + +export const PROVIDER_LOGOS = { + Stripe: '/images/icons/stripe.svg', + Airwallex: '/images/icons/airwallex.svg', + Wise: '/images/icons/wise.svg', + Remitly: '/images/icons/remitly.svg', + PayPal: '/images/icons/paypal.svg', + 'Western Union': '/images/icons/western-union.svg', + OFX: '/images/icons/ofx.svg', + InstaReM: '/images/icons/instarem.svg', + Instarem: '/images/icons/instarem.svg', + 'BNP Paribas': '/images/icons/bnp.svg', + Xoom: '/images/icons/xoom.svg', +}; + +export const BankIcon = () => ( + + + +); + +export const ProviderTh = ({ name }) => { + if (name === 'Bank avg.') return <>Bank avg.; + const src = PROVIDER_LOGOS[name]; + if (!src) return name; + return {name}; +}; + +export const DropdownList = ({ currencies, highlightIdx, setHighlightIdx, addCurrency, dropdownQuery, setDropdownQuery, setShowDropdown, disabledCodes }) => { + const listRef = React.useRef(null); + const [scrollState, setScrollState] = React.useState('top'); + const disabled = disabledCodes || new Set(); + const hasDisabled = disabled.size > 0; + const enabledItems = hasDisabled ? currencies.filter(c => !disabled.has(c.code)) : currencies; + const disabledItems = hasDisabled ? currencies.filter(c => disabled.has(c.code)) : []; + const sortedCurrencies = hasDisabled ? [...enabledItems, ...disabledItems] : currencies; + + const updateScroll = React.useCallback(() => { + const el = listRef.current; + if (!el) return; + const canScroll = el.scrollHeight > el.clientHeight + 1; + if (!canScroll) { setScrollState('none'); return; } + const atTop = el.scrollTop < 2; + const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 2; + if (atTop) setScrollState('top'); + else if (atBottom) setScrollState('bottom'); + else setScrollState('middle'); + }, []); + + React.useEffect(() => { + updateScroll(); + }, [currencies.length, updateScroll]); + + const masks = { + none: undefined, + top: 'linear-gradient(to bottom, black calc(100% - 40px), transparent 100%)', + bottom: 'linear-gradient(to bottom, transparent 0%, black 40px)', + middle: 'linear-gradient(to bottom, transparent 0%, black 40px, black calc(100% - 40px), transparent 100%)', + }; + const mask = masks[scrollState]; + + return ( +
+
+ + { setDropdownQuery(e.target.value); setHighlightIdx(-1); }} + onKeyDown={(e) => { + if (e.key === 'ArrowDown') { + e.preventDefault(); + setHighlightIdx(i => Math.min(i + 1, sortedCurrencies.length - 1)); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + setHighlightIdx(i => Math.max(i - 1, -1)); + } else if (e.key === 'Enter' && sortedCurrencies.length > 0) { + e.preventDefault(); + const idx = highlightIdx >= 0 ? highlightIdx : 0; + if (!disabled.has(sortedCurrencies[idx].code)) { + addCurrency(sortedCurrencies[idx].code); + } + } else if (e.key === 'Escape') { + setShowDropdown(false); + setDropdownQuery(''); + } + }} + autoFocus + /> +
+
+ {enabledItems.map((c, idx) => ( + + ))} + {disabledItems.length > 0 && ( +
Coming soon
+ )} + {disabledItems.map((c, idx) => ( + + ))} + {sortedCurrencies.length === 0 && ( +
No currencies found
+ )} +
+
+ ); +}; + +export const SkeletonTable = ({ cols }) => ( +
+ + + + + + {cols.map((name, i) => )} + + + + + {CURRENCIES.filter(c => c.code !== 'USD').slice(0, 40).map((c) => ( + + + + {cols.map((_, i) => )} + + + ))} + +
Destination currencyGrid{name ? :
}
delta
+
+ +
+ {c.code} + {c.name} +
+
+
+
+); + +export const StaticPlaceholder = () => ( +
+
+
+ Send +
+ +
+
+ + USD + +
+
+
+ to +
+ +
+
+
+
+ + +
+
+
+ +
+); + +export const RateExplorer = () => { + const [sourceCurrency, setSourceCurrency] = React.useState('USD'); + const [amount, setAmount] = React.useState(1000); + const [amountDisplay, setAmountDisplay] = React.useState('1,000'); + const [selectedCurrencies, setSelectedCurrencies] = React.useState([]); + const [showDropdown, setShowDropdown] = React.useState(false); + const [dropdownQuery, setDropdownQuery] = React.useState(''); + const [highlightIdx, setHighlightIdx] = React.useState(-1); + const [competitorData, setCompetitorData] = React.useState(() => { + try { + var cached = sessionStorage.getItem('wise_USD_1000'); + if (cached) { + var parsed = JSON.parse(cached); + if (parsed && parsed.ts && Date.now() - parsed.ts < CACHE_TTL) return parsed.data; + } + } catch (e) {} + return {}; + }); + const [topProviders, setTopProviders] = React.useState(() => { + try { + var cached = sessionStorage.getItem('wise_USD_1000'); + if (cached) { + var parsed = JSON.parse(cached); + if (parsed && parsed.ts && Date.now() - parsed.ts < CACHE_TTL) return parsed.providers; + } + } catch (e) {} + return []; + }); + const [competitorDone, setCompetitorDone] = React.useState(() => { + try { + var cached = sessionStorage.getItem('wise_USD_1000'); + if (cached) { + var parsed = JSON.parse(cached); + if (parsed && parsed.ts && Date.now() - parsed.ts < CACHE_TTL) return true; + } + } catch (e) {} + return false; + }); + const [viewMode, setViewMode] = React.useState('bps'); + const [liveRates, setLiveRates] = React.useState(() => { + try { + const cached = sessionStorage.getItem('grid_live_rates'); + if (cached) { + const parsed = JSON.parse(cached); + if (parsed && parsed.ts && Date.now() - parsed.ts < CACHE_TTL) { + return parsed.rates; + } + } + } catch (e) {} + return null; + }); + const [showSourceDropdown, setShowSourceDropdown] = React.useState(false); + const [sourceDropdownQuery, setSourceDropdownQuery] = React.useState(''); + const [sourceHighlightIdx, setSourceHighlightIdx] = React.useState(-1); + const [debouncedAmount, setDebouncedAmount] = React.useState(1000); + const [isVisible, setIsVisible] = React.useState(false); + const containerRef = React.useRef(null); + const dropdownRef = React.useRef(null); + const sourceDropdownRef = React.useRef(null); + const explorerRef = React.useRef(null); + const headerBarRef = React.useRef(null); + + React.useEffect(() => { + const timer = setTimeout(() => setDebouncedAmount(amount), 1200); + return () => clearTimeout(timer); + }, [amount]); + + React.useEffect(() => { + const el = containerRef.current; + if (!el) return; + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting) { + setIsVisible(true); + observer.disconnect(); + } + }, + { rootMargin: '200px' } + ); + observer.observe(el); + return () => observer.disconnect(); + }, []); + + React.useEffect(() => { + if (!isVisible) return; + if (liveRates !== null) return; + fetch('https://api.lightspark.com/grid/2025-10-13/exchange-rates?sourceCurrency=USD') + .then(res => { + if (!res.ok) throw new Error(res.status); + return res.json(); + }) + .then(json => { + const rates = {}; + if (json && json.data && json.data.length > 0) { + json.data.forEach(r => { + rates[r.destinationCurrency.code] = String(r.exchangeRate); + }); + } + setLiveRates(rates); + try { sessionStorage.setItem('grid_live_rates', JSON.stringify({ ts: Date.now(), rates })); } catch (e) {} + }) + .catch(() => { + setLiveRates({}); + try { sessionStorage.setItem('grid_live_rates', JSON.stringify({ ts: Date.now(), rates: {} })); } catch (e) {} + }); + }, [isVisible]); + + React.useEffect(() => { + if (!isVisible) return; + const bucketed = bucketAmount(debouncedAmount); + const cacheKey = 'wise_' + sourceCurrency + '_' + bucketed; + try { + const cached = sessionStorage.getItem(cacheKey); + if (cached) { + const parsed = JSON.parse(cached); + if (parsed && parsed.ts) { + const age = Date.now() - parsed.ts; + if (age < CACHE_TTL) { + setCompetitorData(parsed.data); + setTopProviders(parsed.providers); + setCompetitorDone(true); + return; + } + if (age < STALE_TTL) { + setCompetitorData(parsed.data); + setTopProviders(parsed.providers); + } + } + } + } catch (e) {} + const targets = CURRENCIES.filter(c => c.code !== sourceCurrency).map(c => c.code); + const fetchOne = (dest) => + fetch(WISE_API_BASE + '?sourceCurrency=' + sourceCurrency + '&targetCurrency=' + dest + '&sendAmount=' + bucketed) + .then(res => { + if (!res.ok) throw new Error(res.status); + return res.json(); + }) + .then(json => ({ target: dest, data: json })) + .catch(() => ({ target: dest, data: null })); + fetchBatch(targets, BATCH_SIZE, fetchOne).then(results => { + const processed = processWiseResults(results, targets, bucketed); + setCompetitorData(processed.data); + setTopProviders(processed.providers); + setCompetitorDone(true); + if (processed.anySuccess) { + try { + sessionStorage.setItem(cacheKey, JSON.stringify({ ts: Date.now(), data: processed.data, providers: processed.providers })); + } catch (e) {} + } + }).catch(() => { setCompetitorDone(true); }); + }, [sourceCurrency, debouncedAmount, isVisible]); + + React.useEffect(() => { + const handleClick = (e) => { + if (dropdownRef.current && !dropdownRef.current.contains(e.target)) { + setShowDropdown(false); + setDropdownQuery(''); + } + if (sourceDropdownRef.current && !sourceDropdownRef.current.contains(e.target)) { + setShowSourceDropdown(false); + setSourceDropdownQuery(''); + } + }; + document.addEventListener('mousedown', handleClick); + return () => document.removeEventListener('mousedown', handleClick); + }, []); + + React.useEffect(() => { + const el = explorerRef.current; + const header = headerBarRef.current; + if (!el || !header) return; + const navbar = document.querySelector('#navbar'); + + const getNavH = () => navbar ? navbar.getBoundingClientRect().bottom : 0; + + const updateVars = () => { + const navH = getNavH(); + const hH = header.offsetHeight; + el.style.setProperty('--re-navbar-h', navH + 'px'); + el.style.setProperty('--re-header-h', hH + 'px'); + el.style.setProperty('--re-thead-top', (navH + hH) + 'px'); + }; + + requestAnimationFrame(updateVars); + + const ro = new ResizeObserver(updateVars); + ro.observe(header); + if (navbar) ro.observe(navbar); + + var lastY = window.scrollY; + var hidden = false; + var DIR_THRESHOLD = 5; + + const onScroll = () => { + const y = window.scrollY; + const delta = y - lastY; + lastY = y; + + const navH = getNavH(); + const hH = header.offsetHeight; + const elRect = el.getBoundingClientRect(); + + if (elRect.top >= navH) { + if (hidden) { + header.classList.remove('re-header-hidden'); + hidden = false; + } + el.style.setProperty('--re-thead-top', (navH + hH) + 'px'); + return; + } + + if (delta > DIR_THRESHOLD && !hidden) { + header.classList.add('re-header-hidden'); + hidden = true; + el.style.setProperty('--re-thead-top', navH + 'px'); + } else if (delta < -DIR_THRESHOLD && hidden) { + header.classList.remove('re-header-hidden'); + hidden = false; + el.style.setProperty('--re-thead-top', (navH + hH) + 'px'); + } + }; + + window.addEventListener('scroll', onScroll, { passive: true }); + return () => { + ro.disconnect(); + window.removeEventListener('scroll', onScroll); + header.classList.remove('re-header-hidden'); + }; + }, []); + + React.useEffect(() => { + const el = explorerRef.current; + if (!el) return; + var prevTh = null; + const onOver = (e) => { + const td = e.target.closest('td'); + if (!td) return; + const table = td.closest('table'); + if (!table) return; + const th = table.querySelector('thead tr').children[td.cellIndex]; + if (th === prevTh) return; + if (prevTh) prevTh.classList.remove('re-col-hover'); + if (th) th.classList.add('re-col-hover'); + prevTh = th; + }; + const onLeave = () => { + if (prevTh) { prevTh.classList.remove('re-col-hover'); prevTh = null; } + }; + el.addEventListener('mouseover', onOver); + el.addEventListener('mouseleave', onLeave); + return () => { el.removeEventListener('mouseover', onOver); el.removeEventListener('mouseleave', onLeave); }; + }, []); + + const handleAmountChange = (e) => { + const raw = e.target.value.replace(/,/g, ''); + if (raw === '') { setAmount(1); setAmountDisplay(''); return; } + const val = parseFloat(raw); + if (!isNaN(val) && val > 0) { + setAmount(val); + setAmountDisplay(formatAmountInput(val)); + } + }; + + const handleAmountBlur = () => { + setAmountDisplay(formatAmountInput(amount)); + }; + + const handleAmountFocus = (e) => { + setAmountDisplay(String(amount)); + setTimeout(() => e.target.select(), 0); + }; + + const addCurrency = (code) => { + if (!selectedCurrencies.includes(code)) { + setSelectedCurrencies([...selectedCurrencies, code]); + } + setShowDropdown(false); + setDropdownQuery(''); + }; + + const removeCurrency = (code) => { + setSelectedCurrencies(selectedCurrencies.filter(c => c !== code)); + }; + + const clearCurrencies = () => { + setSelectedCurrencies([]); + }; + + const getUsdMid = (code) => { + if (code === 'USD') return 1; + if (liveRates && liveRates[code]) return parseFloat(liveRates[code]); + const fallback = CURRENCIES.find(c => c.code === code); + return fallback ? fallback.usdMid : null; + }; + + const sourceUsdMid = getUsdMid(sourceCurrency); + const destinations = CURRENCIES.filter(c => c.code !== sourceCurrency); + const hasLiveData = Object.keys(competitorData).length > 0; + const displayProviders = hasLiveData ? topProviders : []; + + const allCorridors = destinations.map(dest => { + const destUsdMid = getUsdMid(dest.code); + const midRate = destUsdMid / sourceUsdMid; + const cd = competitorData[dest.code]; + const midReceive = cd && cd.midReceive ? cd.midReceive : debouncedAmount * midRate; + const gridBps = Math.round(dest.gridSpread * 100); + const gridReceive = midReceive * (1 - dest.gridSpread / 100); + const providers = {}; + if (cd && cd.providers) { + cd.providers.forEach(p => { + const pBps = midReceive > 0 + ? Math.round(((midReceive - p.receivedAmount) / midReceive) * 10000) + : 0; + providers[p.name] = { + receivedAmount: p.receivedAmount, + bps: pBps, + }; + }); + } + if (!providers['Wise']) { + const wiseRate = midRate * (1 - dest.wiseSpread / 100); + providers['Wise'] = { + receivedAmount: debouncedAmount * wiseRate, + bps: Math.round(dest.wiseSpread * 100), + }; + } + const stripeBps = getStripeTotalBps(sourceCurrency, dest.code, debouncedAmount); + const stripeReceive = midReceive * (1 - stripeBps / 10000); + const airwallexBps = getAirwallexBps(dest.code); + const airwallexReceive = midReceive * (1 - airwallexBps / 10000); + let bankAvgBps = null; + let bankAvgReceive = null; + if (cd && cd.providers) { + const banks = cd.providers.filter(p => p.type === 'bank'); + if (banks.length > 0) { + const avgReceived = banks.reduce((sum, b) => sum + b.receivedAmount, 0) / banks.length; + bankAvgReceive = avgReceived; + bankAvgBps = midReceive > 0 ? Math.round(((midReceive - avgReceived) / midReceive) * 10000) : 0; + } + } + return { + code: dest.code, name: dest.name, + midRate: midRate, midReceive: midReceive, + gridReceive: gridReceive, gridBps: gridBps, + providers: providers, gridSpread: dest.gridSpread, + stripeBps: stripeBps, stripeReceive: stripeReceive, + airwallexBps: airwallexBps, airwallexReceive: airwallexReceive, + bankAvgBps: bankAvgBps, bankAvgReceive: bankAvgReceive, + }; + }); + + const corridors = selectedCurrencies.length > 0 + ? allCorridors.filter(c => selectedCurrencies.includes(c.code)) + : allCorridors; + + const dropdownCurrencies = CURRENCIES.filter(c => + c.code !== sourceCurrency && !selectedCurrencies.includes(c.code) + ).filter(c => + !dropdownQuery || c.code.toLowerCase().includes(dropdownQuery.toLowerCase()) || c.name.toLowerCase().includes(dropdownQuery.toLowerCase()) + ); + + const sourceDropdownCurrencies = CURRENCIES.filter(c => + !sourceDropdownQuery || c.code.toLowerCase().includes(sourceDropdownQuery.toLowerCase()) || c.name.toLowerCase().includes(sourceDropdownQuery.toLowerCase()) + ); + + const disabledSourceCodes = new Set( + CURRENCIES.filter(c => !SOURCE_CODES.includes(c.code)).map(c => c.code) + ); + + const selectSource = (code) => { + setSourceCurrency(code); + setShowSourceDropdown(false); + setSourceDropdownQuery(''); + }; + + return ( +
+
+
+
+ Send +
+ +
+
+ + {showSourceDropdown && ( + + )} +
+
+
+ to + {selectedCurrencies.map(code => { + return ( +
+
+ + {code} +
+ +
+ ); + })} +
+ {selectedCurrencies.length === 0 ? ( + + ) : ( + + )} + {showDropdown && ( + + )} +
+
+
+ {selectedCurrencies.length > 0 && ( + + )} +
+ + +
+
+
+ + {liveRates === null || !competitorDone ? ( + + ) : ( +
+ + + + + + + + + {displayProviders.map((name) => ( + + ))} + + + + + + {corridors.length === 0 && ( + + + + )} + {corridors.map((row) => { + const recDec = getReceiveDecimals(row.midRate); + const wise = row.providers['Wise']; + let bestBps = row.gridBps; + let bestKey = 'Grid'; + if (row.stripeBps < bestBps) { bestBps = row.stripeBps; bestKey = 'Stripe'; } + if (row.airwallexBps < bestBps) { bestBps = row.airwallexBps; bestKey = 'Airwallex'; } + if (wise && wise.bps >= 0 && wise.bps < bestBps) { bestBps = wise.bps; bestKey = 'Wise'; } + displayProviders.forEach(name => { + const p = row.providers[name]; + if (p && p.bps >= 0 && p.bps < bestBps) { bestBps = p.bps; bestKey = name; } + }); + if (row.bankAvgBps !== null && row.bankAvgBps < bestBps) { bestBps = row.bankAvgBps; bestKey = 'BankAvg'; } + let bestCompBps = null; + let bestCompReceive = null; + if (row.stripeBps >= 0) { bestCompBps = row.stripeBps; bestCompReceive = row.stripeReceive; } + if (row.airwallexBps >= 0 && (bestCompBps === null || row.airwallexBps < bestCompBps)) { bestCompBps = row.airwallexBps; bestCompReceive = row.airwallexReceive; } + if (wise && wise.bps >= 0 && (bestCompBps === null || wise.bps < bestCompBps)) { bestCompBps = wise.bps; bestCompReceive = wise.receivedAmount; } + displayProviders.forEach(name => { + const p = row.providers[name]; + if (p && p.bps >= 0 && (bestCompBps === null || p.bps < bestCompBps)) { + bestCompBps = p.bps; + bestCompReceive = p.receivedAmount; + } + }); + if (row.bankAvgBps !== null && (bestCompBps === null || row.bankAvgBps < bestCompBps)) { bestCompBps = row.bankAvgBps; bestCompReceive = row.bankAvgReceive; } + const bpsDelta = bestCompBps !== null ? bestCompBps - row.gridBps : null; + const amtDelta = bestCompReceive !== null ? row.gridReceive - bestCompReceive : null; + return ( + + + + + + + {displayProviders.map((name) => { + const provider = row.providers[name]; + return ( + + ); + })} + + + + ); + })} + +
Destination currencyGridBank avg.delta
+ No corridors match your selection. +
+
+ +
+ {row.code} + {row.name} +
+
+
+ {viewMode === 'bps' ? ( + {row.gridBps} bps + ) : ( + {formatNumber(row.gridReceive, recDec)} {row.code} + )} + + {viewMode === 'bps' ? ( + {row.stripeBps} bps + ) : ( + {formatNumber(row.stripeReceive, recDec)} {row.code} + )} + + {viewMode === 'bps' ? ( + {row.airwallexBps} bps + ) : ( + {formatNumber(row.airwallexReceive, recDec)} {row.code} + )} + + {wise ? ( + viewMode === 'bps' ? ( + wise.bps >= 0 ? ( + {wise.bps} bps + ) : ( + {'\u2014'} + ) + ) : ( + {formatNumber(wise.receivedAmount, recDec)} {row.code} + ) + ) : ( + {'\u2014'} + )} + + {provider ? ( + viewMode === 'bps' ? ( + provider.bps >= 0 ? ( + {provider.bps} bps + ) : ( + {'\u2014'} + ) + ) : ( + {formatNumber(provider.receivedAmount, recDec)} {row.code} + ) + ) : ( + {'\u2014'} + )} + + {row.bankAvgBps !== null ? ( + viewMode === 'bps' ? ( + {row.bankAvgBps} bps + ) : ( + {formatNumber(row.bankAvgReceive, recDec)} {row.code} + ) + ) : ( + {'\u2014'} + )} + 0 ? ' rate-explorer-delta-positive' : bpsDelta !== null && bpsDelta < 0 ? ' rate-explorer-delta-negative' : '') : (amtDelta !== null && amtDelta > 0 ? ' rate-explorer-delta-positive' : amtDelta !== null && amtDelta < 0 ? ' rate-explorer-delta-negative' : ''))}> + {viewMode === 'bps' ? ( + bpsDelta !== null ? (bpsDelta > 0 ? '+' + bpsDelta + ' bps' : bpsDelta < 0 ? bpsDelta + ' bps' : '0 bps') : '\u2014' + ) : ( + amtDelta !== null ? (amtDelta > 0 ? '+' + formatNumber(amtDelta, recDec) + ' ' + row.code : amtDelta < 0 ? formatNumber(amtDelta, recDec) + ' ' + row.code : '0 ' + row.code) : '\u2014' + )} +
+
+ )} + +
+ Grid rates reflect standard published pricing, not personalized quotes. Stripe and Airwallex costs are based on published pricing schedules, not live API data. Wise and other provider rates are live via public comparison data and include all fees. Bank avg. is the average of bank-type providers from comparison data. Actual rates may vary — get real-time quotes via the Quote API. +
+
+
+ ); +}; + +
+ +
+
+
+ Get custom pricing + Standard rates shown +
+ Contact sales +
+
+
+ +
+ + diff --git a/mintlify/images/fx-rates/contact-sales-bg.jpg b/mintlify/images/fx-rates/contact-sales-bg.jpg new file mode 100644 index 00000000..453e5ca4 Binary files /dev/null and b/mintlify/images/fx-rates/contact-sales-bg.jpg differ diff --git a/mintlify/images/icons/airwallex.svg b/mintlify/images/icons/airwallex.svg new file mode 100644 index 00000000..8497142d --- /dev/null +++ b/mintlify/images/icons/airwallex.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/mintlify/images/icons/barclays.svg b/mintlify/images/icons/barclays.svg new file mode 100644 index 00000000..88d05d00 --- /dev/null +++ b/mintlify/images/icons/barclays.svg @@ -0,0 +1,4 @@ + + + + diff --git a/mintlify/images/icons/bnp.svg b/mintlify/images/icons/bnp.svg new file mode 100644 index 00000000..1a33bf16 --- /dev/null +++ b/mintlify/images/icons/bnp.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/mintlify/images/icons/bofa.svg b/mintlify/images/icons/bofa.svg new file mode 100644 index 00000000..a5a25c77 --- /dev/null +++ b/mintlify/images/icons/bofa.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/mintlify/images/icons/chase.svg b/mintlify/images/icons/chase.svg new file mode 100644 index 00000000..bd70b6ec --- /dev/null +++ b/mintlify/images/icons/chase.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/mintlify/images/icons/instarem.svg b/mintlify/images/icons/instarem.svg new file mode 100644 index 00000000..336799ad --- /dev/null +++ b/mintlify/images/icons/instarem.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/mintlify/images/icons/magnifying-glass.svg b/mintlify/images/icons/magnifying-glass.svg index 405eeba5..44d47945 100644 --- a/mintlify/images/icons/magnifying-glass.svg +++ b/mintlify/images/icons/magnifying-glass.svg @@ -1,3 +1,3 @@ - - \ No newline at end of file + + diff --git a/mintlify/images/icons/ofx.svg b/mintlify/images/icons/ofx.svg new file mode 100644 index 00000000..560695fe --- /dev/null +++ b/mintlify/images/icons/ofx.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/mintlify/images/icons/paypal.svg b/mintlify/images/icons/paypal.svg new file mode 100644 index 00000000..bc281eb8 --- /dev/null +++ b/mintlify/images/icons/paypal.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/mintlify/images/icons/remitly.svg b/mintlify/images/icons/remitly.svg new file mode 100644 index 00000000..c44ae24a --- /dev/null +++ b/mintlify/images/icons/remitly.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/mintlify/images/icons/stripe.svg b/mintlify/images/icons/stripe.svg new file mode 100644 index 00000000..64629ec4 --- /dev/null +++ b/mintlify/images/icons/stripe.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/mintlify/images/icons/western-union.svg b/mintlify/images/icons/western-union.svg new file mode 100644 index 00000000..49541432 --- /dev/null +++ b/mintlify/images/icons/western-union.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mintlify/images/icons/wise.svg b/mintlify/images/icons/wise.svg new file mode 100644 index 00000000..fb612244 --- /dev/null +++ b/mintlify/images/icons/wise.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/mintlify/images/icons/xoom.svg b/mintlify/images/icons/xoom.svg new file mode 100644 index 00000000..c0817f6b --- /dev/null +++ b/mintlify/images/icons/xoom.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/mintlify/images/og/og-fx-rates.png b/mintlify/images/og/og-fx-rates.png new file mode 100644 index 00000000..84a7e9f6 Binary files /dev/null and b/mintlify/images/og/og-fx-rates.png differ diff --git a/mintlify/style.css b/mintlify/style.css index 3dd6aba7..35113064 100644 --- a/mintlify/style.css +++ b/mintlify/style.css @@ -96,6 +96,7 @@ --ls-sky-500: #00B3E0; --ls-teal-500: #44DDB5; --ls-green-600: #11A967; + --ls-green-400: #4ADE80; --ls-yellow-600: #E09000; --ls-orange-500: #F77D26; --ls-pink-800: #A90087; @@ -257,6 +258,11 @@ strong, b, th, letter-spacing: normal !important; } +/* Eyebrow: muted gray instead of primary color */ +.eyebrow { + color: var(--ls-gray-400) !important; +} + /* Page title and H2s: Regular weight (400) */ h1#page-title, #content h2 { @@ -2637,11 +2643,19 @@ html.dark #api-playground-2-operation-page [data-component-part="tab-button"][da background-color: var(--ls-gray-075) !important; } +#content .rate-explorer-table tbody tr:hover { + background-color: transparent !important; +} + html.dark #content table tbody tr:hover, html.dark .prose table tbody tr:hover { background-color: var(--ls-white-02) !important; } +html.dark #content .rate-explorer-table tbody tr:hover { + background-color: transparent !important; +} + /* =========================================== Accordion Component =========================================== */ @@ -3963,3 +3977,1169 @@ html.dark .trusted-by-logo { html.dark .homepage-divider { border-color: var(--ls-white-06); } + +/* =========================================== + FX Rates Pricing Banner + =========================================== */ + +.fx-rates-pricing-banner { + position: relative; + display: flex; + align-items: center; + gap: 48px; + padding: 20px 24px; + margin: 0 40px; + border-radius: var(--ls-radius-md); + border: 0.5px solid var(--ls-white-10); + overflow: hidden; + text-decoration: none; + cursor: pointer; +} + +.fx-rates-pricing-banner-bg { + position: absolute; + inset: 0; + background-image: url('/images/fx-rates/contact-sales-bg.jpg'); + background-size: cover; + background-position: center; + background-repeat: no-repeat; + transform: scale(1.03); + transition: transform 0.4s ease, filter 0.3s ease; + z-index: 0; +} + +.fx-rates-pricing-banner:hover .fx-rates-pricing-banner-bg { + transform: scale(1); + filter: brightness(0.92); +} + +.fx-rates-pricing-banner-content { + position: relative; + z-index: 1; + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + gap: 24px; +} + +.fx-rates-pricing-banner-text { + display: flex; + flex-direction: column; + color: white; + font-size: 14px; + line-height: 20px; + font-feature-settings: "salt" 1; +} + +.fx-rates-pricing-banner-subtext { + opacity: 0.5; +} + +.fx-rates-pricing-banner-button { + padding: 0 16px; + height: 36px; + min-width: 36px; + background: var(--ls-gray-050); + color: var(--ls-gray-950); + border-radius: var(--ls-radius-sm); + font-size: 14px; + font-weight: 450 !important; + line-height: 20px; + text-decoration: none; + display: flex; + align-items: center; + justify-content: center; + transition: background 0.2s ease; + flex-shrink: 0; + white-space: nowrap; +} + +.fx-rates-pricing-banner-button:hover { + background: var(--ls-white); +} + +html.dark .fx-rates-pricing-banner-button { + background: var(--ls-gray-950); + color: var(--ls-gray-050); +} + +html.dark .fx-rates-pricing-banner-button:hover { + background: var(--ls-gray-900); +} + +@media (max-width: 1023px) { + .fx-rates-pricing-banner { + margin: 0 16px; + } +} + +@media (max-width: 640px) { + .fx-rates-pricing-banner { + gap: 16px; + } +} + +/* =========================================== + Rate Explorer + =========================================== */ + +/* Full-width: A hidden marker div (#rate-explorer-page) is placed in the MDX + as static content so it's server-rendered in the initial HTML. This lets the + :has() selector match on the very first paint — no hydration flash. */ +#content-container:has(#rate-explorer-page) { + max-width: 100% !important; + padding-left: 0 !important; + padding-right: 0 !important; +} + +#content-container:has(#rate-explorer-page) > div[class*="max-w"] { + max-width: 100% !important; +} + +/* Restore padding on everything except the rate explorer itself */ +#content-container:has(#rate-explorer-page) #header, +#content-container:has(#rate-explorer-page) #content-area > :not(#content) { + padding-left: 48px !important; + padding-right: 48px !important; +} + +@media (max-width: 1023px) { + #content-container:has(#rate-explorer-page) #header, + #content-container:has(#rate-explorer-page) #content-area > :not(#content) { + padding-left: 24px !important; + padding-right: 24px !important; + } +} + +/* Hide static placeholder once the React component mounts */ +#rate-explorer-static { + margin-top: 40px; + border-top: 0.5px solid var(--ls-black-10); + border-bottom: 0.5px solid var(--ls-black-10); +} + +html.dark #rate-explorer-static { + border-color: var(--ls-white-06); +} + +#content:has(.rate-explorer) #rate-explorer-static { + display: none; +} + +.rate-explorer { + width: 100%; + margin-top: 40px; + background: transparent; + border-top: 0.5px solid var(--ls-black-10); + border-bottom: 0.5px solid var(--ls-black-10); +} + +html.dark .rate-explorer { + border-color: var(--ls-white-06); +} + +/* ---------- Header bar ---------- */ +.rate-explorer-header { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + padding: 12px 20px; + padding-right: 340px; + background: var(--ls-gray-025); + border-bottom: 0.5px solid var(--ls-black-10); + position: sticky; + top: var(--re-navbar-h, 0px); + z-index: 11; + transition: transform 0.25s ease, margin-bottom 0.25s ease; +} + +.rate-explorer-header.re-header-hidden { + transform: translateY(-100%); + margin-bottom: calc(-1 * var(--re-header-h, 0px)); + pointer-events: none; +} + +html.dark .rate-explorer-header { + background: var(--ls-gray-950); + border-color: var(--ls-white-06); +} + +.rate-explorer-header-left { + display: flex; + align-items: center; + gap: 8px; + flex-shrink: 0; +} + +.rate-explorer-header-mid { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} + +.rate-explorer-header-right { + position: absolute; + right: 20px; + top: 12px; + display: flex; + align-items: center; + gap: 8px; +} + +.rate-explorer-header-label { + font-size: 14px !important; + font-weight: 450 !important; + line-height: 20px !important; + color: var(--ls-gray-950); + font-feature-settings: "salt" 1; + white-space: nowrap; +} + +html.dark .rate-explorer-header-label { + color: var(--ls-gray-050); +} + +/* ---------- Amount input ---------- */ +.rate-explorer-input-wrapper { + display: flex; + align-items: center; + height: 36px; + background: var(--ls-white); + border: 0.5px solid var(--ls-black-10); + border-radius: var(--ls-radius-sm); + overflow: hidden; +} + +html.dark .rate-explorer-input-wrapper { + background: var(--ls-gray-850); + border-color: var(--ls-white-06); +} + +.rate-explorer-input-wrapper:focus-within { + border-color: var(--ls-sky-500); + box-shadow: 0 0 0 1px var(--ls-sky-500); +} + +.rate-explorer-input { + width: 124px; + height: 100%; + padding: 0 12px; + font-size: 14px; + font-weight: 400; + font-family: "Suisse Intl", sans-serif; + font-feature-settings: "salt" 1; + color: var(--ls-gray-950); + background: transparent; + border: none; + text-align: left; + outline: none; +} + +html.dark .rate-explorer-input { + color: var(--ls-gray-050); +} + +/* ---------- Source currency trigger ---------- */ +.rate-explorer-source-trigger { + display: flex; + align-items: center; + gap: 8px; + height: 36px; + padding: 0 8px 0 12px; + background: var(--ls-white); + border: 0.5px solid var(--ls-black-10); + border-radius: var(--ls-radius-sm); + cursor: pointer; + font-family: "Suisse Intl", sans-serif; + font-feature-settings: "salt" 1; +} + +html.dark .rate-explorer-source-trigger { + background: var(--ls-gray-850); + border-color: var(--ls-white-06); +} + +.rate-explorer-source-label { + font-size: 14px; + font-weight: 500; + color: var(--ls-gray-950); + font-feature-settings: "salt" 1; +} + +html.dark .rate-explorer-source-label { + color: var(--ls-gray-050); +} + +.rate-explorer-chevron { + color: var(--ls-gray-400); + flex-shrink: 0; +} + +/* ---------- Flag images ---------- */ +.rate-explorer-flag-img { + border-radius: 50%; + flex-shrink: 0; + display: block; +} + +/* ---------- Destination trigger / placeholder ---------- */ +.rate-explorer-dest-trigger { + display: flex; + align-items: center; + gap: 8px; + height: 36px; + padding: 0 8px 0 12px; + background: var(--ls-white); + border: 0.5px solid var(--ls-black-10); + border-radius: var(--ls-radius-sm); + cursor: pointer; + font-family: "Suisse Intl", sans-serif; + font-feature-settings: "salt" 1; +} + +html.dark .rate-explorer-dest-trigger { + background: var(--ls-gray-850); + border-color: var(--ls-white-06); +} + +.rate-explorer-dest-placeholder { + font-size: 14px; + color: var(--ls-gray-500); + white-space: nowrap; +} + +/* ---------- Currency chips ---------- */ +.rate-explorer-chip { + display: flex; + align-items: center; + height: 36px; + background: var(--ls-white); + border: 0.5px solid var(--ls-black-10); + border-radius: var(--ls-radius-sm); + overflow: hidden; +} + +html.dark .rate-explorer-chip { + background: var(--ls-gray-800); + border-color: var(--ls-white-06); +} + +.rate-explorer-chip-info { + display: flex; + align-items: center; + gap: 6px; + padding: 0 8px; +} + +.rate-explorer-chip-code { + font-size: 14px; + font-weight: 500; + color: var(--ls-gray-950); + line-height: 20px; + font-feature-settings: "salt" 1; +} + +html.dark .rate-explorer-chip-code { + color: var(--ls-gray-050); +} + +.rate-explorer-chip-dismiss { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 36px; + padding: 0 6px; + background: none; + border: none; + border-left: 0.5px solid var(--ls-black-06); + cursor: pointer; + color: var(--ls-gray-400); + transition: color 0.1s; +} + +html.dark .rate-explorer-chip-dismiss { + border-color: var(--ls-white-06); +} + +.rate-explorer-chip-dismiss:hover { + color: var(--ls-gray-950); +} + +html.dark .rate-explorer-chip-dismiss:hover { + color: var(--ls-gray-050); +} + +/* ---------- Add / Clear buttons ---------- */ +.rate-explorer-add-btn { + display: flex; + align-items: center; + gap: 4px; + height: 36px; + padding: 0 10px; + font-size: 12px !important; + font-weight: 450 !important; + font-family: "Suisse Intl", sans-serif; + font-feature-settings: "salt" 1; + color: var(--ls-gray-950); + background: none; + border: 0.5px dashed var(--ls-black-10); + border-radius: var(--ls-radius-sm); + cursor: pointer; + white-space: nowrap; +} + +html.dark .rate-explorer-add-btn { + color: var(--ls-gray-050); + border-color: var(--ls-white-10); +} + +.rate-explorer-add-btn:hover { + background: var(--ls-gray-050); +} + +html.dark .rate-explorer-add-btn:hover { + background: var(--ls-white-04); +} + +.rate-explorer-clear-btn { + display: flex; + align-items: center; + gap: 4px; + height: 36px; + padding: 0 10px; + font-size: 12px !important; + font-weight: 450 !important; + font-family: "Suisse Intl", sans-serif; + font-feature-settings: "salt" 1; + color: var(--ls-gray-500); + background: none; + border: none; + cursor: pointer; + white-space: nowrap; +} + +.rate-explorer-clear-btn:hover { + color: var(--ls-gray-950); +} + +html.dark .rate-explorer-clear-btn:hover { + color: var(--ls-gray-050); +} + +/* ---------- Dropdown ---------- */ +.rate-explorer-dropdown-anchor { + position: relative; +} + +@keyframes rate-explorer-dropdown-in { + from { + opacity: 0; + transform: translateY(-4px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.rate-explorer-dropdown { + position: absolute; + top: calc(100% + 4px); + left: 0; + width: 260px; + max-height: 424px; + background: var(--ls-white); + border: 0.5px solid var(--ls-black-10); + border-radius: var(--ls-radius-sm); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12); + z-index: 50; + display: flex; + flex-direction: column; + overflow: hidden; + animation: rate-explorer-dropdown-in 0.15s ease-out; +} + +html.dark .rate-explorer-dropdown { + background: var(--ls-gray-900); + border-color: var(--ls-white-10); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4); +} + +.rate-explorer-dropdown-search-row { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 12px; + border-bottom: 0.5px solid var(--ls-black-06); +} + +html.dark .rate-explorer-dropdown-search-row { + border-color: var(--ls-white-06); +} + +.rate-explorer-dropdown-search-icon { + width: 16px; + height: 16px; + flex-shrink: 0; + opacity: 0.4; +} + +html.dark .rate-explorer-dropdown-search-icon { + filter: invert(1); +} + +.rate-explorer-dropdown-search { + width: 100%; + padding: 0; + font-size: 13px; + font-family: "Suisse Intl", sans-serif; + font-feature-settings: "salt" 1; + color: var(--ls-gray-950); + background: transparent; + border: none; + outline: none; +} + +html.dark .rate-explorer-dropdown-search { + color: var(--ls-gray-050); +} + +.rate-explorer-dropdown-search::placeholder { + color: var(--ls-gray-400); +} + +.rate-explorer-dropdown-list { + overflow-y: auto; + flex: 1; +} + +.rate-explorer-dropdown-item { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + padding: 8px 12px; + font-size: 13px; + font-family: "Suisse Intl", sans-serif; + font-feature-settings: "salt" 1; + color: var(--ls-gray-950); + background: none; + border: none; + cursor: pointer; + text-align: left; +} + +html.dark .rate-explorer-dropdown-item { + color: var(--ls-gray-050); +} + +.rate-explorer-dropdown-item:hover, +.rate-explorer-dropdown-item-active { + background: var(--ls-black-04); +} + +html.dark .rate-explorer-dropdown-item:hover, +html.dark .rate-explorer-dropdown-item-active { + background: var(--ls-white-04); +} + +.rate-explorer-dropdown-code { + font-weight: 500; + min-width: 32px; +} + +.rate-explorer-dropdown-name { + color: var(--ls-gray-500); +} + +.rate-explorer-dropdown-empty { + padding: 16px 12px; + font-size: 13px; + color: var(--ls-gray-400); + text-align: center; +} + +.rate-explorer-dropdown-item-disabled { + opacity: 0.45; + cursor: default; +} + +.rate-explorer-dropdown-item-disabled:hover { + background: none; +} + +html.dark .rate-explorer-dropdown-item-disabled:hover { + background: none; +} + +.rate-explorer-dropdown-divider { + font-size: 11px; + font-weight: 500; + color: var(--ls-gray-400); + padding: 8px 12px 4px; + font-family: "Suisse Intl", sans-serif; + font-feature-settings: "salt" 1; + border-top: 0.5px solid var(--ls-black-06); + margin-top: 4px; +} + +html.dark .rate-explorer-dropdown-divider { + color: var(--ls-gray-500); + border-color: var(--ls-white-06); +} + +/* ---------- View toggle ---------- */ +.rate-explorer-toggle { + display: flex; + background: var(--ls-gray-075); + border-radius: var(--ls-radius-sm); + padding: 4px; + gap: 6px; +} + +html.dark .rate-explorer-toggle { + background: var(--ls-gray-800); +} + +.rate-explorer-toggle-btn { + height: 28px; + padding: 0 12px; + font-size: 12px !important; + font-weight: 450 !important; + line-height: 16px !important; + font-family: "Suisse Intl", sans-serif; + font-feature-settings: "salt" 1; + color: var(--ls-gray-500); + background: transparent; + border: 0.5px solid transparent; + border-radius: var(--ls-radius-xs); + cursor: pointer; + white-space: nowrap; + transition: color 0.15s ease, background 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease; + text-align: center; +} + +.rate-explorer-toggle-btn:hover { + color: var(--ls-gray-600); +} + +html.dark .rate-explorer-toggle-btn:hover { + color: var(--ls-gray-300); +} + +.rate-explorer-toggle-btn.active { + color: var(--ls-gray-950); + background: var(--ls-gray-025); + border-color: var(--ls-black-10); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08); +} + +html.dark .rate-explorer-toggle-btn.active { + color: var(--ls-gray-050); + background: var(--ls-gray-700); + border-color: var(--ls-white-06); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); +} + +/* ---------- Table ---------- */ + +/* Reset Mintlify's auto-injected table wrapper */ +.rate-explorer [data-table-wrapper] { + margin: 0 !important; + padding: 0 !important; + width: 100% !important; +} + +.rate-explorer [data-table-wrapper] > div { + padding: 0 !important; +} + +/* Override Mintlify's injected [&_td]:min-w-[150px] and min-w-full/w-full */ +.rate-explorer-table { + width: auto !important; + min-width: 0 !important; + overflow: clip; +} + +.rate-explorer-table td { + min-width: 0 !important; +} + +/* Override #content table td/th font-size */ +#content .rate-explorer-table thead th { + font-size: 14px !important; + line-height: 18px !important; +} + +#content .rate-explorer-table tbody td { + font-size: 14px !important; + line-height: 20px !important; +} + +.rate-explorer-table-wrapper { + overflow-x: clip; +} + +.rate-explorer-table-wrapper [data-table-wrapper] { + margin: 0 !important; + padding: 0 !important; + overflow-x: clip !important; +} + +.rate-explorer-table { + border-collapse: collapse; + font-size: 14px; + line-height: 20px; + font-feature-settings: "salt" 1; +} + +.rate-explorer-table thead th { + height: 48px; + padding: 0 20px; + font-size: 13px !important; + font-weight: 450 !important; + line-height: 18px !important; + color: var(--ls-gray-500); + text-align: left; + vertical-align: middle; + white-space: nowrap; + width: 1%; + box-shadow: inset 0 -0.5px 0 var(--ls-black-10); + position: sticky; + top: calc(var(--re-thead-top, 0px) - 1px); + z-index: 10; + background: var(--ls-gray-025); + transition: top 0.25s ease; +} + +html.dark .rate-explorer-table thead th { + color: var(--ls-gray-400); + box-shadow: inset 0 -0.5px 0 var(--ls-white-06); + background: var(--ls-gray-950); +} + + +.rate-explorer-th-dest { + width: auto; +} + +.rate-explorer-col-grid { + border-left: 0.5px solid var(--ls-black-10); + border-right: 0.5px solid var(--ls-black-10); +} + +.rate-explorer-table thead th.rate-explorer-col-grid { + color: var(--ls-gray-950); +} + +html.dark .rate-explorer-col-grid { + border-left-color: var(--ls-white-06); + border-right-color: var(--ls-white-06); +} + +html.dark .rate-explorer-table thead th.rate-explorer-col-grid { + color: var(--ls-gray-200); +} + +.rate-explorer-grid-logo { + height: 13px; + width: auto; + min-width: fit-content; + vertical-align: middle; + border-radius: 0; + pointer-events: none; +} + +html.dark .rate-explorer-grid-logo { + filter: invert(1) brightness(0.87); +} + +.rate-explorer-grid-icon { + display: inline-block; + width: 13px; + height: 13px; + vertical-align: middle; + position: relative; + top: -1px; + margin-right: 4px; + background: currentColor; + -webkit-mask-image: url('/images/icons/grid-icon.svg'); + mask-image: url('/images/icons/grid-icon.svg'); + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; +} + +.rate-explorer-bank-icon { + display: inline-block; + width: 15px; + height: 15px; + vertical-align: middle; + position: relative; + top: -1px; + margin-right: 4px; + color: currentColor; +} + +.rate-explorer-provider-logo { + height: 24px; + width: auto; + min-width: fit-content; + vertical-align: middle; + border-radius: 0; + pointer-events: none; +} + +html.dark .rate-explorer-provider-logo { + filter: brightness(0.82); +} + +.rate-explorer-table tbody td { + position: relative; +} + +.rate-explorer-table tbody td:hover::after { + content: ''; + position: absolute; + left: 0; + right: 0; + top: -9999px; + bottom: -9999px; + background: var(--ls-black-02); + pointer-events: none; + z-index: -1; +} + +html.dark .rate-explorer-table tbody td:hover::after { + background: var(--ls-white-02); +} + +.rate-explorer-table thead th.re-col-hover { + background: var(--ls-gray-075); +} + +html.dark .rate-explorer-table thead th.re-col-hover { + background: var(--ls-gray-925); +} + +.rate-explorer-th-delta { + text-align: right !important; + width: 1%; +} + +.rate-explorer-table tbody td { + padding: 0 20px; + height: 56px; + border-bottom: 0.5px solid var(--ls-black-06); + white-space: nowrap; + color: var(--ls-gray-950); + vertical-align: middle; +} + +html.dark .rate-explorer-table tbody td { + border-color: var(--ls-white-04); + color: var(--ls-gray-050); +} + +.rate-explorer-table tbody tr:last-child td { + border-bottom: none; +} + +/* Row hover */ +.rate-explorer-row:hover td { + background: var(--ls-black-02); +} + +.rate-explorer-row:hover td.rate-explorer-provider-best { + background: rgba(17, 169, 103, 0.08); +} + +html.dark .rate-explorer-row:hover td { + background: var(--ls-white-02); +} + +html.dark .rate-explorer-row:hover td.rate-explorer-provider-best { + background: rgba(74, 222, 128, 0.10); +} + +/* Empty state */ +.rate-explorer-empty { + text-align: center; + color: var(--ls-gray-400) !important; + padding: 32px 20px !important; + white-space: normal !important; +} + +/* ---------- Destination cell ---------- */ +.rate-explorer-dest { + display: flex; + align-items: center; + gap: 12px; +} + +.rate-explorer-dest-text { + display: flex; + flex-direction: column; +} + +.rate-explorer-currency-code { + font-weight: 500; + font-size: 14px; + line-height: 20px; + color: var(--ls-gray-950); +} + +html.dark .rate-explorer-currency-code { + color: var(--ls-gray-050); +} + +.rate-explorer-currency-name { + font-size: 13px; + line-height: 18px; + color: var(--ls-gray-500); +} + +/* ---------- Provider cells ---------- */ +.rate-explorer-provider-cell { + vertical-align: middle; + width: 1%; + white-space: nowrap; +} + +/* Best provider highlight */ +.rate-explorer-provider-best { + background: rgba(17, 169, 103, 0.04); +} + +html.dark .rate-explorer-provider-best { + background: rgba(74, 222, 128, 0.06); +} + +.rate-explorer-provider-best .rate-explorer-provider-amount { + color: var(--ls-green-600); +} + +html.dark .rate-explorer-provider-best .rate-explorer-provider-amount { + color: var(--ls-green-400); +} + +.rate-explorer-provider-best.rate-explorer-col-grid .rate-explorer-provider-amount { + color: var(--ls-green-600); +} + +html.dark .rate-explorer-provider-best.rate-explorer-col-grid .rate-explorer-provider-amount { + color: var(--ls-green-400); +} + +/* Amount/bps text in provider cells */ +.rate-explorer-provider-amount { + font-family: "Suisse Intl", sans-serif; + font-size: 14px; + font-weight: 400; + color: var(--ls-gray-500); +} + +html.dark .rate-explorer-provider-amount { + color: var(--ls-gray-500); +} + +.rate-explorer-col-grid .rate-explorer-provider-amount { + color: var(--ls-gray-950); +} + +html.dark .rate-explorer-col-grid .rate-explorer-provider-amount { + color: var(--ls-gray-050); +} + +/* N/A dash */ +.rate-explorer-na { + font-family: "Suisse Intl", sans-serif; + font-size: 14px; + color: var(--ls-gray-300); +} + +html.dark .rate-explorer-na { + color: var(--ls-gray-600); +} + +/* ---------- Delta column ---------- */ +.rate-explorer-delta { + text-align: right !important; + font-size: 13px !important; + color: var(--ls-gray-400) !important; + font-family: "Suisse Intl", sans-serif; + white-space: nowrap; + width: 1%; +} + +.rate-explorer-delta-positive { + color: var(--ls-green-600) !important; +} + +html.dark .rate-explorer-delta-positive { + color: var(--ls-green-400) !important; +} + +.rate-explorer-delta-negative { + color: var(--ls-orange-500) !important; +} + +/* ---------- Disclaimer ---------- */ +.rate-explorer-disclaimer { + padding: 12px 20px; + font-size: 13px; + line-height: 20px; + color: var(--ls-gray-400); + border-top: 0.5px solid var(--ls-black-06); + font-feature-settings: "salt" 1; +} + +html.dark .rate-explorer-disclaimer { + border-color: var(--ls-white-04); +} + +.rate-explorer-disclaimer a { + color: var(--ls-sky-500); + text-decoration: none; +} + +.rate-explorer-disclaimer a:hover { + text-decoration: underline; +} + +/* ---------- Skeleton loader ---------- */ +@keyframes rate-explorer-shimmer { + 0% { background-position: -200% 0; } + 100% { background-position: 200% 0; } +} + +.rate-explorer-skeleton-cell, +.rate-explorer-skeleton-flag { + background: linear-gradient(90deg, var(--ls-black-04) 25%, transparent 50%, var(--ls-black-04) 75%); + background-size: 200% 100%; + animation: rate-explorer-shimmer 1.5s ease-in-out infinite; +} + +html.dark .rate-explorer-skeleton-cell, +html.dark .rate-explorer-skeleton-flag { + background: linear-gradient(90deg, var(--ls-white-06) 25%, transparent 50%, var(--ls-white-06) 75%); + background-size: 200% 100%; +} + +.rate-explorer-skeleton-row { + display: flex; + align-items: center; + height: 56px; + padding: 0 20px; + gap: 20px; + border-bottom: 0.5px solid var(--ls-black-06); +} + +html.dark .rate-explorer-skeleton-row { + border-color: var(--ls-white-04); +} + +.rate-explorer-skeleton-row:last-child { + border-bottom: none; +} + +.rate-explorer-skeleton-flag { + width: 28px; + height: 28px; + border-radius: 50%; + flex-shrink: 0; +} + +.rate-explorer-skeleton-cell { + height: 14px; + border-radius: 4px; +} + +.rate-explorer-skeleton-bar { + height: 14px; + width: 56px; + border-radius: 4px; + background: linear-gradient(90deg, var(--ls-black-04) 25%, transparent 50%, var(--ls-black-04) 75%); + background-size: 200% 100%; + animation: rate-explorer-shimmer 1.5s ease-in-out infinite; +} + +html.dark .rate-explorer-skeleton-bar { + background: linear-gradient(90deg, var(--ls-white-06) 25%, transparent 50%, var(--ls-white-06) 75%); + background-size: 200% 100%; +} + +/* ---------- Mobile responsive ---------- */ +@media (max-width: 768px) { + .rate-explorer-table-wrapper { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } + + .rate-explorer-table-wrapper [data-table-wrapper] { + overflow-x: auto !important; + } + + .rate-explorer-header { + position: relative; + top: auto; + z-index: auto; + box-shadow: none; + transform: none !important; + padding: 10px 20px; + flex-direction: column; + align-items: stretch; + } + + .rate-explorer-table thead th { + position: relative; + top: auto; + z-index: auto; + } + + .rate-explorer-header-left { + flex-wrap: wrap; + } + + .rate-explorer-header-right { + position: static; + width: 100%; + justify-content: flex-start; + } + + .rate-explorer-toggle { + flex: 1; + } + + .rate-explorer-toggle-btn { + flex: 1; + } + + .rate-explorer-th-dest { + min-width: 0; + } + + .rate-explorer-table thead th, + .rate-explorer-table tbody td { + padding-left: 20px; + padding-right: 20px; + } + + .rate-explorer-delta, + .rate-explorer-th-delta { + display: none; + } + + .rate-explorer-disclaimer { + padding: 10px 20px; + } + + .rate-explorer-provider-amount { + font-size: 13px; + } +}