Brazil's biggest MU Online portal — since 2003
Tutorial Advanced Website

How to build a web item shop (webshop) for MU Online

A complete guide to building a secure webshop for your MU Online server, integrating credit balances, automatic item delivery to the warehouse, and protection against fraud.

GA Gabriel · Updated on Jun 28, 2026 · ⏱ 16 min read
Quick answer

A webshop (web item shop) is one of the features that most sets an amateur MU Online server apart from a professional project. It lets players trade credits — bought through donations or earned in events — for items, wings, kits, and services directly in the browser, without depending on an online G

A webshop (web item shop) is one of the features that most sets an amateur MU Online server apart from a professional project. It lets players trade credits — bought through donations or earned in events — for items, wings, kits, and services directly in the browser, without depending on an online GM. Built well, it runs 24 hours a day, cuts down the team's manual work, and becomes one of the server's main sources of revenue and retention. Built badly, it turns into a security hole that hands out infinite items, corrupts characters, and allows balance fraud.

This tutorial is advanced because a webshop touches the three most sensitive parts of your ecosystem at the same time: the account/character database, the binary item format of your Season, and the web layer exposed to the internet. We'll build a correct architecture from scratch, with secure delivery, atomic transactions, and protection against the classic frauds. If you don't have the server up yet, start with the base guide on how to create a MU Online server and come back here afterward.

Prerequisites

Before writing a single line of shop code, make sure you have the minimum environment and knowledge:

  • A working MU Online server (GameServer, ConnectServer, and DataServer up).
  • A database accessible from the web application — SQL Server (MuOnline/MuOnline DB) on most Seasons, or MySQL/MariaDB on modern distros.
  • A website already published with PHP 8.1+ (or whatever stack you use) and a working database connection.
  • An account system with working login on the site and a balance/credits field (WCoin, Credits, Cash, or an equivalent column).
  • Knowledge of your Season's item format — index, type, level, options, excellent, ancient, luck, and skill.
  • A test environment (a disposable account and character) to validate deliveries before releasing them to players.

> Security warning: never develop the webshop directly against the production database. A single mistake in an item INSERT can corrupt real characters. Always work with a test dump first.

How a webshop actually delivers items

There's a common misconception: many people think the webshop "talks to the server." In practice, the overwhelming majority of MU web shops do not talk directly to the GameServer — they write the item into the warehouse table in the database, and the GameServer reads that table when the player opens the warehouse.

This has a critical consequence: the item only appears reliably when the character is offline. While the player is logged in, the server keeps a copy of the inventory and warehouse in memory. If you write to the database at that moment, the server overwrites everything when it saves on logout, and the purchased item vanishes — generating a ticket, an angry player, and a refund request.

That's why the correct architecture is asynchronous:

  1. The player buys on the web; the balance is debited and an order is recorded.
  2. The delivery goes into a queue (table webshop_delivery_queue).
  3. A process checks whether the character is offline.
  4. When offline, the item is written to the warehouse inside a transaction and the order is marked as delivered.
Delivery modelHow it worksProsCons
Direct write to warehouse (offline)Shop inserts the item into the warehouse tableSimple, no source changesOnly works when the player is logged out
Queue + workerOrder is queued and processed by a serviceSecure, auditable, scalableRequires a scheduled process running
Real-time socketWeb sends a command to the GameServer via custom sourceInstant delivery while onlineRequires editing and recompiling the source
In-game coupon/redeemWeb generates a code, player redeems via NPC/commandZero direct database writesFriction for the player

For 90% of servers, the queue + worker model is the right one. That's what we'll build.

Step 1 — Model the shop tables

Create tables that belong to the webshop, separate from the game tables. Never reuse the MU tables to control the shop. Example in SQL Server (adjust the types for MySQL if that's your case):

-- Shop product catalog
CREATE TABLE WebshopProducts (
    ProductId      INT IDENTITY(1,1) PRIMARY KEY,
    Name           NVARCHAR(120)  NOT NULL,
    Category       NVARCHAR(40)   NOT NULL,
    PriceCredits   INT            NOT NULL,
    ItemHex        VARCHAR(64)    NOT NULL,  -- item hex for the Season
    DurabilityQty  INT            NOT NULL DEFAULT 1,
    Active         BIT            NOT NULL DEFAULT 1,
    CreatedAt      DATETIME       NOT NULL DEFAULT GETDATE()
);

-- Orders (auditing and idempotency)
CREATE TABLE WebshopOrders (
    OrderId        BIGINT IDENTITY(1,1) PRIMARY KEY,
    AccountId      VARCHAR(20)  NOT NULL,
    CharName       VARCHAR(20)  NOT NULL,
    ProductId      INT          NOT NULL,
    PriceCredits   INT          NOT NULL,
    IdemToken      CHAR(36)     NOT NULL UNIQUE, -- prevents duplicate purchase
    Status         VARCHAR(20)  NOT NULL DEFAULT 'PAID', -- PAID/QUEUED/DELIVERED/FAILED
    IpAddress      VARCHAR(45)  NULL,
    CreatedAt      DATETIME     NOT NULL DEFAULT GETDATE()
);

-- Delivery queue
CREATE TABLE WebshopDeliveryQueue (
    QueueId    BIGINT IDENTITY(1,1) PRIMARY KEY,
    OrderId    BIGINT     NOT NULL,
    CharName   VARCHAR(20) NOT NULL,
    ItemHex    VARCHAR(64) NOT NULL,
    Attempts   INT        NOT NULL DEFAULT 0,
    Delivered  BIT        NOT NULL DEFAULT 0,
    CreatedAt  DATETIME   NOT NULL DEFAULT GETDATE()
);

The IdemToken column is the heart of the duplicate protection: each purchase attempt generates a UUID; the UNIQUE index guarantees the same attempt never becomes two orders, even if the player double-clicks or the connection drops and they resend.

Step 2 — Understand your Season's item format

This is the point that breaks webshops the most. Each item in MU is represented by a binary sequence with fields at fixed offsets. The format varies by version — below is an ILLUSTRATIVE example based on the classic 32-byte layout (Season 6 style); confirm the offsets in your distro's documentation before using it in production.

Field (example)Approx. bytesMeaning
Index/Section0-1Which item (e.g., 0x0007 = sword X)
Level1Refine level +0 to +15
Options (dur)2Additional option / durability
Excellent flags1Bitmask of the excellent options
Ancient/Set1Ancient item and set bonus
Luck/SkillbitsLuck and skill flags
Serial4-8Unique item serial

A builder in PHP that assembles the hex from readable parameters makes registering products far less error-prone:

<?php
// SIMPLIFIED example — the REAL offsets vary by version/Season.
function buildItemHex(array $it): string {
    $index   = $it['index']   ?? 0;   // item code
    $level   = $it['level']   ?? 0;   // 0..15
    $skill   = !empty($it['skill'])  ? 1 : 0;
    $luck    = !empty($it['luck'])   ? 1 : 0;
    $option  = $it['option']  ?? 0;   // 0..3 (option +4/+8/...)
    $exc     = $it['exc']     ?? 0;   // excellent bitmask (0..63)

    // Level byte: usually level << 3 combined with the skill bit
    $lvlByte = (($level & 0x0F) << 3) | ($skill << 7);
    // Option byte: option in the low bits + luck
    $optByte = ($option & 0x03) | ($luck ? 0x04 : 0x00);

    $bytes = [
        $index & 0xFF,
        $lvlByte & 0xFF,
        $optByte & 0xFF,
        $exc & 0xFF,
    ];
    return strtoupper(bin2hex(pack('C*', ...$bytes)));
}

echo buildItemHex(['index'=>7,'level'=>11,'exc'=>0x3F,'luck'=>true,'skill'=>true]);

> Important: the code above is illustrative. Don't copy the offsets blindly — take the correct layout from your Season (the distro's documentation or the source's own ItemManager/ItemConvert). A wrong offset produces an invalid item that freezes the player's warehouse.

Step 3 — The purchase flow with an atomic transaction

The purchase must be atomic: debiting the balance and recording the order have to happen together or not at all. Never debit credit in one command and insert the order in another without a transaction — a failure in between leaves the player with no credit and no item.

<?php
function comprarItem(PDO $pdo, string $accountId, string $charName, int $productId): array {
    $idem = bin2hex(random_bytes(16)); // idempotency token
    $idem = substr(preg_replace('/(.{8})(.{4})(.{4})(.{4})(.{12})/','$1-$2-$3-$4-$5', $idem),0,36);

    $pdo->beginTransaction();
    try {
        // 1) Lock the product row and validate
        $prod = $pdo->prepare("SELECT PriceCredits, ItemHex, Active FROM WebshopProducts WHERE ProductId = ?");
        $prod->execute([$productId]);
        $p = $prod->fetch(PDO::FETCH_ASSOC);
        if (!$p || !$p['Active']) throw new RuntimeException('Product unavailable');

        // 2) Debit the balance ONLY if there is enough credit (check in the WHERE clause)
        $upd = $pdo->prepare(
          "UPDATE MEMB_CREDITS SET Credits = Credits - ?
           WHERE AccountId = ? AND Credits >= ?");
        $upd->execute([$p['PriceCredits'], $accountId, $p['PriceCredits']]);
        if ($upd->rowCount() === 0) throw new RuntimeException('Insufficient balance');

        // 3) Record the order (UNIQUE(IdemToken) prevents duplicates)
        $ins = $pdo->prepare(
          "INSERT INTO WebshopOrders (AccountId, CharName, ProductId, PriceCredits, IdemToken, Status, IpAddress)
           VALUES (?,?,?,?,?, 'QUEUED', ?)");
        $ins->execute([$accountId,$charName,$productId,$p['PriceCredits'],$idem,$_SERVER['REMOTE_ADDR'] ?? null]);
        $orderId = (int)$pdo->lastInsertId();

        // 4) Queue the delivery
        $q = $pdo->prepare(
          "INSERT INTO WebshopDeliveryQueue (OrderId, CharName, ItemHex) VALUES (?,?,?)");
        $q->execute([$orderId, $charName, $p['ItemHex']]);

        $pdo->commit();
        return ['ok'=>true, 'orderId'=>$orderId];
    } catch (\Throwable $e) {
        $pdo->rollBack();
        return ['ok'=>false, 'error'=>$e->getMessage()];
    }
}

Notice two fundamental security details:

  • The debit is done with WHERE ... AND Credits >= ?. That way, even with two simultaneous requests (a race condition), the database guarantees that only one succeeds in debiting when the balance is tight — the second one returns rowCount() === 0.
  • Every query uses prepared statements. Never concatenate $accountId or $productId directly into the SQL.

Step 4 — The delivery worker (with an offline check)

The worker is a script run periodically (Task Scheduler on Windows, cron on Linux) that processes the queue. It only delivers if the character is offline.

<?php
// worker_entrega.php — run every 1 minute
require 'db.php';

$pendentes = $pdo->query(
  "SELECT TOP 50 QueueId, OrderId, CharName, ItemHex
   FROM WebshopDeliveryQueue WHERE Delivered = 0 AND Attempts < 5")->fetchAll(PDO::FETCH_ASSOC);

foreach ($pendentes as $row) {
    // 1) Is it online? (the MEMB_STAT/ConnectStat table varies by version)
    $st = $pdo->prepare("SELECT ConnectStat FROM MEMB_STAT ms
        JOIN Character c ON c.AccountID = ms.memb___id WHERE c.Name = ?");
    $st->execute([$row['CharName']]);
    $online = (int)($st->fetchColumn() ?: 0);
    if ($online === 1) { continue; } // leave it in the queue until logout

    $pdo->beginTransaction();
    try {
        // 2) Write the item to a free warehouse slot
        //    The warehouse table and offsets vary by version.
        $ok = inserirItemNoBau($pdo, $row['CharName'], $row['ItemHex']);
        if (!$ok) throw new RuntimeException('Warehouse full or invalid slot');

        // 3) Mark as delivered
        $pdo->prepare("UPDATE WebshopDeliveryQueue SET Delivered=1 WHERE QueueId=?")
            ->execute([$row['QueueId']]);
        $pdo->prepare("UPDATE WebshopOrders SET Status='DELIVERED' WHERE OrderId=?")
            ->execute([$row['OrderId']]);
        $pdo->commit();
    } catch (\Throwable $e) {
        $pdo->rollBack();
        $pdo->prepare("UPDATE WebshopDeliveryQueue SET Attempts=Attempts+1 WHERE QueueId=?")
            ->execute([$row['QueueId']]);
    }
}

The inserirItemNoBau function finds an empty slot in the character's warehouse and writes the item blob at the correct position. Since the warehouse layout (the warehouse table, the Items column, the size of each slot) varies by version, this part needs to be adapted to your Season. The principle, though, is universal: find free space, write the hex at the slot's offset, and don't overflow the capacity.

Step 5 — Front-end and player experience

On the front-end, display the catalog by category, the current balance, and a purchase button with confirmation. One security point many people forget: disable the button after the click to prevent resubmission, and validate everything again on the server — the front-end is never the source of truth.

// Block double-clicks on the purchase button
const btn = document.querySelector('#comprar');
btn.addEventListener('click', async (e) => {
  btn.disabled = true;
  btn.textContent = 'Processing...';
  try {
    const r = await fetch('/loja/comprar', {
      method: 'POST',
      headers: {'Content-Type':'application/json','X-CSRF-Token': window.csrf},
      body: JSON.stringify({ productId: btn.dataset.pid, char: window.charName })
    });
    const data = await r.json();
    alert(data.ok ? 'Item queued! Log out to receive it in the warehouse.' : 'Error: ' + data.error);
  } finally {
    btn.disabled = false;
    btn.textContent = 'Buy';
  }
});

Always tell the player clearly: "Log the character out to receive the item in the warehouse". That single sentence cuts most of the "I bought it and it didn't arrive" tickets.

Step 6 — Pricing and balancing

The technical part is half the work; the other half is what you sell. Selling raw power (full excellent items, +15 sets) turns the server into pay-to-win and drives the free base away. Healthy strategies:

  • Cosmetics and convenience: name change, class change, stat reset, visual wings, transformations.
  • Moderate progression items: starter kits, jewel bundles, temporary XP buffs.
  • Services: extra warehouse slot, guild expansion, VIP rental.

Set the price in credits based on how much play time that item represents. An item that takes 10 hours of farming can't cost the equivalent of $1 in credits, or the farming loses its meaning.

Common errors and fixes

SymptomLikely causeFix
Purchased item doesn't appear in the warehouseCharacter was online during deliveryConfirm the offline check in the worker; deliver only with ConnectStat=0
Corrupted warehouse / freezes when openingItem hex with the wrong offset for the SeasonRebuild the hex with the correct table for your version in a test environment
Player bought twice with 1 clickMissing idempotencyImplement the UNIQUE IdemToken and disable the button on the front
Balance went negativeDebit without a check in the WHEREUse UPDATE ... WHERE Credits >= price and check rowCount
Duplicate item after a network failureRetry without a tokenAlways resend the same IdemToken from the original attempt
Slow shop / freezes under loadQueries without indexes and without transactionsIndex CharName/Status and wrap the purchase in a transaction
Price changed by the clientTrusting the value coming from the frontAlways read the price from the database by ProductId, never from the request

Launch checklist

  • Shop tables created and kept separate from the game tables
  • Item format validated on your Season with a test account
  • Purchase runs inside an atomic transaction (debit + order + queue)
  • UNIQUE IdemToken active against duplicate purchases
  • Credit debit with WHERE Credits >= price
  • Delivery worker processes only offline characters
  • All queries using prepared statements
  • Purchase button disables after the click and validates CSRF
  • Prices always read from the database, never from the front-end
  • Indexes on CharName, Status, and Delivered for performance
  • Clear "log out to receive" notice visible in the shop
  • Audit logs (who bought what, when, and from which IP)
  • Full-warehouse scenario tested (retry without losing the item or the credit)

With this architecture you have a reliable, auditable webshop that resists the classic MU Online frauds. Start small, with a few convenience products, validate delivery exhaustively in testing, and only then open it to players.

Frequently asked questions

Does the webshop deliver items while the player is online?

Ideally you should deliver only while the character is offline. If the player is logged in, the server keeps the inventory data in memory and writing directly to the database will be overwritten on logout. Queue the delivery and process it once the status is offline.

Do I need to touch the GameServer source code to have a webshop?

Not necessarily. Most webshops deliver items by writing directly to the warehouse table in the database. You only need to touch the source if you want real-time delivery over a socket, which is more advanced.

How do I generate an item's hexadecimal correctly?

The item format varies by version (Season 6 uses 32 bytes; modern seasons use larger blobs). Use a reference table for your Season and a builder that places the index, level, options, luck, skill, excellent, and ancient at the correct offsets.

Doesn't a webshop create a pay-to-win advantage and drive players away?

It depends on what you sell. Cosmetics, visual wings, name changes, and convenience services are usually well received. Selling full excellent items breaks the balance and empties the server over the medium term.

How do I stop a player from buying the same item twice by double-clicking?

Use an idempotency lock: generate a unique token per purchase attempt, debit the balance inside a SQL transaction, and record the order before queuing the delivery. Block repeated clicks on the front-end too.

GA
Guides & builds editor

Gabriel covers gameplay, class builds, PvP and progression. He tests every strategy on a live server before publishing.

Keep reading

Related articles