How to Create an Offerwall Website: Complete Builder's Guide Aug 20, 2026

How to Create an Offerwall Website: Complete Builder's Guide

71 views Aug 20, 2026 0 comments

Building an offerwall website looks simple from the outside: show offers, let users complete them, and add rewards to their balances.

The difficult part starts after the first conversion.

A production-ready offerwall website has to connect user attribution, offer tracking, server-to-server postbacks, wallet accounting, fraud controls, reversals, withdrawals, and provider reporting. One duplicated callback can create an incorrect balance; one unverified request can turn into a direct financial loss.

This guide focuses on the parts that usually need the most engineering attention. It covers the architecture, database design, S2S security, provider integrations, testing, withdrawals, and the decisions that make an offerwall easier to maintain as traffic grows.

For mobile products, the same backend can support Android and iOS applications. See our guide on How to Add an Offerwall SDK to Android & iOS Apps for the mobile SDK side of the architecture.

Table of Contents

  1. 1. Plan the Offerwall Architecture
  2. 2. Build the Data and Reward Layer
  3. 3. Secure Postbacks and Provider Integrations
  4. 4. Test the Conversion and Withdrawal Pipeline
  5. 5. Scale, Monitor, and Maintain the Platform

<a id="architecture"></a>

1. Plan the Offerwall Architecture

What an offerwall website actually does

An offerwall sits between users and one or more offer providers.

The user starts on your platform, clicks an offer, completes the advertiser's requirement, and the provider reports the conversion back to your infrastructure. Your backend then decides whether the event is valid and whether a reward should be added.

The basic flow is:

User
│ Opens an offer
Your Offerwall
│ Tracking / redirect
Offer Network
│ User completes offer
Provider Validation
│ S2S postback
Your Backend
├─ Verify request
├─ Validate user
├─ Check transaction ID
├─ Calculate reward
└─ Write ledger entry
User Wallet

The browser starts the process, but the browser should not be the authority that confirms the reward.

Start with one working conversion path

A common mistake is trying to integrate every offer provider before the first provider has been tested end to end.

For an MVP, build this path first:

Registration
User dashboard
Offerwall
Offer click
Provider conversion
S2S postback
Reward credit
Wallet history

Once that pipeline is reliable, additional networks become adapters rather than entirely new systems.

Choose a stack that makes transactions easy

A Laravel application is a practical choice for this type of project because it gives you routing, database access, queues, caching, authentication, validation, and transactions without having to assemble those pieces yourself.

A typical stack might be:

Frontend
├── Blade / Livewire
or
├── React / Vue

Backend
└── Laravel / PHP

Database
└── MySQL / PostgreSQL

Cache
└── Redis

Queue
└── Redis or another supported driver

Web server
└── Nginx

Transport
└── HTTPS

Laravel's database layer supports transactions for operations that need to succeed or roll back as one unit. Its queue system is useful for background work such as analytics and notifications. See the Laravel database documentation and Laravel queue documentation.

Keep provider-specific logic out of your wallet code

This becomes important as soon as you add the second network.

One provider might call the user identifier sub_id; another might call it uid. One might sign a callback with HMAC-SHA256; another may use a token or a different signature scheme.

Your application should normalize those differences.

A simple internal object can look like:

final class Conversion
{
public function __construct(
public readonly string $provider,
public readonly string $transactionId,
public readonly string $userReference,
public readonly string $offerId,
public readonly float $providerReward,
public readonly string $status,
) {}
}

Then each provider has its own adapter.

Provider A ──┐
Provider B ──┼──> Conversion object ──> Reward engine
Provider C ──┘

That separation saves a lot of cleanup later.

Recommended architecture

┌─────────────────────────────┐
│ User Interface │
│ Offers • Wallet • Withdraw │
└──────────────┬──────────────┘
┌──────────────▼──────────────┐
│ Application API │
│ Auth • Offers • Wallet • UI │
└──────────────┬──────────────┘
┌──────────────▼──────────────┐
│ Offerwall Layer │
│ Providers • Tracking • S2S │
└──────────────┬──────────────┘
┌──────────────▼──────────────┐
│ Trust & Finance │
│ Ledger • Fraud • Withdrawals │
└──────────────┬──────────────┘
┌──────────────▼──────────────┐
│ Infrastructure │
│ DB • Redis • Queues • Logs │
└─────────────────────────────┘

<a id="data-reward"></a>

2. Build the Data and Reward Layer

Do not store the wallet as a single number

A field such as:

users.balance

is useful as a cached balance, but it should not be your complete accounting system.

A better design keeps a wallet ledger.

CREATE TABLE wallet_transactions (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT UNSIGNED NOT NULL,
type VARCHAR(30) NOT NULL,
amount DECIMAL(18,2) NOT NULL,
source VARCHAR(50) NOT NULL,
offer_id VARCHAR(190) NULL,
external_transaction_id VARCHAR(190) NULL,
status VARCHAR(30) NOT NULL DEFAULT 'confirmed',
metadata JSON NULL,
created_at TIMESTAMP NULL,
updated_at TIMESTAMP NULL,

UNIQUE KEY wallet_external_tx_unique (
source,
external_transaction_id
)
);

Now a conversion has a history:

TX-80152
+500 points
Provider: ExampleNetwork
Offer: game_level_10
Status: confirmed

If that conversion is later reversed:

TX-80152-R
-500 points
Reason: provider reversal

The original record stays intact.

Why the transaction ID matters

Offer networks often retry callbacks.

Imagine your server receives:

{
"transaction_id": "TX-1007",
"reward": 250,
"user_id": "8421"
}

Then the same request arrives again because the provider did not receive a successful response quickly enough.

Without idempotency:

TX-1007 → +250
TX-1007 → +250

With a unique transaction key:

TX-1007 → +250
TX-1007 → already processed

This is not an optional optimization. It is one of the basic protections for a reward system.

Use a postback log as well as a reward ledger

Keep a separate table for incoming provider requests.

CREATE TABLE offerwall_postbacks (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
provider VARCHAR(80) NOT NULL,
external_transaction_id VARCHAR(190) NULL,
user_reference VARCHAR(190) NULL,
payload JSON NOT NULL,
signature VARCHAR(500) NULL,
source_ip VARCHAR(64) NULL,
status VARCHAR(30) NOT NULL,
failure_reason VARCHAR(255) NULL,
created_at TIMESTAMP NULL,
updated_at TIMESTAMP NULL
);

This gives your support team something concrete to investigate.

When a user says "the offer completed but my balance did not change," you can trace:

Provider request
Signature validation
User mapping
Transaction lookup
Ledger entry
Balance change

Separate provider reward from platform reward

Do not hard-code the assumption that one provider credit equals one platform point.

For example:

Provider reward: 25 credits
Platform reward: 250 points

Keep the conversion rule configurable:

function convertReward(float $providerReward): int
{
$multiplier = 10;

return (int) round($providerReward * $multiplier);
}

The exact multiplier is a business decision, not a technical constant.

It is also useful to preserve the original provider amount in transaction metadata so revenue and payout reports can be reconciled later.

Keep an immutable history

Do not rewrite old wallet transactions when something changes.

For example, avoid:

"Change the original +500 transaction to +0"

Prefer:

Original conversion +500
Reversal transaction -500

That gives you an audit trail and makes balance disputes much easier to resolve.

<a id="postbacks-security"></a>

3. Secure Postbacks and Provider Integrations

Build the postback endpoint explicitly

For a Laravel application:

use App\Http\Controllers\OfferwallPostbackController;
use Illuminate\Support\Facades\Route;

Route::post(
'/api/postbacks/{provider}',
[OfferwallPostbackController::class, 'handle']
);

Your actual HTTP method and parameters should match the provider specification.

Do not place a normal logged-in-user middleware in front of a webhook endpoint unless the provider actually supports that authentication model.

Validate before crediting anything

A postback should not go directly from:

$request->input('reward')

to:

$user->balance += reward

Validate:

Provider
Transaction ID
User reference
Offer ID
Reward amount
Conversion status
Signature / authentication
Timestamp or replay data when supported

An example controller structure:

public function handle(Request $request, string $provider)
{
$validated = $request->validate([
'user_id' => ['required', 'string'],
'transaction_id' => ['required', 'string'],
'offer_id' => ['nullable', 'string'],
'reward' => ['required', 'numeric'],
'signature' => ['required', 'string'],
]);

// 1. Verify provider-specific authentication.
// 2. Resolve the user.
// 3. Check transaction uniqueness.
// 4. Calculate the platform reward.
// 5. Write the ledger and update the balance atomically.

return response()->json([
'success' => true,
]);
}

Those field names are examples. Never force a provider into a format it does not use.

Verify signatures instead of trusting the request

If a provider specifies HMAC-SHA256, the verification pattern is similar to:

function verifyHmac(
string $payload,
string $receivedSignature,
string $secret
): bool {
$expectedSignature = hash_hmac(
'sha256',
$payload,
$secret
);

return hash_equals(
$expectedSignature,
$receivedSignature
);
}

hash_equals() is preferable to a normal string comparison for secret-derived values.

Not every offer network uses HMAC-SHA256, though. Some use a token, a parameter hash, a shared secret, or another signing method. The provider's current documentation must define the actual verification algorithm.

Do not rely on IP allowlisting alone

IP allowlisting can be an additional layer when the provider publishes supported source addresses, but it should not replace request authentication.

A more useful security stack is:

HTTPS
+
Provider signature / token
+
Input validation
+
Transaction deduplication
+
Optional IP allowlisting
+
Postback logging

That gives you multiple independent checks.

Never put server secrets in the client

Your Android or iOS application can contain credentials that the provider explicitly documents as public.

It should not contain:

Postback secret
Private signing key
Database password
Admin API secret
Payout API private credential

Those belong on your backend.

This is especially important when the same reward system supports both a web offerwall and the Android/iOS SDK integration.

Process rewards inside a database transaction

A race condition can happen when two requests for the same conversion arrive nearly simultaneously.

The reward operation should be atomic.

use App\Models\User;
use App\Models\WalletTransaction;
use Illuminate\Support\Facades\DB;

DB::transaction(function () use (
$userId,
$provider,
$transactionId,
$reward,
$offerId
) {
$existing = WalletTransaction::query()
->where('source', $provider)
->where('external_transaction_id', $transactionId)
->first();

if ($existing) {
return;
}

WalletTransaction::create([
'user_id' => $userId,
'type' => 'credit',
'amount' => $reward,
'source' => $provider,
'offer_id' => $offerId,
'external_transaction_id' => $transactionId,
'status' => 'confirmed',
]);

User::query()
->whereKey($userId)
->increment('balance', $reward);
});

The application-level existence check is useful, but the database uniqueness constraint is still important. The database should enforce the rule even when concurrent requests reach the application at the same time.

Track a stable user identifier

The user ID has to survive the complete attribution journey.

For example:

Your database user ID
Provider tracking parameter
Offer completion
S2S postback
Your database user ID

The provider might call the parameter:

sub_id
sub1
uid
external_id
customer_id
user_id

The name changes; the purpose does not.

Prefer an opaque internal identifier rather than sending a user's email address through tracking URLs when the provider does not require it.

Add provider adapters

A provider interface keeps the wallet system independent:

interface OfferwallProvider
{
public function verify(Request $request): bool;

public function parseConversion(Request $request): Conversion;
}

A provider implementation can then handle its own signature and parameter names:

final class ExampleOfferwallProvider implements OfferwallProvider
{
public function verify(Request $request): bool
{
$payload = $request->getContent();
$signature = $request->header('X-Signature');

return verifyHmac(
$payload,
$signature,
config('offerwalls.example.secret')
);
}

public function parseConversion(Request $request): Conversion
{
return new Conversion(
provider: 'example',
transactionId: (string) $request->input('transaction_id'),
userReference: (string) $request->input('user_id'),
offerId: (string) $request->input('offer_id'),
providerReward: (float) $request->input('reward'),
status: (string) $request->input('status'),
);
}
}

This design is much easier to extend when a third provider arrives.

Use the flowchart as the visual explanation


The diagram above belongs directly beside the S2S explanation. It communicates the trust boundary much faster than another paragraph of terminology.

<a id="testing-operations"></a>

4. Test the Conversion and Withdrawal Pipeline

Test the complete conversion journey

Do not stop after confirming that an offer loads.

Create a test account and follow the entire path:

Test user
Open offer
Complete offer
Provider reports conversion
Postback received
Signature accepted
Transaction created
Wallet updated

If one step fails, log that specific stage instead of treating the conversion as a generic "offerwall issue."

Test duplicate callbacks

Send the same transaction twice:

{
"transaction_id": "TEST-5001",
"user_id": "TEST-10001",
"reward": 250
}

Expected result:

First request:
+250

Second request:
+0

The second request should be recognized as a duplicate.

Test invalid signatures

Change the signature by one character.

The expected behavior is:

Request rejected
No wallet credit
Failure logged

Do not accidentally create a fallback branch that credits the user when verification fails.

Test invalid users

Try a postback containing:

user_id = does-not-exist

The server should not create a new user automatically just because a provider referenced an unknown identifier.

Log the event and follow your chosen provider retry policy.

Test impossible reward values

A reward endpoint should reject data such as:

reward = -500
reward = 0 when not allowed
reward = 999999999
reward = non-numeric text

The exact limits depend on your business model, but the backend should never blindly trust external numerical values.

Test reversals

Simulate:

Conversion
+500

Then:

Reversal
-500

The wallet should return to the correct state while keeping both events visible in history.

Test account and device changes

Also test:

New account
Returning account
Logout / login as another user
Different browser
Different mobile device
Cold start
Network interruption
Provider timeout
Repeated postback
Delayed conversion

Offerwall bugs often appear in lifecycle transitions rather than the happy path.

Design the withdrawal workflow carefully

An early-stage platform should not necessarily pay every withdrawal immediately.

A safer workflow is:

User earns rewards
User requests withdrawal
Risk checks
Review
Approve
Payment
Mark as paid

Typical withdrawal states are:

pending
reviewing
approved
processing
paid
rejected
failed
cancelled

A simple schema:

CREATE TABLE withdrawals (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT UNSIGNED NOT NULL,
amount DECIMAL(18,2) NOT NULL,
method VARCHAR(50) NOT NULL,
destination TEXT NOT NULL,
status VARCHAR(30) NOT NULL DEFAULT 'pending',
admin_note TEXT NULL,
provider_reference VARCHAR(190) NULL,
processed_at TIMESTAMP NULL,
created_at TIMESTAMP NULL,
updated_at TIMESTAMP NULL
);

Delay automation until you understand your fraud profile

Instant payouts can look attractive from a marketing perspective, but offerwall revenue can later be reversed.

That creates an unpleasant situation:

Offer converts
User receives reward
User withdraws cash
Provider reverses conversion
Platform absorbs the loss

A practical approach is to combine a minimum withdrawal threshold with risk rules and manual review for new or unusual accounts.

<a id="scale-maintain"></a>

5. Scale, Monitor, and Maintain the Platform

Add fraud signals gradually

You do not need a machine-learning fraud engine on day one.

Start with signals that are easy to explain:

Very high conversion velocity
Multiple accounts sharing suspicious identifiers
Repeated withdrawals from newly created accounts
Unusual high-value completion patterns
Large reward spikes
High reversal rate
Suspicious proxy / VPN activity

A simple risk score can route accounts into different workflows:

$score = 0;

if ($user->recentHighValueOffers > 10) {
$score += 20;
}

if ($user->isNewAccount()) {
$score += 15;
}

if ($user->proxyDetected) {
$score += 40;
}

if ($user->sharedDeviceCount > 3) {
$score += 25;
}

This does not need to block every flagged user. It can simply determine whether the withdrawal should be reviewed.

Rate-limit sensitive endpoints

Authentication, withdrawals, password reset, and public API endpoints all deserve protection.

Laravel provides route and rate-limiter facilities that can be tuned per endpoint. See the Laravel rate limiting documentation.

For example:

Route::post(
'/withdrawals',
[WithdrawalController::class, 'store']
)->middleware('throttle:10,1');

Use different policies for different routes rather than forcing one global limit onto the whole application.

Move non-critical work into queues

The postback request should not spend several seconds generating reports, sending emails, and recalculating dashboards.

Keep the critical path short:

Receive
Verify
Validate
Deduplicate
Commit reward
Respond

Then queue secondary work:

Analytics
Notifications
Fraud analysis
Reporting
Admin alerts

This also makes it easier to recover from temporary failures.

Monitor the events that actually affect revenue

A useful monitoring dashboard should show:

Postbacks received
Rejected postbacks
Invalid signatures
Duplicate transactions
Conversion latency
Reward credits
Reversals
Pending withdrawals
Paid withdrawals
Failed payouts
Provider-level revenue

A sudden drop in conversions could mean a provider problem.

A sudden increase in rejected signatures could indicate a configuration issue or an attack.

A sudden increase in duplicate callbacks could simply mean the provider is retrying aggressively because your endpoint is responding too slowly.

Those are very different incidents, and good logging lets you tell them apart.

Keep a clear audit trail for admin actions

If an administrator manually changes a user's balance, record:

Admin ID
User ID
Amount
Reason
Timestamp
Related transaction

Avoid silently editing balances from a database console.

A reward platform eventually becomes an accounting system. Once that happens, "who changed this?" becomes an operational question, not a theoretical one.

Decide when to use a ready-made platform

Building everything yourself gives you complete control, but it also means owning:

Authentication
Wallet accounting
Admin panel
Provider adapters
Postbacks
Fraud controls
Withdrawals
Support tools
Monitoring
Maintenance

A ready-made GPT or offerwall platform can shorten the time to launch when its existing architecture matches your requirements.

For example, Hansal Dev's VibeCash currently describes multi-offerwall support, transaction history, withdrawals, referrals, analytics, and centralized postback management. Preads is another offerwall and monetization product listed by Hansal Dev. These are useful reference points when comparing a custom build against a ready-made starting point.

The important comparison is not simply "custom versus script." Check:

Source code ownership
Provider integrations
Postback implementation
Ledger design
Withdrawal controls
Fraud tooling
Customization
Documentation
Ongoing maintenance

Keep documentation next to the integration

Every provider adapter should document:

Tracking URL format
User identifier
Reward mapping
Postback method
Signature algorithm
Expected parameters
Conversion states
Reversal behavior
Retry behavior
Test procedure

This prevents the integration from becoming tribal knowledge that only one developer understands.

Think about the system as a financial pipeline

The most useful mental model is not:

User → Offer → Coins

It is:

Traffic
Attribution
Provider conversion
Authenticated postback
Idempotent transaction
Ledger entry
Available balance
Withdrawal
Settlement

Every stage has a different failure mode.

That is why a good offerwall backend is closer to a small financial-processing system than a simple affiliate landing page.


Frequently Asked Questions

How much does it cost to build an offerwall website?

The cost depends on whether you are using an existing platform or building the entire system yourself. A custom build requires development time for the user system, wallet ledger, provider integrations, fraud controls, admin tools, withdrawals, and maintenance.

Can I build an offerwall website with Laravel?

Yes. Laravel is well suited to the backend because it provides routing, validation, database access, transactions, queues, caching, and authentication. The important part is designing the reward and postback systems correctly rather than relying on the framework to solve business logic automatically.

What is an S2S postback?

An S2S, or server-to-server, postback is a notification sent by the offer provider directly to your backend after a conversion event. Your server can then verify the request and record the reward without trusting the user's browser.

Why shouldn't JavaScript credit the user's balance?

Because JavaScript runs in the user's environment. A malicious user can manipulate client-side requests. The backend should determine whether a conversion is valid and whether the corresponding reward can be added.

How do I prevent duplicate rewards?

Use the provider's unique transaction ID, store it in a wallet ledger, and enforce uniqueness at the database level. Also process the ledger update and balance change inside a database transaction.

Should I integrate multiple offerwall providers?

Usually, yes—but only after the first provider works reliably. Multiple providers can improve offer availability and revenue diversification, but they also increase reconciliation and support work.

Should withdrawals be automatic?

They can be, but automatic payouts are safer after you have enough data to understand fraud and reversal patterns. New or high-risk accounts can be routed through manual review first.

Do I need IP whitelisting?

It can be a useful additional control when the provider supports it, but it should not replace provider-specific authentication such as signatures or secrets.

Can the same backend power an Android or iOS app?

Yes. The web frontend, mobile application, and offerwall can all use the same user identity, reward ledger, postback endpoint, and withdrawal system. The mobile client becomes another interface to the same backend rather than a second rewards engine.

What should I build first?

Build one complete conversion path:

User
→ Offer
→ Conversion
→ Verified Postback
→ Ledger
→ Reward
→ Withdrawal

Once that path is stable, add providers, referrals, leaderboards, advanced fraud scoring, and other features.


Final Takeaway

Creating an offerwall website is less about displaying offers and more about building a trustworthy conversion pipeline.

The core architecture should answer five questions for every reward:

Who earned it?
Which provider reported it?
Which transaction ID identifies it?
Was the request authenticated?
Which ledger entry changed the user's balance?

If your system can answer those questions from stored records, you have a strong foundation.

The practical architecture is straightforward:

Frontend
Offer tracking
Provider conversion
Verified S2S postback
Idempotent reward processing
Wallet ledger
Fraud checks
Withdrawal

The most expensive bugs in this space are rarely visual bugs. They are duplicate credits, incorrect attribution, unverified callbacks, reversals that were never accounted for, and withdrawals made before the underlying revenue was trustworthy.

Build those parts carefully first. Everything else becomes easier to improve later.

  1. For developers working on the mobile side, continue with How to Add an Offerwall SDK to Android & iOS Apps. For broader Hansal Dev resources, visit the blog and projects.
Share
Hansal Dev.
Written by

Hansal Dev.

The team behind Hansal Dev. — building premium digital products and sharing insights on development, design, and technology.

Comments (0)

No comments yet. Be the first to share your thoughts!