Sending a cryptocurrency reward manually is easy. Building software that can send thousands of tiny rewards without paying users twice, leaking an API key, losing track of failed payments, or draining your balance because somebody discovered an exploitable endpoint is a much more interesting problem.FaucetPay is particularly useful for this type of application because it is designed around small cryptocurrency payouts. Its current API v2 provides scoped API keys, REST endpoints for balances and payouts, automatic payout sending, idempotency protection, per-key payout caps, and signed webhooks.In this tutorial, we will build the core of a real automatic payout system using:Node.jsExpressPostgreSQLFaucetPay API v2a background payout workerHMAC-verified webhooksThe architecture can be used for much more than a classic faucet. It can power:reward websites,games,loyalty systems,micro-task platforms,promotional campaigns,educational applications,referral systems,pay-per-action services.The important part is that your application decides who deserves a reward, while FaucetPay handles the actual micropayment.What We Are Going to BuildOur application will follow this flow:User completes an action | v Your backend verifies eligibility | v Reward is stored in the database | v Payout worker picks up the reward | v FaucetPay API /send | v User receives crypto | v Webhook confirms success or failure The important architectural decision is this: The browser never sends money directly.The client can ask your backend to claim a reward, but only trusted backend code decides whether a payout should happen.This is not just a design preference. FaucetPay explicitly recommends keeping API credentials server-side and warns against exposing keys in browsers or public repositories.1. Create a FaucetPay API KeyFaucetPay supports the older v1 API, but v2 is the more appropriate choice for new automation because it uses scoped, revocable keys.The available scopes include:readsendmanageadminFor our payout worker, we only need:send If we later build an administration dashboard that reads balances and transaction history, I would create a second API key containing:read Do not give the payout process manage or admin access unless it genuinely needs those capabilities.This is basic least privilege.FaucetPay also allows a scoped sending key to have an IP whitelist and a daily USD payout cap, which gives us additional protection if the key is ever compromised.Store the key as an environment variable:FAUCETPAY_SEND_KEY=fpk_your_secret_key_here FAUCETPAY_WEBHOOK_SECRET=your_webhook_secret_here DATABASE_URL=postgresql://user:password@localhost/rewards And make sure .env is excluded from Git:.env .env.* node_modules/ Never write this:const FAUCETPAY_KEY = "fpk_my_real_secret_key"; inside code that will end up in GitHub.2. Create the ProjectCreate a directory:mkdir faucetpay-auto-payout cd faucetpay-auto-payout Initialize Node:npm init -y Install the required packages:npm install express pg dotenv Our project can start with this structure:faucetpay-auto-payout/ │ ├── src/ │ ├── server.js │ ├── faucetpay.js │ ├── payouts.js │ ├── worker.js │ └── webhook.js │ ├── .env ├── .gitignore └── package.json We will use modern JavaScript modules, so add this to package.json:{ "type": "module" } 3. Understand FaucetPay's Payout FormatThe API v2 base URL is:https://faucetpay.io/api/v2 Authentication uses a Bearer token:Authorization: Bearer YOUR_SCOPED_KEY The payout endpoint is:POST /send A payout request contains:{ "idempotency_key": "claim-12345", "to": "user@example.com", "amount": 100, "currency": "BTC", "ip_address": "203.0.113.4" } The amount is an integer expressed in the cryptocurrency's smallest unit, not a floating-point coin value. FaucetPay's own documentation specifically calls incorrect amount units a common integration error.For example, do not casually assume:amount: 0.000001 means what you think it means.Your application should work internally with integer units.That also avoids floating-point mistakes.4. Build a Small FaucetPay ClientCreate:src/faucetpay.js const API_URL = "https://faucetpay.io/api/v2"; export async function sendFaucetPayPayout({ payoutId, recipient, amount, currency, ipAddress }) { const response = await fetch(`${API_URL}/send`, { method: "POST", headers: { "Authorization": `Bearer ${process.env.FAUCETPAY_SEND_KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ idempotency_key: payoutId, to: recipient, amount, currency, ip_address: ipAddress }) }); let body; try { body = await response.json(); } catch { throw new Error( `FaucetPay returned invalid JSON. HTTP ${response.status}` ); } if (!response.ok) { const error = new Error( body.message || `FaucetPay request failed: HTTP ${response.status}` ); error.status = response.status; error.response = body; throw error; } return body; } Notice what this function does not do.It does not:decide whether the user earned money,calculate the reward,accept reward amounts from the browser,retry forever,store results.It only communicates with FaucetPay.That separation makes the system easier to test and much harder to abuse.5. Never Let the Browser Choose the RewardThis endpoint would be dangerous:app.post("/payout", async (req, res) => { await sendFaucetPayPayout({ recipient: req.body.wallet, amount: req.body.amount, currency: req.body.currency }); }); Why?Because the user controls:amount currency recipient An attacker could simply send:{ "amount": 100000000, "currency": "BTC" } Instead, the request should look more like:{ "taskId": "daily-login" } Your server determines the reward.For example:const REWARDS = { "daily-login": { currency: "DOGE", amount: 100 }, "watch-tutorial": { currency: "LTC", amount: 50 } }; The browser identifies what happened.The backend decides what it is worth.6. Store Every Reward Before Paying ItOne of the most important rules in payment software is:Never make the external API call your only record of the transaction.Create a database table:CREATE TABLE payouts ( id UUID PRIMARY KEY, user_id BIGINT NOT NULL, recipient TEXT NOT NULL, currency VARCHAR(10) NOT NULL, amount BIGINT NOT NULL, status VARCHAR(30) NOT NULL, ip_address VARCHAR(45), faucetpay_payout_id TEXT, created_at TIMESTAMP NOT NULL DEFAULT NOW(), paid_at TIMESTAMP, error_message TEXT ); Possible states:pending processing paid failed When the user earns a reward, first create:pending Only then should a worker attempt payment.7. Prevent Duplicate Claims at the Database LevelImagine a user double-clicks the Claim button.Or their mobile connection is slow and the browser retries.Or your frontend accidentally sends the same request twice.JavaScript checks alone are not enough.Create a uniqueness rule.For example:CREATE TABLE reward_claims ( id UUID PRIMARY KEY, user_id BIGINT NOT NULL, reward_type VARCHAR(100) NOT NULL, reward_period DATE NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT NOW(), UNIQUE(user_id, reward_type, reward_period) ); Now this combination:user 248 daily-login 2026-08-13 can exist only once.Even if ten requests arrive simultaneously, the database becomes the final authority.8. FaucetPay Gives Us a Second Layer of Duplicate ProtectionThe v2 /send endpoint requires:idempotency_key FaucetPay documents this as mandatory and states that retrying the same logical payout with the same key will not double-pay the recipient.This is extremely useful.Suppose our database payout UUID is:935d46fc-793c-4d96-a18d-f501ab1637a4 Use exactly that as the FaucetPay idempotency key.idempotency_key: "935d46fc-793c-4d96-a18d-f501ab1637a4" Now we have two defenses:Database uniqueness + FaucetPay idempotency This is much safer than trying to detect duplicates after the payment has already happened.9. Build the Payout WorkerThe HTTP request handling the user's claim should not have to wait for every external payment operation.Instead:User claims reward | v Database: pending | v HTTP response: accepted | v background worker | v FaucetPay A simplified worker might look like this:import { sendFaucetPayPayout } from "./faucetpay.js"; export async function processPayout(db, payout) { try { await db.query( ` UPDATE payouts SET status = 'processing' WHERE id = $1 AND status = 'pending' `, [payout.id] ); const result = await sendFaucetPayPayout({ payoutId: payout.id, recipient: payout.recipient, amount: Number(payout.amount), currency: payout.currency, ipAddress: payout.ip_address }); await db.query( ` UPDATE payouts SET status = 'paid', faucetpay_payout_id = $2, paid_at = NOW() WHERE id = $1 `, [ payout.id, result.data?.payout_id ?? null ] ); } catch (error) { await db.query( ` UPDATE payouts SET status = 'failed', error_message = $2 WHERE id = $1 `, [ payout.id, error.message ] ); throw error; } } Production code should use stronger locking when multiple worker processes are running, but the architecture remains the same.10. Do Not Retry Every ErrorThis is another place where payout systems go wrong.Suppose FaucetPay responds:429 Too Many Requests Retrying later can make sense.But suppose you receive:400 Bad Request because the recipient is invalid.Retrying it 50 times will accomplish nothing.FaucetPay v2 uses standard HTTP responses including:200 400 401 403 409 429 with a descriptive response message.A reasonable strategy might be:function shouldRetry(status) { return ( status === 429 || status >= 500 ); } Then use exponential backoff:attempt 1: immediately attempt 2: 5 seconds attempt 3: 30 seconds attempt 4: 2 minutes attempt 5: 10 minutes But always keep the same idempotency key.Generating a new one for every retry defeats the entire protection mechanism.11. Include the User's IP AddressFaucetPay recommends including the recipient's IP address with /send because it can contribute to cross-faucet anti-abuse detection.That means this:ip_address: userIp is worth including.In Express:const ip = req.headers["x-forwarded-for"] ?.split(",")[0] ?.trim() || req.socket.remoteAddress; There is one caveat.Only trust X-Forwarded-For when your server is actually behind a trusted reverse proxy such as Nginx or Cloudflare and your proxy configuration is correct.Otherwise, an attacker may simply invent the header.12. Add a Daily Safety LimitImagine a bug gives every user 10,000 times the intended reward. Without a hard limit, your automated payout system might obediently continue paying until the account is empty. This is why infrastructure-level limits matter.FaucetPay allows a v2 sending key to have a daily USD payout cap. If that cap is exceeded, the payout is rejected rather than sent.I would still implement an application-level limit as well.For example:const MAX_DAILY_PAYOUT_USD = 25; Then monitor:normal average: $4/day warning level: $10/day hard stop: $25/day If your system suddenly jumps from $4 to $24 in thirty minutes, something probably deserves investigation.13. Monitor the Faucet BalanceA payout system should know when it is running out of money.FaucetPay v2 provides endpoints including:/balance /balances for reading faucet balances.I recommend using a separate read scoped key for monitoring.Example:export async function getBalances() { const response = await fetch( "https://faucetpay.io/api/v2/balances", { method: "POST", headers: { Authorization: `Bearer ${process.env.FAUCETPAY_READ_KEY}`, "Content-Type": "application/json" }, body: "{}" } ); if (!response.ok) { throw new Error( `Balance request failed: ${response.status}` ); } return response.json(); } Your monitoring service can then trigger an alert when a balance drops below a threshold.For example:DOGE balance { if (req.body.event === "payout.sent") { markAsPaid(); } }); Anybody on the internet could POST:{ "event": "payout.sent" } to that URL.Instead:receive raw body | v read X-FaucetPay-Signature | v calculate your HMAC | v constant-time comparison | +---- invalid ---> HTTP 401 | v parse JSON | v process event Only after cryptographic verification should the event be trusted.16. Make Webhook Processing Idempotent TooWebhooks can be delivered more than once.That should not break anything.Store the external event ID:CREATE TABLE webhook_events ( event_id TEXT PRIMARY KEY, event_type TEXT NOT NULL, received_at TIMESTAMP NOT NULL DEFAULT NOW() ); Then:INSERT INTO webhook_events ( event_id, event_type ) VALUES ($1, $2) ON CONFLICT DO NOTHING; If the insert affects zero rows:we already processed it Return:200 OK and do nothing else.17. A Better Database State MachineAs the application grows, four payout states may not be enough.I would eventually use:earned queued processing sent confirmed retry_wait failed cancelled For example:earned | v queued | v processing | +------ temporary error ------> retry_wait | | | v |------------------------------ processing | +------ permanent error -------> failed | v sent | v confirmed This gives the administrator a clear picture of what the system is actually doing.18. Separate Reward Logic from Payment LogicThis is perhaps the most valuable architectural lesson in the entire project.Do not combine:How much did the user earn? with:How do we send cryptocurrency? These are separate systems.Your reward engine might say:{ userId: 8712, reward: 150, currency: "DOGE" } The payout engine does not care whether those 150 units came from:completing a quiz,watching an advertisement,finishing a game level,winning a contest,referral commission,daily login.It only needs to know:recipient currency amount payout ID That separation means you can completely change your business logic without rewriting the FaucetPay integration.19. Example: Automatic Reward for Completing a TaskImagine our application gives a small DOGE reward for completing a programming exercise.The user sends:POST /api/tasks/42/claim The server:app.post( "/api/tasks/:id/claim", requireAuthentication, async (req, res) => { const user = req.user; const task = await getTask(req.params.id); const completed = await verifyTaskCompletion( user.id, task.id ); if (!completed) { return res.status(403).json({ error: "Task is not completed." }); } const payout = await createRewardOnce({ userId: user.id, rewardType: `task-${task.id}`, recipient: user.faucetpayRecipient, currency: "DOGE", amount: 100, ipAddress: req.ip }); res.status(202).json({ message: "Reward queued for payment.", payoutId: payout.id }); } ); Notice that there is no FaucetPay request inside this controller.We only queue the payment.The worker sends it independently.That gives us:faster HTTP responses,safer retries,better auditing,easier maintenance.20. What the Frontend Should SeeThe frontend does not need access to FaucetPay credentials.It only needs your own API.Example:const response = await fetch( "/api/tasks/42/claim", { method: "POST" } ); const result = await response.json(); console.log(result); Later:GET /api/payouts/935d46fc... might return:{ "status": "confirmed", "amount": 100, "currency": "DOGE" } This gives users a good experience without exposing the payment infrastructure.21. Add Anti-Abuse Before Going PublicThe moment your application automatically distributes money, somebody will try to automate your application too.At minimum, I would consider:account verification,server-side claim cooldowns,per-user limits,per-IP limits,device/session analysis,CAPTCHA only where needed,duplicate account detection,suspicious behaviour scoring,maximum daily reward per account,manual review thresholds.FaucetPay has its own anti-abuse capabilities and recommends supplying ip_address with payout requests, but your application still needs its own rules.A payout API should be the last line in your reward process, not your fraud detection system.22. The Architecture I Would Use in ProductionA small project can run everything on one server.A more serious implementation could look like: Internet | v Nginx / Cloudflare | v Node.js API / \ / \ v v PostgreSQL Redis | | | v | payout queue | | | v | payout worker | | | v | FaucetPay API | | | v | FaucetPay | | |<-----------+ | v webhook endpoint For a small hobby project this may be excessive.For an application distributing real money continuously, it becomes much more reasonable.23. The Five Mistakes I Would AvoidIf I were reviewing a new FaucetPay integration, these are the first things I would check.Mistake 1: API key in JavaScript sent to the browserNever.Mistake 2: Client chooses payout amountThe server must calculate rewards.Mistake 3: No idempotency keyRetries can become duplicate payments. FaucetPay v2 makes idempotency mandatory specifically for this reason.Mistake 4: Floating-point crypto amounts everywhereUse integer smallest units internally.Mistake 5: No database transaction historyIf your answer to: “Why did user 1837 receive this payment?” is: “I don't know, FaucetPay sent it.” your application is not ready for production.Every payment should have an internal reason and audit trail.24. What FaucetPay Actually SolvesFaucetPay does not build your reward application for you.It solves a much narrower but very useful problem:Your software decides: WHO should be paid WHY they should be paid HOW MUCH they should receive FaucetPay handles: DELIVERING the micropayment That boundary is important.The interesting software engineering still happens on your side:eligibility,queues,databases,fraud prevention,accounting,retries,observability,user experience.And that is precisely why the API is useful.You do not need to implement blockchain payout infrastructure simply to send very small crypto rewards.Final ArchitectureThe finished system should behave like this:User completes something valuable | v Backend verifies the action | v Database creates ONE reward | v Reward enters payout queue | v Worker calls FaucetPay /api/v2/send | v Same payout UUID becomes idempotency_key | v FaucetPay sends the micropayment | v Signed webhook reports result | v Backend verifies HMAC | v Database marks payout confirmed The API request itself is the easy part.Building everything around it so that a payment happens exactly once, for the correct amount, to the correct user, for a legitimate reason is where a real payout system begins.That is the difference between a demo script and software I would actually be comfortable connecting to a funded FaucetPay account.Official Documentation UsedThis tutorial is based on the current FaucetPay API documentation, including the v2 scoped-key API, payout endpoint, idempotency requirements, webhook signing, API scopes, rate limiting and security recommendations.
How to Build Automatic Crypto Payouts with the FaucetPay API
Full Article
Original Source
Read the full article at Hackernoon →KhanList aggregates and links to publicly available news content. We do not host full articles from third-party sources. Always verify important information with original sources.