slowbeardigger / xmr-pay-laravel
Laravel service, facade and configuration for the XMRPay PHP payment engine.
Requires
- php: >=8.0
- illuminate/support: ^9.0 || ^10.0 || ^11.0
- slowbeardigger/xmr-pay: ^0.1.1
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Accept Monero in a Laravel application using the PHP engine. This package provides configuration, a service and a facade. Verification runs in PHP with a private view key and configured Monero nodes. It does not hold a spend key.
Requires PHP 8.0+, GMP, BCMath and a supported Laravel version from composer.json
(Laravel 9 through 13). Your application owns payment persistence and fulfillment.
For new installations, use a Laravel version covered by the framework security support policy.
Install
composer require slowbeardigger/xmr-pay-laravel
php artisan vendor:publish --tag=xmr-pay-config
Then set your wallet in .env. The view key and any node password are sensitive. Stagenet coins
are free test XMR:
XMRPAY_ADDRESS=5...your stagenet primary address...
XMRPAY_VIEW_KEY=...your private view key...
XMRPAY_NETWORK=stagenet
XMRPAY_NODES=http://node.monerodevs.org:38089
XMRPAY_MIN_CONFIRMATIONS=10
For authenticated nodes, use structured JSON instead of putting credentials in the URL. JSON
takes precedence over the legacy XMRPAY_NODES list:
XMRPAY_NODES_JSON='[{"url":"https://node.example:18081","auth":"digest","username":"merchant","password":"replace-me","allow_insecure_http":false}]'
Supported authentication modes are none, basic, and digest. Authenticated plain HTTP is
blocked unless that node sets allow_insecure_http to true; use the opt-in only for a trusted
private network. Keep the JSON in .env, never in version control.
Laravel resolves these values into bootstrap/cache/config.php after php artisan config:cache.
Protect that file with the same server permissions as .env and clear/rebuild the cache when node
credentials change.
Use it
Give each order a unique receiving subaddress, show the buyer a QR, and check the chain when you poll or run a job. The facade resolves the configured service.
use XmrPay\Laravel\Facades\XmrPay; // when an order is created: derive its own subaddress from your address + view key $index = $order->id; // any unique integer per order $address = XmrPay::subaddressFor($index); $uri = XmrPay::paymentUri($address, '0.25', "Order #{$order->id}"); // Do not present a payable order until its birthday height is known and stored. $height = XmrPay::tipHeight(); if ($height === null) { throw new \RuntimeException('Payment node unavailable'); } $order->update(['xmr_index' => $index, 'xmr_from' => $height]); // later: in a scheduled command, a queued job, or a poll route: ask if it is paid $r = XmrPay::checkOrder($order->xmr_index, '0.25', $order->xmr_from); if (! empty($r['paid'])) { $order->markPaid(); // funds are already in your wallet }
checkOrder() summarizes only the bounded range scanned during that call. It does
not retain matches between calls. Defaults limit a call to 200 blocks and eight
seconds. Reusing the birthday rechecks that range; advancing to scanned_to + 1
without retaining matches loses earlier partial payments and their confirmation updates.
For long-lived orders, use scanner()->scan_all() with durable matches and
checkpoints, or the shared adapter core's Settler. Preserve earlier payments,
rescan recent blocks for reorganisations, and update confirmations before summing.
The snippet above is a bounded verification example, not a complete settlement worker.
Make markPaid() atomic and idempotent in your application's order store.
If you would rather have the buyer paste a transaction id ("I've paid"), verify a single one:
$r = XmrPay::verifyPayment($txid, $address); // Evidence only: compare amount and enforce confirmations, locks and replay protection. // A found transaction alone does not authorize fulfillment.
Operational limits
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. You are trusting the node you point it at. A public node is fine for tips; for real revenue run your own or configure independent nodes. All configured nodes must answer and agree; agreement is not a substitute for consensus validation. Few confirmations is fast but reversible. Raise
XMRPAY_MIN_CONFIRMATIONSfor higher-value orders. That is your risk dial. The browser is never trusted. Release goods only after your server applies the order's amount, confirmation, lock and replay policy to verified evidence.
How it works
Nothing always-on is required. checkOrder() reads public chain data from the node and does the
Monero math in PHP with your view key: it derives the order subaddress, detects the output, verifies
the RingCT amount commitment, and counts confirmations. The same engine powers
xmr-pay for WooCommerce; the verification is
cross-checked against the reference Monero library on real stagenet payments.
Validation
Test the configured Laravel application on stagenet, including partial payments across scan batches, delayed confirmations and duplicate fulfillment attempts. Engine tests do not validate application-owned storage or release of goods.
To check package discovery, configuration and facade bindings in an installed application:
XMRPAY_LARAVEL_APP=/path/to/app php tests/framework.test.php
Run it before and after php artisan config:cache. Laravel 12 and 13 were exercised
with PHP 8.5, SQLite persistence and stagenet payments. Application-specific checkout,
database locking and fulfillment still need their own validation.