How to create a referral system in MU Online
Implement a referral system on your MU Online server's website, with unique links, Cash rewards, and protection against self-referral fraud.
A referral system turns your own players into the server's best acquisition channel. The logic is simple and powerful: each player gets a unique link; when a friend signs up through that link and actually starts playing, both earn a reward. The result is cheap, organic growth, driven by people who a
A referral system turns your own players into the server's best acquisition channel. The logic is simple and powerful: each player gets a unique link; when a friend signs up through that link and actually starts playing, both earn a reward. The result is cheap, organic growth, driven by people who already trust the server. But there's a chasm between the idea and an implementation that isn't immediately exploited by people trying to farm rewards with fake accounts. This tutorial shows how to build a referral system on your MU Online server's website that is both appealing to the honest player and resistant to fraud, with trackable links, release gated on real engagement, and automatic Cash crediting. We'll use PHP and SQL Server as the example, since they're the standard on most private servers, but the logic applies to any stack. Table and column names vary by version and emulator, so treat every query as a skeleton to adapt.
Prerequisites
The referral system lives on the website, so you need a working site integrated with the server's database. If you haven't integrated site and game yet, start with the guide on how to create a MU Online server before continuing.
- The server's website running on PHP 7.4+ with access to the database.
- SQL Server with the MU account table (often
MEMB_INFO) accessible. - An account/login system already working on the site.
- A column or table where the player's Cash (cash shop currency) is stored — the name varies by version (e.g.,
CashShopData,WCoinC,Credits). - Basic knowledge of PHP, SQL, and login sessions.
How the referral flow works
Before the code, lock in the flow. Each stage exists to close a fraud loophole:
- The logged-in player opens the "Refer a friend" page and sees their unique referral code and a ready-to-copy link.
- A new visitor clicks the link; the code is stored in their session/cookie.
- On signup, the new account is linked to the referrer with a pending status.
- The system monitors the referred player's progress. When they reach a real goal (e.g., level 150 or 3 days played), the status becomes released.
- Only then are the rewards credited to both sides, and the link is marked paid so it never pays twice.
The critical point is step 4: a reward released at signup is an invitation to fraud. A reward released by real engagement filters out 90% of the abuse.
Step 1 — Data structure
Create a dedicated table for referrals. It keeps an auditable history and separates the referral logic from the game tables:
CREATE TABLE Referrals (
Id INT IDENTITY(1,1) PRIMARY KEY,
ReferrerAcc VARCHAR(20) NOT NULL, -- account that referred
ReferredAcc VARCHAR(20) NOT NULL, -- referred account
ReferredIP VARCHAR(45) NULL,
Status VARCHAR(10) NOT NULL DEFAULT 'pendente', -- pending/released/paid
CreatedAt DATETIME NOT NULL DEFAULT GETDATE(),
RewardedAt DATETIME NULL,
CONSTRAINT UQ_Referred UNIQUE (ReferredAcc) -- each account is referred only once
);
Generate the referral code from the account name itself (with a short hash) or store a random code per account. For simplicity, many servers use the username itself as the code.
Step 2 — Capture the code on arrival
When someone visits the site with ?ref=CODE, store that code before it gets lost. A long-lived cookie covers visitors who don't sign up in the same session:
<?php
// include at the top of the site's index.php
if (isset($_GET['ref']) && !isset($_COOKIE['ref_code'])) {
$ref = preg_replace('/[^a-zA-Z0-9_]/', '', $_GET['ref']);
if ($ref !== '') {
// 30-day attribution window
setcookie('ref_code', $ref, time() + 60*60*24*30, '/');
}
}
Note the sanitization: we accept only valid account characters, which already rules out injection attempts through the URL.
Step 3 — Link on signup
In the signup processing, after successfully creating the account, record the link — but only after the anti-fraud checks:
<?php
function registerReferral(PDO $db, string $newAccount, string $newIP): void {
if (empty($_COOKIE['ref_code'])) return;
$ref = preg_replace('/[^a-zA-Z0-9_]/', '', $_COOKIE['ref_code']);
if ($ref === '' || strcasecmp($ref, $newAccount) === 0) return; // self-referral
// The referrer must actually exist
$stmt = $db->prepare("SELECT memb___id FROM MEMB_INFO WHERE memb___id = ?");
$stmt->execute([$ref]);
if (!$stmt->fetch()) return;
// Block the same IP between referrer and referred
$ipInd = $db->prepare("
SELECT r.ReferredIP FROM Referrals r
WHERE r.ReferrerAcc = ? AND r.ReferredIP = ?");
$ipInd->execute([$ref, $newIP]);
if ($ipInd->fetch()) return; // already referred someone from this same IP
$ins = $db->prepare("
INSERT INTO Referrals (ReferrerAcc, ReferredAcc, ReferredIP)
VALUES (?, ?, ?)");
$ins->execute([$ref, $newAccount, $newIP]);
}
Step 4 — Release the reward on engagement
This is the anti-fraud core. A scheduled script (cron or Windows Task Scheduler) runs periodically, checks which referred players reached the goal, and releases the rewards. The example goal is level 150 on any character of the account — adjust the Character table name and the level column to your version:
<?php
// cron/release_referrals.php — run every 10 min
require 'db.php';
$pending = $db->query("
SELECT Id, ReferrerAcc, ReferredAcc
FROM Referrals WHERE Status = 'pendente'")->fetchAll(PDO::FETCH_ASSOC);
foreach ($pending as $r) {
// Did the referred player reach the goal?
$chk = $db->prepare("
SELECT TOP 1 cLevel FROM Character
WHERE AccountID = ? AND cLevel >= 150");
$chk->execute([$r['ReferredAcc']]);
if ($chk->fetch()) {
creditCash($db, $r['ReferrerAcc'], 200); // referrer
creditCash($db, $r['ReferredAcc'], 100); // referred
$up = $db->prepare("
UPDATE Referrals SET Status = 'pago', RewardedAt = GETDATE()
WHERE Id = ?");
$up->execute([$r['Id']]);
}
}
function creditCash(PDO $db, string $account, int $amount): void {
// The Cash column/table name VARIES by version
$up = $db->prepare("
UPDATE MEMB_INFO SET Credits = Credits + ? WHERE memb___id = ?");
$up->execute([$amount, $account]);
}
Note that the reward is credited only once, because the status becomes pago in the same logical transaction.
Step 5 — Player page
Give the logged-in player a page that shows the link and the history. That's what drives sharing:
<?php
$account = $_SESSION['username'];
$link = "https://yoursite.com/?ref=" . urlencode($account);
$stats = $db->prepare("
SELECT
SUM(CASE WHEN Status = 'pago' THEN 1 ELSE 0 END) AS paid,
COUNT(*) AS total
FROM Referrals WHERE ReferrerAcc = ?");
$stats->execute([$account]);
$row = $stats->fetch(PDO::FETCH_ASSOC);
?>
<h2>Refer and earn</h2>
<input type="text" value="<?= htmlspecialchars($link) ?>" readonly onclick="this.select()">
<p>Friends who have already played: <?= (int)$row['paid'] ?> of <?= (int)$row['total'] ?> referred</p>
Anti-fraud protection layers
No single check solves it. Combine several and the cost of committing fraud outweighs the reward:
| Layer | What it checks | Limitation |
|---|---|---|
| Engagement goal | Minimum level/time of the referred player | Filters out throwaway accounts |
| Self-referral block | Same account name | Stops the most obvious case |
| IP comparison | Same IP between the accounts | Dynamic IPs and VPNs get around it |
| HWID/hardware | Same machine (when available) | Depends on client support |
| Unique email | Same email on both accounts | Only if email is mandatory |
| Per-referrer limit | Maximum referrals per day | Reduces mass farming |
Common errors and fixes
| Symptom | Likely cause | Solution |
|---|---|---|
| Rewards handed out en masse to ghost accounts | Release at signup | Gate it on the engagement goal in the cron |
| Same player earns multiple times for the same referred | No "paid" status | Use the unique constraint and mark as paid on crediting |
| Cash isn't credited | Wrong column name | Check the schema; the Cash column varies by version |
The ?ref= code disappears before signup | No attribution cookie | Store the code in a 30-day cookie |
| Self-referral slips through | Only compares the name | Combine IP, HWID, and email in addition to the name |
| Cron doesn't run on Windows | No scheduled task | Create a task in Task Scheduler to call the PHP via CLI |
Best practices
Always use prepared statements — the example above never concatenates user input into SQL. Log every reward release to audit disputes. Set rewards modest enough not to break the server economy, but attractive enough to motivate sharing; Cash is preferable to items because it's trivial to credit and reverse. And communicate the rules clearly on the referral page: goals, deadlines, and what counts as fraud that can lead to a ban.
Launch checklist
Referralstable created with a unique-referred constraint- Capture of the
?ref=code via a 30-day cookie - Link created on signup only after anti-fraud checks
- Self-referral block by name, IP, and email
- Cron/task releasing rewards on the engagement goal
- Cash crediting tested with the server's real schema
- Player page with link and history working
- All queries using prepared statements
- Rules published and rewards balanced with the economy
- Release logs enabled for auditing
Frequently asked questions
When should I release the referral reward?
Never at signup. Release it only when the referred player reaches a real engagement goal, such as a minimum level or a minimum time played, to avoid fake accounts created just for the reward.
How do I stop someone from referring themselves?
Compare IP, hardware/HWID when available, and email between referrer and referred, and block rewards when they match. No single check is perfect; combine several.
Where do I store the referral code?
In a column on the accounts table or in a dedicated referrals table. The ideal is a dedicated table that records referrer, referred, status, and date, keeping an auditable history.
Can I give item rewards instead of Cash?
Yes, but Cash (the cash shop currency) is simpler and safer to credit via the website. Delivering items requires manipulating the inventory in the database, which is riskier and varies a lot by version.
Does the referral system work on any MU version?
The website logic is independent of the version. What changes is the name of the account tables and the way you credit Cash, which vary by emulator; adapt the queries to your schema.