turnley.dev

PHP and MySQL code for faucet operators

Tools & Calculators

FP-008

Call the FaucetPay v2 API with a scoped key

Bearer auth, JSON POST, and an envelope that is documented one way and returned another on the endpoint you care about most.

v2 replaces the single faucet api_key with scoped keys and Bearer auth. Every call is a JSON POST to https://faucetpay.io/api/v2, which makes the transport uniform — the v1 habit of form-encoding a few fields is gone.

The one thing to handle defensively is the envelope. The docs describe {status, message, data}, but the /send example returns {success, ...}. Accept both or your payout path breaks on the response shape rather than on anything real.

Returning a result array rather than throwing is worth the small extra typing. A low balance, a daily cap and a rate limit are all normal faucet flow, not exceptional conditions, and code that branches on them reads better than code that catches them.

PHP
function fp_call(string $apiKey, string $endpoint, array $body = []): array
{
    if ($apiKey === '') {
        return ['ok' => false, 'http' => 0, 'status' => 0, 'message' => 'no api key', 'data' => null, 'raw' => ''];
    }

    $json = json_encode((object) $body, JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION);

    $ch = curl_init('https://faucetpay.io/api/v2/' . ltrim($endpoint, '/'));
    curl_setopt_array($ch, [
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => $json,
        CURLOPT_HTTPHEADER     => [
            'Authorization: Bearer ' . $apiKey,
            'Content-Type: application/json',
            'Accept: application/json',
        ],
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 25,
        CURLOPT_CONNECTTIMEOUT => 8,
        CURLOPT_SSL_VERIFYPEER => true,
        CURLOPT_SSL_VERIFYHOST => 2,
    ]);
    $raw  = curl_exec($ch);
    $http = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $err  = curl_error($ch);
    curl_close($ch);

    if ($raw === false) {
        // http 0 means the request may or may not have arrived. Never final.
        return ['ok' => false, 'http' => 0, 'status' => 0, 'message' => 'network: ' . $err, 'data' => null, 'raw' => ''];
    }

    $b = json_decode((string) $raw, true);
    if (!is_array($b)) {
        return ['ok' => false, 'http' => $http, 'status' => $http, 'message' => 'invalid json', 'data' => null, 'raw' => (string) $raw];
    }

    // Documented as {status, message, data}; /send answers {success, ...}.
    $status = isset($b['status']) && is_numeric($b['status']) ? (int) $b['status'] : $http;
    $ok = ($b['success'] ?? null) === true || ($status === 200 && $http < 400);

    return [
        'ok'      => $ok,
        'http'    => $http,
        'status'  => $status,
        'message' => (string) ($b['message'] ?? ''),
        'data'    => $b['data'] ?? $b,
        'raw'     => (string) $raw,
    ];
}

Using it

Branch on http before ok. An http of 0 is unknown and must be retried; a 4xx is an answer and usually final.

Keep the key in config, never in the database and never in the repository. A send-scoped key is a wallet.

Start with a read-only call such as balances to prove the key and the transport before anything moves money.

What bites people

The docs do not give methods for every endpoint. JSON POST works across them; do not assume GET for the read calls.

Rate-limit and anti-fraud settings endpoints take structures the docs do not specify. Read the current settings first and send the same shape back.

Do not log the raw body of a send response at debug level forever. It carries payout detail you probably do not want sitting in a shared log.

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