MERCHANT INTEGRATION

Accept XMZ with your own wallet

This guide shows a self-hosted, non-custodial setup. Payments go straight to your wallet. Monzero.org does not hold funds, see your invoices or run a payment service for you. The commands use the wallet RPC that ships with the Monzero release.

How it fits together

  1. Create a view-only wallet from your address and secret view key. It can see incoming payments but cannot spend. Keep the spend key and seed offline, never on the shop server.
  2. Run monzero-wallet-rpc next to your shop, listening only on loopback and protected with an RPC login.
  3. For each invoice, create a new subaddress. The subaddress is how you tell payments apart; Monzero does not need a payment ID or memo for this.
  4. Show the customer a monzero: payment link and QR code for that subaddress.
  5. Poll the wallet for transfers to that subaddress, count confirmations, and mark the invoice paid once your threshold is reached.

1. Create a view-only wallet

On a trusted machine, run the wallet CLI and enter your primary address and secret view key when prompted:

monzero-wallet-cli --generate-from-view-key merchant-view

Copy only the resulting merchant-view and merchant-view.keys files to the shop server. Never copy the seed, the spend key or the full wallet. Anyone with the view key can see your incoming payments, so protect it like customer data.

2. Run the wallet RPC on loopback

monzero-wallet-rpc \
  --wallet-file merchant-view --password-file /etc/monzero/merchant-view.pass \
  --daemon-address 127.0.0.1:6175 --trusted-daemon \
  --rpc-bind-ip 127.0.0.1 --rpc-bind-port 18090 \
  --rpc-login merchant:CHANGE-THIS-SECRET

Port 6175 is the mainnet daemon RPC default. Running your own monzerod is recommended: a remote node can see when your wallet asks for blocks and could withhold data. Never expose the wallet RPC to the internet; only your backend should reach it. Choose your own port and keep the login out of source control.

3. Create an address for each invoice

curl -s -u merchant:CHANGE-THIS-SECRET --digest http://127.0.0.1:18090/json_rpc \
  -d '{"jsonrpc":"2.0","id":0,"method":"create_address","params":{"account_index":0,"label":"invoice-1042"}}'

Store the returned address and address_index with the invoice. Use a new subaddress for every invoice, even for the same customer. Reusing one makes it impossible to tell which payment belongs to which order.

4. Build the payment link

curl -s -u merchant:CHANGE-THIS-SECRET --digest http://127.0.0.1:18090/json_rpc \
  -d '{"jsonrpc":"2.0","id":0,"method":"make_uri","params":{"address":"SUBADDRESS","amount":150000000000,"recipient_name":"Example Shop","tx_description":"Invoice 1042"}}'

Amounts are in atomic units: 1 XMZ = 100,000,000,000 (11 decimal places). Store and compare amounts as integers, never as floating-point numbers. The result has the form monzero:<address>?tx_amount=1.50000000000&recipient_name=…&tx_description=…. The name and description are shown in the payer's wallet only; they are not sent on chain. The payment request tool produces the same format in the browser.

The wallet's own encoder leaves = and ? unescaped, and its parser then rejects them. Keep these characters out of names and descriptions, or percent-encode them yourself.

5. Detect payments and count confirmations

curl -s -u merchant:CHANGE-THIS-SECRET --digest http://127.0.0.1:18090/json_rpc \
  -d '{"jsonrpc":"2.0","id":0,"method":"get_transfers","params":{"in":true,"pool":true,"account_index":0,"subaddr_indices":[7]}}'

Entries under pool are seen but unconfirmed. Entries under in are in a block and carry txid, amount, height, confirmations, locked and double_spend_seen.

6. Expire invoices

Give each invoice an expiry time and a fixed XMZ amount. After it expires, keep watching the subaddress: a payment sent before expiry can confirm afterwards, and the funds still arrive in your wallet. Decide in advance how you refund or honour late payments. Refunds need the customer's address, because a Monzero transaction does not reveal the sender.

7. Signed callbacks inside your system

If your payment watcher notifies your shop or order system, sign every callback so it cannot be forged. A common pattern: send X-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256> computed over t + "." + raw body with a shared secret. The receiver should verify it like this:

import hmac, hashlib, time

def verify(secret: bytes, header: str, body: bytes, tolerance=300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
    try:
        timestamp = int(parts["t"])
    except (KeyError, ValueError):
        return False
    if abs(time.time() - timestamp) > tolerance:
        return False
    expected = hmac.new(secret, f"{timestamp}.".encode() + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))

Verify against the raw request bytes before parsing JSON. Reject stale timestamps, keep a short list of recently seen signatures to stop replays, and make the handler idempotent so a retried callback does not mark an order twice.

What this guide does not cover

Monzero.org does not offer a hosted merchant service, custody, automatic currency conversion or refunds. A hosted or custodial service would need separate security, legal and operational review before anyone runs it. Test your integration on testnet first; publicly verified testnet connection details will be listed on the developer portal when available.

Read the source: make_uri, parse_uri and get_transfers are in src/wallet/wallet2.cpp and src/wallet/wallet_rpc_server.cpp of the official repository.