Add MoonPay card on-ramp (same underpinning as RM Circle)

Signed /api/moonpay-url endpoint: public key from MOONPAY_PUBLIC_KEY env or site
config, SECRET from MOONPAY_SECRET_KEY env ONLY (never site config, since
/api/config exposes siteConfig). Wallet-prefilled signed MoonPay URL when keys
are set, else a generic buy page. "Buy POL with a card" button under the Buy
packages tiles. Zero custody: MoonPay is merchant of record; crypto goes
straight to the buyer's wallet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-08 06:26:15 -05:00
parent 6e8777b1d4
commit 8ec6286ff7
3 changed files with 48 additions and 1 deletions
+26
View File
@@ -1182,6 +1182,23 @@
// ── in-dashboard package buying ──
const PKG = { 1: 'Micro', 2: 'Activation', 3: 'Builder', 4: 'Growth', 5: 'Leader' };
// Card on-ramp: buy POL with a card via MoonPay, delivered to the buyer's own
// wallet. Signed + wallet-prefilled once MoonPay keys are set; generic page
// otherwise. The site never touches funds — MoonPay is merchant of record.
async function openMoonpay(pol) {
try {
let addr = '';
try { const me = await (await fetch('/api/me')).json(); addr = me.address || ''; } catch (e) {}
const q = '/api/moonpay-url?pol=' + encodeURIComponent(pol || '') + (addr ? '&address=' + encodeURIComponent(addr) : '');
const r = await (await fetch(q)).json();
if (r && r.url) {
window.open(r.url, '_blank', 'noopener');
IAP.status(r.signed
? 'MoonPay opened in a new tab with your wallet address pre-filled. Choose POL on Polygon, finish the purchase, then come back and buy your package.'
: 'MoonPay opened in a new tab. Choose POL on the Polygon network and paste your own wallet address as the destination, then come back.', 'ok');
}
} catch (e) { IAP.status('Could not open MoonPay: ' + ((e && e.message) || e), 'bad'); }
}
async function loadBuyTiles() {
try {
const { products } = await (await fetch('/api/catalog')).json();
@@ -1221,6 +1238,15 @@
} catch (e) { IAP.status('Purchase failed: ' + ((e && e.message) || e), 'bad'); }
finally { b.disabled = false; }
}));
// brand new to crypto? buy POL with a card, sent straight to the wallet
const builder = products.find(p => p.priceCents === 5000) || products[products.length - 1];
const needPol = (builder && builder.costWei) ? Math.max(30, Math.ceil(Number(builder.costWei) / 1e18) + 3) : 30;
const cta = document.createElement('div');
cta.style.cssText = 'grid-column:1/-1;margin-top:10px;text-align:center';
cta.innerHTML = '<p class="muted small" style="margin:0 0 8px">New to crypto? Buy POL with a debit or credit card, Apple Pay, or Google Pay. It lands straight in your own wallet, and this site never touches your money.</p>'
+ '<button class="btn small sec" id="moonpayBtn" type="button">💳 Buy POL with a card</button>';
wrap.appendChild(cta);
const mb = $('moonpayBtn'); if (mb) mb.addEventListener('click', () => openMoonpay(needPol));
} catch (e) {}
}
loadBuyTiles();
+1 -1
View File
@@ -687,7 +687,7 @@
</div>
<script src="/assets/common.js?v=20260908c"></script>
<script src="/assets/wallet.js?v=20260907q"></script>
<script src="/assets/my.js?v=20260908e"></script>
<script src="/assets/my.js?v=20260908f"></script>
<script src="/assets/chat.js?v=20260907l"></script>
</body>
</html>