Add a Buy Now button to your website. Three lines of code. No backend required.
Copy this into any HTML page. It works immediately if the customer has Kyoto installed.
1<button id="buy-btn">Buy Now — 1 MOB</button> 2 3<script> 4document.getElementById('buy-btn').addEventListener('click', async () => { 5 // Check if Kyoto wallet is installed 6 if (!window.kyoto?.installed) { 7 alert('Please install Kyoto wallet to pay with MobileCoin'); 8 window.open('https://kyoto.antelopeswap.com', '_blank'); 9 return; 10 } 11 12 // Request payment 13 const result = await window.kyoto.requestPayment({ 14 recipientB58: 'YOUR_MOB_ADDRESS_HERE', 15 amount: '1000000000000', // 1 MOB in picoMOB 16 memo: 'Purchase #1234' 17 }); 18 19 if (result.ok) { 20 // Payment approved — listen for confirmation 21 window.addEventListener('kyoto-payment-update', (e) => { 22 if (e.detail.status === 'confirmed') { 23 document.getElementById('buy-btn').textContent = 'Paid ✓'; 24 } 25 }); 26 } 27}); 28</script>
The payment flow happens entirely in the browser. No server integration needed.
The complete window.kyoto payment API.
1// Check if Kyoto is installed 2const hasKyoto = typeof window.kyoto !== 'undefined' 3 && window.kyoto.installed === true;
Request a MobileCoin payment from the user. Opens the Kyoto approval popup.
| Parameter | Type | Required | Description |
|---|---|---|---|
| recipientB58 | string |
Yes | Your MobileCoin b58 address where funds will be sent |
| amount | string |
Yes | Amount in picoMOB. 1 MOB = 1,000,000,000,000 picoMOB |
| memo | string |
No | Memo attached to the transaction (visible to sender and recipient) |
| merchantRef | string |
No | Your reference ID. Used for idempotency — duplicate requests with the same ref from the same tab are deduplicated |
Returns: Promise<{ ok: boolean, intentId?: string, error?: string }>
1const result = await window.kyoto.requestPayment({ 2 recipientB58: 'b58addr...', 3 amount: '5000000000000', // 5 MOB 4 memo: 'Order #12345', 5 merchantRef: 'order-12345' 6}); 7 8if (result.ok) { 9 console.log('Payment intent:', result.intentId); 10} else { 11 console.error('Payment failed:', result.error); 12}
1// Payment status updates 2window.addEventListener('kyoto-payment-update', (event) => { 3 const { intentId, status, txId } = event.detail; 4 // status: 'confirmed' | 'rejected' | 'failed' | 'expired' 5}); 6 7// Intent registered (fires after requestPayment resolves) 8window.addEventListener('kyoto-payment-registered', (event) => { 9 const { intentId } = event.detail; 10});
| Status | Meaning | When it fires |
|---|---|---|
| confirmed | Payment confirmed on-chain | ~5–10 seconds after approval |
| rejected | User declined in the popup | Immediately |
| failed | Transaction failed (network error, insufficient funds) | Immediately |
| expired | Tab was closed or navigated away | On tab close |
For sites that prefer declarative payment requests:
1<meta name="mobilecoin-payment" 2 content="mob://pay?to=b58addr&amount=1000000000000&memo=Order+12345&ref=order-12345">
Kyoto detects these tags automatically via MutationObserver. Works with SPAs that add meta tags dynamically.
MobileCoin amounts are specified in picoMOB (1012 picoMOB = 1 MOB).
| MOB | picoMOB | String value |
|---|---|---|
| 0.01 | 10,000,000,000 | "10000000000" |
| 0.1 | 100,000,000,000 | "100000000000" |
| 1 | 1,000,000,000,000 | "1000000000000" |
| 10 | 10,000,000,000,000 | "10000000000000" |
| 100 | 100,000,000,000,000 | "100000000000000" |
1function mobToPico(mob) { 2 return String(BigInt(Math.round(mob * 1e12))); 3} 4 5// Usage 6const fiveMob = mobToPico(5); // "5000000000000"
Always pass amounts as strings, not numbers. JavaScript numbers lose precision above 253. picoMOB values for amounts over ~9 MOB exceed this limit.
1<button onclick="donate()">Donate 1 MOB</button> 2<p id="status"></p> 3 4<script> 5async function donate() { 6 const status = document.getElementById('status'); 7 8 if (!window.kyoto?.installed) { 9 status.textContent = 'Please install Kyoto wallet'; 10 return; 11 } 12 13 status.textContent = 'Opening wallet...'; 14 15 const result = await window.kyoto.requestPayment({ 16 recipientB58: 'YOUR_ADDRESS', 17 amount: '1000000000000', 18 memo: 'Donation' 19 }); 20 21 if (result.ok) { 22 status.textContent = 'Confirming...'; 23 window.addEventListener('kyoto-payment-update', (e) => { 24 if (e.detail.status === 'confirmed') { 25 status.textContent = 'Thank you for your donation!'; 26 } else if (e.detail.status === 'failed') { 27 status.textContent = 'Transaction failed. Please try again.'; 28 } 29 }, { once: true }); 30 } else { 31 status.textContent = result.error || 'Payment cancelled'; 32 } 33} 34</script>
1async function checkout(orderId, amount, description) { 2 if (!window.kyoto?.installed) { 3 window.location.href = 'https://kyoto.antelopeswap.com'; 4 return; 5 } 6 7 const result = await window.kyoto.requestPayment({ 8 recipientB58: STORE_MOB_ADDRESS, 9 amount: mobToPico(amount), 10 memo: description, 11 merchantRef: orderId // Prevents double-payment on refresh 12 }); 13 14 if (!result.ok) { 15 showError(result.error); 16 return; 17 } 18 19 showStatus('Payment submitted. Waiting for confirmation...'); 20 21 window.addEventListener('kyoto-payment-update', async (e) => { 22 const { status, txId } = e.detail; 23 24 switch (status) { 25 case 'confirmed': 26 showStatus('Payment confirmed!'); 27 // Notify your backend 28 await fetch('/api/orders/' + orderId + '/paid', { 29 method: 'POST', 30 headers: { 'Content-Type': 'application/json' }, 31 body: JSON.stringify({ txId, amount, currency: 'MOB' }) 32 }); 33 window.location.href = '/order/' + orderId + '/confirmation'; 34 break; 35 36 case 'failed': 37 showError('Payment failed. Please try again.'); 38 break; 39 40 case 'rejected': 41 showStatus('Payment cancelled.'); 42 break; 43 } 44 }, { once: true }); 45}
1// Adapt UI based on wallet availability 2function initPaymentOptions() { 3 const kyotoButton = document.getElementById('pay-with-kyoto'); 4 const installBanner = document.getElementById('kyoto-install'); 5 6 if (window.kyoto?.installed) { 7 kyotoButton.style.display = 'block'; 8 installBanner.style.display = 'none'; 9 } else { 10 kyotoButton.style.display = 'none'; 11 installBanner.style.display = 'block'; 12 } 13} 14 15window.addEventListener('DOMContentLoaded', initPaymentOptions);
merchantRef provides idempotency — the same ref from the same tab won't trigger duplicate paymentswindow.kyoto API object is frozen and non-configurable — other scripts on the page cannot tamper with itconfirmed event fires as soon as the transaction is included in a block.window.kyoto?.installed. If false, show an install link or offer an alternative payment method.