Skip to content

Operator Wiki

pk910 edited this page Mar 5, 2026 · 39 revisions

This operator wiki describes the v2 version of the faucet.

Installing the faucet

Use Release with local NodeJS

You can download packaged releases of the faucet from the Releases Page.

You need a machine with NodeJS >= 18 installed.

Download powfaucet-server-all.tar.gz from the latest release and run the following commands to start the faucet:

tar -xzf powfaucet-server-all.tar.gz
./run-faucet.sh

For Windows, download and extract powfaucet-server-all.zip from the latest release and run run-faucet.bat.

After running these commands, the faucet should be accessible at http://localhost:8080

The faucet server process needs to run continuously. Consider running it in a screen session or as a systemd service.

On first start, the faucet creates a copy of the example configuration at faucet-config.yaml. Some settings are initialized with random values for security (faucetSecret, ethWalletKey). Read through the configuration file to customize the faucet for your needs.

The configuration is described in more detail in this wiki.

Use all-in-one executable from Release

There are also platform-specific all-in-one executables available for Windows and Linux. These are standalone Node.js binaries with the faucet scripts and static folder bundled in. No local Node.js installation is required.
This works fine for testing environments, but a locally installed Node.js is recommended for better performance.

Use docker image

A docker image is available for this faucet: pk910/powfaucet

There are various images available:

  • v2-stable: The latest v2.x.x release
  • v2-latest: The latest master branch version (automatically built)
  • v2.x.x: Version-specific images for all v2.x.x releases.

The docker image uses nginx to serve static files and proxies API/WebSocket requests to the node backend internally. The default working directory inside the container is /data.

Follow these steps to run the docker image:

  1. Create a new directory that will be used for persistent faucet data

    mkdir faucet-data
    cd faucet-data
    
  2. Create default config

    docker run --rm -v $(pwd):/data pk910/powfaucet:v2-stable --create-config
    

    This should create a copy of the example config called faucet-config.yaml.

  3. Edit faucet-config.yaml to configure the faucet for your needs.

    nano faucet-config.yaml
    

    Change ethRpcHost to use a reliable RPC host (Infura, Alchemy, ...) and configure the faucet modules for your needs.

  4. Start the container

    docker run -d --restart unless-stopped --name=powfaucet -v $(pwd):/data -p 8080:8080 pk910/powfaucet:v2-stable
    

You should now be able to access the faucet via http://localhost:8080

Read logs:

docker logs powfaucet --follow

Stop container:

docker rm -f powfaucet

Docker environment variables

The following environment variables can be used to control the docker image behavior:

  • DISABLE_NGINX: Set to any value to skip launching nginx (useful when running behind your own reverse proxy).
  • FAUCET_NGINX_LOG: Set to 1 to enable nginx access logs to stdout.

Build from source

To build the faucet from source, you need a machine with git and NodeJS >= 22 installed. The OS doesn't matter; it is tested on Debian and Windows, but others should work as well.

Clone the repository, install dependencies & build the faucet:

git clone https://github.com/pk910/PoWFaucet.git
cd PoWFaucet
npm install
cd faucet-client
npm install
node ./build-client.js
cd ..
npm run start

After running these commands, the faucet should be accessible at http://localhost:8080

The faucet server process needs to run continuously. Consider running it in a screen session or as a systemd service.

On first start, the faucet creates a copy of the example configuration at faucet-config.yaml. Some settings are initialized with random values for security (faucetSecret, ethWalletKey). Read through the configuration file to customize the faucet for your needs.

The configuration is described in more detail in this wiki.

Run it behind a webserver

For productive setups I suggest using a more complex webserver than the built in low-level static server, because it does not support ssl, caching and stuff.

Note: When using the docker image, nginx is already included and handles static file serving and proxying internally. You can still put another reverse proxy in front of it if needed (e.g. for TLS termination). Set DISABLE_NGINX=1 if you want to handle all proxying yourself.

To setup the faucet with a proper webserver, you just need to point the document root to the /static folder of the faucet and forward websocket (Endpoint: /ws/) and api (Endpoint: /api/) calls to the faucet process.

See a more detailed description and example configs for apache & nginx here: docs/webserver-setup.md

Embed Faucet into an Existing Website

The PoWFaucet UI can be embedded as an independent component into an existing website. This allows a wide range of customization as the faucet can be styled for custom needs.

An example implementation that loads the demo1 instance as a component on a JSFiddle page: https://jsfiddle.net/pk910/nf1k9vxq

The integration is easily done with the following steps:

  1. Install the faucet and make the faucet site accessible via a public reachable URL
  2. Add faucet style to the page you want to show the faucet on
<link rel="Stylesheet" type="text/css" href="https://faucet.example.com/css/powfaucet.css">
  1. Add faucet script to the page you want to show the faucet on
<script src="https://faucet.example.com/js/powfaucet.js"></script>
  1. Add container element where the faucet gets rendered to
<div class="faucet-bootstrap" id="powfaucet"></div>
  1. Initialize & render the faucet
<script type="text/javascript">
(function() {
// get container element
var container = document.getElementById("powfaucet");

// faucet UI config
var config = {
  baseUrl: "https://faucet.example.com"
};

// render UI
PoWFaucet.initializeFaucet(container, config);
})();
</script>

Always load the faucet script and style from the faucet server instead of including the JS/CSS files in your site directly, to avoid version mismatches after faucet upgrades.

For security, the faucet must explicitly allow embedding from certain origins via the CORS policy:

corsAllowOrigin:
  - "https://fiddle.jshell.net"

Reloading & Restarting the faucet

Most configuration settings can be changed without restarting the faucet process.

This is helpful because restarting the process causes a high number of session reconnects and might even result in a loss of rewards when losing connectivity in time-critical situations (e.g. a few seconds before session timeout).

To reload the configuration without restarting, send a SIGUSR1 signal to the faucet process.

You can use the following command in combination with the faucetPidFile configuration to reload the config of a running instance:

kill -SIGUSR1 $(cat ./faucet-pid.txt)

The faucet should reload the config file and print something like this when receiving the signal: # Received SIGUSR1 signal - reloading faucet config

All session progress is restored from the database when the server restarts, so nothing is lost even if you do need to restart.

Updating the faucet

To update the faucet, you just need to load the latest release and overwrite the existing files. The faucet automatically upgrades a existing database when needed.

Demo Instances

To demonstrate some scenarios with different protection methods, I've created a bunch of demo instances of the faucet.

You can find them here: https://github.com/pk910/PoWFaucet/blob/master/docs/demo/README.md

Database

The faucet supports two database drivers: SQLite (default) and MySQL.

SQLite (default)

database:
  driver: "sqlite"
  file: "faucet-store.db"

The file path is relative to the data directory. SQLite is the simplest option and works well for most setups.

MySQL

database:
  driver: "mysql"
  host: "127.0.0.1"
  port: 3306
  username: "faucet"
  password: "secret"
  database: "powfaucet"
  poolLimit: 5  # optional, default: 5

MySQL is recommended for high-traffic deployments or when you want to separate the database from the faucet process.

Distribute ERC20 tokens

The faucet can distribute standard ERC20 tokens instead of native network tokens.

To enable this, configure the following settings:

  • faucetCoinType: Set to "erc20"
  • faucetCoinContract: Set to the address of the ERC20 token contract

The faucet uses a standard ERC20 ABI to transfer funds via the transfer call. The number of decimals and balances are queried via decimals / balanceOf.

Be aware that most amount-related configuration options are based on the smallest unit values ("wei") and assume 18 decimal places.
For tokens with a different number of decimals, you need to adjust these settings to reflect the intended values for your token.

Faucet drop amount

The default drop amount can be configured via the maxDropAmount and minDropAmount settings:

# min/max drop amount (max is the default if no module lowers it)
maxDropAmount: 1000000000000000000 # 1 ETH
minDropAmount: 10000000000000000 # 0.01 ETH

By default, the faucet drops the amount specified by maxDropAmount, but faucet modules may change the drop size according to their protection logic. The final drop size is limited by the min/max amounts specified.

Protection Mechanisms / Modules

The faucet has a modular protection system.

Despite the name "PoW Faucet", the PoW protection is an optional module just like the others.

There are various modules available that can be enabled independently to fit your protection needs.

Module: captcha

Add captcha protection to front page (session start) and/or claim page (session claim)

You need to set a proper siteKey / secret from HCaptcha, Google / reCaptcha or Cloudflare / Turnstile (since v2.2.5)

modules:
  captcha:
    # enable / disable captcha protection
    enabled: true

    # captcha provider
    # hcaptcha:  HCaptcha (https://hcaptcha.com)
    # recaptcha: ReCAPTCHA (https://developers.google.com/recaptcha)
    # turnstile: Turnstile (https://www.cloudflare.com/products/turnstile)
    provider: "hcaptcha"

    # captcha site key
    siteKey: "00000000-0000-0000-0000-000000000000"

    # captcha secret key
    secret: "0xCensoredHCaptchaSecretKey"

    # require captcha to start a new session (default: false)
    checkSessionStart: false

    # require captcha to start claim payout (default: false)
    checkBalanceClaim: false

Module: concurrency-limit

Limits the number of concurrent sessions on a per IP / per ETH Address base.

Concurrent sessions are sessions in running state. However, sessions leave the running state almost immediately if no module introduces a long-running task (e.g. mining). So this module is only effective in combination with such a module.

modules:
  concurrency-limit:
    # enable / disable concurrency limit
    enabled: true

    concurrencyLimit: 1 # only allow 1 concurrent session per IP / target addr
    byAddrOnly: false # only check concurrency by target address
    byIPOnly: false # only check concurrency by IP address
    #messageByAddr: "" # custom error message when limit is exceeded by same target address
    #messageByIP: "" # custom error message when limit is exceeded by same IP address

Module: ensname

This module allows entering a mainnet ENS name instead of a wallet address.

By default this is a optional feature. But ENS names can also be made a strict requirement and therefore be used as a protection method via the required setting.

modules:
  ensname:
    # enable / disable ENS module
    enabled: true

    # RPC host for ENS name resolver (mainnet RPC)
    rpcHost: "https://rpc.flashbots.net/"
    # Custom ENS Resolver contract address
    #ensAddr: "0x"

    # require ENS name for sessions
    required: false

Module: ethinfo

This module checks the target wallet on the testnet the faucet is running on whenever a session is started.
There are several restrictions that can be enforced:

  • maxBalance: Max amount of funds the user is allowed to have in his wallet to be allowed to use the faucet.
  • denyContract: Deny sessions for contract addresses to avoid farming with forwarder contracts.
modules:
  ethinfo:
    # enable / disable max balance protection
    enabled: true

    # check balance and deny session if balance exceeds the limit (in wei)
    maxBalance: 50000000000000000000 # 50 ETH

    # deny sessions for contract addresses
    denyContract: true

Module: faucet-balance

This module introduces a global reward factor based on the faucet wallet balance.

It is used to lower the drop amounts when the faucet wallet gets low on funds.

modules:
  faucet-balance:
    # enable / disable faucet balance protection
    enabled: true

    # reward restriction based on faucet wallet balance (lowest value applies)
    fixedRestriction:
      1000000000000: 90 # 90% if lower than 1000 ETH
      500000000000: 50  # 50% if lower than 500 ETH
      200000000000: 10  # 10% if lower than 200 ETH
      100000000000: 5   # 5% if lower than 100 ETH

    # dynamic reward restriction based on faucet balance
    # alternative to fixedRestriction; if both are set, the lower factor is applied
    # limits rewards linearly (100% >= targetBalance, 0% <= spareFundsAmount)
    dynamicRestriction:
      targetBalance: 1100000000000000000000 # no restriction with faucet balance > 1100 ETH

Module: faucet-outflow

This module introduces a global reward factor based on the faucet outflow.

To keep track of the outflow, the module uses an internal "outflow balance".
The outflow balance initially starts at 0 and gets increased by the configured amount (amount/duration) every second.
Whenever a session gets a reward assigned, that amount is subtracted from the outflow balance.

When the outflow balance gets negative, there are more funds requested than the outflow limit allows and the reward is reduced. The reduction is applied linearly down to 0% the closer the outflow balance gets to lowerLimit.

When the outflow balance gets positive, there are less funds requested than allowed. The positive balance is a 'buffer' for future activity spikes. However, the balance won't grow higher than upperLimit.

This logic ensures that the average mining reward does not exceed the specified limit over the long term. In the short term, the reward for a single day may exceed the limit by the configured boundaries, but the algorithm then becomes more restrictive the next day (negative outflow balance), so the average converges back to the limit.

The status of the faucet-outflow module is displayed on the faucet status page, allowing the operator to monitor the internal outflow balance and the applied reward reduction.

modules:
  faucet-outflow:
    # enable / disable faucet outflow protection
    enabled: true

    # limit outflow to 1000 ETH per day
    amount: 1000000000000000000000 # 1000 ETH
    duration: 86400 # 1 day

    # outflow balance limits
    lowerLimit: -500000000000000000000 # -500 ETH (reward approaches 0% as balance reaches this)
    upperLimit: 500000000000000000000  # 500 ETH (buffer for activity spikes)

Module: github

This module adds the ability to authenticate with a GitHub account before session start.
Besides strict barriers, the authentication can also be made optional to apply reward factors based on GitHub profile stats.

The module needs a GitHub API key and secret to handle the OAuth authentication flow.
To register an application, go to the GitHub Developer Settings and register an "OAuth App".
The authorization callback URL for the faucet is {faucet-host}/api/githubCallback (replace {faucet-host} with your hostname).

Note that the callback URL specified in the GitHub app settings matches subdomains as well.
So for all faucets running under xy-faucet.pk910.de, the same GitHub app can be used with the callback URL set to https://pk910.de/api/githubCallback. That URL doesn't need to actually work; it's used as a pattern. When the authentication workflow is triggered from a valid subdomain, the OAuth flow automatically redirects to the correct subdomain on completion.
See the GitHub documentation for details.

Additional settings:

  • cacheTime: How long to cache GitHub profile data (in seconds, default: 86400).
  • redirectUrl: Custom redirect URL for the OAuth callback (optional).
modules:
  ## GitHub login protection
  github:
    # enable / disable GitHub login protection
    enabled: true

    # GitHub API credentials
    appClientId: "" # client id from GitHub app
    appSecret: "" # app secret from GitHub app

    # authentication timeout
    authTimeout: 86400

    # cache time for GitHub profile data
    cacheTime: 86400 # 1 day

    # GitHub account checks
    checks:
      - required: true # simplest check - just requires being logged in

      - minAccountAge: 604800 # min account age (7 days)
        minRepoCount: 10 # min number of repositories (includes forked ones)
        minFollowers: 10 # min number of followers
        minOwnRepoCount: 5 # min number of own repos (non-forked)
        minOwnRepoStars: 5 # min number of stars on own repos (non-forked)
        required: true # require passing this check or throw error
        message: "Your github account does not meet the minimum requirements" # custom error message

      - minFollowers: 50
        minOwnRepoCount: 10
        minOwnRepoStars: 1000
        rewardFactor: 2 # double reward if account looks trustful

    # recurring restrictions based on github account
    restrictions:
      # max 10 sessions / 10 ETH per day for each github account
      - limitCount: 10
        limitAmount: 10000000000000000000 # 10 ETH
        duration: 86400 # 1 day

Module: ipinfo

This module fetches IP-related information from an external API and allows defining restrictions based on it.

The API endpoint is specified via the apiUrl setting. It defaults to the free JSON API provided by ip-api.com. When using a custom API, ensure the request/response formats match the format provided by ip-api.com.

The gathered information used for restrictions looks as follows:

ETH: <Wallet-Address>
IP: <IP-Address>
Ident: <Browser-Fingerprint>
Country: <Country-Code>
Region: <Region-Code>
City: <City-Name>
ISP: <ISP-Name>
Org: <Organization-Name>
AS: <AS-Number-and-Name>
Proxy: <true/false>
Hosting: <true/false>

Based on this information, several optional restrictions can be applied:

  • Basic property restrictions (restrictions)
    Restricts rewards based on country code or the Proxy/Hosting flag.
    Values can be a simple number (reward percentage) or an object with additional options.
  • Regex-based restrictions (restrictionsPattern)
    Restricts rewards via regular expressions matched against any of the gathered information.
  • Dynamically loaded restrictions from file (restrictionsFile)
    Similar to the regex-based mechanism, but loads restrictions from an external file. The file is refreshed periodically, so it can be updated dynamically by an external process.
    The format of the YAML file is as follows:
    restrictions:
    - pattern: "^IP: 1\\.2\\."
      reward: 0 # set reward to 0%
      message: "Sorry, this faucet is not intended for farmers!"
      blocked: true # kill session, but allow claiming the already collected reward
    - pattern: "^ETH: 0x0000000000000000000000000000000000001337"
      reward: 50 # set reward to 50%
      message: "I just don't like you! Reward reduced to 50%"
      notify: true # display message as notification
    - pattern: "^Org: Hostingrsdotcom"
      reward: 0
      message: "Sorry, this faucet is not intended for farmers!"
      blocked: "kill" # "kill" terminates the session without allowing to claim the collected rewards

The three restriction classes can be configured individually and can be left out entirely if not needed. All configured restriction classes are processed in the order listed above.

modules:
  ipinfo:
    # enable / disable IP-Info protection
    enabled: true

    # ip info lookup api url (default: http://ip-api.com/json/{ip}?fields=21155839)
    apiUrl: "http://ip-api.com/json/{ip}?fields=21155839"

    # ip info caching time (stored in database)
    cacheTime: 86400 # 1 day

    # require valid ip info (throw error and fail session if lookup failed)
    required: false

    # Basic property restrictions
    restrictions:
      # percentage of drop amount if IP is in a hosting range (default: 100), 0 to block entirely
      hosting: 1

      # percentage of drop amount if IP is in a proxy range (default: 100), 0 to block entirely
      proxy: 1

      # percentage of drop amount if IP is from given country code (DE/US/...), 0 to block entirely
      #US: 100

    # Regex-based restrictions
    restrictionsPattern:
      "^.*Tencent Cloud.*$": 1 # 1% if info matches pattern
      "^.*UCLOUD.*$": 1
      "^.*Server Hosting.*$": 1
      "^.*SCloud.*$": 1

    # Dynamically loaded restrictions from file
    restrictionsFile:
      yaml: "./restrictions.yaml"
      refresh: 30 # reload file every 30 sec

Module: mainnet-wallet

This module checks the target wallet on mainnet when a session is started.
The following restrictions can be enforced:

  • minBalance: Minimum balance the user must hold in their mainnet wallet.
  • minTxCount: Minimum number of transactions the user must have sent from their mainnet wallet.
  • minErc20Balances: List of minimum ERC20 token balances required in the user's mainnet wallet. Each entry includes the token contract address (address), the number of decimals (decimals), the token name, and the minimum required balance (minBalance).
modules:
  mainnet-wallet:
    # enable / disable mainnet wallet protection
    enabled: true

    # RPC host for ETH mainnet
    rpcHost: "https://rpc.flashbots.net/"

    # require minimum balance on mainnet wallet
    minBalance: 10000000000000000  # 0.01 ETH

    # require minimum number of transactions from mainnet wallet (nonce count)
    minTxCount: 5

    # require minimum ERC20 token balances on mainnet wallet
    minErc20Balances:
      - name: "WETH"
        address: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" # WETH on mainnet
        decimals: 18
        minBalance: 10000000000000000 # 0.01 WETH

Module: passport

This module fetches the Gitcoin Passport for a target address when a session is started.

The passport is scored using the configured scoring matrix. Each stamp can be given an individual score value. The resulting score is then used to select a reward factor applied to all session rewards.

The module can be used to increase rewards for more legitimate users in mining scenarios, or lower rewards for less legitimate users in other scenarios.

You need an API Key from the Gitcoin Passport Scorer Dashboard to load passports automatically.

To allow optional manual submission and verification of the passport JSON, specify the public key of the stamp signer for verification.
For Gitcoin Passport, that is currently did:key:z6MkghvGHLobLEdj1bgRLhS4LPGJAvbMA1tn2zcRyqmYU5LC

Additional settings:

  • requireMinScore: Minimum passport score required to start a session (0 to disable).
  • skipProxyCheckScore: Skip proxy check for IP when wallet score exceeds this value (0 to disable).
  • skipHostingCheckScore: Skip hosting check for IP when wallet score exceeds this value (0 to disable).
  • allowGuestRefresh: Allow passport refresh without an active session (e.g. for skipping hosting/proxy checks).
  • guestRefreshCooldown: Refresh cooldown without an active session (defaults to refreshCooldown if 0).
modules:
  passport:
    enabled: true
    scorerApiKey: "" # Gitcoin Passport Scorer API Key
    trustedIssuers:
      - "did:key:z6MkghvGHLobLEdj1bgRLhS4LPGJAvbMA1tn2zcRyqmYU5LC" # Gitcoin Passport
    passportCachePath: "passport-cache"

    refreshCooldown: 300 # allow refreshing every 5 mins
    cacheTime: 86400 # cache passports for 1 day (does not affect manual refreshing)

    stampDeduplicationTime: 604800 # allow reusing a stamp in another passport after 7 days

    skipProxyCheckScore: 0  # skip proxy check for IP when wallet score exceeds this
    skipHostingCheckScore: 0 # skip hosting check for IP when wallet score exceeds this
    allowGuestRefresh: false # allow passport refresh without active session
    guestRefreshCooldown: 0 # refresh cooldown without active session (default to refreshCooldown if 0)

    # applied reward factors based on passport score
    boostFactor:
      1: 1.1 # x1.1 if score >= 1
      5: 1.5 # x1.5 if score >= 5
      10: 2 # x2 if score >= 10
    
    # stamp scoring matrix
    stampScoring:
      "Google": 1
      "Twitter": 1
      "Ens": 2
      "GnosisSafe": 2
      "Poh": 4
      "POAP": 1
      "GitPOAP": 1
      "Facebook": 1
      "FacebookFriends": 1
      "FacebookProfilePicture": 1
      "Brightid": 10
      "Github": 1
      "Coinbase": 1
      "FiveOrMoreGithubRepos": 1
      "TenOrMoreGithubFollowers": 2
      "FiftyOrMoreGithubFollowers": 4
      "ForkedGithubRepoProvider": 1
      "StarredGithubRepoProvider": 1
      "Linkedin": 1
      "Discord": 1
      "TwitterTweetGT10": 1
      "TwitterFollowerGT100": 1
      "TwitterFollowerGT500": 2
      "TwitterFollowerGTE1000": 3
      "TwitterFollowerGT5000": 5
      "SelfStakingBronze": 1
      "SelfStakingSilver": 1
      "SelfStakingGold": 1
      "CommunityStakingBronze": 1
      "CommunityStakingSilver": 1
      "CommunityStakingGold": 1
      "ClearTextSimple": 1
      "ClearTextTwitter": 1
      "ClearTextGithubOrg": 1
      "SnapshotProposalsProvider": 1
      "SnapshotVotesProvider": 1
      "EthGasProvider": 1
      "FirstEthTxnProvider": 1
      "EthGTEOneTxnProvider": 1
      "GitcoinContributorStatistics#numGrantsContributeToGte#1": 1
      "GitcoinContributorStatistics#numGrantsContributeToGte#10": 1
      "GitcoinContributorStatistics#numGrantsContributeToGte#25": 2
      "GitcoinContributorStatistics#numGrantsContributeToGte#100": 2
      "GitcoinContributorStatistics#totalContributionAmountGte#10": 1
      "GitcoinContributorStatistics#totalContributionAmountGte#100": 2
      "GitcoinContributorStatistics#totalContributionAmountGte#1000": 2
      "GitcoinContributorStatistics#numRoundsContributedToGte#1": 1
      "GitcoinContributorStatistics#numGr14ContributionsGte#1": 1
      "GitcoinGranteeStatistics#numOwnedGrants#1": 1
      "GitcoinGranteeStatistics#numGrantContributors#10": 1
      "GitcoinGranteeStatistics#numGrantContributors#25": 1
      "GitcoinGranteeStatistics#numGrantContributors#100": 2
      "GitcoinGranteeStatistics#totalContributionAmount#100": 1
      "GitcoinGranteeStatistics#totalContributionAmount#1000": 2
      "GitcoinGranteeStatistics#totalContributionAmount#10000": 4
      "GitcoinGranteeStatistics#numGrantsInEcoAndCauseRound#1": 1
      "gtcPossessionsGte#100": 2
      "gtcPossessionsGte#10": 1
      "ethPossessionsGte#32": 1
      "ethPossessionsGte#10": 1
      "ethPossessionsGte#1": 1
      "NFT": 1
      "Lens": 1
      "ZkSync": 1

Module: pow

This module introduces the mining requirement the faucet is originally known for.

When enabled, session rewards are not dropped immediately. Instead, they must be collected by providing processing power for mining in the browser.

The module is highly configurable. The most important settings are:

  • powShareReward: The reward a session gets for each eligible hash.
  • powSessionTimeout: The maximum time a user may mine on the faucet.
    Keep in mind that abusive users might use devices they don't own for mining. Don't make this too long, and consider using captchas to enforce some user interaction.
  • powDifficulty: The difficulty of the mining algorithm. See Eligible Hashes for more details.
  • powHashrateSoftLimit / powHashrateHardLimit: Controls the maximum allowed mining hashrate. The soft limit is enforced client-side only and may be slightly inaccurate due to clock drifts. The hard limit is checked server-side to prevent abusive users from bypassing the client-side limitation. It should be slightly higher than the soft limit to avoid unwanted nonce rejections.
  • powSessionsPerServer: Maximum number of mining sessions per server sub-process (0 = unlimited).

See Eligible Hashes and Hash Verification Process for more details about additional settings.

All other settings default to the values shown below and can be omitted to keep the config readable.

modules:
  pow:
    # enable / disable PoW protection
    enabled: true

    # reward amount per eligible hash (in wei)
    powShareReward: 12500000000000000  # 0.0125

    # maximum mining session time (in seconds)
    powSessionTimeout: 18000   # 5h

    # maximum number of seconds a session can idle (no connection) until it gets closed
    powIdleTimeout: 1800 # 30min

    # maximum allowed mining hashrate (will be throttled to this rate when faster)
    powHashrateSoftLimit: 1000 # soft limit (enforced client-side)
    powHashrateHardLimit: 1100 # hard limit (reject shares with too high nonces)

    # number of 0-bits a hash needs to start with to be eligible for a reward
    powDifficulty: 11

    ## All settings below can be omitted to keep the config readable.
    ## They default to the values shown here.

    # penalty for not responding to a verification request (percent of powShareReward)
    verifyMinerMissPenaltyPerc: 10  # 10% of powShareReward

    # reward for responding to a verification request in time (percent of powShareReward)
    verifyMinerRewardPerc:   15  # 15% of powShareReward

    # websocket ping interval
    #powPingInterval: 60

    # kill websocket if no ping/pong for that number of seconds
    #powPingTimeout: 120

    # mining algorithm to use
    powHashAlgo: "argon2"  # scrypt / cryptonight / argon2 / nickminer

    # scrypt mining parameters
    powScryptParams:
      # N - iterations count: affects memory and CPU usage, must be a power of 2
      cpuAndMemory: 4096
      # r - block size: affects memory and CPU usage
      blockSize: 8
      # p - parallelism factor: threads to run in parallel, affects the memory & CPU usage, should be 1 as webworker is single threaded
      parallelization: 1
      # klen - how many bytes to generate as output, e.g. 16 bytes (128 bits)
      keyLength: 16

    # cryptonight mining parameters
    powCryptoNightParams:
      # crypto night hash algo & variant
      algo: 0
      variant: 0
      height: 0

    # argon2 mining parameters
    powArgon2Params:
      type: 0
      version: 13
      timeCost: 4
      memoryCost: 4096
      parallelization: 1
      keyLength: 16

    # nickminer mining parameters (see 
    powNickMinerParams:
      # signature hash of the transaction to mine
      hash: "cb60ff3d42eea9d30a249c26a957310b57e5dfacd6c2f56b842f40682d676e8f"
      sigR: "0539"      # R value of the signature
      sigV: 27          # V value of the signature
      count: 100        # number of hashes to check per round
      suffix: "beac02"  # contract address suffix to search for (high prio)
      prefix: "0000"    # contract address prefix to search for (low prio)

    # Proof of Work shares need to be verified to prevent malicious users from just sending in random numbers.
    # As that can lead to a huge verification workload on the server, this faucet can redistribute shares back to other miners for verification.
    # These randomly selected miners need to check the share and return its validity to the server within 10 seconds or they're penalized.
    # If there's a mismatch in validity-result the share is checked again locally and miners returning a bad verification result are slashed.
    # Bad shares always result in a slashing (termination of session and loss of all collected mining balance)

    # percentage of shares validated locally (0 - 100)
    verifyLocalPercent: 10

    # max number of shares in local validation queue
    verifyLocalMaxQueue: 100

    # min number of mining sessions for verification redistribution
    # only local verification if not enough active sessions (should be higher than verifyMinerIndividuals)
    verifyMinerPeerCount: 4

    # percentage of shares validated locally if there are not enough sessions for verification redistribution (0 - 100)
    verifyLocalLowPeerPercent: 80

    # percentage of shares to redistribute to miners for verification (0 - 100)
    verifyMinerPercent: 75

    # number of other mining sessions to redistribute a share to for verification
    verifyMinerIndividuals: 2

    # max number of pending verifications per miner before not sending any more verification requests
    verifyMinerMaxPending: 5

    # max number of missed verifications before not sending any more verification requests
    verifyMinerMaxMissed: 10

    # timeout for verification requests 
    # client gets penalized if not responding within this timespan
    verifyMinerTimeout: 30

Eligible Hashes

Hashes eligible for a reward must start with a specific number of 0-bits. This requirement is called difficulty and is configured via the powDifficulty setting within the pow module.

The default is 11, which is reasonable for small testnets and test instances. For higher activity (> 500 sessions), increasing the difficulty is strongly recommended to reduce traffic and server-side processing time.

A lower difficulty is more user-friendly because eligible hashes are found more frequently and the mining balance increases faster. However, a lower difficulty also increases traffic and server-side validation workload.

A difficulty of 11 means on average 1 of 2^11 hashes is eligible for a reward. With an average hashrate of ~300 H/s, that leads to about 1 eligible hash every 7 seconds per mining session.

Difficulty 100 H/s 300 H/s 500 H/s 800 H/s 1000 H/s
11 20,5 s 6,8 s 4,1 s 2,6 s 2 s
12 41 s 13,6 s 8,2 s 5,1 s 4,1 s
13 82 s 27,3 s 16,4 s 10,2 s 8,2 s

Hash Verification Process

To prevent malicious users from sending random hashes that don't meet the required criteria, all hashes must be verified. This can create an extremely high verification load on the server. To mitigate this, the faucet can redistribute the verification work to other randomly selected miners ("verifiers") who check whether a hash is valid.

The redistribution feature is optional but enabled by default. For security, it only activates when a minimum number of sessions are actively mining.

The security of verification redistribution relies on random selection of multiple verifiers. The miner does not know whether their result is redistributed to an honest or malicious verifier. There should always be at least 2 verifiers per hash. If the verifiers' results differ, the hash is re-checked locally.

Invalid hashes or incorrect verifications always result in immediate session termination and loss of all collected funds. Because of this, it is not critical if a group of malicious users manages to get a few invalid hashes through. If they repeatedly submit invalid hashes, one will eventually be redistributed to an honest verifier, the invalidity is detected, and the attacker loses all rewards.

The redistribution process is highly configurable via the verify* settings. The defaults should work fine for most setups.

Reward Portions

The reward accumulated during mining is a combination of two portions:

  • Reward for eligible hashes
  • Reward for verifying hashes from other miners

The reward for eligible hashes is configured via the powShareReward option. It is also the base value (100%) for all other reward and penalty options.

The reward for verifications is configured via verifyMinerRewardPerc and the penalty for not completing verifications via verifyMinerMissPenaltyPerc. Both are percentage values based on powShareReward. For example, setting verifyMinerRewardPerc to 15 means 15% of powShareReward is awarded for completing a verification.

The verification reward primarily benefits slow miners who don't find many eligible hashes themselves. It shouldn't be set too high to avoid farming verification rewards without actually mining.

As described above, there should always be at least 2 verifiers per hash to prevent malicious verifier behavior. The number of verifiers is configured via verifyMinerIndividuals. Keep in mind that verifyMinerRewardPerc is paid to each verifier, so the total cost per eligible hash from an operator perspective is powShareReward + verifyMinerIndividuals * (powShareReward / 100 * verifyMinerRewardPerc).

nickminer Mining Algorithm

The hashpower from the faucet can be used to vanity-generate keyless contract deployments as commonly used for EIPs. The algorithm is called nickminer (derived from "Nick's method") and can be used like any other mining algorithm.

Some extra steps are needed to make the mining results usable for real on-chain deployments. Here are the steps to generate a keyless deployment with a signature mined from the faucet. You need Go installed on your system.

  1. Install dependencies (nick tool)

    go install github.com/pk910/nick@latest
  2. Prepare Transaction:

    nick build --initcode="0x60425000"

    Replace 0x60425000 with the initcode of the contract you want to deploy.

    In the build command output, you'll find the signature hash:

    ...
    Sig Hash: 0x45fe116882ed0b5439bbfca93afa1373823d3186063fc9b1d1aa440ced7f97f7  # <- this one!
    TX Hash: 0xcb18738fbdf0c28b373466d9c72dd18f0c4ea60222484c6b9a54968a76cdea9b
    ...
    
  3. Configure faucet:
    Now configure the faucet to mine addresses for this transaction.
    You can specify a suffix which clients mine on, so it needs to be at least 3 bytes (6 hex characters) long.
    In addition you can specify a prefix, which is used to further filter relevant hashes on server side.
    You also need to specify a filename where relevant matches are written to.

      ## Proof of Work (mining) protection
      pow:
        # enable / disable PoW protection
        enabled: true
    
        # number of matching bits the hash needs to include from start or end to be eligible for a reward
        powDifficulty: 19  # note: with nickminer this needs to be about 7-8 bits higher than for the other mining algorithms
    
        # mining algorithm to use
        powHashAlgo: "nickminer"  # scrypt / cryptonight / argon2 / nickminer
    
        powNickMinerParams:
          hash: "45fe116882ed0b5439bbfca93afa1373823d3186063fc9b1d1aa440ced7f97f7"  # sig hash from nick tool without 0x prefix
          sigR: "0539"      # sig-r value, default as used by nick tool
          sigV: 27          # sig-v value, leave as 27!
          count: 100        # number of hashes to check in a single loop
          suffix: "beac02"  # suffix bytes to search for (needs to be at least 2 bytes)
          prefix: "0000"    # prefix bytes to search for (in addition to the suffix)
    
          relevantDifficulty: 24  # number of matching bits from where results are logged to the relevantFile
          relevantFile: "./nickminer.log" # filename to write relevant hashes to
  4. Run the faucet
    Run the faucet and wait for relevant results.
    The faucet will output a file like this and append new matches as found:

    0xf5003e69642af091decca15ef431589e11beac02  (d: 24): hash: 0xcb60ff3d42eea9d30a249c26a957310b57e5dfacd6c2f56b842f40682d676e8f, sigR: 0x0539, sigS: 0x075a71494c945f471480588a03b724b175004a
    0xcc6fc16c4da4530fe3ec9b9afe9d605119beac02  (d: 24): hash: 0xcb60ff3d42eea9d30a249c26a957310b57e5dfacd6c2f56b842f40682d676e8f, sigR: 0x0539, sigS: 0x0c53ce494c945f471480588a03b724b1750017
    0x9f17d858a5e7e05dc324906f90ec2fcdb9beac02  (d: 24): hash: 0xcb60ff3d42eea9d30a249c26a957310b57e5dfacd6c2f56b842f40682d676e8f, sigR: 0x0539, sigS: 0x0282df0e7d57b72642348f062eec81b29f003d
    

    In the first column you can see the found contract addresses with matching suffix.
    The d: column shows the number of matching bits, starting from the right of the suffix and continuing from the left of prefix.
    The sigR & sigS values are the relevant information to reconstruct the full transaction to deploy the contract to the address in the first column.

  5. Build the final transaction for a mined result

    nick build --initcode="0x60425000" --sig-r "0x0539" --sig-s "0x0282df0e7d57b72642348f062eec81b29f003d"
    

    Replace sig-r & sig-s with the values returned from the faucet.
    The tool now returns the final transaction in various formats and prints the deployer & contract address.
    You can now simply go ahead funding the deployer and deploying the contract to the mined address.

Module: recurring-limits

This module defines restrictions for recurring users.

Restrictions are based on IP and/or ETH wallet address. Multiple restrictions can be defined for different amounts and durations.

Keep in mind that session data is removed from the database after the time defined by the global sessionCleanup setting. Restrictions can only aggregate data within that time frame.

Each limit entry supports the following options:

  • limitCount: Maximum number of sessions allowed within the duration.
  • limitAmount: Maximum total drop amount (in wei) allowed within the duration.
  • duration: Time window in seconds to aggregate data from.
  • byAddrOnly: Only check by target address (ignore IP).
  • byIPOnly: Only check by IP address (ignore target address).
  • ip4Subnet: Subnet mask for IPv4 grouping (e.g. 24 for /24 subnets).
  • action: Action to take when the limit is exceeded.
  • message: Custom error message shown when the limit is exceeded.
  • rewards: Reward percentage when this limit is exceeded (instead of blocking).
modules:
  recurring-limits:
    # enable / disable recurring limits protection
    enabled: true

    limits: # array of individual limits, which all need to pass
      - limitCount: 10 # limit number of sessions to 10
        duration: 3600 # aggregate data from last 1 hour
      - limitCount: 100 # limit number of sessions to 100
        limitAmount: 10000000000000000000 # limit total drop amount to 10 ETH
        duration: 86400 # aggregate data from last 1 day
      - limitAmount: 20000000000000000000 # 20 ETH
        duration: 172800 # 2 days
        byAddrOnly: true

Module: whitelist

This module allows whitelisting specific IPs via regex patterns. Whitelisted sessions can receive modified reward factors and optionally skip other protection modules.

Patterns can be defined inline or loaded from an external YAML file that is refreshed periodically.

modules:
  whitelist:
    enabled: true

    # inline whitelist patterns (matched against remote IP)
    whitelistPattern:
      "^10\\.0\\.0\\.": # match IPs starting with 10.0.0.
        reward: 200       # double reward (200%)
        skipModules: ["pow", "recurring-limits"]  # skip these modules

    # load patterns from external YAML file
    whitelistFile:
      yaml: "./whitelist.yaml"  # path to YAML file (can be a list of paths)
      refresh: 30               # reload interval in seconds

The YAML file format:

restrictions:
  - pattern: "^192\\.168\\."
    reward: 150
    skipModules: ["pow"]
    message: "Internal user"
    notify: true

Module: voucher

This module requires a valid voucher code to start a session. Voucher codes are single-use and can optionally override the drop amount.

Voucher codes can be mass-generated using the built-in CLI tool:

node bundle/powfaucet.cjs create-voucher --count 10 --amount 1ETH --prefix TEST

This prints the generated codes to stdout, one per line. The codes are stored in the faucet database.

Each voucher code can only be used once. If the session associated with a voucher fails, the voucher can be reused.

modules:
  voucher:
    enabled: true
    voucherLabel: "Enter your voucher code"  # label shown in the UI
    infoHtml: "<p>Enter the voucher code you received.</p>"  # optional info HTML

Module: zupass

This module adds authentication via Zupass zero-knowledge proofs, typically used for event-based access (e.g. conference attendees).

Authenticated users can receive special grants that override normal restrictions, such as skipping certain modules or receiving boosted rewards.

modules:
  zupass:
    enabled: true
    zupassUrl: "https://zupass.org/"
    zupassApiUrl: "https://api.zupass.org/"
    redirectUrl: "https://your-faucet.example.com/"
    requireLogin: false  # require zupass login for all sessions
    concurrencyLimit: 2  # max concurrent sessions per zupass identity

    # event configuration
    event:
      name: "My Event"
      eventIds:
        - "event-uuid-here"

    # proof verification
    verify:
      signer: ["public-key-hex"]
      #productId: ["product-uuid"]
      #eventId: ["event-uuid"]

    # grants for authenticated users
    grants:
      - limitAmount: 100000000000000000000 # 100 ETH
        duration: 86400
        skipModules: ["recurring-limits"]
        rewardFactor: 2
      - limitCount: 1
        duration: 86400
        rewardFactor: 2
        skipModules: ["recurring-limits", "pow", "faucet-outflow", "faucet-balance", "ipinfo"]

    # UI customization
    loginLogo: "/images/zupass_logo.jpg"
    loginLabel: "Event attendee? Login with your ticket."
    userLabel: "Authenticated with event ticket."
    infoHtml: |
      Benefits of authenticating with your ticket:<br>
      <ul>
        <li>Request funds without mining (once a day)</li>
        <li>Skip IP-based limitations</li>
        <li>Mine with boosted rewards</li>
      </ul>

Faucet Status

The faucet can display status messages to users on the frontend. Status messages can come from internal events (e.g. low balance warnings) or from external files.

faucetStatus:
  json: "faucet-status.json"   # path to JSON status file
  yaml: "faucet-status.yaml"   # path to YAML status file
  refresh: 10                  # refresh interval in seconds (default: 10)

The status file can contain a single message or an array of messages:

# single message
- level: "warn"
  prio: 10
  text: "The faucet is under maintenance."

# with filters (only show to specific users)
- level: "info"
  prio: 5
  text: "Mining at {hashrate} total hashrate."
  filter:
    session: true          # only show to users with active sessions
    country: "US"          # only show to users from specific countries
    hosting: true          # only show to hosting IPs
    proxy: true            # only show to proxy IPs
    lt_version: "2.0.0"    # only show to clients older than this version
    gt_hashrate: 1000      # only show when total hashrate exceeds this

The {hashrate} placeholder is replaced with the current total hashrate.

Wallet Fund Management

There are two supported ways to manage funds in the faucet wallet.

You can keep all funds in the faucet wallet directly, which works fine for most setups. Or you can use the more complex but more secure automatic wallet refill function.

Automatic Wallet Refill / Vault Contract

The automatic wallet refill feature allows the faucet to operate with a low-balance hot wallet that is refilled automatically when it runs low. A smart contract protects the majority of funds and limits the amount available to the hot wallet.

The idea is that even if the faucet server is compromised, the attacker can only steal funds on the hot wallet. The funds in the vault contract remain safe because the hot wallet is limited to a specific withdrawal amount per interval.

A custom Vault Contract can be used to secure the funds. The contract limits the withdrawable balance to a specific amount per interval, configurable by the owner via the setAllowance(address addr, uint256 amount, uint256 interval) function.

The refill contract configuration also supports an overflow mechanism: when the hot wallet balance exceeds overflowBalance, excess funds are deposited back into the vault contract via the depositFn.

Function argument placeholders:

  • {walletAddr} - the faucet wallet address
  • {amount} - the withdrawal/deposit amount
  • {token} - the token contract address (for ERC20 mode)
ethRefillContract:
  contract: "0xA5058fbcD09425e922E3E9e78D569aB84EdB88Eb" # vault contract address
  abi: '[...]' # vault contract ABI (JSON string)

  allowanceFn: "getAllowance"         # function to check withdrawable amount
  allowanceFnArgs: ["{walletAddr}"]   # arguments for allowance call
  withdrawFn: "withdraw"             # function to call for withdrawals
  withdrawFnArgs: ["{amount}"]       # arguments for withdraw call
  depositFn: "deposit"               # function to call for overflow deposits (optional)
  depositFnArgs: []                  # arguments for deposit call

  withdrawGasLimit: 300000            # gas limit for withdraw/deposit transactions
  checkContractBalance: true          # check contract balance before withdrawing
  contractDustBalance: 1000000000000000000  # keep 1 ETH in the contract

  triggerBalance: 1100000000000000000000    # refill when hot wallet balance falls below 1100 ETH
  overflowBalance: 2000000000000000000000   # deposit back to vault when balance exceeds 2000 ETH (optional)
  cooldownTime: 5430                        # minimum time between refill calls: 1.5h + 30sec
  requestAmount: 125000000000000000000      # amount per withdrawal: 125 ETH