Building a Testnet Faucet Bot for Ethereum using Go
This article offers tutorials on how to build a Testnet Faucet Bot for Ethereum using Golang. Introduction Recently Web3 technology has developed rapidly. This new technology is considered the next evolution of the internet. Blockchain as the foundation of Web3, enables decentralized, transparent, and secure systems that eliminate the need for intermediaries. Many developers are trying to implement blockchain in various aspects of human life with the aim of increasing data security and integrity. One of the most prominent platforms driving this innovation is Ethereum. Ethereum allows users to create a wide variety of programs within their network. Do you know that to develop on Ethereum, we need some test tokens? These tokens are essential for testing smart contracts, interacting with decentralized applications (dApps), and experimenting with transactions without spending real cryptocurrency. Then, to gain these tokens we need a faucet bot to automatically dripping tokens for the developers. What is Faucet Bot A faucet bot is an automated bot designed to provide small amounts of cryptocurrency or tokens to users, allowing users to conduct various experiments on technology. This bot is important in blockchain application development so that we no longer need to manually send tokens to developers. In this article, we will discover how to build a Testnet Faucet Bot for Ethereum using Golang, with a user interface provided through Discord. The bot that we build is simple and only consists of token drip operations, which serve as the core engine of any faucet bot. Additional functionality such as conditional dripping based on user criteria, rate limiting, or integration with external services is considered part of further development. Prerequisites To follow along, you should have: Basic understanding of Blockchain Golang (v1.23+) installed Blockchain Network: A local or test network like Ethereum’s Rinkeby, Goerli, or a private Ethereum network for testing and development of the faucet functionality. In this article I will use hardhat to provide Ethereum development environment (testnet) in our local machine. For more information about how to setup and run hardhat you could refer to the quick start documentation. A Go project already set up If you don't have a project yet, create one with: mkdir go-faucet go mod init go-faucet Installing Dependency Before we begin, we need to install a few necessary Go modules that will help us interact with the Ethereum network and handle message from Discord application. To install them, run the following commands in your terminal: go get github.com/bwmarrin/discordgo go get github.com/ethereum/go-ethereum go get github.com/joho/godotenv Preparing Project Structure After installing the dependencies, the next step is to prepare the project structure. A well-organized structure will make it easier for us to manage the code. mkdir pkg mkdir cmd mkdir internal mkdir config pkg/: Contains reusable code and libraries that can be imported by other projects or services. cmd/: Holds the entry points (main.go). internal/: Stores project-internal code that cannot be accessed or imported by external projects. config/: Stores configuration files and logic to manage settings and behavior of the application. After executing those commands, our project structure will looks like this. go-faucet │ go.mod │ go.sum ├───cmd ├───config ├───internal └───pkg Creating Configuration File In the config directory, create a new file named config.go by running the following command: touch config/config.go The code in config.go will manage all operations related to application configuration, including getting env value and providing credentials for connecting to external systems. // ./config/config.go package config import "os" type Config struct { BotToken string WalletPrivateKey string RpcUrl string } func GetConfig() *Config { return &Config{ BotToken: os.Getenv("BOT_TOKEN"), WalletPrivateKey: os.Getenv("WALLET_PRIVATE_KEY"), RpcUrl: os.Getenv("RPC_URL"), } } There are three environment variables that need to be provided for the faucet application. These values should be declared in the .env file as follows: // .env BOT_TOKEN= # A string for the bot's authentication token. WALLET_PRIVATE_KEY= # A string for the wallet's private key (used as the token source). RPC_URL= # A string for the URL of the RPC (Remote Procedure Call) endpoint. Details for each variable: BOT_TOKEN: This value can be obtained from Discord Developer Portal. WALLET_PRIVATE_KEY and RPC_URL: These values can be obtained from your blockchain network (e.g., Hardhat). If you are using hardhat, choose one of the twenty accounts provided. Creating Transfer Service Now it's time to build the core en

This article offers tutorials on how to build a Testnet Faucet Bot for Ethereum using Golang.
Introduction
Recently Web3 technology has developed rapidly. This new technology is considered the next evolution of the internet. Blockchain as the foundation of Web3, enables decentralized, transparent, and secure systems that eliminate the need for intermediaries. Many developers are trying to implement blockchain in various aspects of human life with the aim of increasing data security and integrity.
One of the most prominent platforms driving this innovation is Ethereum. Ethereum allows users to create a wide variety of programs within their network. Do you know that to develop on Ethereum, we need some test tokens? These tokens are essential for testing smart contracts, interacting with decentralized applications (dApps), and experimenting with transactions without spending real cryptocurrency. Then, to gain these tokens we need a faucet bot to automatically dripping tokens for the developers.
What is Faucet Bot
A faucet bot is an automated bot designed to provide small amounts of cryptocurrency or tokens to users, allowing users to conduct various experiments on technology. This bot is important in blockchain application development so that we no longer need to manually send tokens to developers.
In this article, we will discover how to build a Testnet Faucet Bot for Ethereum using Golang, with a user interface provided through Discord. The bot that we build is simple and only consists of token drip operations, which serve as the core engine of any faucet bot.
Additional functionality such as conditional dripping based on user criteria, rate limiting, or integration with external services is considered part of further development.
Prerequisites
To follow along, you should have:
- Basic understanding of Blockchain
- Golang (v1.23+) installed
- Blockchain Network: A local or test network like Ethereum’s Rinkeby, Goerli, or a private Ethereum network for testing and development of the faucet functionality. In this article I will use hardhat to provide Ethereum development environment (testnet) in our local machine. For more information about how to setup and run hardhat you could refer to the quick start documentation.
- A Go project already set up
If you don't have a project yet, create one with:
mkdir go-faucet
go mod init go-faucet
Installing Dependency
Before we begin, we need to install a few necessary Go modules that will help us interact with the Ethereum network and handle message from Discord application. To install them, run the following commands in your terminal:
go get github.com/bwmarrin/discordgo
go get github.com/ethereum/go-ethereum
go get github.com/joho/godotenv
Preparing Project Structure
After installing the dependencies, the next step is to prepare the project structure. A well-organized structure will make it easier for us to manage the code.
mkdir pkg
mkdir cmd
mkdir internal
mkdir config
-
pkg/
: Contains reusable code and libraries that can be imported by other projects or services. -
cmd/
: Holds the entry points (main.go
). -
internal/
: Stores project-internal code that cannot be accessed or imported by external projects. -
config/
: Stores configuration files and logic to manage settings and behavior of the application.
After executing those commands, our project structure will looks like this.
go-faucet
│ go.mod
│ go.sum
├───cmd
├───config
├───internal
└───pkg
Creating Configuration File
In the config
directory, create a new file named config.go
by running the following command:
touch config/config.go
The code in config.go
will manage all operations related to application configuration, including getting env value and providing credentials for connecting to external systems.
// ./config/config.go
package config
import "os"
type Config struct {
BotToken string
WalletPrivateKey string
RpcUrl string
}
func GetConfig() *Config {
return &Config{
BotToken: os.Getenv("BOT_TOKEN"),
WalletPrivateKey: os.Getenv("WALLET_PRIVATE_KEY"),
RpcUrl: os.Getenv("RPC_URL"),
}
}
There are three environment variables that need to be provided for the faucet application. These values should be declared in the .env
file as follows:
// .env
BOT_TOKEN= # A string for the bot's authentication token.
WALLET_PRIVATE_KEY= # A string for the wallet's private key (used as the token source).
RPC_URL= # A string for the URL of the RPC (Remote Procedure Call) endpoint.
Details for each variable:
- BOT_TOKEN: This value can be obtained from Discord Developer Portal.
- WALLET_PRIVATE_KEY and RPC_URL: These values can be obtained from your blockchain network (e.g., Hardhat). If you are using hardhat, choose one of the twenty accounts provided.
Creating Transfer Service
Now it's time to build the core engine of our Faucet Bot. Each time a user triggers the bot command, the bot will send 0.01 ETH to the user's wallet. Before processing the transaction, we need to ensure that the bot's wallet has sufficient balance. If the balance is adequate and the transaction is valid, the bot will proceed to transfer the tokens to the user's address.
To begin, create a new directory named transfer
inside the internal
directory. Then, within the transfer
directory, create a new file named transfer.service.go
.
mkdir internal/transfer
touch internal/transfer/transfer.service.go
After that, we will write the transfer logic in transfer.service.go
.
// internal/transfer/transfer.service.go
package transfer
import (
"context"
"fmt"
"log"
"math/big"
"go-faucet/config"
"go-faucet/pkg/ether"
)
type TransferService struct {
config *config.Config
ether *ether.Ether
}
func NewTransferService(config *config.Config, ether *ether.Ether) *TransferService {
return &TransferService{
config: config,
ether: ether,
}
}
func (t *TransferService) TransferService(ctx context.Context, publicKey string) string {
// validate balance
balance, err := t.ether.GetBalance(ctx, t.config.WalletPrivateKey)
if err != nil {
log.Println("error: Getting faucet account balance\n", err)
return "Error sending token"
}
if balance.Cmp(big.NewInt(0)) == -1 { // balance < 0
return "Faucet account out of balance, Please contact the customer support"
}
value := big.NewInt(10000000000000000) // in wei (0.01 eth)
transactionHash, err := t.ether.Transfer(ctx, publicKey, t.config.WalletPrivateKey, value)
if err != nil {
log.Println("error: Sending token", err)
return "Error sending token"
}
return fmt.Sprintf("Success sending token to `%s` with transaction hash: `%s`", publicKey, transactionHash)
}
At this point, we still haven't written any utility functions to interact with the Ethereum network. To make things easier, I've written some of the essential functions needed and wrapped them in a struct
.
Create a new directory inside pkg
called ether
.
mkdir pkg/ether
touch pkg/ether/ether.go
touch pkg/ether/utils.go
Then place the following code in that directory.
// pkg/ether/ether.go
package ether
import (
"context"
"math/big"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
)
type Ether struct {
client *ethclient.Client
}
func NewEther(client *ethclient.Client) *Ether {
return &Ether{
client: client,
}
}
func (e *Ether) GetBalance(ctx context.Context, walletPrivateKey string) (*big.Int, error) {
privateKeyECDSA, err := crypto.HexToECDSA(walletPrivateKey)
if err != nil {
return nil, err
}
account, err := GetAccountFromECDSAPrivateKey(privateKeyECDSA)
if err != nil {
return nil, err
}
balance, err := e.client.BalanceAt(ctx, *account, nil)
if err != nil {
return nil, err
}
return balance, nil
}
func (e *Ether) GetPendingNonce(ctx context.Context, account common.Address) (uint64, error) {
nonce, err := e.client.PendingNonceAt(ctx, account)
if err != nil {
return 0, err
}
return nonce, nil
}
func (e *Ether) Transfer(ctx context.Context, toPublicKey string, walletPrivateKey string, value *big.Int) (string, error) {
privateKeyECDSA, err := crypto.HexToECDSA(walletPrivateKey)
if err != nil {
return "", err
}
account, err := GetAccountFromECDSAPrivateKey(privateKeyECDSA)
if err != nil {
return "", err
}
nonce, err := e.GetPendingNonce(ctx, *account)
if err != nil {
return "", err
}
gasLimit := uint64(21000)
gasPrice, err := e.client.SuggestGasPrice(ctx)
if err != nil {
return "", err
}
toAccount := common.HexToAddress(toPublicKey)
var data []byte
tx := types.NewTransaction(nonce, toAccount, value, gasLimit, gasPrice, data)
chainID, err := e.client.NetworkID(ctx)
if err != nil {
return "", err
}
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(chainID), privateKeyECDSA)
if err != nil {
return "", err
}
err = e.client.SendTransaction(ctx, signedTx)
if err != nil {
return "", err
}
return signedTx.Hash().Hex(), nil
}
// pkg/ether/utils.go
package ether
import (
"crypto/ecdsa"
"errors"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
)
func GetAccountFromECDSAPrivateKey(privateKeyECDSA *ecdsa.PrivateKey) (*common.Address, error) {
publicKey := privateKeyECDSA.Public()
publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey)
if !ok {
return nil, errors.New("cannot assert type: publicKey is not of type *ecdsa.PublicKey")
}
account := crypto.PubkeyToAddress(*publicKeyECDSA)
return &account, nil
}
Creating Transfer Controller
After completing the logic of the transfer service, next we will create a controller for this transfer feature. In this case, the controller will filter messages sent by the user and get the appropriate arguments for the public key of the transfer destination.
Create a new file named transfer.controller.go
inside the internal/transfer
directory.
touch internal/transfer/transfer.controller.go
// internal/transfer/transfer.controller.go
package transfer
import (
"context"
"log"
"strings"
"github.com/bwmarrin/discordgo"
)
type TransferHandler struct {
transferService *TransferService
}
func NewTransferHandler(transferService *TransferService) *TransferHandler {
return &TransferHandler{
transferService: transferService,
}
}
func (t *TransferHandler) Handler(s *discordgo.Session, m *discordgo.MessageCreate) {
if strings.HasPrefix(m.Content, "!transfer") {
log.Printf("info: Received transfer operation from %s - (Channel: %s)\n", m.Author.Username, m.ChannelID)
var content string = ""
args := strings.Split(m.Content, " ")
if len(args) < 2 {
content = "Please provide your wallet public key"
} else {
content = t.transferService.TransferService(context.Background(), args[1])
}
_, err := s.ChannelMessageSend(m.ChannelID, content)
if err != nil {
log.Println("error: sending discord message\n", err)
}
return
}
}
Creating the Main Application File
We have completed almost the entire code of this system. The last step is we need to create a main function to be able to run the application and connect all components in the system, starting from loading the configuration file, connecting to the Ethereum network, controllers, to application services.
Inside the cmd
directory, create a new file named main.go
touch cmd/main.go
Then write the following codes inside that file.
// cmd/main.go
package main
import (
"log"
"os"
"os/signal"
"syscall"
"go-faucet/config"
"go-faucet/internal/transfer"
"go-faucet/pkg/ether"
"github.com/bwmarrin/discordgo"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/joho/godotenv"
)
func init() {
err := godotenv.Load()
if err != nil {
log.Fatal("Error reading .env file")
}
}
func main() {
config := config.GetConfig()
// Initialize eth client
client, err := ethclient.Dial(config.RpcUrl)
if err != nil {
log.Fatalln("error: Error connecting to eth client", err)
return
}
defer client.Close()
log.Println("info: Success connecting to eth client")
// Opening discord session
dg, err := discordgo.New("Bot " + config.BotToken)
if err != nil {
log.Fatalln("error: Error creating discord session:\n", err)
return
}
// Registering handler
ether := ether.NewEther(client)
transferService := transfer.NewTransferService(config, ether)
dg.AddHandler(transfer.NewTransferHandler(transferService).Handler)
log.Println("info: Success registering bot handler")
dg.Identify.Intents = discordgo.IntentsGuildMessages
err = dg.Open()
if err != nil {
log.Fatalln("error: Error opening discord session:\n", err)
return
}
defer dg.Close()
log.Println("info: Success opening discord session")
// Wait until CTRL-C or other term signal is received
log.Println("info: Go Faucet BOT is running...")
sc := make(chan os.Signal, 1)
signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt)
<-sc
}
Final Code Structure
To wrap things up, here’s the complete structure of the project as it stands after implementing all the discussed components. This directory layout should give you a clear overview of how everything fits together:
go-faucet
│ .env
│ go.mod
│ go.sum
├───cmd
│ main.go
├───config
│ config.go
├───internal
│ └───transfer
│ transfer.controller.go
│ transfer.service.go
└───pkg
└───ether
ether.go
utils.go
Running and Testing the Application
Run the Faucet Bot by executing the following command:
go run cmd/main.go
Output:
Once the application is running, invite the bot to your Discord server. To receive tokens, send a message in any channel the bot has access to using the format:
!transfer
Output:
The bot will automatically send 0.01 ETH to the specified wallet address.
Conclusion
We have successfully built our own faucet bot using Go. Next you can develop this system further such as implementing conditional dripping based on user criteria or trying to integrate it with other interfaces such as websites and various social media platforms.
You can check out the full source code on GitHub:
alitdarmaputra
/
go-faucet
Core of the faucet engine for dripping tokens written in go
⚡️ Quick start
Prerequisites
The project utilizes the following tools:
-
Go
1.23+
: A statically typed, compiled programming language designed for simplicity, concurrency, and performance. Go is used for building the bot and handling interactions with the blockchain. -
Go-Ethereum
1.14.12
: A Go implementation of the Ethereum protocol, enabling the bot to interact with the Ethereum blockchain for cryptocurrency distribution. - Blockchain Network: A local or test network like Ethereum’s Rinkeby, Goerli, or a private Ethereum network for testing and development of the faucet functionality.
-
Make
4.3+
(optional): A build automation tool used for managing tasks. The Makefile provides predefined commands for setting up, building, and running the project, simplifying the development workflow.
Executing Program
Clone the repository
git clone git@github.com:alitdarmaputra/go-faucet.git
cd go-faucet
Install module
go mod
…
Feel free to clone the repo, explore the implementation, or fork it for your own use. If you have any questions don't be hesitate to ask in the comments section. Thank you for reading this article.