DEV Community

Daniel Ioni
Daniel Ioni

Posted on

Integrating Monero Payments into a Node.js App: A Step-by-Step Guide

Introduction

Privacy-focused cryptocurrencies like Monero are becoming increasingly important for applications that value user anonymity and financial freedom. In this guide, I'll walk you through the process of integrating Monero payments into a Node.js application, from setting up the wallet RPC to generating subaddresses for each order.

What we'll cover:

Installing Monero CLI tools on Ubuntu

Creating a wallet and starting the RPC server

Building a Node.js service to interact with Monero

Generating subaddresses for orders

Testing the integration
Enter fullscreen mode Exit fullscreen mode

Prerequisites:

A Node.js application (Express + MongoDB)

A Ubuntu VPS with root access

Basic knowledge of JavaScript and Linux commands
Enter fullscreen mode Exit fullscreen mode

Step 1: Install Monero CLI Tools

First, download and extract the Monero CLI tools:
bash

cd /root
wget https://downloads.getmonero.org/cli/linux64 -O monero-linux64.tar.bz2
tar -xjf monero-linux64.tar.bz2
mv monero-x86_64-linux-gnu-v* monero
cd monero

Step 2: Create a Monero Wallet (Testnet)

For development and testing, it's recommended to use the testnet. This avoids risking real funds.
bash

mkdir -p /root/monero-wallet
./monero-wallet-cli --generate-new-wallet /root/monero-wallet/myzubster-wallet \
--password MyStrongPassword123 \
--testnet \
--daemon-address testnet.community:28081

Tip: For mainnet, remove --testnet and use node.moneroworld.com:18089 as the daemon address.
Enter fullscreen mode Exit fullscreen mode

After creation, you'll see:
text

-rw------- 1 root root 410274 myzubster-wallet
-rw------- 1 root root 95 myzubster-wallet.address.txt
-rw------- 1 root root 1689 myzubster-wallet.keys

Step 3: Start the Wallet RPC Server

The wallet RPC allows your Node.js app to interact with the wallet.
bash

nohup ./monero-wallet-rpc \
--wallet-file /root/monero-wallet/myzubster-wallet \
--password MyStrongPassword123 \
--rpc-bind-port 18083 \
--daemon-address testnet.community:28081 \
--testnet \
--disable-rpc-login \
--log-level 0 \

/root/monero-wallet-rpc.log 2>&1 &

Verify it's running:
bash

curl -X POST http://127.0.0.1:18083/json_rpc \
-d '{"jsonrpc":"2.0","id":"0","method":"get_version"}' \
-H "Content-Type: application/json"

Expected response:
json

{
"id": "0",
"jsonrpc": "2.0",
"result": {
"release": true,
"version": 65567
}
}

Step 4: Create a systemd Service (Optional but Recommended)

To keep the RPC server running after reboots:
bash

cat > /etc/systemd/system/monero-wallet-rpc.service << 'EOF'
[Unit]
Description=Monero Wallet RPC
After=network.target

[Service]
Type=simple
User=root
WorkingDirectory=/root/monero
ExecStart=/root/monero/monero-wallet-rpc --wallet-file /root/monero-wallet/myzubster-wallet --password MyStrongPassword123 --rpc-bind-port 18083 --daemon-address testnet.community:28081 --testnet --disable-rpc-login --log-level 0
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable monero-wallet-rpc
systemctl start monero-wallet-rpc

Step 5: Build the Monero Service in Node.js

Create services/moneroService.js:
javascript

const axios = require('axios');

const RPC_URL = process.env.MONERO_RPC_URL || 'http://127.0.0.1:18083/json_rpc';

const rpc = async (method, params = {}) => {
try {
const response = await axios.post(RPC_URL, {
jsonrpc: '2.0',
id: '0',
method,
params,
});
return response.data.result;
} catch (error) {
console.error([Monero RPC] Error in ${method}:, error.response?.data || error.message);
throw new Error(Monero RPC error: ${method});
}
};

const generateSubaddress = async (label = '') => {
const result = await rpc('create_address', { label });
return {
address: result.address,
index: result.address_index,
label: result.label || label,
};
};

const getBalance = async () => {
const result = await rpc('get_balance');
return {
balance: result.balance / 1e12,
unlockedBalance: result.unlocked_balance / 1e12,
};
};

const checkPayment = async (address, expectedAmount, confirmations = 1) => {
const transfers = await rpc('get_transfers', { in: true, account_index: 0 });
const incoming = transfers.in || [];
for (const tx of incoming) {
if (tx.address === address && tx.amount / 1e12 >= expectedAmount && tx.confirmations >= confirmations) {
return {
received: true,
amount: tx.amount / 1e12,
txid: tx.txid,
confirmations: tx.confirmations,
timestamp: tx.timestamp,
};
}
}
return { received: false };
};

module.exports = {
generateSubaddress,
getBalance,
checkPayment,
};

Step 6: Add Monero Routes to Express

In server.js:
javascript

const moneroService = require('./services/moneroService');

// Generate a subaddress for an order
app.post('/api/payments/generate-address', async (req, res) => {
try {
const { orderId, label } = req.body;
const sub = await moneroService.generateSubaddress(label || order-${orderId || 'test'});
res.json({
success: true,
subaddress: sub.address,
index: sub.index,
label: sub.label,
});
} catch (error) {
console.error('Errore generazione subaddress:', error);
res.status(500).json({ error: 'Errore nella generazione del subaddress' });
}
});

// Check wallet balance
app.get('/api/payments/balance', async (req, res) => {
try {
const balance = await moneroService.getBalance();
res.json({ success: true, balance });
} catch (error) {
console.error('Errore recupero saldo:', error);
res.status(500).json({ error: 'Errore nel recupero del saldo' });
}
});

Step 7: Update Environment Variables

In .env:
env

MONERO_RPC_URL=http://127.0.0.1:18083/json_rpc
MONERO_WALLET_ADDRESS=9wSYFu8Ln6wPMQwWGdXRpb6hRdKk9pFd72PmV9BNcsDRTgbYuqsk5P5Xwc6nFHN5Q73RGzk5MhNdSeZnGE6NxTSfG1fHjGk
MONERO_NETWORK=testnet
PAYMENT_MODE=monero

Step 8: Test the Integration
bash

curl -X POST http://localhost:3000/api/payments/generate-address \
-H "Content-Type: application/json" \
-d '{"orderId":"123","label":"test-order"}'

Successful response:
json

{
"success": true,
"subaddress": "BgRBa3mvUDUWHcZ2n9FhoVXfRGh8sJjPTQiqvBx9Sf1YBuZ3kL1E8qFHUusxQESPukXXi2PeuUMsK4ej9oRxXBHnTsDBzmQ",
"index": 2,
"label": "test-order"
}

Common Issues and Solutions
Issue 1: "no connection to daemon"

This happens when the wallet RPC can't connect to a Monero node. It doesn't prevent subaddress generation, but it does prevent balance checks and payment verification.

Solution: Use a public node or run a local node.
Issue 2: "await is only valid in async functions"

This error occurs when using await outside an async function.

Solution: Wrap your code in an async function or move the route inside the proper Express handler.
Issue 3: RPC port already in use

If you see address already in use, change the --rpc-bind-port value.
Next Steps

Now that the backend can generate subaddresses, you can:

Integrate subaddress generation into the order creation flow.

Build a payment page in the frontend with QR code and status monitoring.

Implement a payment monitoring service that checks for incoming transactions and updates order status.
Enter fullscreen mode Exit fullscreen mode

Conclusion

Integrating Monero payments into a Node.js app is straightforward once you understand the RPC interface. With subaddress generation, you can create a unique payment address for each order, preserving user privacy and simplifying reconciliation.

This integration brings your application one step closer to being truly decentralized and privacy-focused.
🔗 My Project: MyZubster

MyZubster is a skills exchange platform with Monero payments, built with:

Node.js + Express

MongoDB

Nginx + Let's Encrypt

Tor onion service

Monero RPC

Live site (clearnet): https://myzubster.com

Tor onion: http://olqcnbdlt35k2stmmwvzhvuetu2fc4us2jnn5wg6y6wlcddihfmdomid.onion

GitHub: https://github.com/DanielIoni-creator/MyZubsterGateway
Enter fullscreen mode Exit fullscreen mode

Top comments (0)