DEV Community

Daniel Ioni
Daniel Ioni

Posted on

πŸ” MyZubster Monero Integration: From Zero to Hero

yZubster Monero Integration: From Zero to Hero

Complete guide to integrating Monero payments – from wallet setup to production monitoring
πŸ“Œ What is This Guide?

This guide covers everything you need to know about Monero integration in MyZubster:

βœ… Monero basics – Understanding XMR, wallets, and transactions

βœ… Wallet setup – Creating and configuring a Monero wallet

βœ… RPC client – Interacting with the Monero blockchain

βœ… Subaddress generation – Creating unique addresses per order

βœ… Payment monitoring – Real-time transaction detection

βœ… Webhook integration – Notifying the Marketplace

βœ… Testnet vs Mainnet – Testing and production environments

βœ… Production considerations – Scaling and security
Enter fullscreen mode Exit fullscreen mode

🧩 Monero Architecture Overview
text

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Monero Integration Architecture β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ MyZubster Gateway β”‚ β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚
β”‚ β”‚ β”‚ Payment β”‚ β”‚ Subaddress β”‚ β”‚ Webhook β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ Initiation β”‚ β”‚ Generation β”‚ β”‚ Sender β”‚ β”‚ β”‚
β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚ β”‚
β”‚ β–Ό β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ Monero RPC Client β”‚ β”‚
β”‚ β”‚ (monero-javascript) β”‚ β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚
β”‚ β”‚ β”‚ Wallet β”‚ β”‚ Transfer β”‚ β”‚ Monitor β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ API β”‚ β”‚ API β”‚ β”‚ API β”‚ β”‚ β”‚
β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚ β”‚
β”‚ β–Ό β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ Monero Node β”‚ β”‚
β”‚ β”‚ (monerod) β”‚ β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚
β”‚ β”‚ β”‚ Blockchain β”‚ β”‚ Transactionβ”‚ β”‚ Mempool β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ Storage β”‚ β”‚ Pool β”‚ β”‚ Monitor β”‚ β”‚ β”‚
β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚ β”‚
β”‚ β–Ό β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ Monero Network β”‚ β”‚
β”‚ β”‚ (Testnet/Mainnet) β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ“¦ Step 1: Setting Up Monero Node
1.1 Run Monero Node with Docker
bash

Pull Monero node image

docker pull ghcr.io/tao-o/monero:latest

Run Monero node (testnet)

docker run -d \
--name monero-node \
-p 18081:18081 \
-p 18082:18082 \
-v monero_data:/home/monero/.bitmonero \
ghcr.io/tao-o/monero:latest \
--testnet \
--rpc-bind-ip=0.0.0.0 \
--rpc-bind-port=18081 \
--confirm-external-bind \
--non-interactive

Run Monero node (mainnet)

docker run -d \
--name monero-node \
-p 18081:18081 \
-p 18082:18082 \
-v monero_data:/home/monero/.bitmonero \
ghcr.io/tao-o/monero:latest \
--rpc-bind-ip=0.0.0.0 \
--rpc-bind-port=18081 \
--confirm-external-bind \
--non-interactive

1.2 Check Node Status
bash

Check node status

curl -X POST http://localhost:18081/get_info

Check node sync status

curl -X POST http://localhost:18081/sync_info

Check blockchain height

curl -X POST http://localhost:18081/get_height

1.3 Create Monero Wallet
bash

Create wallet via RPC

curl -X POST http://localhost:18081/create_wallet \
-H "Content-Type: application/json" \
-d '{
"filename": "myzubster_wallet",
"password": "your_wallet_password",
"language": "English"
}'

Open wallet

curl -X POST http://localhost:18081/open_wallet \
-H "Content-Type: application/json" \
-d '{
"filename": "myzubster_wallet",
"password": "your_wallet_password"
}'

Get wallet address

curl -X POST http://localhost:18081/get_address

πŸ› οΈ Step 2: Monero RPC Client Implementation
2.1 Monero Service

File: gateway/services/monero.js
javascript

const monerojs = require('monero-javascript');

// ============================================
// CONFIGURATION
// ============================================
const MONERO_CONFIG = {
rpcUrl: process.env.MONERO_RPC_URL || 'http://localhost:18081',
username: process.env.MONERO_RPC_USERNAME || '',
password: process.env.MONERO_RPC_PASSWORD || '',
network: process.env.MONERO_NETWORK || 'testnet',
walletPassword: process.env.MONERO_WALLET_PASSWORD,
walletPath: process.env.MONERO_WALLET_PATH || './monero_wallet',
};

let walletInstance = null;

// ============================================
// CONNECT TO MONERO
// ============================================
const connectMonero = async () => {
if (walletInstance) {
return walletInstance;
}

try {
// Create wallet client
walletInstance = await monerojs.createWalletRpc({
url: MONERO_CONFIG.rpcUrl,
username: MONERO_CONFIG.username,
password: MONERO_CONFIG.password,
network: MONERO_CONFIG.network,
});

// Open wallet
await walletInstance.openWallet({
  path: MONERO_CONFIG.walletPath,
  password: MONERO_CONFIG.walletPassword,
});

console.log('βœ… Connected to Monero node');
console.log(`πŸ“¦ Network: ${MONERO_CONFIG.network}`);
console.log(`πŸ”‘ Wallet: ${MONERO_CONFIG.walletPath}`);

return walletInstance;
Enter fullscreen mode Exit fullscreen mode

} catch (error) {
console.error('❌ Error connecting to Monero:', error);
throw error;
}
};

// ============================================
// GENERATE SUBADDRESS
// ============================================
const generateSubaddress = async (accountIndex = 0) => {
try {
const wallet = await connectMonero();

const subaddress = await wallet.createSubaddress(accountIndex);

return {
  address: subaddress.getAddress(),
  index: subaddress.getIndex(),
  accountIndex: subaddress.getAccountIndex(),
  label: subaddress.getLabel(),
};
Enter fullscreen mode Exit fullscreen mode

} catch (error) {
console.error('❌ Error generating subaddress:', error);
throw error;
}
};

// ============================================
// GET BALANCE
// ============================================
const getBalance = async () => {
try {
const wallet = await connectMonero();
const balance = await wallet.getBalance();

return {
  balance: balance.getBalance(),
  unlockedBalance: balance.getUnlockedBalance(),
};
Enter fullscreen mode Exit fullscreen mode

} catch (error) {
console.error('❌ Error getting balance:', error);
throw error;
}
};

// ============================================
// GET TRANSFERS
// ============================================
const getTransfers = async (subaddress = null) => {
try {
const wallet = await connectMonero();

const transfers = await wallet.getTransfers({
  subaddress: subaddress,
  isIncoming: true,
});

return transfers.map((transfer) => ({
  txHash: transfer.getTxHash(),
  amount: transfer.getAmount(),
  confirmations: transfer.getNumConfirmations(),
  timestamp: transfer.getTimestamp(),
  isConfirmed: transfer.isConfirmed(),
}));
Enter fullscreen mode Exit fullscreen mode

} catch (error) {
console.error('❌ Error getting transfers:', error);
throw error;
}
};

// ============================================
// MONITOR PAYMENT
// ============================================
const monitorPayment = async (subaddress, expectedAmount, maxConfirmations = 10) => {
try {
const wallet = await connectMonero();

// Get recent transfers
const transfers = await wallet.getTransfers({
  subaddress: subaddress,
  isIncoming: true,
});

// Filter confirmed transfers
const confirmed = transfers
  .filter((t) => t.isConfirmed())
  .filter((t) => t.getAmount() >= expectedAmount)
  .sort((a, b) => b.getNumConfirmations() - a.getNumConfirmations());

if (confirmed.length === 0) {
  return null;
}

const last = confirmed[0];

return {
  txHash: last.getTxHash(),
  amount: last.getAmount(),
  confirmations: last.getNumConfirmations(),
  timestamp: last.getTimestamp(),
  isConfirmed: last.getNumConfirmations() >= maxConfirmations,
};
Enter fullscreen mode Exit fullscreen mode

} catch (error) {
console.error('❌ Error monitoring payment:', error);
throw error;
}
};

// ============================================
// SEND PAYMENT
// ============================================
const sendPayment = async (toAddress, amount) => {
try {
const wallet = await connectMonero();

const result = await wallet.send({
  amount: amount,
  address: toAddress,
  priority: 1, // 0=default, 1=unimportant, 2=normal, 3=elevated
});

return {
  txHash: result.getTxHash(),
  amount: result.getAmount(),
  fee: result.getFee(),
  status: result.getStatus(),
};
Enter fullscreen mode Exit fullscreen mode

} catch (error) {
console.error('❌ Error sending payment:', error);
throw error;
}
};

// ============================================
// CHECK NETWORK STATUS
// ============================================
const checkNetworkStatus = async () => {
try {
const wallet = await connectMonero();
const status = await wallet.getStatus();

return {
  height: status.getHeight(),
  blockTime: status.getBlockTime(),
  difficulty: status.getDifficulty(),
};
Enter fullscreen mode Exit fullscreen mode

} catch (error) {
console.error('❌ Error checking network status:', error);
throw error;
}
};

// ============================================
// GET TRANSACTION DETAILS
// ============================================
const getTransactionDetails = async (txHash) => {
try {
const wallet = await connectMonero();
const tx = await wallet.getTx({
txHash: txHash,
});

return {
  txHash: tx.getTxHash(),
  amount: tx.getAmount(),
  fee: tx.getFee(),
  confirmations: tx.getNumConfirmations(),
  timestamp: tx.getTimestamp(),
  isConfirmed: tx.isConfirmed(),
  isFailed: tx.isFailed(),
};
Enter fullscreen mode Exit fullscreen mode

} catch (error) {
console.error('❌ Error getting transaction details:', error);
throw error;
}
};

module.exports = {
connectMonero,
generateSubaddress,
getBalance,
getTransfers,
monitorPayment,
sendPayment,
checkNetworkStatus,
getTransactionDetails,
};

2.2 Payment Service

File: gateway/services/paymentService.js
javascript

const Payment = require('../models/Payment');
const {
generateSubaddress,
monitorPayment,
getBalance,
} = require('./monero');
const { sendWebhook } = require('./webhookSender');

// ============================================
// INITIATE PAYMENT
// ============================================
const initiatePayment = async (orderId, amount, currency) => {
try {
// Generate subaddress
const subaddress = await generateSubaddress();

// Create payment record
const payment = await Payment.create({
  orderId: orderId,
  subaddress: subaddress.address,
  amount: amount,
  currency: currency || 'USD',
  moneroAmount: await convertToMonero(amount, currency),
  addressIndex: subaddress.index,
  status: 'pending',
  network: process.env.MONERO_NETWORK || 'testnet',
});

// Start monitoring in background
startPaymentMonitoring(payment._id);

return {
  paymentId: payment._id,
  moneroAddress: payment.subaddress,
  moneroAmount: payment.moneroAmount,
  addressIndex: payment.addressIndex,
  network: payment.network,
  status: payment.status,
};
Enter fullscreen mode Exit fullscreen mode

} catch (error) {
console.error('❌ Error initiating payment:', error);
throw error;
}
};

// ============================================
// CONVERT TO MONERO
// ============================================
const convertToMonero = async (amount, currency) => {
// In production, use an exchange rate API
// For now, use a fixed rate (1 XMR = 150 USD)
const exchangeRate = 150;
return amount / exchangeRate;
};

// ============================================
// PAYMENT MONITORING
// ============================================
const paymentMonitors = new Map();

const startPaymentMonitoring = (paymentId) => {
if (paymentMonitors.has(paymentId.toString())) {
return;
}

console.log(πŸ” Starting payment monitor: ${paymentId});

let checkCount = 0;
const maxChecks = 60; // 30 minutes (60 * 30s)

const interval = setInterval(async () => {
checkCount++;

try {
  const payment = await Payment.findById(paymentId);
  if (!payment) {
    clearInterval(interval);
    paymentMonitors.delete(paymentId.toString());
    return;
  }

  if (payment.status === 'confirmed' || payment.status === 'expired') {
    clearInterval(interval);
    paymentMonitors.delete(paymentId.toString());
    return;
  }

  // Check for payment
  const result = await monitorPayment(
    payment.subaddress,
    payment.moneroAmount,
    10
  );

  if (result && result.isConfirmed) {
    // Payment confirmed!
    payment.status = 'confirmed';
    payment.txHash = result.txHash;
    payment.amountReceived = result.amount;
    payment.confirmations = result.confirmations;
    payment.confirmedAt = new Date();
    await payment.save();

    // Send webhook
    await sendWebhook(payment.orderId, {
      status: 'confirmed',
      txHash: result.txHash,
      amount: payment.amount,
      moneroAmount: result.amount,
    });

    console.log(`βœ… Payment confirmed: ${paymentId}`);

    clearInterval(interval);
    paymentMonitors.delete(paymentId.toString());
  }

  // Check for expiry (30 minutes)
  if (checkCount >= maxChecks) {
    payment.status = 'expired';
    await payment.save();

    console.log(`⏰ Payment expired: ${paymentId}`);

    clearInterval(interval);
    paymentMonitors.delete(paymentId.toString());
  }
} catch (error) {
  console.error(`❌ Error monitoring payment ${paymentId}:`, error);
}
Enter fullscreen mode Exit fullscreen mode

}, 30000); // Check every 30 seconds

paymentMonitors.set(paymentId.toString(), interval);
};

// ============================================
// GET PAYMENT STATUS
// ============================================
const getPaymentStatus = async (paymentId) => {
try {
const payment = await Payment.findById(paymentId);
if (!payment) {
throw new Error('Payment not found');
}

return {
  paymentId: payment._id,
  status: payment.status,
  moneroAddress: payment.subaddress,
  moneroAmount: payment.moneroAmount,
  confirmations: payment.confirmations,
  amountReceived: payment.amountReceived,
  txHash: payment.txHash,
  createdAt: payment.createdAt,
  confirmedAt: payment.confirmedAt,
};
Enter fullscreen mode Exit fullscreen mode

} catch (error) {
console.error('❌ Error getting payment status:', error);
throw error;
}
};

// ============================================
// GET ALL PAYMENTS
// ============================================
const getPayments = async (filters = {}) => {
try {
const where = {};
if (filters.orderId) where.orderId = filters.orderId;
if (filters.status) where.status = filters.status;
if (filters.from) where.createdAt = { $gte: new Date(filters.from) };
if (filters.to) where.createdAt = { ...where.createdAt, $lte: new Date(filters.to) };

const payments = await Payment.find(where)
  .sort({ createdAt: -1 })
  .limit(filters.limit || 100);

return payments;
Enter fullscreen mode Exit fullscreen mode

} catch (error) {
console.error('❌ Error getting payments:', error);
throw error;
}
};

module.exports = {
initiatePayment,
convertToMonero,
startPaymentMonitoring,
getPaymentStatus,
getPayments,
};

πŸ”„ Step 3: Payment Monitoring
3.1 Background Monitoring Service

File: gateway/services/monitorService.js
javascript

const Payment = require('../models/Payment');
const { monitorPayment } = require('./monero');
const { sendWebhook } = require('./webhookSender');

// ============================================
// MONITOR BACKGROUND SERVICE
// ============================================
class PaymentMonitorService {
constructor() {
this.isRunning = false;
this.interval = null;
this.intervalTime = 30000; // 30 seconds
}

async start() {
if (this.isRunning) {
console.log('⚠️ Monitor service already running');
return;
}

console.log('πŸš€ Starting payment monitor service...');
this.isRunning = true;

// Run once immediately
await this.scanPayments();

// Then run periodically
this.interval = setInterval(async () => {
  await this.scanPayments();
}, this.intervalTime);
Enter fullscreen mode Exit fullscreen mode

}

async stop() {
console.log('πŸ›‘ Stopping payment monitor service...');
this.isRunning = false;
if (this.interval) {
clearInterval(this.interval);
this.interval = null;
}
}

async scanPayments() {
try {
// Get all pending payments
const payments = await Payment.find({
status: 'pending',
createdAt: { $gte: new Date(Date.now() - 30 * 60 * 1000) }, // Last 30 minutes
});

  if (payments.length === 0) {
    // Uncomment for debugging
    // console.log('No pending payments to monitor');
    return;
  }

  console.log(`πŸ” Scanning ${payments.length} pending payments...`);

  for (const payment of payments) {
    try {
      const result = await monitorPayment(
        payment.subaddress,
        payment.moneroAmount,
        10
      );

      if (result && result.isConfirmed) {
        // Payment confirmed
        payment.status = 'confirmed';
        payment.txHash = result.txHash;
        payment.amountReceived = result.amount;
        payment.confirmations = result.confirmations;
        payment.confirmedAt = new Date();
        await payment.save();

        // Send webhook
        await sendWebhook(payment.orderId, {
          status: 'confirmed',
          txHash: result.txHash,
          amount: payment.amount,
          moneroAmount: result.amount,
        });

        console.log(`βœ… Payment confirmed: ${payment._id} (${payment.orderId})`);
      }

      // Check for expiry (30 minutes)
      const ageMinutes = (Date.now() - payment.createdAt.getTime()) / 60000;
      if (ageMinutes >= 30) {
        payment.status = 'expired';
        await payment.save();
        console.log(`⏰ Payment expired: ${payment._id} (${payment.orderId})`);
      }
    } catch (error) {
      console.error(`❌ Error processing payment ${payment._id}:`, error);
    }
  }
} catch (error) {
  console.error('❌ Error scanning payments:', error);
}
Enter fullscreen mode Exit fullscreen mode

}
}

// Singleton instance
const monitorService = new PaymentMonitorService();

module.exports = monitorService;

3.2 Start Monitor Service

File: gateway/server.js (Update)
javascript

// ... existing code ...

// Start payment monitor service
const monitorService = require('./services/monitorService');

// Start after database connection
mongoose.connect(MONGODB_URI)
.then(() => {
console.log('βœ… Connesso a MongoDB');
console.log(πŸ“¦ Database: ${MONGODB_URI});

// Start monitor service
monitorService.start();
Enter fullscreen mode Exit fullscreen mode

})
.catch((err) => {
console.error('❌ Errore connessione MongoDB:', err);
process.exit(1);
});

// Graceful shutdown
process.on('SIGTERM', async () => {
console.log('πŸ›‘ Received SIGTERM signal');
await monitorService.stop();
process.exit(0);
});

process.on('SIGINT', async () => {
console.log('πŸ›‘ Received SIGINT signal');
await monitorService.stop();
process.exit(0);
});

πŸ“‘ Step 4: Webhook Integration
4.1 Webhook Sender

File: gateway/services/webhookSender.js
javascript

const axios = require('axios');
const crypto = require('crypto');

const WEBHOOK_URL = process.env.WEBHOOK_URL || 'http://localhost:4000/webhook/order-update';
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET || 'your_webhook_secret';
const MAX_RETRIES = 5;
const RETRY_DELAY = 5000;

// ============================================
// SEND WEBHOOK
// ============================================
const sendWebhook = async (orderId, payload) => {
const data = {
orderId: orderId,
...payload,
timestamp: new Date().toISOString(),
};

// Generate HMAC signature
const signature = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(JSON.stringify(data))
.digest('hex');

const headers = {
'Content-Type': 'application/json',
'X-Webhook-Signature': signature,
'X-Webhook-Timestamp': data.timestamp,
};

// Attempt delivery with retries
let attempt = 0;
let lastError = null;

while (attempt < MAX_RETRIES) {
try {
const response = await axios.post(WEBHOOK_URL, data, {
headers: headers,
timeout: 10000,
});

  console.log(`βœ… Webhook delivered: ${orderId} (attempt ${attempt + 1})`);
  return response.data;
} catch (error) {
  attempt++;
  lastError = error;
  console.warn(`⚠️ Webhook attempt ${attempt}/${MAX_RETRIES} failed: ${error.message}`);

  if (attempt < MAX_RETRIES) {
    await new Promise(resolve => setTimeout(resolve, RETRY_DELAY * attempt));
  }
}
Enter fullscreen mode Exit fullscreen mode

}

console.error(❌ Webhook delivery failed after ${MAX_RETRIES} attempts: ${orderId});
throw lastError;
};

module.exports = {
sendWebhook,
};

4.2 Webhook Receiver (Marketplace)

File: marketplace/routes/webhook.js
javascript

const express = require('express');
const router = express.Router();
const crypto = require('crypto');
const { Order } = require('../models');

const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET || 'your_webhook_secret';

// ============================================
// VERIFY WEBHOOK SIGNATURE
// ============================================
const verifyWebhookSignature = (req, res, next) => {
try {
const signature = req.headers['x-webhook-signature'];
const timestamp = req.headers['x-webhook-timestamp'];
const payload = req.body;

if (!signature || !timestamp) {
  return res.status(401).json({ error: 'Missing signature headers' });
}

// Verify timestamp (prevent replay attacks)
const now = Date.now();
const webhookTime = new Date(timestamp).getTime();
if (Math.abs(now - webhookTime) > 300000) { // 5 minutes
  return res.status(401).json({ error: 'Webhook timestamp expired' });
}

// Verify signature
const expectedSignature = crypto
  .createHmac('sha256', WEBHOOK_SECRET)
  .update(JSON.stringify(payload))
  .digest('hex');

if (signature !== expectedSignature) {
  return res.status(401).json({ error: 'Invalid webhook signature' });
}

next();
Enter fullscreen mode Exit fullscreen mode

} catch (error) {
console.error('❌ Webhook verification error:', error);
res.status(500).json({ error: 'Webhook verification failed' });
}
};

// ============================================
// WEBHOOK ENDPOINT
// ============================================
router.post('/order-update', verifyWebhookSignature, async (req, res) => {
try {
const { orderId, status, txHash, amount, moneroAmount } = req.body;

if (!orderId || !status) {
  return res.status(400).json({ error: 'Missing required fields' });
}

console.log(`πŸ“₯ Webhook received: order ${orderId}, status ${status}`);

// Find order
const order = await Order.findByPk(orderId);
if (!order) {
  return res.status(404).json({ error: 'Order not found' });
}

// Update order based on status
if (status === 'confirmed') {
  order.status = 'paid';
  order.moneroTxHash = txHash;
  order.paidAt = new Date();
} else if (status === 'expired') {
  order.status = 'cancelled';
  order.cancelledAt = new Date();
}

await order.save();

console.log(`βœ… Order ${orderId} updated to ${order.status}`);

res.json({
  success: true,
  order: {
    id: order.id,
    status: order.status,
    updatedAt: order.updatedAt,
  },
});
Enter fullscreen mode Exit fullscreen mode

} catch (error) {
console.error('❌ Webhook error:', error);
res.status(500).json({ error: error.message });
}
});

module.exports = router;

πŸ§ͺ Step 5: Testing
5.1 Test Payment Flow
bash

1. Initiate payment

curl -X POST http://localhost:3000/api/payments/initiate \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"orderId":1,"amount":150,"currency":"USD"}'

Response:

{

"paymentId": "pay_123456",

"moneroAddress": "8A1B2C3D4E5F6G7H8I9J0K",

"moneroAmount": 0.0125,

"status": "pending"

}

2. Check payment status

curl -X GET http://localhost:3000/api/payments/status/pay_123456 \
-H "Authorization: Bearer $TOKEN"

3. Simulate payment (for testing)

curl -X POST http://localhost:3000/api/payments/webhook \
-H "Content-Type: application/json" \
-d '{"paymentId":"pay_123456","txHash":"mock_tx_hash","amount":150}'

4. Check order status (Marketplace)

curl -X GET http://localhost:4000/api/orders/1 \
-H "Authorization: Bearer $BUYER_TOKEN"

5.2 Test Webhook
bash

1. Send test webhook to Marketplace

curl -X POST http://localhost:4000/webhook/order-update \
-H "Content-Type: application/json" \
-H "X-Webhook-Signature: test_signature" \
-H "X-Webhook-Timestamp: $(date -Iseconds)" \
-d '{"orderId":1,"status":"confirmed","txHash":"mock_tx_hash"}'

2. Check order status

curl -X GET http://localhost:4000/api/orders/1 \
-H "Authorization: Bearer $BUYER_TOKEN"

πŸ”§ Step 6: Environment Variables

File: .env (Gateway)
env

Monero Configuration

MONERO_RPC_URL=http://localhost:18081
MONERO_RPC_USERNAME=
MONERO_RPC_PASSWORD=
MONERO_NETWORK=testnet
MONERO_WALLET_PASSWORD=your_wallet_password
MONERO_WALLET_PATH=./monero_wallet

Payment Configuration

PAYMENT_EXPIRY_MINUTES=30
PAYMENT_MONITOR_INTERVAL=30
MIN_CONFIRMATIONS=10

Webhook Configuration

WEBHOOK_URL=http://localhost:4000/webhook/order-update
WEBHOOK_SECRET=your_webhook_secret
WEBHOOK_MAX_RETRIES=5
WEBHOOK_RETRY_DELAY=5000

Rate Limiting

PAYMENT_RATE_LIMIT=100
PAYMENT_RATE_WINDOW=900000

πŸ› Troubleshooting
"Cannot connect to Monero node"
bash

Check if node is running

docker ps | grep monero

Check node logs

docker logs monero-node

Test RPC connection

curl -X POST http://localhost:18081/get_info

"Wallet not found"
bash

Create wallet

curl -X POST http://localhost:18081/create_wallet \
-H "Content-Type: application/json" \
-d '{"filename":"myzubster_wallet","password":"password","language":"English"}'

"Payment not detected"
bash

Check balance

curl -X POST http://localhost:18081/get_balance

Check transfers

curl -X POST http://localhost:18081/get_transfers \
-H "Content-Type: application/json" \
-d '{"in": true}'

"Webhook delivery failed"
bash

Check Marketplace is running

curl http://localhost:4000/health

Check webhook endpoint

curl -X POST http://localhost:4000/webhook/order-update \
-H "Content-Type: application/json" \
-d '{"orderId":"test","status":"test"}'

πŸ“š Resources

Monero Documentation: docs.getmonero.org

monero-javascript: npmjs.com/package/monero-javascript

Monero RPC Reference: docs.getmonero.org/interacting/monero-wallet-rpc-reference
Enter fullscreen mode Exit fullscreen mode

🌐 Connect with Me

πŸ“– Blog & Articles: DEV.to - Daniel Ioni
🐦 X (Twitter): @myzubster
πŸ’Ό LinkedIn: Daniel Ioni
πŸ™ GitHub: DanielIoni-creator
🎡 TikTok: @h4x0r_23

⭐ Star the project on GitHub – it helps a lot!

Built with ❀️ for privacy, freedom, and decentralization.

Happy integrating! πŸš€
βœ… Instructions for Publishing

Copy all the content above

Go to dev.to/new

Set the editor to Markdown mode

Paste the content

Title: MyZubster Monero Integration: From Zero to Hero

Tags: monero, blockchain, cryptocurrency, opensource

Publish!
Enter fullscreen mode Exit fullscreen mode

Top comments (0)