turnley.dev

PHP and MySQL code for faucet operators

Tools & Calculators

FP-002

Verify a FaucetPay merchant payment before crediting it

Verifying a merchant callback server-side, including the endpoint shape that quietly 404s. Still current; see the v2 entry for the retry schedule and order-first ordering.

When a deposit comes back from FaucetPay's merchant checkout, your callback URL receives a token. That callback is a browser redirect. Anyone can hit it with a made-up token, so crediting on the callback alone means handing out free balance.

The token has to be verified server to server. The catch is the endpoint shape: the token is a path segment, not a query parameter. Calling it with ?token= returns a 404 HTML page, which json_decode turns into null, which reads exactly like "not paid yet" — so deposits sit pending forever and nothing in your logs says why.

Log the raw response body the first time you build this. It is the difference between a five minute fix and a five day one.

The Merchant API is not versioned alongside the v2 API, so this check is still the current one. What has been added since is the retry behaviour: FaucetPay retries immediately, then at 5, 15, 30, 60, 120 and 240 minutes, and the token is single use — so check your own order before verifying. That is covered in its own entry.

PHP
function faucetpay_verify_payment(string $token): ?array
{
    // Token is a PATH segment. Not ?token=... — that 404s.
    $url = 'https://faucetpay.io/merchant/get-payment/' . rawurlencode($token);

    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 20,
        CURLOPT_SSL_VERIFYPEER => true,
        CURLOPT_SSL_VERIFYHOST => 2,
    ]);
    $body = curl_exec($ch);
    $http = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    // Keep this. When a deposit sticks on pending, this row tells you why.
    ipn_log($token, $http, (string) $body);

    if ($body === false || $http !== 200) {
        return null;
    }
    $data = json_decode((string) $body, true);
    if (!is_array($data) || ($data['valid'] ?? false) !== true) {
        return null;
    }

    return $data; // amount, currency, custom, merchant_username, ...
}

Using it

In your callback handler: read the token, call this, and only credit if it returns an array. Credit against your own custom field, wrapped in a uniqueness check so a replayed callback cannot credit twice.

The token is single use on FaucetPay's side, so a 404 during development does not burn it. If you fix the endpoint later, the stored token from a stuck deposit is usually still valid and can be re-verified by hand.

What bites people

Never trust the amount in the callback query string. Use the amount from the verify response.

Never credit without an idempotency key. Use your own custom value as a unique column so a double callback is a no-op rather than a double credit.

Log the raw body, not just the decoded array. A 404 HTML page and a legitimate "not valid" both decode to nothing useful.

This one touches real money. Point it at a throwaway wallet and watch a full cycle before you trust it with a live balance.

Also in FaucetPay