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

How to integrate the Cash Shop (WCoin) into your MU server site

Connect the game's Cash Shop to your MU Online server site, managing WCoin, WCoinP, and Goblin Point balances with atomic SQL transactions, a top-up panel, and secure credit delivery.

GA Gabriel · Updated on Jul 2, 2025 · ⏱ 26 min read
Quick answer

The Cash Shop is where a MU Online server turns engagement into revenue, and the site is the gateway to that flow. When a player tops up credits on the web panel, they expect to open the in-game shop and see the balance there. Making that bridge feel instantaneous and, above all, failure-proof is wh

The Cash Shop is where a MU Online server turns engagement into revenue, and the site is the gateway to that flow. When a player tops up credits on the web panel, they expect to open the in-game shop and see the balance there. Making that bridge feel instantaneous and, above all, failure-proof is what separates a reliable cash system from one that breeds complaints and losses. In this tutorial you'll build the integration between the site and the Cash Shop, taking care of what really matters: where the balance lives in the database, how to credit it with transactional safety, and how to avoid duplicate or lost credits. Column and table names vary by emulator version, so treat each code snippet as an example to adapt to your schema.

Before touching anything, it's important to understand that the MU client's Cash Shop doesn't talk to the site directly. It reads a numeric balance sitting in the database's accounts table. The site, therefore, doesn't "converse" with the shop: it merely changes that number. The whole integration comes down to manipulating that balance correctly and recording every movement. That mental clarity keeps you from hunting for APIs that don't exist and focuses you on the part that actually matters, which is the database.

Prerequisites

If the server isn't live yet, first follow the guide on how to create a MU Online server. With the server ready, you need:

  • SQL Server with the MuOnline database and access to the accounts table (MEMB_INFO or your version's equivalent).
  • PHP 7.4 or higher with the sqlsrv/pdo_sqlsrv extension enabled.
  • A working site login system, with the player's session authenticated.
  • A site-specific SQL user with read and write permission only on the balance columns and the log tables, never with db_owner.
  • Knowledge of your version's currency columns: WCoinC, WCoinP, GoblinPoint, or the equivalent names.
  • A payment gateway already chosen (credit should only be released after payment confirmation).

Confirm first of all which currency columns exist in your database. A quick query saves headaches:

SELECT COLUMN_NAME, DATA_TYPE
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'MEMB_INFO'
  AND COLUMN_NAME IN ('WCoinC', 'WCoinP', 'GoblinPoint', 'GameIDC');

If the columns don't show up under those names, look in your emulator's documentation. Changing the wrong column makes the balance add up in a place the game shop doesn't read.

Where the currency balance lives

The three most common Cash Shop currencies sit, in most versions, in the accounts table itself. Understanding each one's role guides what the site should credit.

CurrencyTypical columnCommon useOrigin on the site
WCoin / WCoinCWCoinCMain buyable currencyPaid top-up
WCoinPWCoinPPremium or bonus currencyBonuses, promotions
Goblin PointGoblinPointCurrency for events and alternative shopsRewards, events

The in-game Cash Shop reads these fields when the player opens the shop. This means the site's job is, essentially, to run a safe UPDATE on those columns and record the operation. All the complexity lies in doing that UPDATE in a way that never credits too much or too little, even in the face of network failures, double-clicks, and repeated webhooks.

Modeling the control tables

Never credit directly without recording it. Create two support tables: one for the transaction history and another that serves as an idempotency lock. In many cases a single well-modeled table fulfills both roles.

CREATE TABLE CashShop_Transacao (
    Id            BIGINT IDENTITY(1,1) PRIMARY KEY,
    ContaId       VARCHAR(10)  NOT NULL,
    Moeda         VARCHAR(20)  NOT NULL,   -- 'WCoinC', 'WCoinP', 'GoblinPoint'
    Quantidade    INT          NOT NULL,
    SaldoAntes    INT          NOT NULL,
    SaldoDepois   INT          NOT NULL,
    Origem        VARCHAR(40)  NOT NULL,   -- 'mercadopago', 'admin', 'bonus'
    GatewayRef    VARCHAR(80)  NULL,       -- payment ID (idempotency)
    CriadoEm      DATETIME     NOT NULL DEFAULT GETDATE()
);
GO

-- A unique index guarantees the same payment never credits twice
CREATE UNIQUE INDEX UX_CashShop_GatewayRef
ON CashShop_Transacao (GatewayRef)
WHERE GatewayRef IS NOT NULL;
GO

The filtered unique index is the centerpiece of the security. It makes it physically impossible to write two transactions with the same GatewayRef. If a webhook arrives duplicated, the second insert fails at the database, and your code treats that error as "already processed." You don't need to rely on the application logic alone; the database enforces the rule.

Storing SaldoAntes and SaldoDepois looks redundant, but it's gold for auditing. When a player opens a dispute claiming they didn't receive it, you have the exact value before and after each operation, timestamped. That resolves complaints in seconds.

The crediting procedure with a transaction

The heart of the system is the crediting operation. It has to be atomic: read the balance, add to it, write the new balance, and record the transaction, all within a single SQL transaction. A stored procedure encapsulates that logic and makes it reusable.

CREATE PROCEDURE dbo.CreditarWCoin
    @ContaId    VARCHAR(10),
    @Quantidade INT,
    @Origem     VARCHAR(40),
    @GatewayRef VARCHAR(80) = NULL
AS
BEGIN
    SET NOCOUNT ON;
    SET XACT_ABORT ON;      -- any error rolls back the transaction

    BEGIN TRAN;

    -- Lock the account row for a consistent read
    DECLARE @saldoAntes INT;
    SELECT @saldoAntes = WCoinC
    FROM MEMB_INFO WITH (UPDLOCK, ROWLOCK)
    WHERE memb___id = @ContaId;

    IF @saldoAntes IS NULL
    BEGIN
        ROLLBACK TRAN;
        THROW 50001, 'Account does not exist', 1;
    END

    -- Idempotency: if already processed, abort silently
    IF @GatewayRef IS NOT NULL AND EXISTS (
        SELECT 1 FROM CashShop_Transacao WHERE GatewayRef = @GatewayRef)
    BEGIN
        ROLLBACK TRAN;
        RETURN;   -- already credited before
    END

    UPDATE MEMB_INFO
    SET WCoinC = WCoinC + @Quantidade
    WHERE memb___id = @ContaId;

    INSERT INTO CashShop_Transacao
        (ContaId, Moeda, Quantidade, SaldoAntes, SaldoDepois, Origem, GatewayRef)
    VALUES
        (@ContaId, 'WCoinC', @Quantidade, @saldoAntes,
         @saldoAntes + @Quantidade, @Origem, @GatewayRef);

    COMMIT TRAN;
END
GO

Three details make this procedure safe. The XACT_ABORT ON ensures any error automatically rolls everything back. The WITH (UPDLOCK, ROWLOCK) on the balance read locks the account row so two simultaneous executions don't read the same balance and overwrite each other, which would cause credit loss. And the GatewayRef check gives the second layer of idempotency, complementing the unique index. With this, even two webhooks arriving in the same millisecond result in a single credit.

The PHP layer that calls the credit

In PHP, the call stays clean because all the complexity is in the procedure. PHP's role is to validate who's requesting and pass the parameters with binding.

<?php
// lib/cashshop.php
require_once __DIR__ . '/../config/db.php';

function creditarWCoin(string $contaId, int $qtd, string $origem, ?string $ref = null): bool {
    if ($qtd <= 0) return false;

    $conn = getDB();
    $sql = "{CALL dbo.CreditarWCoin(?, ?, ?, ?)}";
    $params = [
        [$contaId, SQLSRV_PARAM_IN],
        [$qtd,     SQLSRV_PARAM_IN],
        [$origem,  SQLSRV_PARAM_IN],
        [$ref,     SQLSRV_PARAM_IN],
    ];

    $stmt = sqlsrv_query($conn, $sql, $params);
    if ($stmt === false) {
        error_log('Error crediting WCoin: ' . print_r(sqlsrv_errors(), true));
        return false;
    }
    return true;
}

Using bound parameters (?) is non-negotiable. Never build the call by concatenating $contaId or the amount directly into the string, because that opens the door to SQL injection. With binding, the driver treats each value as data, not as a command, and the attack simply doesn't work.

The top-up panel on the site

The panel the player sees organizes the credit packages and leads to payment. The credit itself is only released after the gateway confirms, never on the button click.

<?php
// recarga.php
session_start();
require_once __DIR__ . '/../lib/cashshop.php';

if (empty($_SESSION['conta_mu'])) {
    header('Location: /login');
    exit;
}

$pacotes = [
    'p1' => ['wcoin' => 1000,  'preco' => 'R$ 10,00'],
    'p2' => ['wcoin' => 2500,  'preco' => 'R$ 20,00', 'bonus' => 300],
    'p3' => ['wcoin' => 6000,  'preco' => 'R$ 40,00', 'bonus' => 1000],
];

// Query the current balance to display
$conn = getDB();
$stmt = sqlsrv_query($conn,
    "SELECT WCoinC, WCoinP FROM MEMB_INFO WHERE memb___id = ?",
    [[$_SESSION['conta_mu'], SQLSRV_PARAM_IN]]);
$saldo = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC);
?>
<h1>WCoin Top-up</h1>
<p>Current balance: <strong><?= (int)$saldo['WCoinC'] ?> WCoin</strong></p>

<div class="pacotes">
<?php foreach ($pacotes as $id => $p): ?>
  <div class="pacote">
    <h3><?= number_format($p['wcoin'], 0, ',', '.') ?> WCoin</h3>
    <?php if (!empty($p['bonus'])): ?>
      <span class="bonus">+<?= $p['bonus'] ?> bonus</span>
    <?php endif; ?>
    <p class="preco"><?= $p['preco'] ?></p>
    <a href="/pagamento?pacote=<?= $id ?>" class="btn">Buy</a>
  </div>
<?php endforeach; ?>
</div>

The button leads to the payment flow, not to the credit. That separation is a fundamental security rule: the credit happens exclusively in the gateway callback, when you're sure the money came in. Crediting on the click would let anyone earn WCoin for free just by calling the URL.

Delivering the credit after payment confirmation

When the gateway confirms the payment, it calls your callback endpoint. That's where the credit happens, using the payment ID as the idempotency key.

<?php
// callback-pagamento.php  (called by the gateway, not by the player)
require_once __DIR__ . '/../lib/cashshop.php';

// 1) Validate the authenticity of the notification (gateway signature/token)
//    The method varies by gateway; NEVER credit without validating.

// 2) Retrieve the confirmed data
$pagamentoId = $notificacao['id'];        // unique gateway ID
$statusPago  = $notificacao['status'] === 'approved';
$contaId     = $notificacao['metadata']['conta'];
$wcoin       = (int)$notificacao['metadata']['wcoin'];

// 3) Credit only if approved, using the ID as the reference
if ($statusPago) {
    $ok = creditarWCoin($contaId, $wcoin, 'gateway', $pagamentoId);
    // If the webhook repeats, the unique index blocks the second credit
    http_response_code($ok ? 200 : 500);
} else {
    http_response_code(200); // acknowledge the notification even without crediting
}

Note that $pagamentoId becomes the transaction's GatewayRef. If the gateway resends the notification, which is common and expected, the second attempt hits the unique index and doesn't credit again. That's the protection that keeps a network retry from becoming doubled WCoin.

When the player sees the balance in the game

A point that raises many support questions: I credited it in the database, why doesn't the player see it? The answer depends on how the Cash Shop reads the balance, and that varies by version. In general there are two behaviors:

  1. Direct read from the database each time the shop opens: the player just needs to close and reopen the Cash Shop. It's the most common case and the most comfortable.
  2. Balance cached in the GameServer: the server keeps the value in memory and only reloads it on login. Here the player needs to relog to see the credit.

Make this clear on the top-up success screen, something like "your WCoin has been credited; reopen the shop or relog to view it." A simple notice eliminates half the support tickets. If your emulator offers a real-time top-up command or port in the GameServer, integrating through it delivers the best experience, but it isn't required for a working system.

Security and best practices

The cash system handles real money, so it deserves extra rigor. Beyond the parameter binding and idempotency already covered, apply these principles:

  • Least privilege on the database: the site's SQL user only needs EXECUTE on the credit procedure and SELECT on the balance columns, nothing beyond that.
  • Gateway notification validation: validate the signature or token of every payment notification; without it, anyone can forge a callback and earn credits.
  • Complete, immutable log: never allow DELETE or UPDATE on the transactions table by the site's user; it is a ledger.
  • Sanity limits: reject absurd WCoin amounts per transaction; a value far above the largest package is a sign of tampering.
  • HTTPS required: the entire top-up and callback flow must travel under TLS.

Common errors and fixes

SymptomLikely causeFix
Credit doesn't appear in the gameCash Shop reads the balance from cacheAsk the player to relog; document it on the screen
Player received double WCoinDuplicate webhook without idempotencyAdd the unique index on GatewayRef and the check in the procedure
Balance added to the wrong columnIncorrect currency columnConfirm the real column name in the schema before crediting
Credit released without paymentCrediting on the click instead of the callbackMove the credit to the gateway's validated callback
Deadlock error under concurrencyInconsistent lock orderUse UPDLOCK, ROWLOCK and keep the transaction short
sqlsrv_query returns false when creditingInsufficient permissionGrant EXECUTE on the procedure to the site's user
Someone forged a notificationCallback without signature validationValidate the token/signature of every notification before crediting

Launch checklist

  • Currency columns (WCoinC, WCoinP, GoblinPoint) confirmed in the real schema
  • CashShop_Transacao table created with SaldoAntes and SaldoDepois
  • Unique index on GatewayRef working
  • CreditarWCoin stored procedure with XACT_ABORT and UPDLOCK
  • Site's SQL user with only the necessary EXECUTE and SELECT
  • PHP using bound parameters in every call
  • Credit happening only in the gateway's validated callback
  • Payment-notification signature/token validation implemented
  • Success screen warning to reopen the shop or relog
  • Per-transaction sanity limits configured
  • The entire flow under HTTPS
  • Duplicate-webhook test confirming a single credit
  • Nonexistent-account test returning a handled error

With this foundation, your site's Cash Shop credits currency reliably, audits every cent, and withstands the network failures that inevitably happen. The lesson running through it all is the same: credit is money, so treat every operation like a bank transaction, with atomicity, idempotency, and a complete record.

Frequently asked questions

What's the difference between WCoin, WCoinP, and Goblin Point?

WCoin (also called WCoinC) is the buyable credits currency, WCoinP is the premium or bonus currency, and Goblin Point is an alternative currency used in some events and shops. The exact name and column vary by version, but all three sit in the accounts table and are read by the Cash Shop inside the game.

Does the credit added on the site show up in the game instantly?

It depends on where the Cash Shop reads the balance from. If it reads straight from the accounts table each time the shop opens, the player just needs to reopen the Cash Shop. Some servers keep the balance cached in the GameServer, and in that case the player needs to relog to refresh it.

Why do I need a SQL transaction to add credits?

Because adding credit involves reading the balance, adding to it, and writing it back, plus recording the purchase log. If the process fails midway without a transaction, the player may receive credit without a record or pay without receiving. The transaction guarantees that either everything happens or nothing does.

How do I stop someone from getting credited twice for the same payment?

Use a unique idempotency key per transaction, normally the gateway's payment ID, stored in a control table with a unique index. Before crediting, check whether that ID has already been processed; if it has, ignore it. This neutralizes duplicate webhooks and page reloads.

Do I need a new column in the accounts table for WCoin?

In most versions the column already exists, with a name like WCoinC, WCoinP, or GameIDC depending on the emulator. Confirm the schema before creating anything; creating a duplicate column can make the game's Cash Shop read the wrong balance.

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