npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

xmr-pay

v1.0.1

Published

Sovereign Monero payments toolkit. Client-side payment links + QR (core), stateless on-chain proof verification (verify), signed fulfillment webhooks (webhook), and a zero-dependency checkout widget. No accounts, no API keys, no third parties in the payme

Readme

xmr-pay

Accept Monero on your own site. The money goes straight to your wallet. There is no company in the middle, no account to open, no fee paid to anyone, and nobody can freeze, see, or hold your funds but you.

xmr-pay is a small toolkit you run yourself: payment links and QR codes, an embeddable checkout widget, and trustless on-chain payment detection. It is code, not a service.

npm · MIT licensed · zero runtime dependencies · signed releases

Running a WooCommerce store?

There is a separate, standalone plugin built on this engine:

xmr-pay for WooCommerce

If you just want to accept Monero in a WordPress store with no code, start there. That plugin stands on its own (install it, paste your address, done) and this README is about the library underneath it. The two projects are independent; each links the other.

See it live (stagenet, no real money)

live.xmrpay.shop : configure it yourself and watch it verify a payment demo.xmrpay.shop : a full demo store, pay with free test XMR xmrpay.shop/demo.html : the checkout widget and the "prove you paid" flow

You do not need a dedicated server

This is worth being blunt about, because it is where most Monero tooling adds friction: confirming a Monero payment is, underneath, just reading public blockchain data and doing some math. So none of the ways to do it require a box that stays on 24/7.

Proof mode is a stateless function that runs on demand. It can live in a serverless function or one small route on the site you already have. Nothing is always-on. Watch mode runs wherever you already run code, against your own wallet-rpc or a built-in view-only scanner that needs no daemon at all. Inside WordPress, the plugin does both in pure PHP. No Node, no daemon, no separate process. WordPress's own cron and the buyer's checkout page trigger the check; the rest of the time nothing runs.

Why that is reasonably trustworthy (and where the limits honestly are)

It can see, it cannot spend. Detection only ever holds your view key. It can read incoming payments; it can never move your money. The key that spends funds is never asked for and never stored. The amount is proven, not claimed. Monero commits the real amount of every output on-chain. The verifier checks that commitment, so a forged or edited amount is rejected. It cannot be talked into seeing money that is not there. It fails closed. Several independent checks must all pass: the amount commitment, enough confirmations, the funds are not time-locked, and no transaction is counted twice. If anything is missing, or a node will not answer, the order stays unpaid. It never guesses "paid". The honest caveat: verification trusts the Monero node you point it at to tell the truth. A public node is fine for most sites. For serious money, point it at your own node, or require two nodes to agree. That one thing is on you, and the setting is right there.

The verification math is cross-checked against the reference Monero library and the whole money path is covered by an adversarial test suite, so "no server" does not mean "cut corners".

What you get

Your money, directly. Payments land in your wallet. xmr-pay never holds, routes, or can touch a cent. There is no xmr-pay in the payment path. No accounts, no API keys, no monthly fee. It is code you run, not a service you subscribe to. Privacy by default. Monero hides amounts and parties on-chain, and nothing in the buyer's browser is trusted to settle an order. Underpaid, or paid in two goes? Watch mode sums the payments, so the order finishes itself once the total adds up, and the buyer can top up to the same address.

The truths (please read before taking real money)

We would rather tell you the rough edges than have you find them with a customer.

Monero is irreversible, and the sender is hidden, so there are no automatic refunds. If you need to refund someone you send them XMR back by hand. That is the trade for having no chargebacks and no middleman. You are trusting a node to tell the truth. A single public node could lie, be slow, or go down. Fine for tips. For real revenue, run your own node or require two nodes to agree (it then refuses to confirm rather than trust one source). Few confirmations is fast but reversible. Accepting at zero confirmations is instant, but a payment can still vanish in a chain reorg. Use more confirmations for higher-value orders. This is your risk dial to set. A brand-new transaction can take a moment to verify on a public node. If a buyer submits a proof for a transaction still in the mempool, a public node may not serve it yet and the check comes back "try again", never a false "paid". It clears once the transaction is in a block. Your own node removes the wait. The browser is never trusted. Anything shown in a buyer's browser can be faked by that buyer. Goods are released only after your own server verified a real payment on-chain. Same rule as every serious payment system.

Install

npm i xmr-pay monero-ts        # monero-ts only needed for server-side detection

The network is explicit at every entry point. Default is mainnet; use stagenet to test with no real money:

Widget: <xmr-pay network="stagenet" ...> (omit for mainnet). Verify: verifyPayment({ networkType: 'stagenet', ... }). Agent: the XMR_NETWORK=stagenet env var (the npx xmr-pay wizard asks).

Run the agent in one command (non-custodial; it holds only your view key):

npx xmr-pay        # setup wizard (address + view key + node), then it runs

It scans from the current block (no historical rescan), generates the token and webhook secret, asks your settlement speed (instant 0-conf, fast 1 block, secure 10 blocks), persists its wallet and orders, and prints the exact values to paste into your store. npx xmr-pay start runs it again later.

How it works

| Module | Runs | Purpose | |---|---|---| | xmr-pay/core | browser + server | payment URIs (links), QR as SVG, per-order amount nonces | | xmr-pay (verify) | your backend or serverless fn | re-verify a buyer's tx proof on-chain, trustless | | xmr-pay/watch | your backend | auto-detection through your own monero-wallet-rpc | | xmr-pay/scanner | your backend | view-only WASM scanner, auto-detection with NO wallet-rpc daemon | | xmr-pay/agent | your backend | long-running order manager: per-order subaddress, summing, signed paid webhook | | xmr-pay/config | offline + browser | signed merchant configs, tamper-evident addresses | | xmr-pay/webhook | your backend | signed fulfillment webhooks to YOUR systems | | widget/xmr-pay.js | browser | full checkout UI, one self-hosted file, zero dependencies |

Two detection modes, freely combined:

| | Proof mode (default) | Watch mode | |---|---|---| | Infra (yours) | a stateless verify endpoint, on demand | a long-running process you host | | Buyer effort | pastes txid + proof | none, just pays | | View key | not needed | yours, in-process (view-only) | | Partial / top-up auto-complete | manual (single-tx proofs) | automatic, sums transfers | | Best for | tips, a single product, lowest infra | a real store, installments, hands-off |

proof mode (no always-on process; your verify endpoint runs on demand):
  buyer's browser:  pays > wallet makes a tx proof > widget POSTs {txid, proof}
  YOUR server:      > verify endpoint > verifyPayment re-checks on YOUR nodes > paid

watch mode (the agent; no monero-wallet-rpc needed):
  order > fresh subaddress > buyer pays > your agent scans and SUMS transfers
        > paid (handles partial / split / top-up payments) > signed order.paid webhook

They share the same exact-math core, so a payment counts identically either way. Many shops run watch mode and keep proof as a dispute path. Watch mode is documented in full in docs/AGENT.md.

Checkout widget

One self-hosted file (widget/xmr-pay.js, ~98 KB, bundles its own QR encoder, no external requests ever). Drop it in and you have a Monero checkout.

<script src="/xmr-pay.js"></script>

<!-- tips / donations, nothing else needed -->
<xmr-pay address="4YOUR_ADDRESS…" label="Buy me a coffee"></xmr-pay>

<!-- store checkout, detection against YOUR endpoint -->
<xmr-pay
  address="4YOUR_ADDRESS…"
  amount="0.050000004821"
  order="ord_123"
  verify-url="/api/verify-payment"
  theme="light" lang="en"></xmr-pay>

What the buyer gets: amount + QR (generated locally, with the exact tx_amount prefilled so wallets can't be sent the wrong amount), click-to-copy address with a highlighted fingerprint, "open in wallet" deep link, an always-there trust panel, and a "paid? prove it" panel that submits txid + tx proof to your endpoint.

Attributes: address (required unless config is set), amount, label, order, verify-url, redirect-url, lang (en/es), theme (light), skin (brutal), config (base64 signed envelope), fingerprint/pubkey (pin the signer). Events: xmr-pay:paid, xmr-pay:result (CustomEvent, verify result in detail).

Buyer-error feedback (built in). Bad txid gives "that transaction ID should be 64 characters"; not a proof gives "paste the tx key or the proof block from your wallet", caught instantly before any server round-trip. Underpaid gives "Detected 0.1 XMR, send 0.2 more to complete" plus a fresh QR for exactly the missing amount (piconero-exact, no float drift). The proof box also smart-pastes a whole Feather block and picks out the txid + proof itself.

Skins. Default is a neutral, universal look (system sans, rounded, soft shadows). skin="brutal" is the GOXMR brand look (monospace, square, hard shadow). Both are driven by --xp-* CSS variables, so any brand can retheme without forking.

Payment links

A payment link is just a URL. Host examples/pay-link.html anywhere static and share:

https://your-site.com/pay-link.html#address=4…&amount=0.05&label=Invoice%2042

core.makePaymentURI() builds the monero: URI (also what the widget's QR and "open in wallet" use). The URIs are round-trip tested against the official wallet2 parser (what GUI, CLI and Feather run internally), including 12-decimal nonce amounts and unicode descriptions, so they prefill cleanly across Feather, GUI, CLI, Cake, Monerujo and Stack (mobile + desktop, Win/Mac/Linux).

Prefer the #fragment form for shared links; fragments never reach server logs or proxies. Truly short URLs (/p/x7k2) need a lookup, so add a redirect route on your own server rather than a third-party shortener that would track your buyers.

Buyer-side wallet instructions (Feather/GUI/Cake/CLI menu names, restored-seed caveat): docs/WALLETS.md.

Order creation (amount-nonce)

const { makeAmountNonce } = require('xmr-pay/core');
const amount = makeAmountNonce('0.05');   // '0.050000004821', unique per order
// store { order_id, amount } in YOUR db; render the widget with that amount

The random piconero tail makes each order's on-chain amount unique, so a proof structurally fits only its own order: a secondary anti-replay guard on top of your txid dedup. The added value is dust (default ≤ 0.000001 XMR).

Proof mode

The only server piece, and it is yours: stateless, runs on demand.

const { verifyPayment } = require('xmr-pay');

const r = await verifyPayment({
  txid, proof,                       // what the buyer pasted (tx key or tx proof, auto-detected)
  address: order.address,
  amount: order.amount_xmr,          // string keeps 12-decimal nonces exact
  nodes: ['https://your-node:18081', 'https://fallback:18081'],
  minConfirmations: 1,               // 0 accepts mempool, your risk, your call
  quorum: 1,                         // 2+ means independent nodes must agree
  alreadyUsed: (txid) => db.txidSeen(txid),
});
// { paid, status, reason, receivedXmr, expectedXmr, shortfallXmr, confirmations,
//   txid, nodesAgreed, overpaid }   txid comes back normalized (lowercase)

Full endpoint with anti-spam gates: examples/serverless.js. Drop it in Vercel/Netlify/Express; stateless, your orders table is the only state and it is already yours. One-click deploy template: docs/DEPLOY.md.

A freshly broadcast (mempool) transaction may not be retrievable from a public node yet, so verification returns node-error (retryable, never a false paid) until the tx is in a block. Run your own node to remove the wait.

If you already run monero-wallet-rpc, verifyPaymentViaRpc checks the same proofs through it: same gates, same result shape, no WASM peer to install (so none of monero-ts's transitive advisories; see SECURITY.md):

const { verifyPaymentViaRpc } = require('xmr-pay/watch');
const r = await verifyPaymentViaRpc({
  url: 'http://127.0.0.1:18083',     // your monero-wallet-rpc
  txid, proof, address: order.address, amount: order.amount_xmr,
  nodes: ['https://your-node:18081'], // for the time-lock gate if the wallet has no record of the tx
});

Watch mode

Automatic detection: a fresh subaddress per order, payments summed (so partial payments and top-ups auto-complete), the buyer submits nothing. Two transports: your own monero-wallet-rpc, or a view-only WASM scanner with no daemon at all.

// no daemon: a view-only wallet from (address + view key), the agent does the rest
const { createScanner } = require('xmr-pay/scanner');
const { createPaymentAgent } = require('xmr-pay/agent');

const scanner = await createScanner({ primaryAddress, privateViewKey, networkType, nodes });
const agent = createPaymentAgent({ scanner, minConfirmations: 1, onPaid: (o) => fulfil(o) });
agent.start();

const order = await agent.createOrder({ id: 'ord_42', amount: '0.05' });  // returns { address, … }
const r = await agent.check('ord_42');   // { paid, status, receivedXmr, shortfallXmr, … }

Each order gets its own subaddress, and a second order can never bind a subaddress already in use, so two orders can't credit the same payment. Full guide, the runnable HTTP service, config, and the trust model: docs/AGENT.md.

const { createWatcher } = require('xmr-pay/watch');
const watcher = createWatcher({ url: 'http://127.0.0.1:18083' });
const { address, index } = await watcher.newSubaddress('order ord_123');
const r = await watcher.checkOrder({ subaddressIndex: index, amount: order.amount_xmr });
// { paid, status: paid|partial|mempool|locked|pending, receivedXmr, shortfallXmr, txids }

Per-order subaddresses replace the amount-nonce here (the address identifies the order). Time-locked outputs never count as paid. Keep wallet-rpc on localhost.

Webhooks

There is no xmr-pay server to call you. Your detection IS the webhook moment: when a payment settles, notify whatever needs to know (shop platform, shipping, Discord, Zapier), signed with your own secret:

const { sendWebhook, verifySignature } = require('xmr-pay/webhook');

if (r.paid) {
  await sendWebhook(process.env.FULFILL_WEBHOOK_URL, {
    event: 'order.paid', order_id, txid: r.txid, confirmations: r.confirmations,
  }, { secret: process.env.FULFILL_WEBHOOK_SECRET });   // X-XMR-Pay-Signature: sha256=…
}
// receiver: verifySignature(rawBody, secret, req.headers['x-xmr-pay-signature'])

Retries with backoff built in. (The agent fires this for you, once, on settle.) The signed body carries an event_ts (unix ms): after verifying the signature, reject a delivery whose event_ts is stale, and stay idempotent on order_id, so a replayed webhook can't trigger a second fulfillment. The browser also gets an xmr-pay:paid DOM event; treat it as UX only (a thank-you, a redirect), never the signal to release goods.

Security and trust

The browser decides nothing. Fulfill on your server. A buyer can fake the xmr-pay:paid event in devtools or point the widget at a fake server; it only fools their own screen, and your server never verified a real payment.

Node trust. Verification is only as honest as the nodes you query. The default quorum is 1 (fast, single node); set quorum: 23 for serious volume so independent nodes must agree (it fails closed on disagreement, so availability then rides on your nodes). For the highest confidence run your own monerod and use RPC mode (verifyPaymentViaRpc against your own monero-wallet-rpc), which sidesteps the bundled WASM wallet and its transitive dependencies entirely.

| Attack | Outcome | |---|---| | Buyer claims "I paid" with no proof | nothing to verify, rejected | | Buyer fakes "paid" in devtools (forge the event, edit DOM, point verify-url at a fake server) | cosmetic, only their screen; your order stays unpaid. Fulfill server-side | | Forged or tampered proof | fails cryptographic verification on-chain | | Proof for a payment to someone else | proofs are address-bound, rejected | | Reusing a real proof on another order | amount-nonce + alreadyUsed, returns replay/underpaid | | Off by 1 piconero | integer-piconero compare, returns underpaid | | Amount above Monero's max supply (uint64) | rejected; xmrToPico/atomicToPico enforce the on-chain ceiling, parity with monerod's parse_amount | | Time-locked payment (unlock_time set, confirms but frozen) | raw tx fetched from the daemon; unlock_time ≠ 0 returns locked. Fails closed if no node returns the tx | | A node lies | quorum: 2+ returns node-disagreement | | A node or wallet-rpc is down, slow, or times out | node-error, transient and retryable, never a false paid. Distinct from invalid so you can tell "retry" from "reject"; the example endpoint answers 503 | | Endpoint spam | gate on "order exists and pending" before any RPC | | Double-submit race (same txid, concurrent) | claim the txid atomically with a UNIQUE constraint on tx_hash |

Fulfill server-side, never from the browser. Release goods only after your server returned paid and wrote it to your order record. Same rule as Stripe. UNIQUE constraint on tx_hash in your orders table closes the replay race the alreadyUsed callback only narrows. Use makeAmountNonce for every order (proof mode), so a proof can't fit another order. Scale minConfirmations with value: 1 for small carts, 10 for high-value (reorg safety). minConfirmations: 0 (mempool) is opt-in risk. quorum: 2 for high-value orders, so two independent nodes must agree. Never take address/amount from the request body; always your own order record (the examples do this). Your page is the trust root. If it is compromised the address can be swapped, so use a signed config + published fingerprint (below) for real-money stores.

Signing moves address integrity onto a key the merchant keeps off the web server, so a breach can serve the real signed config or a broken one, but cannot mint a new one for the attacker's address.

const { generateSigningKey, signConfig } = require('xmr-pay/config');
const key = generateSigningKey();                 // keep privateKey offline
const env = signConfig({ address, amount: '0.05', networkType: 'mainnet' }, key.privateKey);
// env.fingerprint e.g. "2847-789f-a55a-bd90-1234-5678", publish where buyers can check
<xmr-pay config="<base64 envelope>" verify-url="/api/verify-payment"></xmr-pay>

The widget verifies the Ed25519 signature (WebCrypto, no extra dependency), uses the signed address, and shows Signed · <fingerprint>. A "signed" config that fails verification shows a red warning and no payable address. Pin a known signer with pubkey="…" or fingerprint="…". With the fingerprint known out of band a buyer catches an address swap even on a fully compromised page.

Verifying a payment asks one node for one transaction by its txid. The node learns the txid and your IP/timing, not the amount or address (derived locally). That is less than a normal wallet exposes. Close the exposure at the connection level: run your own node (list it first in nodes), or egress over Tor / point at an .onion node. nodes takes any URL, configuration not code.

Demo

A complete, deployable demo lives in demo/: a stagenet store checkout that verifies a real payment on-chain, plus a mainnet tip widget with no backend.

cd demo && npm install && npm start    # http://localhost:8780, click "Try it"

Validated

187 offline checks plus a 92,006-case math fuzz, plus live stagenet validation.

Offline: input gates 40, core (links/QR/nonce) 22, signed configs 10, watch summing 14, webhooks 8, wallet-rpc verify 20, adversarial "chaos" 27, agent lifecycle 17, monerod amount parity 29, node-quorum 13, plus order-independence and byzantine-duplicate stress. The fuzz hammers the piconero math (shortfall, summing, round-trips) so paying the displayed difference always completes an order to the exact piconero, including the float traps (0.1 + 0.2 = 0.3). The parity suite mirrors monerod's own parse_amount (overflow ceiling, 13th-decimal, signs) so we never accept an amount the chain rejects. Live on stagenet: proof verify through a 13-case adversarial matrix (exact, underpaid/overpaid to the piconero, replay, address-bound rejection, malformed returns invalid, dead node returns node-error, 2-node quorum, at 0-conf and 1-conf), all through the unlock_time gate; the view-only scanner detecting a real payment via the view key alone; two real payments summed on one subaddress to complete an order; the agent end to end (per-order subaddress, settle, one-time signed webhook). Spot-checked against a real mainnet transaction key.

Donate

If xmr-pay saved you a payment processor's cut, a little Monero back is welcome, never required:

45sEohkyWYxAfHy8ekP7B34Bd3qhgrupcQfUQAHvfUWkfgqJhCA4QYLigrBg8G8TE4WggtMGpmjXrbmvepkWLec58KKLkm9

Releases

The widget is a plain concatenation of widget/xmr-pay.part.js and the vendored qrcode-generator (src/vendor/): no minifier, no timestamps, no npm install:

npm run build
shasum -a 256 widget/xmr-pay.js     # must match SHA256SUMS in the release

Each release ships SHA256SUMS plus a minisign signature. Public key (also minisign.pub in this repo): RWSA/E4ogu5/1mQf2r66pkWK9fYBEeFdf2cvrjkhiALoXCWT3woSSRtH

minisign -Vm SHA256SUMS -P RWSA/E4ogu5/1mQf2r66pkWK9fYBEeFdf2cvrjkhiALoXCWT3woSSRtH
shasum -a 256 -c SHA256SUMS

From npm the package is published with provenance (npm view xmr-pay --json | grep provenance). If a signature or hash does not match, do not use the file, and report it.

Docs

docs/AGENT.md : watch mode and the merchant agent (what it solves, API, trust model) docs/DEPLOY.md : one-click deploy of the verify endpoint docs/WALLETS.md : buyer-side wallet instructions per wallet docs/SUITE.md : how the pieces fit together CHANGELOG.md : release notes SECURITY.md : reporting, dependency advisories, "try to break it"

Acknowledgements

We stand on excellent open-source work. Give them a star:

monero-project: the protocol; our money-math parity suite mirrors parse_amount's own unit tests. monero-integrations / monerophp (MIT): the pure-PHP ed25519, key-derivation and base58 primitives the WordPress-native verifier is vendored on. The breakthrough that made "verify in PHP" possible. kornrunner/php-keccak (MIT): Keccak-256 with Monero's padding, in pure PHP. monero-ts (woodser, MIT): the WASM Monero library powering the watch/proof paths, and our ground-truth reference for cross-checking the PHP verifier. qrcode-generator (MIT): the checkout widget's self-contained QR encoder. Inspiration: BTCPay Server's Monero plugin, MoneroPay, and AcceptXMR. We studied all three to match (and, on reorg-safety, double-spend and arithmetic, exceed) their detection model.

License

MIT, including the vendored qrcode-generator (c) Kazuhiko Arase, bundled so the widget makes zero external requests.

A GoXMR project, also available for WordPress / WooCommerce.