Skip to main content

Faucet

The Hub API provides a testnet faucet that sends CLT to any address via a server-signed Transfer transaction.

Endpoint

POST /faucet
Content-Type: application/json

{
"address": "0xYourPublicKeyOrAddress"
}

Response (success):

{
"ok": true,
"amount_clt": 1000,
"node": { }
}

Response (error): a JSON body with an error string and a real HTTP status (400, 429, or 503):

{
"error": "Faucet is disabled (set faucet_enabled = true in config for test networks)"
}

No JWT required. The faucet is gated by server configuration and a built-in rate limiter instead.

Refuses to start on a non-testnet chain

At startup, the Hub API reads the node's chain info and checks is_testnet. If faucet_enabled = true against a non-testnet chain, the process panics and does not start — this is deliberately the one place in this codebase that fails hard instead of returning an error, because a faucet that survives onto a real, value-bearing network could mint real-looking transfers from an account that is never actually funded from reserve. This is a boot-time check, not a per-request one; there is no runtime path that re-verifies is_testnet on every /faucet call.

SDK

const res = await sdk.requestFaucet(publicKey);
if (res.ok) {
console.log('Received', res.amount_clt, 'CLT');
} else {
console.error(res.error);
}

Configuration

In config/{env}.toml (or APP_* environment overrides):

SettingDescription
faucet_enabledEnable/disable the endpoint
faucet_private_keyHex secp256k1 key for the faucet account (must hold CLT)
faucet_amount_cltCLT sent per request (default: 1000)

Example from clutch-deploy:

faucet_enabled = true
faucet_private_key = "d2c446110cfcecbdf05b2be528e72483de5b6f7ef9c7856df2f81f48e9f2748f"
faucet_amount_clt = 1000
Check this value against the current peg

1000 CLT is $0.001 at 1 USD = 1,000,000 CLT — a drip too small to fund even a single transaction's tx_fee (also 1000 CLT). This value predates the peg and reads like it was sized for an earlier, unpegged notion of CLT; confirm the intended drip amount before relying on the checked-in default for anything beyond exercising the faucet endpoint itself.

Test-only key

The faucet_private_key shown above is the public testnet genesis faucet key. It is for testnet experimentation only — never reuse it on any production or value-bearing network. For a private deployment, generate a fresh secp256k1 keypair and fund that account in your genesis. Never commit a production faucet key to source control.

The faucet account must be funded in the node's genesis configuration.

Address formats

The faucet accepts:

  • 20-byte hex address (0x + 40 hex chars)
  • 130-char uncompressed secp256k1 public key (with or without 0x)

Rate limiting

Cooldowns are enforced by the Hub API itself (in-memory, shared across all worker threads):

KeyCooldown
Client IP30 seconds
Recipient address (case-insensitive)3600 seconds (1 hour)

Either cooldown being active rejects the request with HTTP 429, a Retry-After header, and a body carrying the remaining seconds:

{
"error": "faucet cooldown active, try again later",
"retry_after_secs": 27
}

A rejected request does not refresh the cooldown. The client IP is read from X-Forwarded-For / X-Real-IP when present, falling back to the socket peer — so a public deployment must sit behind a trusted reverse proxy, or callers can spoof the header and bypass the per-IP limit. The per-address cooldown still applies regardless.

The cooldown windows are compile-time constants and cannot be tuned via config.

Security notes

  • Disable in production — set faucet_enabled = false
  • The faucet private key must never be exposed to clients
  • Per-IP and per-address cooldowns are built in (see above); for public testnets layer reverse-proxy limits on top, since the per-IP window relies on a trusted proxy for the real client IP

When faucet fails

Common errors:

StatusErrorCause
503Faucet is disabled (set faucet_enabled = true in config for test networks)faucet_enabled = false
503Faucet is not configured (set faucet_private_key to a funded account private key)faucet_private_key is empty
400Invalid public key length. Expected 40 or 130 characters, got …Malformed address
429faucet cooldown active, try again laterCooldown still active (see Rate limiting)
400faucet account 0x… has insufficient balance (have N, need M)Faucet account needs more CLT in genesis
400node rejected faucet tx: …Node unreachable or rejected the transfer

Check API logs (Seq) and node connectivity via /health.