What We're Building
A checkout page that: (1) creates a payment link, (2) shows the deposit address as a QR code, (3) counts down the 20-minute session, and (4) auto-confirms when payment arrives via WebSocket.
Step 1 — Create the Payment Link
const { data } = await axios.post('/api/create-payment', {
amount: cart.total,
productIds: cart.items.map(i => i.id),
});
// data.payUrl, data.depositAddress, data.expiresAt
Step 2 — QR Code Component
import QRCode from 'qrcode.react';
function CryptoCheckout({ depositAddress, amount, expiresAt }) {
const [status, setStatus] = useState('pending');
const secondsLeft = useCountdown(expiresAt);
usePaymentWebhook(depositAddress, () => setStatus('confirmed'));
if (status === 'confirmed') return <ThankYouScreen />;
return (
<div className="checkout">
<QRCode value={depositAddress} size={200} />
<p>Send exactly <strong>{amount} USDT (BEP-20)</strong></p>
<p>to <code>{depositAddress}</code></p>
<Countdown seconds={secondsLeft} />
</div>
);
}
Step 3 — Countdown Hook
function useCountdown(expiresAt) {
const [left, setLeft] = useState(() => Math.max(0, (new Date(expiresAt) - Date.now()) / 1000));
useEffect(() => {
const id = setInterval(() => setLeft(s => Math.max(0, s - 1)), 1000);
return () => clearInterval(id);
}, []);
return Math.floor(left);
}
Step 4 — WebSocket Hook
function usePaymentWebhook(depositAddress, onConfirmed) {
useEffect(() => {
const socket = io(API_URL);
socket.emit('watchAddress', { depositAddress });
socket.on('paymentConfirmed', onConfirmed);
return () => socket.disconnect();
}, [depositAddress]);
}
UX Best Practices
- Warn users to send the exact amount — USDT amounts must match to the cent.
- Show a "Session expired" screen if the countdown hits zero; offer to generate a new link.
- Copy-to-clipboard button for the deposit address reduces user error.