A small, focused dev-net faucet. Developers enter a testnet/stagenet address, complete a captcha, and receive a fixed payout on a rate limit. It serves Monero (stagenet and testnet) and Litecoin (testnet), runs at cypherfaucet.com, and is listed on getmonero.org.
Two engines share one database, design, and rate-limit logic, with the active
network chosen by the URL via .htaccess:
- xmr-faucet.php for Monero, both nets selected by
?net=. - core-faucet.php for Bitcoin Core-family coins, selected by
?coin=(Litecoin testnet today; Bitcoin testnet ready behind one config entry, no new code).
- Two engines, one shared DB, style, and rate-limit design:
- Monero (xmr-faucet.php): stagenet and testnet in one
file, gated on the wallet's unlocked balance so it never offers a claim
it can't pay (Monero locks outputs for 10 blocks), with payment proof
on each receipt (a
get_tx_proofsignature that verifies in the Monero GUI's Prove/Check or CLI) for when the dev-net explorers are down. - Bitcoin Core-family (core-faucet.php): Litecoin testnet today (tLTC), Bitcoin testnet ready behind one config entry. Talks to a Bitcoin Core JSON-RPC and links each receipt's txid to a block explorer.
- Monero (xmr-faucet.php): stagenet and testnet in one
file, gated on the wallet's unlocked balance so it never offers a claim
it can't pay (Monero locks outputs for 10 blocks), with payment proof
on each receipt (a
- Each coin/net defined as a config map entry, not duplicated code.
- Cloudflare Turnstile captcha.
- Per-address and per-IP rate limiting, serialized with
BEGIN IMMEDIATEso concurrent requests can't double-claim. - Live wallet/daemon sync status and lifetime stats (total sent / payouts / last payout), kept in a counter table that survives data pruning.
- IP retention sweep (cron) so claimer IPs are not kept indefinitely.
- PHP 7.4+ with
pdo_sqliteandcurl. - Apache with
mod_rewrite(andmod_headersfor the security headers). The routing lives in .htaccess; nginx users must translate it. - For Monero: a
monerod+monero-wallet-rpcinstance per net (stagenet and testnet), bound to127.0.0.1. - For Litecoin: a
litecoindon testnet (-testnet) withserver=1and its RPC bound to127.0.0.1(default port19332), holding spendable tLTC. - SQLite 3.24+ (for the
ON CONFLICTupsert used by the stats counter).
-
Config. Copy the example and fill it in:
cp config.example.php config.php
Set your Turnstile sitekey/secret, the Monero wallet RPC credentials (or leave blank if the wallet uses
--disable-rpc-loginon localhost), the Litecoinltc_rpc_user/ltc_rpc_pass, and your publicsource_url.config.phpis gitignored; never commit it. -
Database. Create the SQLite file and tables:
cd db && python3 create_db.py
For production, keep the DB outside the web root and point the app at it:
export FAUCET_DB=/var/lib/cypherfaucet/faucet.db(Set it in your vhost / PHP-FPM pool env.) If unset, it defaults to
db/faucet.db. Whichever directory holds the DB must be writable by the web server user, because SQLite writes a journal there. -
Wallets and daemons.
- Monero: run
monero-wallet-rpcfor each net on the expected ports (stagenet38088, testnet28088) and the matchingmoneroddaemons (38081/28081, used only for the sync-status line). Adjust ports or setdaemon_urlper net in the$FAUCETSmap in xmr-faucet.php if your setup differs. - Litecoin: run
litecoind -testnetwithserver=1andrpcuser/rpcpasswordmatchingconfig.php, RPC bound to127.0.0.1(port19332). Ports and the explorer link live in the$COINSmap in core-faucet.php.
- Monero: run
-
Web server. Serve the directory with Apache +
mod_rewrite. The.htaccessmaps/xmr-stagenet,/xmr-testnet, and/ltc-testnetto the apps and denies web access todb/. -
Retention cron. Prune old claim rows (PII) daily:
17 4 * * * FAUCET_DB=/var/lib/cypherfaucet/faucet.db php /path/to/db/cleanup.php >> /var/log/cypherfaucet-cleanup.log 2>&1
Run it as a user that can write the DB and its directory.
Monero locks every output for 10 blocks (~20 min), including the change that comes back from each payout. If the wallet holds one large output, the first payout locks the rest and claims stall until it unlocks.
Fund the wallet with many small outputs (for example ~0.015 each, covering a 0.01 payout plus fee headroom), batch-sent from your collection/donation address. That keeps a pool of independently spendable outputs ahead of the lock. The app gates on the unlocked balance, so if the pool is ever exhausted it shows a "coins locking, try again shortly" message instead of failing a claim.
tools/fund_faucet.py automates this. Run it on the
machine holding the donation wallet (pointed at that wallet's
monero-wallet-rpc), sending to the faucet wallet's receiving address. It
batches the outputs into as few transactions as possible to save fees:
python3 tools/fund_faucet.py \
--rpc-url http://127.0.0.1:38083/json_rpc \
--address <faucet stagenet address> \
--nettype stagenet \
--count 50 # 50 outputs of 0.015; add --dry-run to previewIt validates the address against the chosen net and checks the donation wallet's unlocked balance before sending. Stdlib only, no dependencies.
For hands-off operation, run it in --target mode on a timer. It reads the
faucet wallet's balance and sends only the shortfall, doing nothing when the
faucet is already funded, so it's safe to run often. It targets the faucet's
total balance (not unlocked) so it won't overfund while recent top-ups are
still locking.
Because dev-net coins have no value, the simplest setup is to run the donation
wallet's monero-wallet-rpc on the faucet server too (bound to 127.0.0.1, on
its own port) so both wallets are local. If you keep the donation wallet on a
separate machine, point --faucet-rpc-url through an SSH tunnel or VPN.
cron, every 15 minutes (one line per net). flock prevents overlapping runs if
one ever hangs past the interval:
*/15 * * * * /usr/bin/flock -n /tmp/cf-topup-stagenet.lock python3 /path/to/tools/fund_faucet.py --rpc-url http://127.0.0.1:38083/json_rpc --faucet-rpc-url http://127.0.0.1:38088/json_rpc --address <faucet stagenet address> --nettype stagenet --target 0.3 --yes >> /var/log/cypherfaucet-topup.log 2>&1(38083 = the donation wallet's RPC; 38088 = the faucet wallet's RPC. Add a
second line with the testnet ports/address and its own lock file for the testnet
faucet.)
Or a systemd timer:
# /etc/systemd/system/cypherfaucet-topup.service
[Service]
Type=oneshot
ExecStart=/usr/bin/python3 /path/to/tools/fund_faucet.py --rpc-url http://127.0.0.1:38083/json_rpc --faucet-rpc-url http://127.0.0.1:38088/json_rpc --address <faucet stagenet address> --nettype stagenet --target 0.3 --yes
# /etc/systemd/system/cypherfaucet-topup.timer
[Timer]
OnCalendar=*:0/15
Persistent=true
[Install]
WantedBy=timers.targetEnable with systemctl enable --now cypherfaucet-topup.timer. (systemd won't
start a second run while one is still active, so no lock file is needed here.)
The Litecoin faucet needs no equivalent: Bitcoin Core manages change outputs itself, so just keep the wallet topped up with spendable tLTC.
core-faucet.php is coin-agnostic; Bitcoin testnet is already
stubbed in its $COINS map. To enable a coin:
- Uncomment (or add) its entry in the
$COINSmap, setting the RPC port, payout, claim window, address hint, and explorer URL. - Add the matching pretty-URL rewrite in
.htaccess(e.g.^btc-testnet/?$ core-faucet.php?coin=btc). - Add its payout table (e.g.
tbtc_payouts) and IP index to db/create_db.py, and the table name to db/cleanup.php. - Set
<coin>_rpc_user/<coin>_rpc_pass(and optionallydonate_<coin>) inconfig.php, and run the daemon (e.g.bitcoind -testnet).
- Keep
config.phpand the SQLite DB out of the repo and out of the web root. - Bind
monero-wallet-rpcto127.0.0.1(or use real--rpc-logincreds). A reachable, unauthenticated wallet RPC can be drained directly. - On SELinux systems (Fedora/RHEL), the DB directory needs the
httpd_sys_rw_content_tcontext for Apache to write it. - Rate limiting trusts
CF-Connecting-IP(falling back toREMOTE_ADDR);X-Forwarded-For/Client-IPare ignored. For that header to be unspoofable, the origin must only be reachable through Cloudflare: firewall it to Cloudflare's IP ranges (https://www.cloudflare.com/ips) and/or enable Authenticated Origin Pulls. Otherwise a direct-to-origin request can forge it.
Copyright (C) 2025-2026 Tech1k
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License along with this program. If not, see https://www.gnu.org/licenses/.
If you run your own instance, change the operator-specific bits (they're
hardcoded, not in config): the domain in the canonical / og: / twitter:
meta and in sitemap.xml / robots.txt, the footer attribution, the operator
and contact details in legal.php, and the PGP identity in contact.php,
SECURITY.md, and tech1k.txt. Secrets, RPC creds, and donation details
(address, OpenAlias, QR path) live in config.php; if you enable donations, add
your own QR image (the operator one is gitignored).
Issues and pull requests are welcome.