๐งช Full platform walkthrough ~2 hr โ
Hands-on, phased walkthrough of the entire Nimiq Simple Faucet so you can exercise every feature, catch regressions, and collect UX feedback. Written so a human tester or an AI coding agent can run it.
Each phase has:
- A time estimate
- Prerequisites from earlier phases
- Concrete commands to run
- Expected outcome + "looks wrong" flags
- An "Ask your AI" prompt you can paste into Claude Code / Cursor / Cody
When you hit something rough (confusing UI, error message you don't understand, docs gap), file a GitHub issue with label ux-polish or qa-finding โ we'll sweep them in a polish release.
Prerequisites โ
- Node 22+, pnpm 9+, Docker (with buildx), openssl
- Optional: Dart 3+ (for the Flutter example), Go 1.22+ (for the Go example), k6 (load tests)
- A public Nimiq testnet faucet URL to fund your generated wallet (e.g. https://faucet.pos.nimiq-testnet.com)
- ~2 hours end-to-end if you run every phase
Phase 0 โ Orientation (2 min) โ
Goal: know what you're looking at before you start clicking.
- Open docs/README.md โ it's the audience-grouped index.
- Skim ../AGENTS.md โ structural overview of apps, packages, SDKs.
- Glance at ../ROADMAP.md so you know what's in-scope for 1.x vs. later.
Ask your AI โ
"Give me a 30-second tour of this repo. Tell me: what does the faucet do, what's in
apps/vspackages/vsexamples/, and what should I try first?"
Phase 1 โ Install + build + unit tests (5 min) โ
Goal: confirm the tree compiles and all 53 unit/integration tests pass on your machine.
pnpm install
pnpm build # 22 turbo tasks โ all green
pnpm typecheck # strict TS across the workspace
pnpm test # 53 vitest tests, excludes @nimiq-faucet/e2e by designExpected: every step exits 0. pnpm test reports Tests: 53 passed.
If it fails: check Node/pnpm versions (node -v && pnpm -v), then look at the failing package and open its test output.
Ask your AI โ
"Run
pnpm install && pnpm build && pnpm testin the repo root and report any failure. Pay extra attention to the server test suite."
Phase 2 โ Generate + fund a testnet wallet (10 min) โ
Goal: a testnet address the faucet can send FROM. Without this the faucet has no balance to dispense.
pnpm generate:walletThis writes .wallet.local.json in the repo root (0600 perms, gitignored) and prints the address + funding instructions. Copy the address from the output.
Fund the address from the public testnet faucet:
Wait a few seconds for the tx to confirm. Verify the balance on https://test.nimiq.watch/ if you want.
Ask your AI โ
"Run
pnpm generate:wallet, capture the address that was generated, and tell me what I need to do to fund it."
Phase 3 โ Start the stack (5 min) โ
Goal: local faucet running at http://localhost:8080 with a real funded wallet.
cd deploy/compose
cp .env.example .envEdit .env:
FAUCET_NETWORK=test
FAUCET_SIGNER_DRIVER=wasm
FAUCET_WALLET_ADDRESS="<address from Phase 2>"
FAUCET_PRIVATE_KEY=<privateKey from .wallet.local.json>
FAUCET_ADMIN_PASSWORD=dev-admin-pw-xxxxxxxx
FAUCET_KEY_PASSPHRASE=dev-passphrase-xxxxxxxx
FAUCET_HASHCASH_DIFFICULTY=16 # lower = faster local tests
FAUCET_DEV=1 # relaxes TLS + CORS checks
FAUCET_CORS_ORIGINS=*Start the stack:
docker compose up -d --buildVerify:
curl http://localhost:8080/healthz # โ "ok" or {"ok":true}
curl http://localhost:8080/v1/config | jq . # โ network:test, claimAmountLuna, abuseLayersExpected: both endpoints return 200. network: "test".
Looks wrong: server won't start, curl connection-refused, or /v1/config says "main".
Ask your AI โ
"Bring up the docker-compose stack in
deploy/compose/and confirmGET /healthzandGET /v1/configboth return 200. If anything fails, pull the faucet container logs and summarise the root cause."
Phase 4 โ Smoke test a real claim (5 min) โ
Goal: a real testnet tx, confirmed on-chain, mediated by the faucet.
cd ../.. # back to repo root
FAUCET_BASE_URL=http://localhost:8080 pnpm smoke:testnetExpected output:
[smoke] base url: http://localhost:8080
[smoke] network=test, hashcash=true
[smoke] generated fresh recipient: NQโฆ
[smoke] solving hashcash (difficulty=16)โฆ
[smoke] hashcash solved
[smoke] claim accepted: id=โฆ status=broadcast
[smoke] confirmed tx: <64-char hex>
[smoke] explorer: https://test.nimiq.watch/#<txid>Open the explorer URL. You should see the transaction.
Looks wrong: "claim rejected" (check reason in logs), "not confirmed in 120000ms" (check wallet balance), or script errors (run once more โ WASM consensus can take ~60s on cold start).
Ask your AI โ
"Run
pnpm smoke:testnetagainst the local faucet and tell me the confirmed tx hash. If the script fails, explain why and propose a fix."
Phase 5 โ Admin dashboard tour (20 min) โ
Goal: click through every admin page, run the key actions, notice anything confusing.
Open http://localhost:8080/admin/login.
Login + TOTP enrolment โ
- Enter the
FAUCET_ADMIN_PASSWORDyou set in.env. - On first login: a TOTP provisioning QR is shown. Scan with Google Authenticator / Authy / 1Password.
- Enter the 6-digit code to confirm enrolment.
- You should land on
/admin/overview.
Overview page โ
- Balance matches Phase 2's funding amount minus any Phase 4 claim.
- Claims/hour shows 1 (the Phase 4 smoke claim).
- Success rate shows 100%.
Claims page โ
- Table includes the Phase 4 claim with
status: confirmed. - Click the row โ explain drawer opens with the full abuse-pipeline signal bundle.
- Try the manual Allow / Deny buttons against a test claim.
Config page โ
- All the 7 abuse-layer toggles are visible.
- Try editing
claimAmountLunaโ save โ refresh โ the persisted override is reflected in/v1/config.
Abuse page โ
- Blocklist is empty by default. Add an entry: kind=
ip, value=192.0.2.1, reason=test, expiresAt in 5 min. Saved โ shows in list โ deletable.
Integrators page โ
- Click Create integrator โ enter name โ API key + HMAC secret shown once. Copy them if you want to test docs/integrator-hmac.md.
- Rotate โ new pair shown โ old is invalid.
Account page โ
- Shows your faucet wallet address + balance.
- Try Send Luna (TOTP step-up required). Send 1 NIM to a throwaway address.
- Rotate TOTP โ requires current TOTP + re-confirm.
Logs page โ
- Every admin action you just took is in the audit log.
- Log streams live (SSE) โ refresh the page; new claims appear without manual refresh.
"Ask your AI" โ
"Walk through every admin page at http://localhost:8080/admin and tell me which ones feel confusing or broken. Flag any error message that says nothing useful."
Phase 6 โ Public claim UI (10 min) โ
Goal: exercise the public / claim page end-to-end, hitting every state.
Open http://localhost:8080/ in an incognito/private window (to avoid dashboard session cookies interfering).
Path A โ happy path โ
- Paste a fresh Nimiq address (use
pnpm generate:walletagain to get one). - Hashcash progress bar fills to 100%.
- Click Claim.
- Status transitions:
pendingโbroadcastโconfirmed. - Explorer link appears.
Path B โ invalid address โ
- Paste
NQ00 00(too short). - Inline validation error appears; submit button stays disabled.
Path C โ rate-limited โ
- Fire multiple claims in a row (or drop
rateLimitPerIpPerDayto 1 in the admin Config page). - The 2nd claim returns status
rejectedwith a friendly message.
Path D โ captcha (optional) โ
If you configured Turnstile or hCaptcha in .env, verify the widget renders, returns a token, and the server accepts it. If you haven't, skip this.
Path E โ WS vs polling โ
Kill the WebSocket in DevTools. The status should still converge via the GET /v1/claim/:id polling fallback.
Ask your AI โ
"Open http://localhost:8080/ and exercise the claim form with a valid address, then with an invalid address, then by hammering it to trigger the rate limit. Screenshot or describe each state and tell me anything that looks broken."
Phase 7 โ Example apps (30 min) โ
Goal: every example runs and can claim against the local faucet.
From the repo root:
docker compose -f deploy/compose/docker-compose.yml -f examples/docker-compose.yml up --build -dVerify each example:
| Example | URL | Expected |
|---|---|---|
| nextjs-claim-page | http://localhost:3001 | Next.js page, claim works |
| vue-claim-page | http://localhost:3002 | Vue page, claim works |
| capacitor-mobile-app (web preview) | http://localhost:3003 | Capacitor React app, claim works |
| go-backend-integration | curl -X POST http://localhost:3005/claim -d '{"address":"NQโฆ"}' | Returns {"status":"confirmed","txId":โฆ} |
| flutter-mobile-app (CLI) | docker compose โฆ logs example-flutter | CLI logs show claim + confirmation |
For each: click / curl, confirm a claim lands on-chain, look at the example's README for the intended demo.
Ask your AI โ
"Start all five examples via
docker compose -f deploy/compose/docker-compose.yml -f examples/docker-compose.yml up. For each one, verify it can claim against the local faucet and report anything that doesn't work."
Phase 8 โ SDK integration spot-checks (15 min) โ
Goal: confirm each SDK can be imported + call the faucet.
TypeScript / Node โ
cd /tmp && mkdir sdk-check && cd sdk-check
pnpm init -y
pnpm add @nimiq-faucet/sdk
node -e "
const { FaucetClient } = require('@nimiq-faucet/sdk');
const c = new FaucetClient({ url: 'http://localhost:8080' });
c.config().then(console.log);
"Expected: prints { network: 'test', claimAmountLuna: '100000', abuseLayers: {โฆ}, โฆ }.
React / Vue / Capacitor / React Native โ
Use the corresponding example app (Phase 7) โ it's already a working SDK integration.
Go โ
cd /tmp && mkdir go-check && cd go-check
go mod init sdk-check
go get github.com/PanoramicRum/nimiq-simple-faucet/packages/sdk-go
cat > main.go <<'EOF'
package main
import (
"context"; "fmt"
faucet "github.com/PanoramicRum/nimiq-simple-faucet/packages/sdk-go"
)
func main() {
c := faucet.New(faucet.Config{URL: "http://localhost:8080"})
cfg, err := c.Config(context.Background())
if err != nil { panic(err) }
fmt.Printf("%+v\n", cfg)
}
EOF
go run main.goFlutter / Dart โ
Already covered by Phase 7's flutter example CLI log.
Ask your AI โ
"Write a minimal script that imports
@nimiq-faucet/sdk, hits the local faucet's/v1/config, and prints the result. Do the same for the Go SDK. Flag any import error."
Phase 9 โ CLI tools (5 min) โ
Goal: the repo-local CLI / script commands all work.
pnpm generate:wallet --force # regenerate a throwaway wallet (force = overwrite)
# Optional: edit .wallet.local.json away if you want to keep the Phase 2 one.
pnpm -F @faucet/server freeze:openapi # regenerates packages/openapi/openapi.{yaml,json}
git diff packages/openapi/ # diff should be empty if the spec is up to dateAlso confirm the MCP inspector route is up:
curl http://localhost:8080/mcp # GET returns the MCP discovery endpointAsk your AI โ
"Run
pnpm -F @faucet/server freeze:openapiand tell me whether the regenerated spec differs from what's committed."
Phase 10 โ MCP server (15 min) โ
Goal: talk to the faucet's MCP endpoint like an AI coding agent would.
Easiest: MCP Inspector.
npx @modelcontextprotocol/inspector http://localhost:8080/mcpPoint your browser at the printed URL and:
- List tools โ you should see
faucet.status,faucet.recent_claims,faucet.stats(public) and the admin-scoped ones. - Call
faucet.statsโ returns the same stats asGET /v1/stats. - Call
faucet.recent_claimswithlimit: 5โ returns the last 5 claims. - Call
faucet.explain_decisionwith the claim id from Phase 4 โ returns the full signal bundle. - Admin tools โ set
FAUCET_ADMIN_MCP_TOKENon the server side and provide it asX-Faucet-Admin-Token; tryfaucet.balance.
Alternative: point Claude Code at the MCP URL and ask it questions like "show me the last 5 claims".
Ask your AI โ
"Connect to http://localhost:8080/mcp via the MCP Inspector or your built-in MCP client. List the available tools, then call
faucet.statsandfaucet.explain_decisionagainst the last claim id. Summarise the result."
Phase 11 โ Abuse layers in action (30 min) โ
Goal: trigger each abuse layer deliberately and confirm it rejects (or scores down) the expected requests.
Work through each layer. After each deliberate rejection, check the admin dashboard's Claims page โ the claim appears with decision: deny and the signal bundle shows which layer flagged it.
Per-IP rate limit โ
- In
.env, setFAUCET_RATE_LIMIT_PER_IP_PER_DAY=1โdocker compose restart faucet. - Submit 2 claims rapidly. Second one
rejectedwith rate-limit reason.
Blocklist โ
- In the admin Abuse page, add blocklist entry kind=
ip, value=127.0.0.1(your loopback). - Submit a claim. Should be denied.
- Remove the entry to unblock.
Hashcash โ
- Submit a claim without solving the puzzle (i.e., manually POST
/v1/claimwith nohashcashSolutionafter disabling the UI solver). Rejected. - Try submitting with a bogus solution. Rejected.
Captcha (optional) โ
- Configure Turnstile (free at Cloudflare), fail the captcha on purpose, confirm rejection.
GeoIP (optional โ needs MaxMind DB or IPinfo key) โ
- Enable
FAUCET_GEOIP_BACKEND=ipinfo+ a token. - Set
FAUCET_GEOIP_DENY_VPN=true. Submit from a known VPN IP. Rejected.
Fingerprint correlation โ
- Set
FAUCET_FINGERPRINT_ENABLED=true+FAUCET_FINGERPRINT_MAX_VISITORS_PER_UID=1. - Submit 2 claims with the same
uidbut 2 differentvisitorIds (via curl). Second one goes toreview.
On-chain heuristics โ
FAUCET_ONCHAIN_ENABLED=true. Submit a claim for an address you know has recent sweeper activity on testnet. Should get scored down / denied.
AI scoring โ
FAUCET_AI_ENABLED=true. Submit unusual request patterns (velocity bursts, weird entropy in hostContext). Score rises.
Ask your AI โ
"Help me trigger each of the 9 abuse layers deliberately so I can see them in action. For each one, tell me what flag to set, what request to send, and what the expected rejection reason should look like in the admin claims drawer."
Phase 12 โ UX review (30+ min, open-ended) โ
Goal: the feedback phase. Note what's rough, file issues.
Admin dashboard checklist โ
- [ ] Login flow: is the TOTP enrolment clear on first login?
- [ ] Overview: do the numbers update without manual refresh? Are they labeled?
- [ ] Claims drawer: signals readable? Can you tell at a glance which layer rejected a claim?
- [ ] Config page: are any field labels unclear? Any values that could be destructive and lack confirmation?
- [ ] Audit log: does live streaming reconnect after a dropped WS? Is the JSON signals blob too wall-of-text?
- [ ] Accessibility: keyboard-only tab order, focus rings visible, ARIA on modals, contrast on disabled buttons.
- [ ] Mobile: does the dashboard work at 375px width? Any horizontal scroll?
- [ ] Errors: are they always actionable? (i.e. "invalid password" โ fine; "Error 500" โ bad)
Example apps checklist โ
- [ ]
pnpm install && pnpm devjust works for each JS example? - [ ] READMEs accurate โ commands match what actually runs?
- [ ] Error states surfaced, or do they just hang?
- [ ] Is it obvious how to point the example at your own faucet URL?
Docs checklist โ
- [ ] All links in
docs/README.mdresolve? - [ ] Does the order of Phase N in this doc match what someone actually needs to do?
- [ ] Any command that doesn't run verbatim anymore?
Output โ
File everything at https://github.com/PanoramicRum/nimiq-simple-faucet/issues with label ux-polish (create the label if it doesn't exist yet). One issue per finding; be specific about what you saw and what you expected.
Ask your AI โ
"You just ran Phases 5, 6, 7 of
docs/qa-testing.md. Make a punchy list of everything that could be improved โ confusing labels, missing confirmation dialogs, broken links, slow pages, anything. One bullet per finding. Top 10 priority items first."
What next โ
- File
ux-polishissues from Phase 12 โ these drive the ROADMAP 1.0.x / 1.1 polish buckets. - If you want to formalise testing further, see the QA program planned in Beyond 1.x.
- For running the faucet in production, see deployment-production.md.
- For backend integration with HMAC-signed hostContext, see integrator-hmac.md.
- For the anti-fraud story (share with non-engineers), see fraud-prevention.md.