Skip to content
RustPluginGenerator

How to make a Rust server currency plugin (Oxide balances + /pay)

A wallet plugin that stores per-player balances in a data file, exposes /balance, /pay and admin give/take console commands, saves on OnServerSave, and publishes GetBalance/Deposit/Withdraw for other plugins.

Rust has no built-in currency, so server owners who run shops, kits or rewards need a single source of truth for money. The usual failure is every economy plugin keeping its own counter, which drifts and breaks when one plugin is reloaded. The approach here is a small wallet plugin that owns the numbers: balances in an Oxide data file, a chat command for players to check and send money, console commands for admins to add or remove funds, and public methods other plugins can call.

The plugin below implements that. OnServerSave writes balances to oxide/data/MoneyPlugin.json, /balance prints your own balance, /pay transfers between players with a max-payment limit from the config, and money.give, money.take, money.balance run from the server console or RCON. Other plugins reach it through Plugin.Call with GetBalance, Deposit and Withdraw.

The plugin code

MoneyPlugin.cs
using System.Linq;
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;

namespace Oxide.Plugins
{
    [Info("Money Plugin", "RustPluginGenerator", "1.0.0")]
    [Description("Virtual currency with balances, /pay, admin grants and an API for other plugins.")]
    public class MoneyPlugin : RustPlugin
    {
        private const string PermUse = "moneyplugin.use";
        private const string PermPay = "moneyplugin.pay";
        private const string PermAdmin = "moneyplugin.admin";
        private const string DataFile = "MoneyPlugin";

        private PluginConfig config;
        private Dictionary<string, long> balances = new Dictionary<string, long>();
        private bool dirty;

        private class PluginConfig
        {
            [JsonProperty("Starting Balance")]
            public long StartingBalance { get; set; } = 100;

            [JsonProperty("Max Transfer")]
            public long MaxTransfer { get; set; } = 50000;

            [JsonProperty("Currency Symbol")]
            public string CurrencySymbol { get; set; } = "$";

            [JsonProperty("Show Balance On Respawn")]
            public bool ShowBalanceOnRespawn { get; set; } = true;
        }

        protected override void LoadDefaultConfig() => config = new PluginConfig();

        protected override void LoadConfig()
        {
            base.LoadConfig();
            try
            {
                config = Config.ReadObject<PluginConfig>();
                if (config == null) throw new Exception();
            }
            catch
            {
                PrintWarning("Configuration file is invalid, using defaults");
                LoadDefaultConfig();
            }
            SaveConfig();
        }

        protected override void SaveConfig() => Config.WriteObject(config);

        protected override void LoadDefaultMessages()
        {
            lang.RegisterMessages(new Dictionary<string, string>
            {
                ["Balance"] = "Your balance: {0}{1}",
                ["NoPermission"] = "You do not have permission to do that.",
                ["Usage"] = "Usage: {0}",
                ["PlayerNotFound"] = "Player '{0}' was not found.",
                ["SelfPay"] = "You cannot pay yourself.",
                ["InvalidAmount"] = "That is not a valid amount.",
                ["MaxTransfer"] = "The maximum single transfer is {0}{1}.",
                ["NotEnough"] = "You do not have enough money. Balance: {0}{1}",
                ["Paid"] = "You sent {0}{1} to {2}.",
                ["Received"] = "{0} sent you {1}{2}."
            }, this);
        }

        private string Msg(string key, string userId = null, params object[] args)
            => string.Format(lang.GetMessage(key, this, userId), args);

        private void Init()
        {
            permission.RegisterPermission(PermUse, this);
            permission.RegisterPermission(PermPay, this);
            permission.RegisterPermission(PermAdmin, this);
        }

        private void OnServerInitialized()
        {
            LoadData();
        }

        private void LoadData()
        {
            try
            {
                balances = Interface.Oxide.DataFileSystem.ReadObject<Dictionary<string, long>>(DataFile)
                           ?? new Dictionary<string, long>();
            }
            catch
            {
                PrintWarning("Money data file is invalid, starting empty");
                balances = new Dictionary<string, long>();
            }
            dirty = false;
        }

        private void SaveData()
        {
            if (!dirty) return;
            Interface.Oxide.DataFileSystem.WriteObject(DataFile, balances);
            dirty = false;
        }

        private void OnServerSave() => SaveData();

        private void Unload() => SaveData();

        private void OnPlayerConnected(BasePlayer player)
        {
            if (player == null) return;
            EnsureAccount(player.UserIDString);
        }

        private void OnPlayerRespawned(BasePlayer player)
        {
            if (player == null || !config.ShowBalanceOnRespawn) return;
            if (!permission.UserHasPermission(player.UserIDString, PermUse)) return;
            player.ChatMessage(Msg("Balance", player.UserIDString, config.CurrencySymbol, GetBalance(player.UserIDString)));
        }

        private void EnsureAccount(string userId)
        {
            if (!balances.ContainsKey(userId))
            {
                balances[userId] = config.StartingBalance;
                dirty = true;
            }
        }

        private long GetBalance(string userId)
        {
            long value;
            return balances.TryGetValue(userId, out value) ? value : config.StartingBalance;
        }

        private void AddBalance(string userId, long amount)
        {
            EnsureAccount(userId);
            balances[userId] = Math.Max(0, balances[userId] + amount);
            dirty = true;
        }

        private bool RemoveBalance(string userId, long amount)
        {
            long current = GetBalance(userId);
            if (current < amount) return false;
            AddBalance(userId, -amount);
            return true;
        }

        // Public API: other plugins call these with Plugin.Call
        public long GetBalance(ulong userId) => GetBalance(userId.ToString());

        public long Deposit(ulong userId, long amount)
        {
            if (amount <= 0) return GetBalance(userId);
            AddBalance(userId.ToString(), amount);
            return GetBalance(userId);
        }

        public bool Withdraw(ulong userId, long amount)
        {
            if (amount <= 0) return false;
            return RemoveBalance(userId.ToString(), amount);
        }

        [ChatCommand("balance")]
        private void CmdBalance(BasePlayer player, string command, string[] args)
        {
            if (player == null) return;
            if (!permission.UserHasPermission(player.UserIDString, PermUse))
            {
                player.ChatMessage(Msg("NoPermission", player.UserIDString));
                return;
            }
            player.ChatMessage(Msg("Balance", player.UserIDString, config.CurrencySymbol, GetBalance(player.UserIDString)));
        }

        [ChatCommand("pay")]
        private void CmdPay(BasePlayer player, string command, string[] args)
        {
            if (player == null) return;
            if (!permission.UserHasPermission(player.UserIDString, PermPay))
            {
                player.ChatMessage(Msg("NoPermission", player.UserIDString));
                return;
            }
            if (args.Length < 2)
            {
                player.ChatMessage(Msg("Usage", player.UserIDString, "/pay <player|steamid> <amount>"));
                return;
            }

            BasePlayer target = FindPlayer(args[0]);
            if (target == null)
            {
                player.ChatMessage(Msg("PlayerNotFound", player.UserIDString, args[0]));
                return;
            }
            if (target == player)
            {
                player.ChatMessage(Msg("SelfPay", player.UserIDString));
                return;
            }

            long amount;
            if (!long.TryParse(args[1], out amount) || amount <= 0)
            {
                player.ChatMessage(Msg("InvalidAmount", player.UserIDString));
                return;
            }
            if (config.MaxTransfer > 0 && amount > config.MaxTransfer)
            {
                player.ChatMessage(Msg("MaxTransfer", player.UserIDString, config.CurrencySymbol, config.MaxTransfer));
                return;
            }
            if (GetBalance(player.UserIDString) < amount)
            {
                player.ChatMessage(Msg("NotEnough", player.UserIDString, config.CurrencySymbol, GetBalance(player.UserIDString)));
                return;
            }

            RemoveBalance(player.UserIDString, amount);
            AddBalance(target.UserIDString, amount);

            player.ChatMessage(Msg("Paid", player.UserIDString, config.CurrencySymbol, amount, target.displayName));
            target.ChatMessage(Msg("Received", target.UserIDString, player.displayName, config.CurrencySymbol, amount));
        }

        [ConsoleCommand("money.give")]
        private void CcmdGive(ConsoleSystem.Arg arg)
        {
            if (!IsAdminArg(arg)) return;
            if (arg.Args == null || arg.Args.Length < 2)
            {
                arg.ReplyWith("Usage: money.give <player|steamid> <amount>");
                return;
            }
            BasePlayer target = FindPlayer(arg.Args[0].ToString());
            if (target == null)
            {
                arg.ReplyWith($"Player '{arg.Args[0]}' not found");
                return;
            }
            long amount;
            if (!long.TryParse(arg.Args[1].ToString(), out amount) || amount <= 0)
            {
                arg.ReplyWith("Invalid amount");
                return;
            }
            AddBalance(target.UserIDString, amount);
            arg.ReplyWith($"{config.CurrencySymbol}{amount} added to {target.displayName}, new balance {GetBalance(target.UserIDString)}");
        }

        [ConsoleCommand("money.take")]
        private void CcmdTake(ConsoleSystem.Arg arg)
        {
            if (!IsAdminArg(arg)) return;
            if (arg.Args == null || arg.Args.Length < 2)
            {
                arg.ReplyWith("Usage: money.take <player|steamid> <amount>");
                return;
            }
            BasePlayer target = FindPlayer(arg.Args[0].ToString());
            if (target == null)
            {
                arg.ReplyWith($"Player '{arg.Args[0]}' not found");
                return;
            }
            long amount;
            if (!long.TryParse(arg.Args[1].ToString(), out amount) || amount <= 0)
            {
                arg.ReplyWith("Invalid amount");
                return;
            }
            long current = GetBalance(target.UserIDString);
            if (amount > current) amount = current;
            AddBalance(target.UserIDString, -amount);
            arg.ReplyWith($"{config.CurrencySymbol}{amount} taken from {target.displayName}, new balance {GetBalance(target.UserIDString)}");
        }

        [ConsoleCommand("money.balance")]
        private void CcmdShowBalance(ConsoleSystem.Arg arg)
        {
            if (!IsAdminArg(arg)) return;
            if (arg.Args == null || arg.Args.Length < 1)
            {
                arg.ReplyWith("Usage: money.balance <player|steamid>");
                return;
            }
            BasePlayer target = FindPlayer(arg.Args[0].ToString());
            if (target == null)
            {
                arg.ReplyWith($"Player '{arg.Args[0]}' not found");
                return;
            }
            arg.ReplyWith($"{target.displayName}'s balance: {config.CurrencySymbol}{GetBalance(target.UserIDString)}");
        }

        private bool IsAdminArg(ConsoleSystem.Arg arg)
        {
            if (arg == null || !arg.IsAdmin)
            {
                arg?.ReplyWith("You do not have permission to do that");
                return false;
            }
            return true;
        }

        private BasePlayer FindPlayer(string nameOrId)
        {
            if (string.IsNullOrEmpty(nameOrId)) return null;
            ulong id;
            if (ulong.TryParse(nameOrId, out id))
            {
                BasePlayer byId = BasePlayer.FindByID(id);
                if (byId != null) return byId;
            }
            return BasePlayer.FindAwakeOrSleeping(nameOrId);
        }
    }
}

Hooks used: OnServerInitialized, OnServerSave, OnPlayerConnected, OnPlayerRespawned

Config file

Created automatically on first load at oxide/config/MoneyPlugin.json.

MoneyPlugin.json
{
  "Starting Balance": 100,
  "Max Transfer": 50000,
  "Currency Symbol": "$",
  "Show Balance On Respawn": true
}

Commands and permissions

Chat and console commands
CommandWhat it doesPermission
/balanceShows your current balance.moneyplugin.use
/pay <player> <amount>Transfers money from you to another player (online name or Steam ID).moneyplugin.pay
money.give <player> <amount>Console/RCON: adds money to a player's balance.moneyplugin.admin
money.take <player> <amount>Console/RCON: removes money from a player's balance.moneyplugin.admin
money.balance <player>Console/RCON: prints a player's balance to the console.moneyplugin.admin
Permissions
PermissionGrants
moneyplugin.useAllows using /balance and shows the balance on respawn.
moneyplugin.payAllows using /pay to send money to other players.
moneyplugin.adminAllows money.give, money.take and money.balance from console or RCON.

How the code works

  1. Data model and storage

    Balances live in a Dictionary<string, long> keyed by player.UserIDString, serialized to oxide/data/MoneyPlugin.json through Interface.Oxide.DataFileSystem. LoadData reads the file in OnServerInitialized and falls back to an empty dictionary if the JSON is corrupt. SaveData writes it again, but only when the dirty flag is set, so an idle server does not rewrite the file every autosave. The dictionary key must stay a string because JSON object keys cannot be numbers, while the public API takes a ulong and calls ToString() on it before lookup.

  2. Config values

    PluginConfig holds Starting Balance, Max Transfer, Currency Symbol and Show Balance On Respawn. LoadConfig calls LoadDefaultConfig and prints a warning if the file cannot be read, then writes the file back. The starting balance is applied by EnsureAccount the first time a key is seen, either on OnPlayerConnected or on the first API write, not on every join. Max Transfer caps /pay; set it to 0 to disable the cap. Show Balance On Respawn controls the message sent from OnPlayerRespawned.

  3. Public methods for other plugins

    Four public members are exposed to Plugin.Call: GetBalance(ulong userId) returns the stored amount, Deposit(ulong userId, long amount) adds and returns the new balance, Withdraw(ulong userId, long amount) returns false when the player cannot afford it, and nothing ever goes negative because AddBalance clamps at zero with Math.Max. A calling plugin declares a Plugin field decorated with the Oxide reference attribute, assigns it by name, and checks it for null before calling. Amounts at or below zero are ignored or rejected.

  4. Player commands and permissions

    [ChatCommand("balance")] checks moneyplugin.use, reads the balance and sends the Balance lang message. [ChatCommand("pay")] checks moneyplugin.pay, requires two arguments, resolves the target with BasePlayer.FindByID or BasePlayer.FindAwakeOrSleeping, rejects self-payment, parses the amount with long.TryParse, enforces Max Transfer and the sender's funds, then moves the money with RemoveBalance and AddBalance and notifies both players. Every user-facing string comes from LoadDefaultMessages and the Msg helper, so no text is hard-coded.

  5. Admin console commands

    money.give, money.take and money.balance are declared with the console command attribute and take a ConsoleSystem.Arg. IsAdminArg checks arg.IsAdmin and replies with a refusal otherwise; the plugin does not check moneyplugin.admin in code, because ConsoleSystem.Arg exposes no player object to test the permission against. money.take clamps the amount to the current balance before subtracting, so an admin command can never push an account below zero.

  6. Saving and unloading safely

    OnServerSave calls SaveData, which returns immediately when dirty is false. Unload calls the same method, so a reload or clean shutdown never loses changes made since the last autosave. No timers are used, so there is nothing to destroy. The plugin only sets the dirty flag inside hooks that fire often, and the only warnings printed are the corrupt-data message from LoadData and the invalid-config message from LoadConfig.

Install it on your server

  1. Save MoneyPlugin.cs

    Keep the file name exactly as the class name. Oxide matches them.
  2. Upload it to the plugins folder

    Oxide: oxide/plugins/. Carbon: carbon/plugins/. The server compiles and loads it within a few seconds, no restart needed.
  3. Edit the config if you want

    On first load the default config is written to oxide/config/MoneyPlugin.json (Carbon: carbon/configs/MoneyPlugin.json). After editing, reload it:oxide.reload MoneyPlugin c.reload MoneyPlugin # Carbon
  4. Grant the permission

    Give it to every player, or swap default for a group such as vip:oxide.grant group default moneyplugin.use c.grant group default moneyplugin.use # Carbon

Variations

Each one opens the generator with a ready prompt you can tweak.

  • Daily stipend

    Adds a timer that pays every online player a fixed amount once per day.

    Generate this version
  • Economics bridge

    Mirrors balances to the Economics plugin so shop plugins see the same numbers.

    Generate this version
  • Transfer fee

    Takes a percentage cut on each /pay and reports it to the console.

    Generate this version

FAQ

How do other plugins read a player's balance?

Add a private Plugin field to your plugin, decorate it with the Oxide reference attribute, and name it MoneyPlugin. Then call MoneyPlugin?.Call("GetBalance", player.userID) and cast the result with Convert.ToInt64. Always null-check the reference, because the wallet may be unloaded or renamed.

Are balances safe across a server crash?

They are saved on every OnServerSave and on Unload. A hard crash between autosaves loses changes since the last save. If that matters, lower server.saveinterval so OnServerSave fires more often, since all changes are flushed in that hook.

Can I edit balances by hand?

Yes. Edit oxide/data/MoneyPlugin.json while the server is stopped, keeping keys as Steam ID strings and values as whole numbers. Loading a file with invalid JSON makes the plugin start with an empty dictionary and print a warning, so back the file up first.

Does this work on Carbon?

Yes. Carbon loads the same .cs file and supports Oxide hooks, Plugin.Call and the config API. Drop the file into carbon/plugins and reload. Data paths differ slightly on Carbon, so look for the data folder inside the Carbon root if you need to find the JSON file.

Why is my balance reset for some players?

Balances are keyed by Steam ID, so a player who joins through a different identity has a separate wallet. If the whole file resets, the JSON was corrupt or deleted; LoadData then starts empty and prints a warning, so restore a backup if that was not intended.

Customize it for your server

Change the reward, add a cooldown, restrict it to VIPs. Start from this prompt and edit anything.

Prompt: Generate the MoneyPlugin Rust currency plugin with a starting balance of 100, a max /pay of 50000, a $ symbol, /balance, /pay, money.give, money.take, money.balance, and GetBalance/Deposit/Withdraw methods for other plugins.
Customize for my server

Edit the prompt on the next screen. 2 free plugins a month, no card.

Last updated Oct 26, 2026.

Customize server currency and balances

Customize