--password "STRONG_PASSWORD_HERE" \
--daemon-address node.moneroworld.com:18089
3.4 Wallet Files Structure
text
/var/lib/monero/wallets/
├── myzubster # Wallet data (binary)
├── myzubster.address.txt # Primary address (public)
└── myzubster.keys # Private keys (DO NOT SHARE)
3.5 Get Primary Address
bash
cat /var/lib/monero/wallets/myzubster.address.txt
Example: 4A6B...XXXX...XXXX... (testnet starts with 9, mainnet with 4)
- Wallet RPC Server 4.1 Manual Start (for Testing) bash
./monero-wallet-rpc \
--wallet-file /var/lib/monero/wallets/myzubster \
--password "STRONG_PASSWORD_HERE" \
--rpc-bind-port 18083 \
--rpc-bind-ip 127.0.0.1 \
--daemon-address testnet.community:28081 \
--testnet \
--disable-rpc-login \
--log-level 0 \
--max-log-file-size 104850000 \
--max-log-files 50 \
--non-interactive \
--pidfile /run/monero-wallet-rpc.pid
4.2 Verify RPC is Running
bash
curl -X POST http://127.0.0.1:18083/json_rpc \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":"0","method":"get_version"}'
Expected response:
json
{
"id": "0",
"jsonrpc": "2.0",
"result": {
"release": true,
"version": 65567
}
}
4.3 Production systemd Service
Create /etc/systemd/system/monero-wallet-rpc.service:
ini
[Unit]
Description=Monero Wallet RPC (MyZubster)
After=network.target
Wants=network.target
[Service]
Type=simple
User=root
Group=root
WorkingDirectory=/opt/monero
ExecStart=/opt/monero/monero-wallet-rpc \
--wallet-file /var/lib/monero/wallets/myzubster \
--password "STRONG_PASSWORD_HERE" \
--rpc-bind-port 18083 \
--rpc-bind-ip 127.0.0.1 \
--daemon-address testnet.community:28081 \
--testnet \
--disable-rpc-login \
--log-level 0 \
--pidfile /run/monero-wallet-rpc.pid
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
Enable and start:
bash
systemctl daemon-reload
systemctl enable monero-wallet-rpc
systemctl start monero-wallet-rpc
systemctl status monero-wallet-rpc
- Node.js Monero Service 5.1 Install Dependencies bash
cd /opt/myzubster
npm install axios
5.2 Create services/moneroService.js
javascript
const axios = require('axios');
const crypto = require('crypto');
// Configuration from environment
const RPC_URL = process.env.MONERO_RPC_URL || 'http://127.0.0.1:18083/json_rpc';
const WALLET_ADDRESS = process.env.MONERO_WALLET_ADDRESS || 'XXXX';
const NETWORK = process.env.MONERO_NETWORK || 'testnet';
const CONFIRMATIONS_REQUIRED = parseInt(process.env.MONERO_CONFIRMATIONS || '1', 10);
/**
-
JSON-RPC call to Monero wallet
*/
const rpc = async (method, params = {}) => {
try {
const response = await axios.post(RPC_URL, {
jsonrpc: '2.0',
id: crypto.randomBytes(8).toString('hex'),
method,
params,
}, {
timeout: 30000,
headers: { 'Content-Type': 'application/json' },
});if (response.data.error) {
throw new Error(Monero RPC error: ${response.data.error.message});
}return response.data.result;
} catch (error) {
console.error([Monero RPC] Method ${method} failed:, error.response?.data || error.message);
throw new Error(Monero RPC error: ${method} - ${error.message});
}
};
/**
- Generate a subaddress for an order */ const generateSubaddress = async (label = '') => { const result = await rpc('create_address', { label: label.slice(0, 64), // Max 64 chars account_index: 0 });
return {
address: result.address,
index: result.address_index,
label: result.label || label,
};
};
/**
- Get wallet balance */ const getBalance = async (accountIndex = 0) => { const result = await rpc('get_balance', { account_index: accountIndex });
return {
balance: result.balance / 1e12, // Convert from atomic units
unlockedBalance: result.unlocked_balance / 1e12,
blocksToUnlock: result.blocks_to_unlock || 0,
};
};
/**
- Check if a subaddress received a payment */ const checkPayment = async (address, expectedAmount, confirmations = CONFIRMATIONS_REQUIRED) => { // Get all incoming transfers const transfers = await rpc('get_transfers', { in: true, account_index: 0, filter_by_height: true, min_height: 0, });
const incoming = transfers.in || [];
const pending = transfers.pending || [];
// Check confirmed transfers
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,
blockHeight: tx.height,
isConfirmed: true,
};
}
}
// Check pending (unconfirmed) transfers
for (const tx of pending) {
if (tx.address === address && tx.amount / 1e12 >= expectedAmount) {
return {
received: true,
amount: tx.amount / 1e12,
txid: tx.txid,
confirmations: 0,
timestamp: tx.timestamp,
blockHeight: null,
isConfirmed: false,
};
}
}
return { received: false };
};
/**
- Send XMR from wallet */ const sendPayment = async (destinationAddress, amount, priority = 1) => { const result = await rpc('transfer', { destinations: [{ address: destinationAddress, amount: Math.floor(amount * 1e12) // Convert to atomic units }], priority, // 1 = default, 2 = unimportant, 3 = normal, 4 = elevated, 5 = priority ring_size: 7, // Standard ring size get_tx_key: true, do_not_relay: false, });
return {
txid: result.tx_hash,
txKey: result.tx_key,
amount: amount,
fee: result.fee / 1e12,
multiSigTx: result.multisig_tx || false,
};
};
/**
- Get transaction details */ const getTransaction = async (txid) => { const result = await rpc('get_transfer_by_txid', { txid }); return { txid: result.txid, amount: result.amount / 1e12, fee: result.fee / 1e12, confirmations: result.confirmations, timestamp: result.timestamp, address: result.address, destination: result.destinations || [], }; };
/**
- Get all subaddresses */ const getSubaddresses = async (accountIndex = 0) => { const result = await rpc('get_address', { account_index: accountIndex, view_all: true, }); return result.addresses.map(addr => ({ address: addr.address, index: addr.address_index, label: addr.label || '', })); };
module.exports = {
rpc,
generateSubaddress,
getBalance,
checkPayment,
sendPayment,
getTransaction,
getSubaddresses,
};
- Environment Configuration
Create .env in project root:
env
Monero Configuration
MONERO_RPC_URL=http://127.0.0.1:18083/json_rpc
MONERO_WALLET_ADDRESS=XXXX
MONERO_VIEW_KEY=XXXX # Optional, for read-only access
MONERO_NETWORK=testnet
MONERO_CONFIRMATIONS=1
Payment Configuration
PAYMENT_MODE=monero
PAYMENT_TIMEOUT=3600 # Seconds to wait for payment
PAYMENT_EXPIRY=86400 # 24 hours
- Order Model Integration
Add Monero fields to models/Order.js:
javascript
const OrderSchema = new mongoose.Schema({
// ... existing fields ...
// Monero payment fields
moneroSubaddress: {
type: String,
default: null,
index: true,
},
moneroAddressIndex: {
type: Number,
default: null,
},
moneroPaymentStatus: {
type: String,
enum: ['pending', 'detected', 'confirmed', 'expired', 'failed'],
default: 'pending',
},
moneroPaymentTxid: {
type: String,
default: null,
},
moneroPaymentAmount: {
type: Number,
default: 0,
},
moneroPaymentConfirmations: {
type: Number,
default: 0,
},
moneroPaymentDetectedAt: {
type: Date,
default: null,
},
moneroPaymentConfirmedAt: {
type: Date,
default: null,
},
moneroPaymentExpiresAt: {
type: Date,
default: null,
},
});
- Order Creation with Subaddress
In routes/orders.js:
javascript
const moneroService = require('../services/moneroService');
router.post('/', async (req, res) => {
try {
const { offerId, quantity = 1 } = req.body;
const buyerId = req.user._id;
// Validate offer
const offer = await Offer.findById(offerId);
if (!offer) {
return res.status(404).json({ error: 'Offer not found' });
}
// Calculate total
const totalPrice = offer.price * quantity;
const expiresAt = new Date(Date.now() + (parseInt(process.env.PAYMENT_EXPIRY, 10) * 1000));
// Create order
const order = new Order({
offer: offerId,
buyer: buyerId,
quantity,
totalPrice,
status: 'pending',
moneroPaymentStatus: 'pending',
moneroPaymentExpiresAt: expiresAt,
});
// Generate Monero subaddress
try {
const sub = await moneroService.generateSubaddress(`order-${order._id}`);
order.moneroSubaddress = sub.address;
order.moneroAddressIndex = sub.index;
await order.save();
} catch (moneroError) {
console.error('Failed to generate subaddress:', moneroError);
// Continue - order is created but payment not yet linked
}
await order.populate('offer', 'title price');
await order.populate('buyer', 'name email');
res.status(201).json({
success: true,
order,
payment: {
moneroSubaddress: order.moneroSubaddress,
addressIndex: order.moneroAddressIndex,
totalPrice: order.totalPrice,
currency: 'XMR',
expiresAt: order.moneroPaymentExpiresAt,
},
});
} catch (error) {
console.error('Order creation error:', error);
res.status(500).json({ error: 'Failed to create order' });
}
});
- Payment Monitoring Service
Create services/paymentMonitor.js:
javascript
const Order = require('../models/Order');
const moneroService = require('./moneroService');
const MONITOR_INTERVAL = parseInt(process.env.MONITOR_INTERVAL || '30000', 10); // 30 seconds
const MAX_RETRIES = 3;
/**
- Monitor pending orders for payments */ const monitorPayments = async () => { console.log('[PaymentMonitor] Scanning for pending payments...');
const pendingOrders = await Order.find({
moneroPaymentStatus: { $in: ['pending', 'detected'] },
moneroSubaddress: { $ne: null },
moneroPaymentExpiresAt: { $gt: new Date() },
});
console.log([PaymentMonitor] Found ${pendingOrders.length} pending orders);
for (const order of pendingOrders) {
try {
await checkOrderPayment(order);
} catch (error) {
console.error([PaymentMonitor] Error checking order ${order._id}:, error);
}
}
};
/**
- Check a single order for payment */ const checkOrderPayment = async (order) => { const result = await moneroService.checkPayment( order.moneroSubaddress, order.totalPrice, parseInt(process.env.MONERO_CONFIRMATIONS || '1', 10) );
if (result.received) {
order.moneroPaymentStatus = result.isConfirmed ? 'confirmed' : 'detected';
order.moneroPaymentTxid = result.txid;
order.moneroPaymentAmount = result.amount;
order.moneroPaymentConfirmations = result.confirmations || 0;
if (result.isConfirmed) {
order.moneroPaymentConfirmedAt = new Date();
order.status = 'paid';
} else {
order.moneroPaymentDetectedAt = new Date();
}
await order.save();
console.log(`[PaymentMonitor] Order ${order._id} updated: ${order.moneroPaymentStatus}`);
}
};
/**
- Start monitoring
*/
const startMonitoring = () => {
console.log(
[PaymentMonitor] Starting (interval: ${MONITOR_INTERVAL}ms)); setInterval(monitorPayments, MONITOR_INTERVAL); monitorPayments(); // Run immediately };
module.exports = { startMonitoring };
- Testing the Integration 10.1 Test Subaddress Generation (Direct RPC) bash
curl -X POST http://127.0.0.1:18083/json_rpc \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": "0",
"method": "create_address",
"params": { "label": "test-order" }
}'
Expected response:
json
{
"id": "0",
"jsonrpc": "2.0",
"result": {
"address": "9xY...XXXX...XXXX",
"address_index": 1,
"label": "test-order"
}
}
10.2 Test Complete Flow
bash
Get token
TOKEN=$(curl -s -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com","password":"Test123!"}' \
| jq -r '.token')
Create skill
SKILL_ID=$(curl -s -X POST http://localhost:3000/api/skills \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"title":"Monero Skill","description":"Test skill","category":"Informatica","zone":"Online"}' \
| jq -r '._id')
Create offer
OFFER_ID=$(curl -s -X POST http://localhost:3000/api/offers \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"title\":\"Test Offer\",\"description\":\"Test\",\"price\":0.5,\"skill\":\"$SKILL_ID\",\"category\":\"Informatica\"}" \
| jq -r '.offer._id')
Create order (generates subaddress)
curl -X POST http://localhost:3000/api/orders \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"offerId\":\"$OFFER_ID\",\"quantity\":1}" \
| jq '.'
- Troubleshooting 11.1 "no connection to daemon"
Cause: Wallet RPC cannot connect to Monero node.
Solution:
bash
Test daemon connectivity
curl -X POST http://testnet.community:28081/json_rpc \
-d '{"jsonrpc":"2.0","id":"0","method":"get_version"}' \
-H "Content-Type: application/json"
If fails, use a different node
Try: node.moneroworld.com:18089 (mainnet)
Try: testnet.moneroworld.com:38081 (testnet)
11.2 "Couldn't connect to server" on RPC
Check:
bash
netstat -tulpn | grep 18083
Should show monero-wallet-rpc listening on 127.0.0.1:18083
Check process
ps aux | grep monero-wallet-rpc
Check logs
journalctl -u monero-wallet-rpc -n 50 --no-pager
11.3 "E11000 duplicate key error"
Cause: Unique index conflict on orderNumber field.
Solution:
bash
mongosh myzubster --eval "db.orders.getIndexes()"
mongosh myzubster --eval "db.orders.dropIndex('orderNumber_1')"
mongosh myzubster --eval "db.orders.createIndex({ orderNumber: 1 }, { unique: true, sparse: true })"
- Production Checklist Item Check Wallet password stored securely (not in code) ✅ RPC bound to localhost only (127.0.0.1) ✅ systemd service configured with restart policy ✅ Environment variables loaded from .env ✅ Logging enabled with rotation ✅ Firewall rules applied (ports 18083 not exposed) ✅ Mainnet wallet ready (if moving to production) ✅ Backup of wallet keys stored securely ✅ Monitoring alerts configured ✅
- Conclusion
You now have a complete Monero payment integration:
✅ Wallet RPC configured as a systemd service
✅ Node.js service with full RPC coverage
✅ Subaddress generation per order
✅ Payment monitoring with automatic status updates
✅ Testnet integration for safe testing
Moving to Mainnet:
Generate a new wallet without --testnet
Use node.moneroworld.com:18089 as daemon
Update .env with MONERO_NETWORK=mainnet
Test with small amounts first
🔗 Project Links
Live site: https://myzubster.com
Tor onion: http://olqcnbdlt35k2stmmwvzhvuetu2fc4us2jnn5wg6y6wlcddihfmdomid.onion
GitHub: https://github.com/DanielIoni-creator/MyZubsterGateway
If you found this guide helpful, please star the repo and share it with others interested in privacy-focused crypto integrations! 🚀
Top comments (0)