turnley.dev

PHP and MySQL code for faucet operators

Tools & Calculators

ADS-007

A PTC offer runner that pays only on a real view

Open the target, run the timer, verify server-side, credit once. The four steps and the checks that belong to each.

A paid-to-click flow is four steps, and each one has an obvious cheat. Opening the offer can be skipped. The timer can be shortened. The credit call can be replayed. The whole offer can be run again immediately.

Signed tokens handle the first three: the token is issued when the offer opens, carries the issue time, and is single use. A unique key on offer and visitor and day handles the fourth.

None of this requires the visitor to have an account, which matters if you are running offers for anonymous traffic.

PHP
// Step 1: open. Issue the token, record nothing yet.
function open_offer(PDO $pdo, int $offerId, string $visitorKey): array
{
    $offer = $pdo->prepare('SELECT * FROM offers WHERE id = ? AND active = 1');
    $offer->execute([$offerId]);
    $o = $offer->fetch(PDO::FETCH_ASSOC);
    if (!$o) {
        throw new RuntimeException('offer unavailable');
    }
    return ['url' => $o['url'], 'seconds' => (int) $o['seconds'],
            'token' => issue_dwell_token($offerId, $visitorKey)];
}

// Step 2 and 3: the browser waits, then posts the token back here.
function complete_offer(PDO $pdo, string $token, string $visitorKey, int $userId): bool
{
    $offerId = check_dwell_token($token, $visitorKey, 0);   // real minimum read below
    if ($offerId === null) {
        return false;
    }

    $st = $pdo->prepare('SELECT * FROM offers WHERE id = ? AND active = 1');
    $st->execute([$offerId]);
    $offer = $st->fetch(PDO::FETCH_ASSOC);
    if (!$offer) {
        return false;
    }
    // Duration comes from the database, never from the client or the token.
    if (check_dwell_token($token, $visitorKey, (int) $offer['seconds'] - 2) === null) {
        return false;
    }

    $pdo->beginTransaction();
    try {
        // UNIQUE (offer_id, visitor_key, day): one payout per offer per visitor per day.
        $pdo->prepare('INSERT INTO offer_views (offer_id, visitor_key, user_id, day, created_at)
                       VALUES (?, ?, ?, CURDATE(), NOW())')
            ->execute([$offerId, $visitorKey, $userId]);

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

        $pdo->prepare('UPDATE offers SET views_left = views_left - 1 WHERE id = ? AND views_left > 0')
            ->execute([$offerId]);

        $pdo->commit();
        return true;
    } catch (PDOException $e) {
        $pdo->rollBack();
        return $e->errorInfo[1] === 1062 ? false : throw $e;   // duplicate = already done
    }
}

Using it

Read the required duration from the offer row at completion time. Anything the client sends is a suggestion.

Decrement the remaining views in the same transaction as the credit, or an offer with a budget will overspend under concurrency.

What bites people

Two seconds of grace on the timer prevents honest failures at the boundary. Without it your support mail fills up with people who waited and got nothing.

Do not require an account to view but forget to require one to earn. The credit path needs a user; the view path does not.

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