turnley.dev

PHP and MySQL code for faucet operators

Tools & Calculators

ADS-006

Handle an offerwall postback without paying twice

Signature check, unique constraint, credit inside a transaction. Networks retry postbacks, so the same completion will arrive more than once.

Offerwall networks retry. If your handler returns anything they do not recognise as success — a timeout, a 500, an empty body — they send the same completion again, sometimes for days. Without a uniqueness constraint, each retry credits the user again.

Three things make the handler safe. The signature proves the callback came from the network and not from someone who read your URL out of a browser tab. The unique index on the network's transaction id makes a duplicate credit impossible at the database level rather than in your logic. And doing the insert and the balance update in one transaction means you cannot credit without recording why.

Return a plain success string when the transaction id is already known. That is the network's cue to stop retrying.

PHP
<?php
// postback.php?user=123&amount=0.0100&txid=abc&sig=...
$userId = (int) ($_GET['user'] ?? 0);
$amount = (float) ($_GET['amount'] ?? 0);
$txid   = trim((string) ($_GET['txid'] ?? ''));
$sig    = trim((string) ($_GET['sig'] ?? ''));

$expected = hash_hmac('sha256', $userId . '|' . $amount . '|' . $txid, OFFERWALL_SECRET);
if (!hash_equals($expected, $sig)) {
    http_response_code(403);
    exit('bad signature');
}
if ($userId <= 0 || $amount <= 0 || $txid === '') {
    http_response_code(400);
    exit('bad request');
}

$units = (int) round($amount * 100000000);   // integers only past this point

$pdo->beginTransaction();
try {
    // UNIQUE KEY (network, txid) — this is what makes retries harmless.
    $ins = $pdo->prepare(
        'INSERT INTO offer_completions (network, txid, user_id, units, created_at)
         VALUES (?, ?, ?, ?, NOW())'
    );
    $ins->execute(['walla', $txid, $userId, $units]);

    $pdo->prepare('UPDATE users SET balance = balance + ? WHERE id = ?')
        ->execute([$units, $userId]);

    $pdo->commit();
    echo 'ok';
} catch (PDOException $e) {
    $pdo->rollBack();
    if ($e->errorInfo[1] === 1062) {     // duplicate: already credited
        echo 'ok';                        // tell them to stop retrying
        exit;
    }
    http_response_code(500);              // real failure: let them retry
    echo 'error';
}

Using it

Store the raw query string alongside the completion. When a user says they did an offer and were not paid, that row is the entire investigation.

Handle reversals. Networks claw back fraudulent completions, and a handler that only knows how to add will leave you carrying the loss.

What bites people

Never trust the amount without the signature covering it. An unsigned amount parameter is a withdrawal form.

A 500 means retry, so do not return 500 for a completion you have deliberately rejected. Return success and log it, or the network will hammer you.

Round to integer units once, at the edge. Floats flowing into a balance column will not reconcile.

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 Ads, Traffic and Offerwalls