Skip to content

Commit f9ed2d6

Browse files
patcapulongclaude
andcommitted
feat: dynamic Grid pricing with live cost floor and Coinbase mid-market
Replace static gridSpread values with dynamic pricing: - Coinbase API for clean mid-market rates (replaces Grid authenticated API) - Grid public API for cost floor (partner fees included) - Rate card (Wise Business - 5bps) as target pricing - Display formula: MAX(rateCardSpread, costFloor) - Cost floor re-fetches when user changes send amount - CSS layout tweaks for rate explorer Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 59d75da commit f9ed2d6

2 files changed

Lines changed: 131 additions & 52 deletions

File tree

mintlify/fx-rates.mdx

Lines changed: 126 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -7,46 +7,46 @@ mode: "wide"
77
---
88

99
export const CURRENCIES = [
10-
{ code: 'USD', name: 'US Dollar', region: 'Americas', usdMid: 1, gridSpread: 0, wiseSpread: 0 },
11-
{ code: 'CAD', name: 'Canadian Dollar', region: 'Americas', usdMid: 1.362, gridSpread: 0.35, wiseSpread: 0.55 },
12-
{ code: 'MXN', name: 'Mexican Peso', region: 'Americas', usdMid: 17.45, gridSpread: 0.5, wiseSpread: 0.95 },
13-
{ code: 'BRL', name: 'Brazilian Real', region: 'Americas', usdMid: 5.05, gridSpread: 0.55, wiseSpread: 1.00 },
14-
{ code: 'CRC', name: 'Costa Rican Colón', region: 'Americas', usdMid: 512, gridSpread: 0.8, wiseSpread: 1.60 },
15-
{ code: 'EUR', name: 'Euro', region: 'Europe', usdMid: 0.921, gridSpread: 0.25, wiseSpread: 0.55 },
16-
{ code: 'GBP', name: 'British Pound', region: 'Europe', usdMid: 0.791, gridSpread: 0.25, wiseSpread: 0.55 },
17-
{ code: 'CHF', name: 'Swiss Franc', region: 'Europe', usdMid: 0.881, gridSpread: 0.3, wiseSpread: 0.55 },
18-
{ code: 'DKK', name: 'Danish Krone', region: 'Europe', usdMid: 6.87, gridSpread: 0.3, wiseSpread: 0.55 },
19-
{ code: 'SEK', name: 'Swedish Krona', region: 'Europe', usdMid: 10.45, gridSpread: 0.4, wiseSpread: 0.90 },
20-
{ code: 'NOK', name: 'Norwegian Krone', region: 'Europe', usdMid: 10.55, gridSpread: 0.4, wiseSpread: 0.90 },
21-
{ code: 'CZK', name: 'Czech Koruna', region: 'Europe', usdMid: 23.15, gridSpread: 0.45, wiseSpread: 0.95 },
22-
{ code: 'HUF', name: 'Hungarian Forint', region: 'Europe', usdMid: 362, gridSpread: 0.5, wiseSpread: 1.00 },
23-
{ code: 'PLN', name: 'Polish Zloty', region: 'Europe', usdMid: 4.02, gridSpread: 0.45, wiseSpread: 0.90 },
24-
{ code: 'RON', name: 'Romanian Leu', region: 'Europe', usdMid: 4.58, gridSpread: 0.5, wiseSpread: 1.00 },
25-
{ code: 'BGN', name: 'Bulgarian Lev', region: 'Europe', usdMid: 1.80, gridSpread: 0.45, wiseSpread: 0.95 },
26-
{ code: 'ISK', name: 'Icelandic Króna', region: 'Europe', usdMid: 138, gridSpread: 0.8, wiseSpread: 1.50 },
27-
{ code: 'NGN', name: 'Nigerian Naira', region: 'Africa', usdMid: 1550, gridSpread: 0.8, wiseSpread: 2.50 },
28-
{ code: 'KES', name: 'Kenyan Shilling', region: 'Africa', usdMid: 129.5, gridSpread: 0.7, wiseSpread: 2.00 },
29-
{ code: 'ZAR', name: 'South African Rand', region: 'Africa', usdMid: 18.5, gridSpread: 0.5, wiseSpread: 0.95 },
30-
{ code: 'GHS', name: 'Ghanaian Cedi', region: 'Africa', usdMid: 14.8, gridSpread: 0.9, wiseSpread: 2.50 },
31-
{ code: 'UGX', name: 'Ugandan Shilling', region: 'Africa', usdMid: 3750, gridSpread: 0.9, wiseSpread: 2.80 },
32-
{ code: 'TZS', name: 'Tanzanian Shilling', region: 'Africa', usdMid: 2530, gridSpread: 0.9, wiseSpread: 2.50 },
33-
{ code: 'ZMW', name: 'Zambian Kwacha', region: 'Africa', usdMid: 26.5, gridSpread: 1.0, wiseSpread: 2.80 },
34-
{ code: 'MWK', name: 'Malawian Kwacha', region: 'Africa', usdMid: 1730, gridSpread: 1.1, wiseSpread: 3.00 },
35-
{ code: 'XOF', name: 'West African CFA', region: 'Africa', usdMid: 605, gridSpread: 0.8, wiseSpread: 2.00 },
36-
{ code: 'XAF', name: 'Central African CFA', region: 'Africa', usdMid: 605, gridSpread: 0.85, wiseSpread: 2.00 },
37-
{ code: 'CDF', name: 'Congolese Franc', region: 'Africa', usdMid: 2780, gridSpread: 1.2, wiseSpread: 3.00 },
38-
{ code: 'BWP', name: 'Botswana Pula', region: 'Africa', usdMid: 13.6, gridSpread: 0.9, wiseSpread: 2.00 },
39-
{ code: 'INR', name: 'Indian Rupee', region: 'Asia-Pacific', usdMid: 83.5, gridSpread: 0.45, wiseSpread: 0.90 },
40-
{ code: 'PHP', name: 'Philippine Peso', region: 'Asia-Pacific', usdMid: 56.2, gridSpread: 0.5, wiseSpread: 1.00 },
41-
{ code: 'IDR', name: 'Indonesian Rupiah', region: 'Asia-Pacific', usdMid: 15650, gridSpread: 0.7, wiseSpread: 1.10 },
42-
{ code: 'SGD', name: 'Singapore Dollar', region: 'Asia-Pacific', usdMid: 1.34, gridSpread: 0.3, wiseSpread: 0.55 },
43-
{ code: 'HKD', name: 'Hong Kong Dollar', region: 'Asia-Pacific', usdMid: 7.81, gridSpread: 0.2, wiseSpread: 0.50 },
44-
{ code: 'CNY', name: 'Chinese Yuan', region: 'Asia-Pacific', usdMid: 7.24, gridSpread: 0.6, wiseSpread: 1.10 },
45-
{ code: 'KRW', name: 'South Korean Won', region: 'Asia-Pacific', usdMid: 1325, gridSpread: 0.45, wiseSpread: 0.90 },
46-
{ code: 'THB', name: 'Thai Baht', region: 'Asia-Pacific', usdMid: 35.2, gridSpread: 0.5, wiseSpread: 0.95 },
47-
{ code: 'VND', name: 'Vietnamese Dong', region: 'Asia-Pacific', usdMid: 25200, gridSpread: 0.8, wiseSpread: 1.10 },
48-
{ code: 'MYR', name: 'Malaysian Ringgit', region: 'Asia-Pacific', usdMid: 4.65, gridSpread: 0.5, wiseSpread: 1.00 },
49-
{ code: 'LKR', name: 'Sri Lankan Rupee', region: 'Asia-Pacific', usdMid: 298, gridSpread: 0.9, wiseSpread: 1.80 },
10+
{ code: 'USD', name: 'US Dollar', region: 'Americas', usdMid: 1, rateCardSpread: 0, wiseSpread: 0 },
11+
{ code: 'CAD', name: 'Canadian Dollar', region: 'Americas', usdMid: 1.362, rateCardSpread: 0.15, wiseSpread: 0.55 },
12+
{ code: 'MXN', name: 'Mexican Peso', region: 'Americas', usdMid: 17.45, rateCardSpread: 0.36, wiseSpread: 0.95 },
13+
{ code: 'BRL', name: 'Brazilian Real', region: 'Americas', usdMid: 5.05, rateCardSpread: 0.45, wiseSpread: 1.00 },
14+
{ code: 'CRC', name: 'Costa Rican Colon', region: 'Americas', usdMid: 512, rateCardSpread: 1.50, wiseSpread: 1.60 },
15+
{ code: 'EUR', name: 'Euro', region: 'Europe', usdMid: 0.921, rateCardSpread: 0.13, wiseSpread: 0.55 },
16+
{ code: 'GBP', name: 'British Pound', region: 'Europe', usdMid: 0.791, rateCardSpread: 0.13, wiseSpread: 0.55 },
17+
{ code: 'CHF', name: 'Swiss Franc', region: 'Europe', usdMid: 0.881, rateCardSpread: 0.21, wiseSpread: 0.55 },
18+
{ code: 'DKK', name: 'Danish Krone', region: 'Europe', usdMid: 6.87, rateCardSpread: 0.17, wiseSpread: 0.55 },
19+
{ code: 'SEK', name: 'Swedish Krona', region: 'Europe', usdMid: 10.45, rateCardSpread: 0.17, wiseSpread: 0.90 },
20+
{ code: 'NOK', name: 'Norwegian Krone', region: 'Europe', usdMid: 10.55, rateCardSpread: 0.16, wiseSpread: 0.90 },
21+
{ code: 'CZK', name: 'Czech Koruna', region: 'Europe', usdMid: 23.15, rateCardSpread: 0.24, wiseSpread: 0.95 },
22+
{ code: 'HUF', name: 'Hungarian Forint', region: 'Europe', usdMid: 362, rateCardSpread: 0.31, wiseSpread: 1.00 },
23+
{ code: 'PLN', name: 'Polish Zloty', region: 'Europe', usdMid: 4.02, rateCardSpread: 0.22, wiseSpread: 0.90 },
24+
{ code: 'RON', name: 'Romanian Leu', region: 'Europe', usdMid: 4.58, rateCardSpread: 0.30, wiseSpread: 1.00 },
25+
{ code: 'BGN', name: 'Bulgarian Lev', region: 'Europe', usdMid: 1.80, rateCardSpread: 0.34, wiseSpread: 0.95 },
26+
{ code: 'ISK', name: 'Icelandic Krona', region: 'Europe', usdMid: 138, rateCardSpread: 0.80, wiseSpread: 1.50 },
27+
{ code: 'NGN', name: 'Nigerian Naira', region: 'Africa', usdMid: 1550, rateCardSpread: 0.80, wiseSpread: 2.50 },
28+
{ code: 'KES', name: 'Kenyan Shilling', region: 'Africa', usdMid: 129.5, rateCardSpread: 0.36, wiseSpread: 2.00 },
29+
{ code: 'ZAR', name: 'South African Rand', region: 'Africa', usdMid: 18.5, rateCardSpread: 0.50, wiseSpread: 0.95 },
30+
{ code: 'GHS', name: 'Ghanaian Cedi', region: 'Africa', usdMid: 14.8, rateCardSpread: 0.41, wiseSpread: 2.50 },
31+
{ code: 'UGX', name: 'Ugandan Shilling', region: 'Africa', usdMid: 3750, rateCardSpread: 0.90, wiseSpread: 2.80 },
32+
{ code: 'TZS', name: 'Tanzanian Shilling', region: 'Africa', usdMid: 2530, rateCardSpread: 0.90, wiseSpread: 2.50 },
33+
{ code: 'ZMW', name: 'Zambian Kwacha', region: 'Africa', usdMid: 26.5, rateCardSpread: 1.00, wiseSpread: 2.80 },
34+
{ code: 'MWK', name: 'Malawian Kwacha', region: 'Africa', usdMid: 1730, rateCardSpread: 1.10, wiseSpread: 3.00 },
35+
{ code: 'XOF', name: 'West African CFA', region: 'Africa', usdMid: 605, rateCardSpread: 0.80, wiseSpread: 2.00 },
36+
{ code: 'XAF', name: 'Central African CFA', region: 'Africa', usdMid: 605, rateCardSpread: 0.85, wiseSpread: 2.00 },
37+
{ code: 'CDF', name: 'Congolese Franc', region: 'Africa', usdMid: 2780, rateCardSpread: 1.20, wiseSpread: 3.00 },
38+
{ code: 'BWP', name: 'Botswana Pula', region: 'Africa', usdMid: 13.6, rateCardSpread: 0.90, wiseSpread: 2.00 },
39+
{ code: 'INR', name: 'Indian Rupee', region: 'Asia-Pacific', usdMid: 83.5, rateCardSpread: 0.32, wiseSpread: 0.90 },
40+
{ code: 'PHP', name: 'Philippine Peso', region: 'Asia-Pacific', usdMid: 56.2, rateCardSpread: 0.20, wiseSpread: 1.00 },
41+
{ code: 'IDR', name: 'Indonesian Rupiah', region: 'Asia-Pacific', usdMid: 15650, rateCardSpread: 0.35, wiseSpread: 1.10 },
42+
{ code: 'SGD', name: 'Singapore Dollar', region: 'Asia-Pacific', usdMid: 1.34, rateCardSpread: 0.15, wiseSpread: 0.55 },
43+
{ code: 'HKD', name: 'Hong Kong Dollar', region: 'Asia-Pacific', usdMid: 7.81, rateCardSpread: 0.20, wiseSpread: 0.50 },
44+
{ code: 'CNY', name: 'Chinese Yuan', region: 'Asia-Pacific', usdMid: 7.24, rateCardSpread: 0.60, wiseSpread: 1.10 },
45+
{ code: 'KRW', name: 'South Korean Won', region: 'Asia-Pacific', usdMid: 1325, rateCardSpread: 0.45, wiseSpread: 0.90 },
46+
{ code: 'THB', name: 'Thai Baht', region: 'Asia-Pacific', usdMid: 35.2, rateCardSpread: 0.34, wiseSpread: 0.95 },
47+
{ code: 'VND', name: 'Vietnamese Dong', region: 'Asia-Pacific', usdMid: 25200, rateCardSpread: 1.18, wiseSpread: 1.10 },
48+
{ code: 'MYR', name: 'Malaysian Ringgit', region: 'Asia-Pacific', usdMid: 4.65, rateCardSpread: 0.31, wiseSpread: 1.00 },
49+
{ code: 'LKR', name: 'Sri Lankan Rupee', region: 'Asia-Pacific', usdMid: 298, rateCardSpread: 0.42, wiseSpread: 1.80 },
5050
];
5151

5252
export const CURRENCY_TO_COUNTRY = {
@@ -430,7 +430,7 @@ export const RateExplorer = () => {
430430
const [viewMode, setViewMode] = React.useState('bps');
431431
const [liveRates, setLiveRates] = React.useState(() => {
432432
try {
433-
const cached = sessionStorage.getItem('grid_live_rates');
433+
const cached = sessionStorage.getItem('coinbase_mid_rates');
434434
if (cached) {
435435
const parsed = JSON.parse(cached);
436436
if (parsed && parsed.ts && Date.now() - parsed.ts < CACHE_TTL) {
@@ -440,6 +440,7 @@ export const RateExplorer = () => {
440440
} catch (e) {}
441441
return null;
442442
});
443+
const [gridCostRates, setGridCostRates] = React.useState(null);
443444
const [showSourceDropdown, setShowSourceDropdown] = React.useState(false);
444445
const [sourceDropdownQuery, setSourceDropdownQuery] = React.useState('');
445446
const [sourceHighlightIdx, setSourceHighlightIdx] = React.useState(-1);
@@ -475,27 +476,64 @@ export const RateExplorer = () => {
475476
React.useEffect(() => {
476477
if (!isVisible) return;
477478
if (liveRates !== null) return;
478-
fetch('https://api.lightspark.com/grid/2025-10-13/exchange-rates?sourceCurrency=USD')
479+
fetch('https://api.coinbase.com/v2/exchange-rates?currency=USD')
479480
.then(res => {
480481
if (!res.ok) throw new Error(res.status);
481482
return res.json();
482483
})
483484
.then(json => {
484485
const rates = {};
485-
if (json && json.data && json.data.length > 0) {
486-
json.data.forEach(r => {
487-
rates[r.destinationCurrency.code] = String(r.exchangeRate);
486+
if (json && json.data && json.data.rates) {
487+
Object.entries(json.data.rates).forEach(([code, val]) => {
488+
rates[code] = val;
488489
});
489490
}
490491
setLiveRates(rates);
491-
try { sessionStorage.setItem('grid_live_rates', JSON.stringify({ ts: Date.now(), rates })); } catch (e) {}
492+
try { sessionStorage.setItem('coinbase_mid_rates', JSON.stringify({ ts: Date.now(), rates })); } catch (e) {}
492493
})
493494
.catch(() => {
494495
setLiveRates({});
495-
try { sessionStorage.setItem('grid_live_rates', JSON.stringify({ ts: Date.now(), rates: {} })); } catch (e) {}
496+
try { sessionStorage.setItem('coinbase_mid_rates', JSON.stringify({ ts: Date.now(), rates: {} })); } catch (e) {}
496497
});
497498
}, [isVisible]);
498499

500+
React.useEffect(() => {
501+
if (!isVisible) return;
502+
const bucketed = bucketAmount(debouncedAmount);
503+
const sendingCents = bucketed * 100;
504+
const cacheKey = 'grid_cost_' + sendingCents;
505+
try {
506+
const cached = sessionStorage.getItem(cacheKey);
507+
if (cached) {
508+
const parsed = JSON.parse(cached);
509+
if (parsed && parsed.ts && Date.now() - parsed.ts < CACHE_TTL) {
510+
setGridCostRates(parsed.rates);
511+
return;
512+
}
513+
}
514+
} catch (e) {}
515+
fetch('https://api.lightspark.com/grid/2025-10-13/public/exchange-rates?sourceCurrency=USD&sendingAmount=' + sendingCents)
516+
.then(res => {
517+
if (!res.ok) throw new Error(res.status);
518+
return res.json();
519+
})
520+
.then(json => {
521+
const rates = {};
522+
if (json && json.data && json.data.length > 0) {
523+
json.data.forEach(r => {
524+
var destDecimals = r.destinationCurrency.decimals || 0;
525+
var actualRate = (r.receivingAmount / Math.pow(10, destDecimals)) / (r.sendingAmount / 100);
526+
rates[r.destinationCurrency.code] = String(actualRate);
527+
});
528+
}
529+
setGridCostRates(rates);
530+
try { sessionStorage.setItem(cacheKey, JSON.stringify({ ts: Date.now(), rates })); } catch (e) {}
531+
})
532+
.catch(() => {
533+
setGridCostRates({});
534+
});
535+
}, [isVisible, debouncedAmount]);
536+
499537
React.useEffect(() => {
500538
if (!isVisible) return;
501539
const bucketed = bucketAmount(debouncedAmount);
@@ -572,9 +610,18 @@ export const RateExplorer = () => {
572610
el.style.setProperty('--re-thead-top', (navH + hH) + 'px');
573611
};
574612

575-
requestAnimationFrame(updateVars);
613+
const measureBelow = () => {
614+
var prev = el.style.minHeight;
615+
el.style.minHeight = '0px';
616+
var elBottom = el.getBoundingClientRect().bottom + window.scrollY;
617+
var pageH = document.documentElement.scrollHeight;
618+
el.style.minHeight = prev;
619+
el.style.setProperty('--re-below-h', (pageH - elBottom) + 'px');
620+
};
621+
622+
requestAnimationFrame(() => { updateVars(); measureBelow(); });
576623

577-
const ro = new ResizeObserver(updateVars);
624+
const ro = new ResizeObserver(() => { updateVars(); measureBelow(); });
578625
ro.observe(header);
579626
if (navbar) ro.observe(navbar);
580627

@@ -661,12 +708,25 @@ export const RateExplorer = () => {
661708
setTimeout(() => e.target.select(), 0);
662709
};
663710

711+
const scrollToHeader = () => {
712+
const header = headerBarRef.current;
713+
const el = explorerRef.current;
714+
if (header) header.classList.remove('re-header-hidden');
715+
if (!el || window.scrollY < 200) return;
716+
el.style.minHeight = el.offsetHeight + 'px';
717+
setTimeout(() => {
718+
el.scrollIntoView({ behavior: 'instant', block: 'start' });
719+
requestAnimationFrame(() => { el.style.minHeight = ''; });
720+
}, 0);
721+
};
722+
664723
const addCurrency = (code) => {
665724
if (!selectedCurrencies.includes(code)) {
666725
setSelectedCurrencies([...selectedCurrencies, code]);
667726
}
668727
setShowDropdown(false);
669728
setDropdownQuery('');
729+
scrollToHeader();
670730
};
671731

672732
const removeCurrency = (code) => {
@@ -675,6 +735,7 @@ export const RateExplorer = () => {
675735

676736
const clearCurrencies = () => {
677737
setSelectedCurrencies([]);
738+
scrollToHeader();
678739
};
679740

680741
const getUsdMid = (code) => {
@@ -694,8 +755,21 @@ export const RateExplorer = () => {
694755
const midRate = destUsdMid / sourceUsdMid;
695756
const cd = competitorData[dest.code];
696757
const midReceive = cd && cd.midReceive ? cd.midReceive : debouncedAmount * midRate;
697-
const gridBps = Math.round(dest.gridSpread * 100);
698-
const gridReceive = midReceive * (1 - dest.gridSpread / 100);
758+
const rateCard = dest.rateCardSpread;
759+
let costFloorSpread = 0;
760+
if (gridCostRates && gridCostRates[dest.code] && destUsdMid > 0) {
761+
const gridApiRate = parseFloat(gridCostRates[dest.code]);
762+
const coinbaseMid = midRate;
763+
const coinbaseDestPerUnit = destUsdMid;
764+
const gridDestPerUnit = gridApiRate;
765+
if (coinbaseDestPerUnit > 0) {
766+
costFloorSpread = ((coinbaseDestPerUnit - gridDestPerUnit) / coinbaseDestPerUnit) * 100;
767+
if (costFloorSpread < 0) costFloorSpread = 0;
768+
}
769+
}
770+
const effectiveSpread = Math.max(rateCard, costFloorSpread);
771+
const gridBps = Math.round(effectiveSpread * 100);
772+
const gridReceive = midReceive * (1 - effectiveSpread / 100);
699773
const providers = {};
700774
if (cd && cd.providers) {
701775
cd.providers.forEach(p => {
@@ -733,7 +807,7 @@ export const RateExplorer = () => {
733807
code: dest.code, name: dest.name,
734808
midRate: midRate, midReceive: midReceive,
735809
gridReceive: gridReceive, gridBps: gridBps,
736-
providers: providers, gridSpread: dest.gridSpread,
810+
providers: providers, effectiveSpread: effectiveSpread,
737811
stripeBps: stripeBps, stripeReceive: stripeReceive,
738812
airwallexBps: airwallexBps, airwallexReceive: airwallexReceive,
739813
bankAvgBps: bankAvgBps, bankAvgReceive: bankAvgReceive,

mintlify/style.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4214,6 +4214,10 @@ html.dark #rate-explorer-static {
42144214
background: transparent;
42154215
border-top: 0.5px solid var(--ls-black-10);
42164216
border-bottom: 0.5px solid var(--ls-black-10);
4217+
display: flex;
4218+
flex-direction: column;
4219+
min-height: calc(100dvh - var(--re-navbar-h, 60px) - var(--re-below-h, 100px));
4220+
scroll-margin-top: var(--re-navbar-h, 60px);
42174221
}
42184222

42194223
html.dark .rate-explorer {
@@ -4763,6 +4767,7 @@ html.dark .rate-explorer-toggle-btn.active {
47634767

47644768
.rate-explorer-table-wrapper {
47654769
overflow-x: clip;
4770+
flex: 1;
47664771
}
47674772

47684773
.rate-explorer-table-wrapper [data-table-wrapper] {

0 commit comments

Comments
 (0)