Learn crypto with fake coins and real protocols.
TestnetHub is the beginner-friendly command center for the Bitcoin, Litecoin, and Monero testnets: a landing page, guide site, and launchpad that routes people into the rest of the Tech1k testnet ecosystem. It is not another explorer, wallet, faucet, or swap frontend; it's the map that points to those tools.
Testnets are practice networks. The coins have no value, but the protocols are real. You can learn how wallets, miners, explorers, nodes, confirmations, fees, and swaps work without touching real BTC, LTC, or XMR.
| Do this | With | Guide |
|---|---|---|
| Get testnet coins | CypherFaucet | /faucet |
| Mine testnet coins | TestnetPool | /mining |
| Use a browser wallet | TestnetWallet | /wallet |
| Explore the chain | TestnetScan | /explorer |
| Swap testnet coins | TestnetSwap | /swaps |
| Read guides | TestnetHub | /guides |
A fully static site with no runtime and no framework. Every page makes zero external
requests, with one deliberate exception: the /status page reads the ecosystem service
APIs from the browser to show live status (scoped with its own connect-src). It reuses
the shared, token-driven design system from the rest of the testnet suite (dark by
default, light via an explicit theme toggle), so the whole family looks like one product.
Shared page chrome (<head>, nav, footer) lives in exactly one place: build.py.
Each page's body is a plain HTML fragment in content/<slug>.html. The build wraps
every fragment in the chrome and writes a flat static <slug>.html to the repo root,
which is what deploys.
content/<slug>.html -> build.py -> <slug>.html (deployed)
python3 build.py # regenerates every *.html + sitemap.xmlNo dependencies (Python 3 stdlib only). Run it after editing any fragment or the
PAGES manifest in build.py.
python3 build.py && python3 check.py # validate before deployingcheck.py is the integrity gate. It fails on broken links/anchors,
heading-level skips, CSP leaks (external resources / unexpected scripts), oversized
meta descriptions, sitemap drift, undefined CSS classes, unbalanced tags, and
donation-address / QR drift. Stdlib only; the QR payload check uses segno if it
is installed.
Pages link to clean, extensionless URLs (/faucet), which Cloudflare Pages serves
from faucet.html. Plain python3 -m http.server won't do that mapping, so use the
included dev server, which mirrors production (and serves the styled 404):
python3 serve.py # http://localhost:8000The favicon/og/PWA images are generated from small SVG sources with rsvg-convert:
rsvg-convert -w 1200 -h 630 assets/og.svg -o assets/og.png
rsvg-convert -w 192 -h 192 favicon.svg -o assets/icon-192.png
rsvg-convert -w 512 -h 512 favicon.svg -o assets/icon-512.png
rsvg-convert -w 512 -h 512 assets/icon-maskable.svg -o assets/icon-maskable.pngThe donation QR codes (assets/donate/*.svg) are generated with segno
(pip install segno) from the mainnet payment URIs. Regenerate them only if the
donation addresses in content/donate.html change.
- Add an entry to the
PAGESlist inbuild.py(slug, path,<title>, description). - If it should appear in the top nav, add it to
NAVtoo. - Write
content/<slug>.html, just the inner-<main>HTML, using the classes inassets/style.css(see any existing fragment for the vocabulary). - Link it from
content/guides.html. python3 build.py.
Do not edit the generated root
*.htmlfiles by hand; they are overwritten on every build. Edit thecontent/fragment instead.
build.py # the static-site builder (owns head/nav/footer + page manifest)
check.py # integrity gate (links, CSP, donate addresses, ...)
serve.py # local clean-URL preview server (mirrors Cloudflare Pages)
content/ # per-page <main> fragments (source of truth for page bodies)
assets/
style.css # shared design system + hub components
theme-init.js # no-flash dark/light theme + toggle wiring
copy.js # clipboard buttons (/donate + any page with code blocks)
status.js # live service checks (loaded only on /status)
fonts/inter.woff2 # self-hosted Inter (OFL)
coins/{btc,ltc,xmr}.svg
donate/{btc,ltc,xmr}.svg # mainnet donation-address QR codes
og.{svg,png} # 1200x630 social share image (+ source)
icon-{192,512,maskable}.png # PWA icons (+ icon-maskable.svg source)
favicon.svg
manifest.webmanifest
_headers # security headers (CSP, HSTS, ...) for Cloudflare Pages
_redirects # keep raw content/ fragments from being served
robots.txt sitemap.xml
*.html # generated output (index.html, testnets.html, ...); edit content/ instead
Internal links use clean, extensionless paths (/faucet), matching each page's
canonical URL and the sitemap; the deployed files stay <slug>.html (Cloudflare
Pages serves the clean path). build.py rewrites href="/x.html" → href="/x" at
build time, so fragments can be authored with either form.
Built for Cloudflare Pages (like the rest of the suite): deploy the repo root as-is.
_headers sets a strict Content-Security-Policy (default-src 'none', same-origin),
HSTS, and frame denial. Only /status relaxes connect-src to reach the ecosystem
service APIs; every other page stays fully locked down. Works on any static host;
_headers/_redirects are Pages/Netlify-specific.
Push this repo to GitHub, then in the Cloudflare dashboard: Workers & Pages -> Create -> Pages -> Connect to Git, and select it with:
- Framework preset: None
- Build command: leave empty (the built
*.htmlare committed). Optional: set it topython3 build.pyto have Pages regenerate fromcontent/fragments on every push. - Build output directory:
/(the repo root)
_headers and _redirects are applied automatically, and clean URLs work natively, so
there is nothing else to configure. Add the custom domain under the project's Custom
domains tab. Nothing server-side is required.
The /status checks live in assets/status.js. TestnetSwap
(api.testnetswap.com/api/status), TestnetPool (testnetpool.com/api/coins), and
TestnetScan (testnetscan.com/{net}/api/blocks/tip/height) all return
Access-Control-Allow-Origin: *, so they show live on deploy. The remaining two:
- CypherFaucet has a rich
/api/v1/info(payout/balance per network) but does not yet send a CORS header for this origin. Addhttps://testnethub.comto the origin allowlist in itsapi.phpand the faucet balances go live automatically (no change needed here). - TestnetWallet is a client-side wallet with no server API, so it stays a static link.
To wire a new origin: add it to PAGE_CONNECT in build.py (the per-page <meta>
connect-src for /status) and the site-wide connect-src in _headers, then add a
fetcher to CHECKS in assets/status.js. The _headers value is broad, but each page's
<meta> CSP tightens connect-src back to 'self' everywhere except /status, and the
browser enforces the intersection. (This is deliberate: Cloudflare Pages combines
duplicate _headers values rather than overriding, so a per-path /status CSP block
would be intersected with /* and block the fetches.)
Testnet coins are fake money. They have no market value, so never buy or sell them, never use a real seed phrase on testnet, and never send mainnet funds to a testnet address. That is the entire point: a risk-free place to learn.
Code is AGPL-3.0-or-later. The guide content (the prose) is CC-BY-SA-4.0. Part of the Tech1k testnet ecosystem.