ADL-003
Credit an AdsLab postback once, and handle reversals
Dedupe on the transaction id, answer 200 to everything, and treat a reversal as the full chain — user credit and referral commission both.
Ad networks retry anything that is not a 200, and they retry quickly. An endpoint that returns 500 on an unknown user, or 404 for a bad path, turns one event into a storm that also inflates your own counts.
So the rule is: always answer 200 with a short plain-text body, and decide inside whether the event does anything. A duplicate transaction id does nothing and still answers 200, which is how the retries stop.
Reversals are the half most sites skip. A chargeback has to take back what the completion paid, including any referral commission it generated, or your ledger drifts upward with every fraudulent conversion.
// adslab_postback.php
require __DIR__ . '/includes/bootstrap.php';
header('Content-Type: text/plain');
$pb = adslab_postback(setting('adslab_security_hash'), setting('adslab_task_pid'));
if ($pb === null) {
echo 'bad'; // 200 even so: they retry anything else
exit;
}
// UNIQUE KEY (txid, action) — a retry inserts nothing and credits nothing.
try {
q('INSERT INTO adslab_events (txid, action, uid, reward, type, created_at) VALUES (?, ?, ?, ?, ?, NOW())',
[$pb['txid'], $pb['action'], $pb['uid'], $pb['reward'], $pb['type']]);
} catch (PDOException $e) {
echo 'ok'; // already seen
exit;
}
$userId = (int) $pb['uid'];
$units = (int) round(((float) $pb['reward']) * 100000000); // integers past this point
switch ($pb['action']) {
case 'credit':
credit_user($userId, $units, 'adslab:' . $pb['type'], $pb['txid']);
pay_referral_commission($userId, $units, $pb['txid']);
break;
case 'reverse':
// The whole chain, not just the user's share.
reverse_user_credit($userId, $units, $pb['txid']);
reverse_referral_commission($userId, $pb['txid']);
break;
case 'reject':
// Nothing was ever credited; record it so support can answer "why not paid".
break;
}
echo 'ok';
Using it
Put the unique key on (txid, action), not txid alone — a credit and a later reversal share the transaction id and both need to land.
Show rejects to the user as "not accepted by the advertiser" somewhere. Silence on a task they completed is the single most common support message on an offerwall site.
Hold large completions for a period before they can be withdrawn. Reversals arrive days later, and money already paid out cannot be reversed.
What bites people
Never return a non-200 to signal your own failure. Log it and answer ok, or you will be rate limited by your own error handler.
A reversal on a user whose balance has already gone leaves a negative. Decide in advance whether that becomes a debt or is written off.
Round the decimal reward to integer units once, at the edge. Keeping it as a float through the ledger is how cents go missing.
This one touches real money. Point it at a throwaway wallet and watch a full cycle before you trust it with a live balance.