19 Commits

Author SHA1 Message Date
schmidt1024
dad2caf69f Bump version to 1.2.2, update SRI hash for i18n.min.js
Some checks failed
Build & Push Docker Image / build (push) Has been cancelled
2026-03-30 16:54:08 +02:00
schmidt1024
604d6aa1f4 Update version to 1.2.1 in source files
Some checks failed
Build & Push Docker Image / build (push) Has been cancelled
2026-03-30 16:50:37 +02:00
schmidt1024
acac49969d Fix critical CVEs by using official Caddy image instead of Alpine package
Copy Caddy binary from caddy:2-alpine multi-stage build to avoid
stale smallstep/certificates (CVE CVSS 10) and grpc vulnerabilities
shipped with the Alpine caddy package.
2026-03-30 16:39:15 +02:00
Alexander Schmidt
651e0d7ab0 Rebrand document.title from xmrpay.link to xmrpay
Some checks failed
Build & Push Docker Image / build (push) Has been cancelled
2026-03-27 13:02:39 +01:00
Alexander Schmidt
e2b6684dcb Fix Turkish translation: add self_host_banner, fix tx_hash label, rates_offline, countdown unit 2026-03-27 13:00:48 +01:00
Schmidt
6149b52b42 Merge pull request #1 from barisbuyukakyol/master
Turkish translation added
2026-03-27 12:59:13 +01:00
Barış Büyükakyol
e7674475cf Update README.md 2026-03-27 14:06:24 +03:00
Barış Büyükakyol
3d917d386f Update i18n.min.js 2026-03-27 14:04:40 +03:00
Barış Büyükakyol
e1d9fcbf28 turkish translation added 2026-03-27 14:03:29 +03:00
Barış Büyükakyol
365871c077 Update privacy.html
turkish translation added
2026-03-27 13:59:45 +03:00
Alexander Schmidt
a5515a65f6 Remove inaccurate client-side claim from meta description 2026-03-27 11:00:42 +01:00
Alexander Schmidt
554286edfa Rebrand to xmrpay, improve meta description 2026-03-27 10:59:33 +01:00
Alexander Schmidt
487b5e9ec8 Fix privacy.html: add script-src to CSP so legal text renders 2026-03-27 10:49:39 +01:00
Alexander Schmidt
67a27f8f59 Fix banner z-index on mobile, update version to 1.1.1 2026-03-27 10:47:26 +01:00
Alexander Schmidt
de1b7b1074 Add Tor hidden service to Docker self-hosting setup
Some checks failed
Build & Push Docker Image / build (push) Has been cancelled
2026-03-27 10:38:52 +01:00
Alexander Schmidt
41c332365b Add self-host banner and rewrite README for self-hosting focus
Some checks failed
Build & Push Docker Image / build (push) Has been cancelled
2026-03-27 10:31:48 +01:00
Alexander Schmidt
d0b70acf39 Fix short URL redirect when PATH_INFO is empty string
Some checks failed
Build & Push Docker Image / build (push) Has been cancelled
2026-03-27 10:09:08 +01:00
Alexander Schmidt
ffd9327e3e Allow self-hosted origins in API verification
Some checks failed
Build & Push Docker Image / build (push) Has been cancelled
2026-03-27 09:44:11 +01:00
Alexander Schmidt
40b81a5dc8 Fix install.sh: correct GitHub repo name in compose URL 2026-03-27 09:32:34 +01:00
14 changed files with 326 additions and 198 deletions

View File

@@ -1,10 +1,8 @@
{$DOMAIN:localhost} { (common) {
root * /srv root * /srv
encode gzip encode gzip
# Security headers
header { header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Content-Type-Options "nosniff" X-Content-Type-Options "nosniff"
X-Frame-Options "DENY" X-Frame-Options "DENY"
Referrer-Policy "no-referrer" Referrer-Policy "no-referrer"
@@ -12,13 +10,20 @@
Content-Security-Policy "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self'; connect-src 'self'; form-action 'none'; frame-ancestors 'none'; base-uri 'none'" Content-Security-Policy "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self'; connect-src 'self'; form-action 'none'; frame-ancestors 'none'; base-uri 'none'"
} }
# Short URL rewrite: /s/CODE -> s.php?c=CODE
@shorturl path_regexp short ^/s/([a-zA-Z0-9]+)$ @shorturl path_regexp short ^/s/([a-zA-Z0-9]+)$
rewrite @shorturl /s.php?c={re.short.1} rewrite @shorturl /s.php?c={re.short.1}
# PHP via FPM
php_fastcgi 127.0.0.1:9000 php_fastcgi 127.0.0.1:9000
# Static files
file_server file_server
} }
# Clearnet (auto-HTTPS)
{$DOMAIN:localhost} {
import common
header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
}
# Tor hidden service (HTTP only, no TLS needed)
:8080 {
import common
}

View File

@@ -1,7 +1,12 @@
FROM caddy:2-alpine AS caddy
FROM php:8.3-fpm-alpine AS base FROM php:8.3-fpm-alpine AS base
# Copy Caddy binary from official image (avoids stale Alpine package)
COPY --from=caddy /usr/bin/caddy /usr/sbin/caddy
# Install PHP curl extension (needed for API proxies) # Install PHP curl extension (needed for API proxies)
RUN apk add --no-cache caddy curl-dev \ RUN apk add --no-cache curl-dev \
&& docker-php-ext-install curl \ && docker-php-ext-install curl \
&& rm -rf /var/cache/apk/* && rm -rf /var/cache/apk/*
@@ -31,7 +36,7 @@ COPY Caddyfile /etc/caddy/Caddyfile
COPY docker-entrypoint.sh /usr/local/bin/ COPY docker-entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/docker-entrypoint.sh RUN chmod +x /usr/local/bin/docker-entrypoint.sh
EXPOSE 80 443 EXPOSE 80 443 8080
VOLUME ["/srv/data", "/data/caddy"] VOLUME ["/srv/data", "/data/caddy"]

247
README.md
View File

@@ -1,89 +1,114 @@
# xmrpay.link — Monero Invoice Generator # xmrpay — Monero Invoice Generator
> Private. Self-hosted. No accounts. No tracking. No bullshit. > Create Monero payment requests in seconds. No accounts. No tracking. No KYC.
**[Live: xmrpay.link](https://xmrpay.link)** · **[Tor: mc6wfe...zyd.onion](http://mc6wfeaqc7oijgdcudrr5zsotmwok3jzk3tu2uezzyjisn7nzzjjizyd.onion)** **[Demo: xmrpay.link](https://xmrpay.link)** — for real payments, self-host your own instance.
--- ---
## What is this? ## Self-Host in 60 Seconds
**xmrpay.link** is a client-side web app that lets anyone create a professional Monero payment request in under 30 seconds — no account registration, no KYC, no custodial services. You need a VPS with a domain pointing to it. Then:
Enter your address, the amount, an optional description — and get a wallet-native `monero:` URI, QR code, and PDF invoice. Short links are optional. ```bash
curl -sL https://xmrpay.link/install.sh | sh -s your-domain.com
```
### Architecture & Transparency Done. HTTPS is automatic (via Caddy + Let's Encrypt). A **Tor hidden service** (.onion) is included — the installer shows your onion address after setup.
xmrpay.link uses a **minimal backend** for the following specific purposes: ### Requirements
| Component | Where it runs | What the server sees | | | Minimum | Recommended |
|-----------|--------------|---------------------| |---|---|---|
| QR code generation | Browser only | Nothing | | **CPU** | 1 vCPU | 2 vCPU |
| PDF invoice | Browser only | Nothing | | **RAM** | 1 GB | 2 GB |
| Payment (TX) verification | Browser only | Nothing | | **Disk** | 10 GB | 20 GB |
| Fiat exchange rates | Server (CoinGecko proxy) | Your IP address | | **OS** | Any Linux with Docker | Ubuntu 22+, Debian 12+ |
| Short URL storage | Server | Invoice hash (address + amount + description), HMAC-signed | | **Domain** | A-Record pointing to server IP | |
| Payment proof storage | Server | TX hash + amount**not** your XMR address | | **Cost** | ~3 EUR/month (Hetzner, Contabo, etc.) | |
**Self-hosting** eliminates trust in the public instance. ### Updates
**No short links** (use wallet URI / long `/#...` URL / QR code) = no shortlink lookup dependency.
### Trust Model (Important) Watchtower runs alongside xmrpay and automatically pulls new images every 6 hours. No action needed.
- **Default mode:** wallet-native URI + QR (no shortlink lookup). Manual update:
- **Short links are opt-in:** convenience feature with a trust trade-off.
- **Public instance caution:** if a server is fully compromised, first-access shortlink resolution can be manipulated.
- **Best security posture:** use wallet URI directly or self-host.
### Security Model ```bash
cd /opt/xmrpay && docker compose pull && docker compose up -d
```
- **HMAC-signed short URLs:** Hashes are signed with a server-side secret. Clients verify the signature on load to detect tampering. ### Configuration
- **Address never stored:** Payment verification is cryptographic and runs client-side. The server never learns your XMR address.
- **Rate-limited APIs:** All write endpoints are rate-limited per IP.
- **Origin-restricted:** API endpoints reject cross-origin requests.
- **Clear scope:** HMAC improves integrity checks, but it is not a complete defense against a fully compromised server.
--- After install, the config is at `/opt/xmrpay/.env`:
## Why? ```bash
DOMAIN=your-domain.com
XMRPAY_IMAGE=schmidt1024/xmrpay:latest
```
| Solution | Problem | ### Docker Images
| Registry | Pull command |
|---|---| |---|---|
| **BTCPay Server** | Requires own server, complex setup | | Docker Hub | `docker pull schmidt1024/xmrpay:latest` |
| **NOWPayments, Globee** | Custodial, KYC, fees, third-party dependency | | GitHub (GHCR) | `docker pull ghcr.io/schmidt1024/xmrpay:latest` |
| **Cake Wallet Invoice** | Mobile-only, no sharing without app |
| **MoneroPay** | Backend daemon required, developer-only |
| **Wallet QR** | No amount, no description, no confirmation |
**The gap:** There's no simple, privacy-respecting tool for freelancers, small merchants, and creators that works without setup and still allows payment confirmation. ### Manual Setup (without install script)
```bash
mkdir -p /opt/xmrpay && cd /opt/xmrpay
curl -fsSL https://raw.githubusercontent.com/schmidt1024/xmrpay/master/docker-compose.yml -o docker-compose.yml
cat > .env <<EOF
DOMAIN=your-domain.com
XMRPAY_IMAGE=schmidt1024/xmrpay:latest
EOF
docker compose pull && docker compose up -d
# Show your onion address
docker exec xmrpay-tor cat /var/lib/tor/hidden_service/hostname
```
### Uninstall
```bash
cd /opt/xmrpay && docker compose down -v
```
---
## Why Self-Host?
**Any server you don't control can steal your funds.** The JavaScript loaded from a third-party instance can swap the address in the QR code or exfiltrate payment data. No HMAC, no SRI hash, no URL fragment can fully prevent this — because the server controls the code your browser runs.
Self-hosting eliminates this risk. You control the server, you control the code.
The public instance at [xmrpay.link](https://xmrpay.link) exists as a demo and for testing only.
--- ---
## Features ## Features
### Invoice Generation - **Invoice generation** — XMR address, amount (XMR or fiat), description, payment deadline
- XMR address input with validation (standard, subaddress, integrated) - **Wallet-native URI** — `monero:` URI with QR code, works with any Monero wallet
- Amount in XMR or fiat (EUR/USD/CHF/GBP/JPY/RUB/BRL via CoinGecko, auto-detected) - **PDF invoice** — downloadable with QR, amount, fiat equivalent, deadline
- Description and payment deadline (7/14/30 days or custom) - **Payment verification** — sender provides TX Hash + TX Key, cryptographic verification in browser
- Wallet-native `monero:` URI with copy action - **Fiat conversion** — EUR/USD/CHF/GBP/JPY/RUB/BRL via CoinGecko, auto-detected from locale
- QR code for the same wallet-native URI - **Short URLs** — optional, with explicit trust trade-off warning
- Optional short URL toggle (`/s/abc123`) with explicit trust trade-off hint - **i18n** — English, German, French, Italian, Spanish, Portuguese, Russian, Turkish
- PDF invoice download (with QR, amount, fiat equivalent, deadline) - **Offline-capable** — Service Worker for offline use
- i18n (EN, DE, FR, IT, ES, PT, RU) with automatic browser detection - **Privacy** — zero cookies, no analytics, no external scripts, self-hosted fonts
### Payment Verification (TX Proof) ### Security
- Sender provides TX Hash + TX Key from their wallet
- Cryptographic verification in the browser (no private keys needed)
- Payment status stored with the invoice (server stores proof, but not your address)
- Invoice link shows "Paid" badge after verification
- Standard and subaddress support
### Performance & Privacy - **CSP** — Content Security Policy blocks exfiltration to foreign domains
- 100% Lighthouse score (Performance, Accessibility, Best Practices, SEO) - **SRI** — Subresource Integrity on all scripts, verified on every load
- Offline-capable via Service Worker - **HMAC-signed short URLs** — detect server-side tampering
- Self-hosted fonts (no Google Fonts dependency) - **Rate-limited APIs** — all write endpoints rate-limited per IP
- Zero external tracking, no cookies - **No private keys** — TX proof uses the sender's TX key, not the receiver's view key
- Dark mode, responsive design - **Client-side crypto** — Ed25519 + Keccak-256 verification runs in browser
--- ---
@@ -94,112 +119,24 @@ Frontend: HTML + Vanilla JS (no frameworks, no build step)
Crypto: @noble/curves Ed25519 + Keccak-256 (30KB bundle) Crypto: @noble/curves Ed25519 + Keccak-256 (30KB bundle)
QR: QRCode.js (client-side) QR: QRCode.js (client-side)
PDF: jsPDF (client-side, lazy-loaded) PDF: jsPDF (client-side, lazy-loaded)
Hosting: Static site + minimal PHP for short URLs and RPC proxy
Backend: Minimal PHP (URL shortener, rates proxy, proof storage) Backend: Minimal PHP (URL shortener, rates proxy, proof storage)
Data: JSON files (no database), LocalStorage (client-side) Data: JSON files (no database)
Hosting: Caddy (auto-HTTPS) + PHP-FPM in Alpine Docker
``` ```
--- ---
## Project Structure ## Development
```
xmrpay.link/
├── index.html # Single-page app
├── app.js / app.min.js # Main logic (URI builder, QR, fiat rates, TX proof)
├── i18n.js / i18n.min.js # Internationalization (DE, EN)
├── style.css # Dark theme, responsive, WCAG AA
├── sw.js # Service Worker (offline support)
├── favicon.svg # Monero coin logo
├── s.php # Short URL redirect
├── api/
│ ├── shorten.php # Short URL creation
│ ├── rates.php # CoinGecko proxy with server-side cache
│ ├── node.php # Monero RPC proxy (4-node failover)
│ └── verify.php # TX proof storage/retrieval
├── data/ # JSON storage (auto-generated)
├── fonts/ # Self-hosted Inter + JetBrains Mono
├── lib/
│ ├── qrcode.min.js # QR code generator
│ ├── jspdf.min.js # PDF generation (lazy-loaded)
│ └── xmr-crypto.bundle.js # Ed25519 + Keccak-256 (lazy-loaded)
├── README.md
└── LICENSE # MIT
```
---
## Invoice Lifecycle
**Optional Deadline:** When creating an invoice, you can set an expiration deadline (7/14/30 days or custom).
**Lazy Cleanup:** When a deadline is enabled, the short URL and payment proof are automatically deleted if accessed after expiration:
- Accessing an expired short URL returns **HTTP 410 Gone** and removes the entry
- Retrieving a proof for an expired invoice returns `verified: false` and cleans up the entry
- No background jobs or cron tasks required
**No deadline?** Invoices without a deadline persist indefinitely (no auto-cleanup).
---
## Self-Hosting
```bash ```bash
git clone https://github.com/schmidt1024/xmrpay.git git clone https://github.com/schmidt1024/xmrpay.git
cd xmrpay.link cd xmrpay
# Serve with any web server that supports PHP
# No build tools, no npm, no database required # Local Docker build
python3 -m http.server 8080 # For development (no PHP features) docker build -t xmrpay:dev .
docker run -p 8080:80 -e DOMAIN=localhost xmrpay:dev
``` ```
Requirements for full functionality:
- PHP 8.x with `curl` extension
- Nginx or Apache (for `/s/` short URL rewrites)
- Writable `data/` directory
### Production Deploy (Safe)
Use the provided deploy script to avoid deleting runtime files in `data/`:
```bash
./scripts/deploy.sh
```
This script deploys with `rsync --delete` but explicitly excludes `data/`.
Hardening built in:
- Creates a remote pre-deploy backup archive of `data/`
- Keeps the latest N backups (`DEPLOY_BACKUP_KEEP`, default `14`)
- Supports dry runs (`DEPLOY_DRY_RUN=1`)
- Configurable via `scripts/.deploy.env`
Restore examples:
```bash
./scripts/restore-data.sh --list
./scripts/restore-data.sh --latest
./scripts/restore-data.sh --file data-YYYYMMDD-HHMMSS.tgz
```
---
## Security
- **No private keys** — TX proof uses the sender's TX key, not the receiver's view key
- **Client-side crypto** — Ed25519 verification runs in the browser
- **No tracking** — zero cookies, no analytics, no external scripts
- **RPC proxy** — allowlisted methods only, rate-limited
- **Self-hostable** — run your own instance for full control
---
## Roadmap
- [ ] Embeddable `<iframe>` payment widget
- [ ] Invoice history (LocalStorage, CSV export)
- [ ] "Pay Button" generator (HTML snippet)
- [x] Auto-cleanup: Lazy-delete invoices after deadline expires
--- ---
## License ## License

View File

@@ -14,13 +14,19 @@ function send_security_headers(): void {
// ── Origin verification ─────────────────────────────────────────────────────── // ── Origin verification ───────────────────────────────────────────────────────
function verify_origin(): void { function verify_origin(): void {
$allowed = [
'https://xmrpay.link',
'http://mc6wfeaqc7oijgdcudrr5zsotmwok3jzk3tu2uezzyjisn7nzzjjizyd.onion',
];
$origin = $_SERVER['HTTP_ORIGIN'] ?? ''; $origin = $_SERVER['HTTP_ORIGIN'] ?? '';
// Allow same-origin (no Origin header from direct same-origin requests) // Allow same-origin (no Origin header from direct same-origin requests)
if ($origin === '') return; if ($origin === '') return;
// Dynamically allow the host this instance runs on
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
$self_origin = $scheme . '://' . ($_SERVER['HTTP_HOST'] ?? '');
$allowed = [
$self_origin,
'https://xmrpay.link',
'http://mc6wfeaqc7oijgdcudrr5zsotmwok3jzk3tu2uezzyjisn7nzzjjizyd.onion',
];
if (!in_array($origin, $allowed, true)) { if (!in_array($origin, $allowed, true)) {
http_response_code(403); http_response_code(403);
echo json_encode(['error' => 'Origin not allowed']); echo json_encode(['error' => 'Origin not allowed']);

19
app.js
View File

@@ -61,6 +61,21 @@
let pdfLoaded = false; let pdfLoaded = false;
let lastPaidData = null; let lastPaidData = null;
// --- Self-host Banner ---
(function () {
var PUBLIC_HOSTS = ['xmrpay.link', 'mc6wfeaqc7oijgdcudrr5zsotmwok3jzk3tu2uezzyjisn7nzzjjizyd.onion'];
var banner = $('#selfHostBanner');
var dismiss = $('#dismissBanner');
if (!banner) return;
if (PUBLIC_HOSTS.indexOf(location.hostname) === -1) return;
if (sessionStorage.getItem('banner_dismissed')) return;
banner.hidden = false;
dismiss.addEventListener('click', function () {
banner.hidden = true;
sessionStorage.setItem('banner_dismissed', '1');
});
})();
// --- Currency Detection --- // --- Currency Detection ---
function detectCurrency() { function detectCurrency() {
var localeToCurrency = { var localeToCurrency = {
@@ -200,7 +215,7 @@
paymentStatus.innerHTML = ''; paymentStatus.innerHTML = '';
paymentStatus.className = 'payment-status'; paymentStatus.className = 'payment-status';
paymentSummary.innerHTML = ''; paymentSummary.innerHTML = '';
document.title = 'xmrpay.link \u2014 Monero Invoice Generator'; document.title = 'xmrpay \u2014 Monero Invoice Generator';
history.replaceState(null, '', location.pathname); history.replaceState(null, '', location.pathname);
window.scrollTo({ top: 0, behavior: 'smooth' }); window.scrollTo({ top: 0, behavior: 'smooth' });
addrInput.focus(); addrInput.focus();
@@ -502,7 +517,7 @@
if (xmrAmount) parts.push(xmrAmount.toFixed(4) + ' XMR'); if (xmrAmount) parts.push(xmrAmount.toFixed(4) + ' XMR');
if (desc) parts.push(desc); if (desc) parts.push(desc);
if (parts.length) { if (parts.length) {
document.title = parts.join(' — ') + ' | xmrpay.link'; document.title = parts.join(' — ') + ' | xmrpay';
} }
} }

2
app.min.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -12,6 +12,29 @@ services:
- xmrpay-data:/srv/data - xmrpay-data:/srv/data
- caddy-data:/data/caddy - caddy-data:/data/caddy
tor:
image: alpine:latest
container_name: xmrpay-tor
restart: unless-stopped
depends_on:
- xmrpay
entrypoint: /bin/sh
command:
- -c
- |
apk add --no-cache tor > /dev/null 2>&1
mkdir -p /var/lib/tor/hidden_service
chmod 700 /var/lib/tor/hidden_service
cat > /etc/tor/torrc <<EOF
SocksPort 0
HiddenServiceDir /var/lib/tor/hidden_service
HiddenServicePort 80 xmrpay:8080
EOF
echo "Starting Tor..."
tor -f /etc/tor/torrc
volumes:
- tor-keys:/var/lib/tor/hidden_service
watchtower: watchtower:
image: containrrr/watchtower image: containrrr/watchtower
container_name: watchtower container_name: watchtower
@@ -25,3 +48,4 @@ services:
volumes: volumes:
xmrpay-data: xmrpay-data:
caddy-data: caddy-data:
tor-keys:

79
i18n.js
View File

@@ -8,10 +8,11 @@ var I18n = (function () {
it: { name: 'Italiano' }, it: { name: 'Italiano' },
es: { name: 'Español' }, es: { name: 'Español' },
pt: { name: 'Português' }, pt: { name: 'Português' },
ru: { name: 'Русский' } ru: { name: 'Русский' },
tr: { name: 'Türkçe' }
}; };
var VERSION = '1.0.0'; var VERSION = '1.2.2';
var footer = 'Open Source &middot; No Tracking &middot; No KYC<br /><a href="https://github.com/schmidt1024/xmrpay" target="_blank" rel="noopener noreferrer">Source</a> &middot; <a href="http://mc6wfeaqc7oijgdcudrr5zsotmwok3jzk3tu2uezzyjisn7nzzjjizyd.onion" title="Tor Hidden Service">Onion</a> &middot; <a href="/privacy.html">Privacy &amp; Terms</a><br /><span class="version">v' + VERSION + '</span>'; var footer = 'Open Source &middot; No Tracking &middot; No KYC<br /><a href="https://github.com/schmidt1024/xmrpay" target="_blank" rel="noopener noreferrer">Source</a> &middot; <a href="http://mc6wfeaqc7oijgdcudrr5zsotmwok3jzk3tu2uezzyjisn7nzzjjizyd.onion" title="Tor Hidden Service">Onion</a> &middot; <a href="/privacy.html">Privacy &amp; Terms</a><br /><span class="version">v' + VERSION + '</span>';
@@ -39,8 +40,9 @@ var I18n = (function () {
pdf_deadline_days: '{d} days', pdf_deadline_days: '{d} days',
pdf_date: 'Date', pdf_date: 'Date',
pdf_scan_qr: 'Scan QR code to pay', pdf_scan_qr: 'Scan QR code to pay',
pdf_footer: 'Created with xmrpay.link', pdf_footer: 'Created with xmrpay',
qr_hint: 'Click QR to save', qr_hint: 'Click QR to save',
self_host_banner: 'This is a public demo. For real payments, <a href="https://github.com/schmidt1024/xmrpay#self-host-in-60-seconds">host your own instance</a> — it takes 60 seconds.',
footer: footer, footer: footer,
aria_currency: 'Currency', aria_currency: 'Currency',
label_share_link: 'Shareable link', label_share_link: 'Shareable link',
@@ -91,7 +93,8 @@ var I18n = (function () {
pdf_deadline_days: '{d} Tage', pdf_deadline_days: '{d} Tage',
pdf_date: 'Datum', pdf_date: 'Datum',
pdf_scan_qr: 'QR-Code scannen zum Bezahlen', pdf_scan_qr: 'QR-Code scannen zum Bezahlen',
pdf_footer: 'Erstellt mit xmrpay.link', pdf_footer: 'Erstellt mit xmrpay',
self_host_banner: 'Dies ist eine öffentliche Demo. Für echte Zahlungen <a href="https://github.com/schmidt1024/xmrpay#self-host-in-60-seconds">eigene Instanz hosten</a> — dauert 60 Sekunden.',
qr_hint: 'Klick auf QR zum Speichern', qr_hint: 'Klick auf QR zum Speichern',
footer: footer, footer: footer,
aria_currency: 'Währung', aria_currency: 'Währung',
@@ -143,7 +146,8 @@ var I18n = (function () {
pdf_deadline_days: '{d} jours', pdf_deadline_days: '{d} jours',
pdf_date: 'Date', pdf_date: 'Date',
pdf_scan_qr: 'Scanner le QR code pour payer', pdf_scan_qr: 'Scanner le QR code pour payer',
pdf_footer: 'Créé avec xmrpay.link', pdf_footer: 'Créé avec xmrpay',
self_host_banner: 'Ceci est une démo publique. Pour de vrais paiements, <a href="https://github.com/schmidt1024/xmrpay#self-host-in-60-seconds">hébergez votre propre instance</a> — ça prend 60 secondes.',
qr_hint: 'Cliquez sur le QR pour enregistrer', qr_hint: 'Cliquez sur le QR pour enregistrer',
footer: footer, footer: footer,
aria_currency: 'Devise', aria_currency: 'Devise',
@@ -195,7 +199,8 @@ var I18n = (function () {
pdf_deadline_days: '{d} giorni', pdf_deadline_days: '{d} giorni',
pdf_date: 'Data', pdf_date: 'Data',
pdf_scan_qr: 'Scansiona il QR per pagare', pdf_scan_qr: 'Scansiona il QR per pagare',
pdf_footer: 'Creato con xmrpay.link', pdf_footer: 'Creato con xmrpay',
self_host_banner: 'Questa è una demo pubblica. Per pagamenti reali, <a href="https://github.com/schmidt1024/xmrpay#self-host-in-60-seconds">ospita la tua istanza</a> — ci vogliono 60 secondi.',
qr_hint: 'Clicca sul QR per salvare', qr_hint: 'Clicca sul QR per salvare',
footer: footer, footer: footer,
aria_currency: 'Valuta', aria_currency: 'Valuta',
@@ -247,7 +252,8 @@ var I18n = (function () {
pdf_deadline_days: '{d} días', pdf_deadline_days: '{d} días',
pdf_date: 'Fecha', pdf_date: 'Fecha',
pdf_scan_qr: 'Escanear QR para pagar', pdf_scan_qr: 'Escanear QR para pagar',
pdf_footer: 'Creado con xmrpay.link', pdf_footer: 'Creado con xmrpay',
self_host_banner: 'Esta es una demo pública. Para pagos reales, <a href="https://github.com/schmidt1024/xmrpay#self-host-in-60-seconds">aloja tu propia instancia</a> — toma 60 segundos.',
qr_hint: 'Clic en QR para guardar', qr_hint: 'Clic en QR para guardar',
footer: footer, footer: footer,
aria_currency: 'Moneda', aria_currency: 'Moneda',
@@ -299,7 +305,8 @@ var I18n = (function () {
pdf_deadline_days: '{d} dias', pdf_deadline_days: '{d} dias',
pdf_date: 'Data', pdf_date: 'Data',
pdf_scan_qr: 'Digitalizar QR para pagar', pdf_scan_qr: 'Digitalizar QR para pagar',
pdf_footer: 'Criado com xmrpay.link', pdf_footer: 'Criado com xmrpay',
self_host_banner: 'Esta é uma demo pública. Para pagamentos reais, <a href="https://github.com/schmidt1024/xmrpay#self-host-in-60-seconds">hospede sua própria instância</a> — leva 60 segundos.',
qr_hint: 'Clique no QR para guardar', qr_hint: 'Clique no QR para guardar',
footer: footer, footer: footer,
aria_currency: 'Moeda', aria_currency: 'Moeda',
@@ -351,7 +358,8 @@ var I18n = (function () {
pdf_deadline_days: '{d} дней', pdf_deadline_days: '{d} дней',
pdf_date: 'Дата', pdf_date: 'Дата',
pdf_scan_qr: 'Сканируйте QR для оплаты', pdf_scan_qr: 'Сканируйте QR для оплаты',
pdf_footer: 'Создано с помощью xmrpay.link', pdf_footer: 'Создано с помощью xmrpay',
self_host_banner: 'Это публичная демо-версия. Для реальных платежей <a href="https://github.com/schmidt1024/xmrpay#self-host-in-60-seconds">разверните свой экземпляр</a> — это займёт 60 секунд.',
qr_hint: 'Нажмите на QR для сохранения', qr_hint: 'Нажмите на QR для сохранения',
footer: footer, footer: footer,
aria_currency: 'Валюта', aria_currency: 'Валюта',
@@ -379,6 +387,59 @@ var I18n = (function () {
status_pending: 'Ожидание', status_pending: 'Ожидание',
proof_confirmed_pending: 'Выход найден: {amount} XMR — {n}/10 подтверждений. Авт. обновление…', proof_confirmed_pending: 'Выход найден: {amount} XMR — {n}/10 подтверждений. Авт. обновление…',
toast_integrity_warning: 'Предупреждение: обнаружено несоответствие подписи' toast_integrity_warning: 'Предупреждение: обнаружено несоответствие подписи'
},
tr: {
subtitle: 'Saniyeler içerisinde Monero ödeme talebi',
label_addr: 'XMR Adresi',
placeholder_addr: '8...',
label_amount: 'Tutar',
label_desc: 'Açıklama (isteğe bağlı)',
placeholder_desc: 'örn. 42 numaralı Fatura, freelance iş için...',
label_timer: 'Ödeme için son tarih (isteğe bağlı)',
days: 'gün',
placeholder_timer_custom: 'Gün',
btn_generate: 'Ödeme talebi oluştur',
btn_open_wallet: 'Cüzdanda aç',
btn_copy_uri: 'URI kopyala',
btn_copy_addr: 'Adresi kopyala',
btn_download_pdf: 'Fatura (PDF)',
pdf_title: 'Ödeme Talebi',
pdf_address: 'XMR Adresi',
pdf_amount: 'Tutar',
pdf_desc: 'Açıklama',
pdf_deadline: 'Ödeme için son tarih',
pdf_deadline_days: '{d} gün',
pdf_date: 'Tarih',
pdf_scan_qr: 'Ödeme için QR kodu tara',
pdf_footer: 'xmrpay ile oluşturulmuştur',
qr_hint: 'QR kodu kaydetmek için tıkla',
self_host_banner: 'Bu bir herkese açık demodur. Gerçek ödemeler için <a href="https://github.com/schmidt1024/xmrpay#self-host-in-60-seconds">kendi sunucunuzu kurun</a> — sadece 60 saniye sürer.',
footer: footer,
aria_currency: 'Para Birimi',
label_share_link: 'Paylaşılabilir bağlantı',
shortlink_toggle_label: 'Kısaltılmış bağlantı kullan (sunucuya güven gerektirir)',
shortlink_toggle_hint: 'Olası dezavantaj: kısaltılmış bağlantılar kullanışlıdır, fakat ilgili sunucu güvende değil ise ilk erişimde fatura verileri değiştirilebilir.',
btn_new_request: 'Yeni ödeme talebi',
toast_copied: 'Kopyalandı!',
countdown_expired: 'Ödeme için son tarih süresi doldu',
countdown_remaining_days: 'Son Tarih: {d} gün, {h} saat',
countdown_remaining_hours: 'Son Tarih: {h}:{m} saat',
rates_offline: 'Kurlar mevcut değil — Yalnızca XMR tutarı',
btn_prove_payment: 'Ödemeyi doğrula',
label_tx_hash: 'İşlem kimliği (TX Hash)',
placeholder_tx_hash: '64 hex karakteri...',
label_tx_key: 'İşlem Anahtarı (TX Key)',
placeholder_tx_key: '64 hex karakteri...',
btn_verify_proof: 'Ödemeyi onayla',
proof_verifying: 'Onaylanıyor...',
proof_verified: 'Ödeme onaylandı: {amount} XMR',
proof_no_match: ıktılar eşleşmiyor — TX anahtarı ya da adresi eşleşmiyor',
proof_tx_not_found: 'İşlem bulunamadı',
proof_error: 'Onaylama hatası',
status_paid: 'Ödeme yapıldı',
status_pending: 'Beklemede',
proof_confirmed_pending: 'Bulunan çıktı: {amount} XMR — {n}/10 tamamlanan. Otomatik yenileniyor…',
toast_integrity_warning: 'Uyarı: eşleşmeyen imza tespit edildi'
} }
}; };

2
i18n.min.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -3,17 +3,22 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>xmrpay.link — Monero Invoice Generator</title> <title>xmrpay — Monero Invoice Generator</title>
<meta name="description" content="Create Monero payment requests in seconds. No account registration, no KYC. Minimal backend for short URLs only."> <meta name="description" content="Self-hosted Monero payment requests in seconds. No accounts, no KYC, no tracking. Generate QR codes, PDF invoices, and verify payments.">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self'; connect-src 'self'; form-action 'none'; base-uri 'none'"> <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self'; connect-src 'self'; form-action 'none'; base-uri 'none'">
<link rel="icon" id="favicon" href="favicon.svg" type="image/svg+xml"> <link rel="icon" id="favicon" href="favicon.svg" type="image/svg+xml">
<link rel="preload" href="fonts/inter-400.woff2" as="font" type="font/woff2" crossorigin> <link rel="preload" href="fonts/inter-400.woff2" as="font" type="font/woff2" crossorigin>
<link rel="stylesheet" href="style.css?v=20260326-3" integrity="sha384-TLao5+UFp5VS0Vn+LamdOYwxjGy1ZB0dNemTi7Za0HpPsnA+koCWOmVM0Szwaf3n" crossorigin="anonymous"> <link rel="stylesheet" href="style.css?v=20260326-3" integrity="sha384-HrVyafi6sY5wzJh/jPfdCAq5WytRoWDiUnZ/Y05Xt2Oz1C+kLZLO47euo7q3fv46" crossorigin="anonymous">
</head> </head>
<body> <body>
<div class="self-host-banner" id="selfHostBanner" hidden>
<span data-i18n-html="self_host_banner">This is a public demo. For real payments, <a href="https://github.com/schmidt1024/xmrpay#self-host-in-60-seconds">host your own instance</a> — it takes 60 seconds.</span>
<button id="dismissBanner" aria-label="Dismiss">&times;</button>
</div>
<header> <header>
<h1><a href="/" id="homeLink">xmr<span>pay</span>.link</a></h1> <h1><a href="/" id="homeLink">xmr<span>pay</span></a></h1>
<p data-i18n="subtitle">Monero payment request in seconds</p> <p data-i18n="subtitle">Monero payment request in seconds</p>
</header> </header>
@@ -115,7 +120,7 @@
</main> </main>
<footer> <footer>
<p data-i18n-html="footer">Open Source &middot; No Tracking &middot; No KYC<br /><a href="https://github.com/schmidt1024/xmrpay" target="_blank" rel="noopener noreferrer">Source</a> &middot; <a href="http://mc6wfeaqc7oijgdcudrr5zsotmwok3jzk3tu2uezzyjisn7nzzjjizyd.onion" title="Tor Hidden Service">Onion</a> &middot; <a href="/privacy.html">Privacy &amp; Terms</a><br /><span class="version">v1.0.0</span></p> <p data-i18n-html="footer">Open Source &middot; No Tracking &middot; No KYC<br /><a href="https://github.com/schmidt1024/xmrpay" target="_blank" rel="noopener noreferrer">Source</a> &middot; <a href="http://mc6wfeaqc7oijgdcudrr5zsotmwok3jzk3tu2uezzyjisn7nzzjjizyd.onion" title="Tor Hidden Service">Onion</a> &middot; <a href="/privacy.html">Privacy &amp; Terms</a><br /><span class="version">v1.2.2</span></p>
</footer> </footer>
<div class="lang-picker" id="langPicker"> <div class="lang-picker" id="langPicker">
@@ -132,7 +137,7 @@
<div class="toast" id="toast"></div> <div class="toast" id="toast"></div>
<script src="lib/qrcode.min.js?v=20260326-3" integrity="sha384-3zSEDfvllQohrq0PHL1fOXJuC/jSOO34H46t6UQfobFOmxE5BpjjaIJY5F2/bMnU" crossorigin="anonymous" defer></script> <script src="lib/qrcode.min.js?v=20260326-3" integrity="sha384-3zSEDfvllQohrq0PHL1fOXJuC/jSOO34H46t6UQfobFOmxE5BpjjaIJY5F2/bMnU" crossorigin="anonymous" defer></script>
<script src="i18n.min.js?v=20260326-3" integrity="sha384-V43P6Gk3Zsd0cmqIx3V1Dffu0i3d3aFyb876gr5ez+1CIAzbFLFATfL1nuEZFzvC" crossorigin="anonymous" defer></script> <script src="i18n.min.js?v=20260326-3" integrity="sha384-SypYeUOf8lYxhYo3OFpksBwpz36UeFwEJ8PcBpjrpkPGDsU67gGCjGiNDf/TN7K1" crossorigin="anonymous" defer></script>
<script src="app.min.js?v=20260326-3" integrity="sha384-tdgiaUZYJ6E+/EqlbzOvxRvySKQZNdxjNktRV3K75fitMLdkR5DXuVU9XTppNku5" crossorigin="anonymous" defer></script> <script src="app.min.js?v=20260326-3" integrity="sha384-Y8cPBLtvKkMhHUuD+ElA1hWJHo86yO5MRs8HTUvhuK9h+lwo9WT9eBvRM7mRgtCr" crossorigin="anonymous" defer></script>
</body> </body>
</html> </html>

View File

@@ -7,7 +7,7 @@ set -e
DOMAIN="${1:-}" DOMAIN="${1:-}"
INSTALL_DIR="/opt/xmrpay" INSTALL_DIR="/opt/xmrpay"
IMAGE="schmidt1024/xmrpay:latest" IMAGE="schmidt1024/xmrpay:latest"
COMPOSE_URL="https://raw.githubusercontent.com/schmidt1024/xmrpay.link/master/docker-compose.yml" COMPOSE_URL="https://raw.githubusercontent.com/schmidt1024/xmrpay/master/docker-compose.yml"
# ── Helpers ─────────────────────────────────────────────────────────────────── # ── Helpers ───────────────────────────────────────────────────────────────────
@@ -52,7 +52,23 @@ docker compose up -d
ok "xmrpay is running!" ok "xmrpay is running!"
echo "" echo ""
echo " https://$DOMAIN" echo " Clearnet: https://$DOMAIN"
# Wait for Tor to generate the onion address (up to 30s)
info "Waiting for Tor hidden service..."
ONION=""
for i in $(seq 1 30); do
ONION=$(docker exec xmrpay-tor cat /var/lib/tor/hidden_service/hostname 2>/dev/null || true)
[ -n "$ONION" ] && break
sleep 1
done
if [ -n "$ONION" ]; then
ok "Tor hidden service ready"
echo " Onion: http://$ONION"
else
echo " Onion: (still starting — run: docker exec xmrpay-tor cat /var/lib/tor/hidden_service/hostname)"
fi
echo "" echo ""
echo " Watchtower checks for updates every 6 hours." echo " Watchtower checks for updates every 6 hours."
echo " Data stored in Docker volume: xmrpay-data" echo " Data stored in Docker volume: xmrpay-data"

View File

@@ -3,11 +3,11 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>xmrpay.link — Privacy & Terms</title> <title>xmrpay — Privacy & Terms</title>
<meta name="description" content="Privacy policy and terms of use for xmrpay.link."> <meta name="description" content="Privacy policy and terms of use for xmrpay.">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self'; font-src 'self'; base-uri 'none'"> <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self'; font-src 'self'; base-uri 'none'">
<link rel="icon" href="favicon.svg" type="image/svg+xml"> <link rel="icon" href="favicon.svg" type="image/svg+xml">
<link rel="stylesheet" href="style.css?v=20260326-3" integrity="sha384-TLao5+UFp5VS0Vn+LamdOYwxjGy1ZB0dNemTi7Za0HpPsnA+koCWOmVM0Szwaf3n" crossorigin="anonymous"> <link rel="stylesheet" href="style.css?v=20260326-3" integrity="sha384-HrVyafi6sY5wzJh/jPfdCAq5WytRoWDiUnZ/Y05Xt2Oz1C+kLZLO47euo7q3fv46" crossorigin="anonymous">
<style> <style>
main.legal-main { main.legal-main {
max-width: 920px; max-width: 920px;
@@ -52,7 +52,7 @@
</head> </head>
<body> <body>
<header> <header>
<h1><a href="/" id="homeLink">xmr<span>pay</span>.link</a></h1> <h1><a href="/" id="homeLink">xmr<span>pay</span></a></h1>
<p>Privacy &amp; Terms</p> <p>Privacy &amp; Terms</p>
</header> </header>
@@ -193,6 +193,25 @@
</ul> </ul>
</section> </section>
<section class="legal-lang" data-lang="tr">
<h2>Türkçe</h2>
<h3>Mahremiyet Politikası</h3>
<p>xmrpay.link veri toplama işlemini asgari seviyeye indirecek şekilde tasarlanmıştır. Hesap oluşturmaya gerek yoktur.</p>
<ul>
<li><strong>Hız sınırlaması:</strong> kötüye kullanım karşıtı koruma mekanizması, IP'lerden gelen istekleri kısa süreli dosyalarda tutar. İlgili IP adresleri ham hali ile değil, hash'lenmiş olarak barındırılır.</li>
<li><strong>Kısaltılmış bağlantılar:</strong> fatura hash verileri, oluşturulan kısa URL'ler için saklanır.</li>
<li><strong>Ödeme onayı:</strong> eğer kullanıldılar ise; tx hash değeri, tutar, onaylamalar ve zaman mührü saklanır. Onay veritabanında hiçbir Monero adresi saklanmaz.</li>
<li><strong>Hiçbir şey takip edilmez:</strong> istatistik ve reklam olmadığı gibi herhangi bir profilleme eylemi barındırmaz.</li>
</ul>
<h3>Kullanım Koşulları</h3>
<ul>
<li>Tüm servis "olduğu gibi" herhangi bir garanti olmadan sunulmaktadır.</li>
<li>Tâbi olduğunuz yasalara uyum hususunda tüm sorumluluk size aittir.</li>
<li>Servisin herhangi bir nedenle istismarı, yasadışı alanda kullanımı ve/veya servise yönelik saldırılar yasaklı eylem statüsündedir.</li>
<li>İlgili kullanılabilirlik garanti edilmemiştir, tüm özellikler herhangi bir zaman aralığında değişebilir.</li>
</ul>
</section>
<p style="margin-top:1rem;color:var(--text-muted);font-size:0.82rem;">Last updated: 2026-03-26</p> <p style="margin-top:1rem;color:var(--text-muted);font-size:0.82rem;">Last updated: 2026-03-26</p>
</div> </div>
</main> </main>
@@ -212,10 +231,10 @@
<div class="lang-dropdown" id="langDropdown"></div> <div class="lang-dropdown" id="langDropdown"></div>
</div> </div>
<script src="i18n.min.js?v=20260326-3" defer></script> <script src="i18n.min.js?v=20260326-3" integrity="sha384-GS62r/FP1LcB9Ec+ow+45oUWdQsjZKKwtPT6D/YXBfgGjUCjtpuxeLE3GMtbItgx" crossorigin="anonymous" defer></script>
<script> <script>
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
var supported = ['en', 'de', 'fr', 'it', 'es', 'pt', 'ru']; var supported = ['en', 'de', 'fr', 'it', 'es', 'pt', 'ru', 'tr'];
var sections = document.querySelectorAll('.legal-lang'); var sections = document.querySelectorAll('.legal-lang');
function applyLang(lang) { function applyLang(lang) {

2
s.php
View File

@@ -1,5 +1,5 @@
<?php <?php
$pathInfo = isset($_SERVER['PATH_INFO']) && is_string($_SERVER['PATH_INFO']) ? $_SERVER['PATH_INFO'] : null; $pathInfo = isset($_SERVER['PATH_INFO']) && is_string($_SERVER['PATH_INFO']) && $_SERVER['PATH_INFO'] !== '' ? $_SERVER['PATH_INFO'] : null;
$queryCode = isset($_GET['c']) && is_string($_GET['c']) ? $_GET['c'] : ''; $queryCode = isset($_GET['c']) && is_string($_GET['c']) ? $_GET['c'] : '';
$code = trim($pathInfo ?? $queryCode, '/'); $code = trim($pathInfo ?? $queryCode, '/');

View File

@@ -58,6 +58,41 @@ body {
align-items: center; align-items: center;
} }
.self-host-banner {
background: var(--accent);
color: #fff;
text-align: center;
padding: 0.6rem 2.5rem 0.6rem 1rem;
font-size: 0.85rem;
line-height: 1.4;
position: relative;
z-index: 60;
}
.self-host-banner a {
color: #fff;
font-weight: 600;
text-decoration: underline;
}
.self-host-banner button {
position: absolute;
right: 0.5rem;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: #fff;
font-size: 1.2rem;
cursor: pointer;
opacity: 0.7;
padding: 0.25rem 0.5rem;
}
.self-host-banner button:hover {
opacity: 1;
}
header { header {
text-align: center; text-align: center;
padding: 2rem 1rem 1rem; padding: 2rem 1rem 1rem;