Block explorer for the Rand Protocol RAND chain (the network served by
rand-node). Live at https://randscan.org.
- Backend: Rust — axum REST API + WebSocket, SQLx/PostgreSQL, and an in-process indexer that
follows a
rand-nodeJSON-RPC endpoint (rand_*methods). - Frontend: Next.js 14, TypeScript, Tailwind, SWR; Leaflet for the nodes map.
- Database: PostgreSQL 16.
RAND is a fully shielded chain (fullnode shielded-pool phases S1–S3): there are no accounts,
and every transaction is a shielded bundle (anchor, two nullifiers, two commitments, fee, burn,
proof) plus an optional public action. The explorer indexes committed blocks (hash, height,
HotStuff view, proposer, roots, justify_view), every transaction's public bundle fields and its
action (transfer, mint, deploy, call, bond, unbond, withdraw, bridge_attest,
bridge_burn), confidential-call receipts (tier, outputs, h_in), the commitment tree leaf by
leaf and the nullifier set, the validator register (stake, rewards, unbonding queue, active
set), deployed programs, the supply audit and bridge state, network stats, and the node's libp2p
peers with geolocated IPs. Balances and a transfer's parties are visible only to a viewing key,
which the explorer does not hold. Design: docs/superpowers/specs/2026-09-12-shielded-chain-design.md.
Requirements: Rust 1.75+, Node 20+, PostgreSQL, and a reachable rand-node RPC
(e.g. ssh -N -L 8545:127.0.0.1:8545 root@<node> to tunnel a remote node).
createdb randscan
cp .env.example .env # DATABASE_URL, RPC_URL, API_PORT ...
cargo run --release --bin randscan-api # API + indexer + WebSocket on :3000
cd frontend
npm ci
NEXT_PUBLIC_API_URL=http://localhost:3000 NEXT_PUBLIC_WS_URL=ws://localhost:3000/ws npm run dev # :3001Set COOKIE_SECURE=false in .env for local http so sign-in works.
The schema (migrations/001_initial_schema.sql) is created automatically on first start. The
indexer catches up from height 0, then polls rand_getHead every POLL_INTERVAL_MS.
crates/randscan-api/tests/mock_node.rs runs the indexer, API and broadcast against a scripted
shielded node (every action kind, the tree, the register, two hard forks); tests/real_node.rs
spawns a real shielded rand-node plus the rand wallet (faucet, a proved transfer, a
deploy and a confidential call) and compares every public number. Both need DATABASE_URL;
the real-node test also needs RAND_NODE_BIN (a build of the fullnode's shielded-s3 branch
or later, with rand beside it or RAND_CLI set).
| Variable | Default | Description |
|---|---|---|
DATABASE_URL |
postgres://randscan:randscan@localhost:5432/randscan |
PostgreSQL connection |
RPC_URL |
http://127.0.0.1:8545 |
rand-node JSON-RPC endpoint |
API_HOST / API_PORT |
0.0.0.0 / 3000 |
API bind address |
POLL_INTERVAL_MS |
1000 |
head polling interval when caught up |
BATCH_SIZE |
200 |
blocks per pass while catching up |
STATS_INTERVAL_SECS |
5 |
minimum interval between stats refreshes |
NODES_INTERVAL_SECS |
60 |
peer list / geolocation refresh interval |
NODE_PUBLIC_IP |
auto-detected | public IP of the node the explorer runs on |
RUST_LOG |
info |
log filter |
COOKIE_SECURE |
true |
set the session cookie Secure (requires https; use false for local http) |
TRUST_PROXY |
false |
read the client IP from X-Forwarded-For (true behind Caddy/nginx) |
PUBLIC_URL |
https://randscan.org |
site origin used in password-reset links |
MAIL_FROM |
RandScan <no-reply@randscan.org> |
sender of password-reset email (domain must be verified in Resend) |
RESEND_API_KEY |
unset | Resend API key; unset disables password reset by email |
ANON_RATE_LIMIT_RPM |
60 |
requests per minute per IP for anonymous traffic |
KEY_RATE_LIMIT_RPM |
600 |
requests per minute per API key |
AUTH_RATE_LIMIT_RPM |
10 |
sign-in/sign-up attempts per minute per IP |
REST under /api/v1 (health, stats, supply, bridge, blocks, blocks/latest,
blocks/:id, transactions, transactions/latest, transactions/:hash, notes, notes/:id,
nullifiers/:nf, transactions/:hash/envelopes, envelopes, validators, validators/:address,
programs, programs/:id, nodes,
search?q=; accounts/* answers 410) and a WebSocket at /ws (channels blocks,
transactions, stats). Amounts are strings of units (1 RAND = 10^9 units); timestamps are
timestamp_ms. Full shapes in docs/superpowers/specs/2026-09-12-shielded-chain-design.md; a
guide for integrators with examples in docs/api.md. Two documents from the
account chain remain for history: how the fullnode review fixes shaped confidential calls
(docs/confidential-transactions-after-review-fixes.md)
and the guardian bridge end to end with a diagram (docs/bridge-architecture.md);
their account-side details (balances, recipients, effects) no longer apply.
Accounts and API keys: sign up at /signup, create keys at /dashboard; keyed requests use
Authorization: Bearer rsk_.... Details and quotas in docs/api.md.
Users reset their own password from /forgot: the API emails a single-use link (valid one hour)
through Resend when RESEND_API_KEY is set, and signed-in users can change
their password from /dashboard. On a node deployed with deploy/vps-setup.sh, put the key in
/etc/randscan/api.secrets.env (created empty by the script, never overwritten) and restart
randscan-api. Without a key the site reports that reset by email is not enabled and the
runbook below is the fallback.
read -rs PW && printf '%s' "$PW" | randscan-api hash-password # prints $argon2id$...
psql "$DATABASE_URL" -c "UPDATE users SET password_hash = '<paste>' WHERE email = 'user@example.com';" \
-c "DELETE FROM sessions WHERE user_id = (SELECT id FROM users WHERE email = 'user@example.com');"read -rs prompts for the new password without echoing it, and printf (rather than echo)
avoids appending a trailing newline — together this keeps the password out of shell history and
process listings.
The indexer records the chain id of the data it holds (indexer_state.chain_id). When the node it
follows serves a different chain id, or a different genesis block under the same id, the indexer
logs a warning, truncates only the chain-derived tables (blocks, transactions, nullifiers, notes,
receipts, programs, validators) and re-indexes from height 0 and tree leaf 0. Users, sessions,
API keys, password resets and the peer geolocation cache are kept. Nothing to do by hand: restart
(or re-point) the node and, if you want it picked up immediately rather than within ten seconds,
systemctl restart randscan-api. Do not drop the database; that would delete user accounts
and API keys.
The switch to the shielded chain is also a schema change: migration 005_shielded_chain.sql
drops and recreates the chain tables (users and keys untouched) the first time this build
starts. This build reads only the shielded node's RPC (bundles, actions, rand_getCommitments,
the register); pointed at an account-chain node it will not index. Deploy it together with the
shielded node.
Transaction kinds the node serves that this build does not decode are indexed as kind other
(hash, bundle, fee and block only) so a newer node never stalls the explorer.
The explorer runs on the same machine as a synced rand-node (its RPC is bound to localhost):
deploy/push-to-vps.sh <ip> [domain] # rsync, build, Postgres + Node 20 + Caddy, systemd unitsdeploy/vps-setup.sh installs randscan-api (:3000) and randscan-frontend (:3001) as systemd
services and a Caddyfile that serves <domain> with automatic HTTPS (/api/* and /ws to the
API, everything else to Next.js; www. and randscan.com redirect). Point the domain's A records
at the server before running it so Caddy can obtain certificates.
docker compose up --build runs Postgres, the API and the frontend (set RPC_URL to a reachable
node).
crates/randscan-core wire types, helpers (amount formatting, query classification)
crates/randscan-db schema, row models, queries
crates/randscan-indexer RPC client, block processor, sync service, peer tracker, broadcaster
crates/randscan-api axum routes/handlers, binary randscan-api
crates/randscan-ws WebSocket manager/handler
crates/randscan-viewing envelope opener compiled to WebAssembly (viewing keys in the browser); built on its own, see its README
frontend/ Next.js app
migrations/ SQL schema
deploy/ VPS install script, systemd units, Caddyfile
MIT