Monero Payment Acceptance Setup
Monero (XMR) is not just another crypto payment method. Its privacy protocol is built into the protocol level and is not optional: every transaction uses stealth addresses, Ring Signatures, and RingCT. This makes receiving XMR fundamentally different from Bitcoin or USDT — standard "look at the address on the blockchain" approaches don't work here.
Cryptographic Foundation: Why Monero is Complex
Each Monero address consists of two key pairs: (public spend key, private spend key) and (public view key, private view key). The public address encodes both public parts.
Stealth addresses: the sender never transfers directly to your public address. They generate a one-time stealth address using your public view key and a random scalar. Only the owner of the private view key can compute that this is an incoming transaction for them.
Ring Signatures: each input in a transaction is signed by a ring consisting of a real UTXO and several decoys (15 by default in Monero since HF v15). An external observer cannot determine which ring participant is the true sender.
RingCT (Ring Confidential Transactions): transaction amounts are hidden using Pedersen commitments. Only sender and recipient know real amounts.
What This Means for Payment Acceptance
You can't simply look at the blockchain and see incoming payments — transactions to your address are only visible if you have the private view key. For monitoring incoming, you must either run a full node with monero-wallet-rpc, or use the view key on the server (which lets you see incoming but not spend).
Architecture: monero-wallet-rpc
The standard tool for integration is monero-wallet-rpc from the official Monero daemon. It provides a JSON-RPC interface for all wallet operations.
Node Deployment
First you need a synchronized monerod (Monero daemon). Blockchain size ~180 GB (pruned ~60 GB), sync from scratch — 12–48 hours depending on hardware.
# Run monerod with pruning
monerod --data-dir /var/lib/monero \
--prune-blockchain \
--db-sync-mode fast \
--rpc-bind-ip 127.0.0.1 \
--rpc-bind-port 18081 \
--no-igd \
--detach
# Run monero-wallet-rpc
monero-wallet-rpc \
--daemon-address 127.0.0.1:18081 \
--rpc-bind-port 18083 \
--wallet-file /etc/monero/payment-wallet \
--password-file /etc/monero/wallet.pass \
--rpc-login payment_server:$(cat /etc/monero/rpc.pass) \
--disable-rpc-login false \
--trusted-daemon \
--non-interactive
For production — separate wallet for each environment, wallet-rpc behind nginx with TLS, authentication via HTTP Basic.
Subaddresses: Correct Architecture for Payments
Monero supports subaddresses — derived addresses from the main wallet that are fully independent at the blockchain level. This is a key feature for payment processing.
Create a subaddress for each order:
import requests
RPC_URL = "http://127.0.0.1:18083/json_rpc"
AUTH = ("payment_server", "rpc_password")
def create_payment_address(order_id: str) -> dict:
# Create new subaddress in account 0
response = requests.post(RPC_URL, auth=AUTH, json={
"jsonrpc": "2.0",
"id": "0",
"method": "create_address",
"params": {
"account_index": 0,
"label": f"order_{order_id}"
}
})
result = response.json()["result"]
return {
"address": result["address"],
"address_index": result["address_index"]
}
def check_incoming_transfers(min_amount_xmr: float) -> list:
response = requests.post(RPC_URL, auth=AUTH, json={
"jsonrpc": "2.0",
"id": "0",
"method": "get_transfers",
"params": {
"in": True,
"pending": False,
"min_height": 0 # can specify last checked block
}
})
transfers = response.json()["result"].get("in", [])
return [t for t in transfers if t["amount"] / 1e12 >= min_amount_xmr]
Why subaddresses are better than one address with payment_id: Payment ID (old method) — deprecated. Integrated addresses exist for backward compatibility but have privacy issues: they reveal that multiple transactions go to one recipient. Subaddresses look like independent addresses — better for privacy and de facto standard since 2018.
Confirmation Monitoring
Monero uses the concept of unlocked balance — funds become available after 10 confirmations (~20 minutes with average blocktime 2 minutes). For payment system:
def poll_payments(expected_payments: dict) -> None:
"""
expected_payments: {address_index: {"order_id": str, "amount_xmr": float}}
"""
response = requests.post(RPC_URL, auth=AUTH, json={
"jsonrpc": "2.0",
"id": "0",
"method": "get_transfers",
"params": {"in": True, "pending": True}
})
for transfer in response.json()["result"].get("in", []):
addr_idx = transfer["subaddr_index"]["minor"]
confirmations = transfer["confirmations"]
amount_xmr = transfer["amount"] / 1e12 # atomic unit piconero = 1e-12 XMR
if addr_idx in expected_payments:
expected = expected_payments[addr_idx]
if amount_xmr >= expected["amount_xmr"] * 0.99: # 1% tolerance for rounding
if confirmations >= 10:
mark_order_paid(expected["order_id"], amount_xmr)
else:
update_order_status(expected["order_id"], "pending_confirmations", confirmations)
Sweep and Security
Private spend key must remain in an isolated environment. For automatic payouts — separate hot wallet with minimal balance. Main funds — in cold wallet, periodic manual sweep.
View-only wallet (public spend key + private view key only) can be safely kept on server for monitoring without risk of fund theft:
monero-wallet-cli --generate-from-view-key view-only-wallet \
--address <main_address> \
--viewkey <private_view_key>
What's Included
- Deployment and synchronization of
monerod(full node or pruned) - Configuration of
monero-wallet-rpcwith authentication and TLS - Subaddress-based payment flow implementation
- Polling service for monitoring incoming transactions with confirmation logic
- Sweep automation and hot/cold storage separation
- Integration with existing payment system via webhook or callback







