Most "accept crypto" guides start with a payment processor. That's fine, but for Monero there's a simpler option that a lot of developers miss: the official monero-wallet-rpc can do almost everything a small shop needs. You don't need a third party, an account, or extra fees, and nobody else sees your orders.
The idea is simple: every order gets its own subaddress. When a payment lands on that subaddress, you know exactly which order it belongs to. You don't need payment IDs, unique amounts, or to ask customers to type a memo.
This post walks through the whole flow with working Python.
Why one subaddress per order works
A Monero wallet can generate practically unlimited subaddresses (they start with 8). They all belong to the same wallet and the same seed, but on-chain they can't be linked to each other or to your main address. That gives you two things for free:
Matching: money arriving on subaddress #1234 belongs to order #1234.
Privacy for your customers: one customer can't see what another paid, or even that both paid the same shop.
- Run the wallet RPC with a view-only wallet
Never put your spend key on the web server. Create a view-only wallet from your main wallet's address and private view key. It can see incoming payments but can't spend anything, so a compromised server can't lose your funds.
bash
once, on the server: create a view-only wallet
monero-wallet-cli --generate-from-view-key shop-view
then run the RPC, bound to localhost only, with a login
monero-wallet-rpc \
--wallet-file shop-view \
--password-file /etc/monero/shop-view.pass \
--daemon-address 127.0.0.1:18081 \
--rpc-bind-ip 127.0.0.1 --rpc-bind-port 18083 \
--rpc-login shop:CHANGE_ME
Use your own node if you can (monerod on the same box or nearby). If you use a remote node, pick one with good uptime; it only sees which blocks your wallet downloads, not your keys.
- A tiny RPC client
The wallet RPC is JSON-RPC 2.0 over HTTP with digest authentication:
python
import requests
from requests.auth import HTTPDigestAuth
`RPC = "http://127.0.0.1:18083/json_rpc"
AUTH = HTTPDigestAuth("shop", "CHANGE_ME")
ATOMIC = 10**12 # 1 XMR = 1e12 atomic units
def rpc(method, **params):
r = requests.post(RPC, json={"jsonrpc": "2.0", "id": "0", "method": method, "params": params},
auth=AUTH, timeout=30)
r.raise_for_status()
data = r.json()
if "error" in data:
raise RuntimeError(data["error"])
return data["result"]`
- Create an address for a new order
python
def new_payment_address(order_id: str) -> dict: res = rpc("create_address", account_index=0, label=f"order-{order_id}") return {"address": res["address"], "index": res["address_index"]}
Store address and index with the order. The label is just for you, so you can find the order later in any Monero wallet that opens the same view-only wallet.
- Price it and show a QR code
Lock the XMR price when the order is created (15–30 minutes is common), and show the amount, the address and a QR code. The standard Monero URI looks like this:
monero:8ABC...?tx_amount=0.2500&tx_description=Order%201234
Most mobile wallets fill in the amount when they scan it. For the price, any public ticker works; cache it server-side for a minute or two instead of calling it on every page view.
- Check whether the order is paid
python
def order_status(index: int, expected_xmr: float, min_conf: int = 10) -> dict: res = rpc("get_transfers", **{"in": True, "pool": True, "account_index": 0, "subaddr_indices": [index]}) confirmed = sum(t["amount"] for t in res.get("in", []) if t["confirmations"] >= min_conf and not t.get("double_spend_seen")) pending = sum(t["amount"] for t in res.get("in", []) + res.get("pool", [])) - confirmed expected = int(round(expected_xmr * ATOMIC)) return { "paid": confirmed >= expected, "seen": confirmed + pending >= expected, # in the mempool or not yet confirmed enough "received_xmr": (confirmed + pending) / ATOMIC, "txids": [t["txid"] for t in res.get("in", []) + res.get("pool", [])], }Run it from a cron job or a background worker every minute or so. in holds transfers already in a block, and pool holds ones still in the mempool. That gives you the usual three states:
Seen: the payment is in the mempool. Show "payment detected" to the customer right away.
Confirmed: enough confirmations. Mark the order paid and deliver.
Underpaid: received less than expected. Show the difference, and let the customer top up to the same subaddress.
How many confirmations?
Received XMR is spendable after 10 blocks (about 20 minutes), so min_conf=10 is a sensible default. For cheap digital goods, many shops deliver on "seen" and accept the small risk. For anything expensive, wait for 10 or more confirmations. In September 2025 the network had an 18-block reorganization, so very large payments deserve extra patience.
Proof of payment, when a customer says "I paid"
Monero hides amounts and addresses on-chain, so a block explorer won't settle a dispute. Ask the customer for the transaction ID and the transaction key from their wallet. Together with your subaddress, these prove the payment and the exact amount, without revealing anything else. You can check it with check_tx_key in the RPC, or paste the three values into a browser-side checker such as the XMRList transaction checker.
When to use something else
Running your own wallet RPC is the most private and cheapest option, but it isn't always the right one:
Option Good for Trade-off
Your own monero-wallet-rpc (this post) Developers, small shops, full control You run and update it yourself
BTCPay Server with the Monero plugin Shops that also want BTC/Lightning and a ready checkout More moving parts to host
A hosted payment gateway No servers at all Fees, and a third party sees your orders
After you go live
Customers who pay with Monero tend to look for shops that accept it. Once your checkout works, list it where they search. XMRList, for example, is a directory of businesses that accept XMR, and you can submit your shop there. Add a line about Monero to your FAQ too, so people know they won't be asked for a card.
That's it: one view-only wallet, one RPC process, and about 70 lines of Python, with no processor, no account and no fees beyond the network's.
Top comments (0)