Run a faucet
bkc-faucet is the server behind faucet.bkc.network. It is plain Node.js with one dependency (the SDK), logs claims in Node's built-in SQLite, and protects itself with a browser proof-of-work or Cloudflare Turnstile.
Design
- The faucet account is not the treasury. It holds only what you top it up with; the worst case is losing that.
- Both spellings of an address are one account.
bkc1…and0x…are the same 20 bytes; the cooldown is keyed on the bytes. - Two cooldowns. One claim per address and per IP per window, so one machine cannot work through a list of addresses and one address cannot be passed around a room.
- Check and payout are one step. Claims are serialised and the cooldown is re-read inside the queue — two simultaneous requests cannot both pass. (It also has to: two payouts in flight would sign with the same account sequence.)
- Payouts go over the EVM. A Cosmos
MsgSendmoves the same coin, but the explorer indexes the EVM side only; an EVM transfer gets its own transaction page and an up-to-date balance. - The client IP is read from the right of
X-Forwarded-For, past exactly the proxies you declare.
It does not stop someone with a pool of IP addresses: the limits raise the cost, the small balance caps the loss.
Run it
Requires Node.js 22.5+ (for node:sqlite).
git clone https://github.com/bkc-network/bkc-faucet && cd bkc-faucet
npm install
cp .env.example .env # set FAUCET_MNEMONIC
npm start # http://localhost:3005Create and fund the faucet wallet
node --experimental-require-module -e '
import("@bkc-network-lib/sdk").then(async ({ DirectMldsa65HdWallet }) => {
const w = await DirectMldsa65HdWallet.generate(24, { prefix: "bkc" });
const [a] = await w.getAccounts();
console.log(a.address);
console.log(w.mnemonic);
})'Send it what you want the faucet to give away, and top it up as it drains. GET /api/status reports the balance and how many claims are left.
Settings
| Variable | Default | Meaning |
|---|---|---|
FAUCET_MNEMONIC | — | the faucet wallet — the only secret |
FAUCET_AMOUNT | 1 | BKC per claim |
FAUCET_COOLDOWN_HOURS | 24 | per address and per IP |
FAUCET_POW_BITS | 18 | proof-of-work difficulty (≈1 s in a browser) |
TURNSTILE_SITE_KEY / TURNSTILE_SECRET_KEY | — | both set → Turnstile replaces proof-of-work |
PORT | 3005 | |
TRUST_PROXY_HOPS | 1 | proxies in front; must match the deployment |
BKC_EVM_RPC | https://rpc.bkc.network | |
BKC_COMETBFT_RPC | https://comet-rpc.bkc.network | |
BKC_GAS_PRICE | 5000000bkc | |
BKC_EXPLORER_URL | — | links in the UI and API responses |
BKC_CHAIN_PREFIX, BKC_DENOM, BKC_DECIMALS | bkc, bkc, 18 | |
BKC_NETWORK_NAME | derived from the chain id | e.g. "BKC Testnet" |
.env is parsed by the server, not sourced by a shell, so the mnemonic needs no quoting.
Behind nginx
location / {
proxy_pass http://127.0.0.1:3005;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}One proxy → TRUST_PROXY_HOPS=1. Add one per extra proxy (a CDN in front makes it 2). Setting it too high lets a visitor forge the IP the cooldown is keyed on.
A systemd unit (bkc-faucet.service) ships in the repository.
API
See Get test BKC.
