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
Prerequisites:
A Node.js application (Express + MongoDB)
A Ubuntu VPS with root access
Basic knowledge of JavaScript and Linux commands
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.
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.
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
Top comments (0)