turnley.dev

PHP and MySQL code for faucet operators

Tools & Calculators

FP-012

FaucetPay merchant callbacks and the retry schedule

The token is single use and they retry at fixed intervals, so check your own order first and answer 200 before verifying again.

Merchant callbacks arrive more than once by design. FaucetPay retries immediately, then at 5, 15, 30, 60, 120 and 240 minutes until you answer 200.

The verification token is single use. So the second callback for an order you already credited cannot be verified again — the token is spent — and a handler that verifies first and checks the order second will log a verification failure for a payment that was perfectly good.

Order of operations fixes it. Look up your own order by the custom value first; if it is already paid, answer 200 and stop. Only verify when the order is still open.

PHP
// ipn.php
$custom = (string) ($_POST['custom'] ?? '');

// 1. Our own record first. A spent token cannot prove anything, and this
//    callback may be the fourth copy of one we handled an hour ago.
if ($custom !== '' && order_is_paid($custom)) {
    http_response_code(200);
    echo 'ok';
    exit;
}

// 2. Only now verify, against FaucetPay's record — never the POSTed figures.
//    Note the token is a PATH segment: /merchant/get-payment/{token}
$token = (string) ($_POST['token'] ?? '');
$record = fp_merchant_verify($token);

if ($record === null) {
    http_response_code(400);
    echo 'unverified';
    exit;
}

// 3. Check it is the payment we expected, from their record.
$order = find_order($custom);
if ($order === null
    || (string) $record['merchant_username'] !== setting('fp_merchant_username')
    || (string) $record['currency1'] !== $order['currency']
    || bccomp((string) $record['amount1'], (string) $order['amount'], 8) < 0) {
    http_response_code(400);
    echo 'mismatch';
    exit;
}

credit_order($custom, (string) ($record['transaction_id'] ?? ''));
http_response_code(200);
echo 'ok';

Using it

Create the order row before you render the checkout form, with your own reference in custom. Without it the callback has nothing to match.

Compare the amount with bccomp, and accept an overpayment rather than rejecting it. An underpayment is a policy decision; make it explicitly.

Keep a re-verify button in admin for a stuck order. If a bug burned the callbacks but never consumed the token, that button recovers the payment.

What bites people

Verifying before checking your own order is the bug this entry is about. It produces alarming logs about valid payments.

Never credit from the POSTed amount or currency. Those are inputs, not evidence.

Answer 200 for anything you have decided not to act on, or the retries continue for four hours.

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