FP-002
Verify a FaucetPay merchant payment before crediting it
The callback from FaucetPay's merchant checkout is not proof of payment. Here is the server-side verify call, including the endpoint shape that quietly 404s if you get it wrong.
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.
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.