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
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.
{
"Starting Balance": 100,
"Max Transfer": 50000,
"Currency Symbol": "$",
"Show Balance On Respawn": true
}Commands and permissions
| Command | What it does | Permission |
|---|---|---|
/balance | Shows 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 |
| Permission | Grants |
|---|---|
moneyplugin.use | Allows using /balance and shows the balance on respawn. |
moneyplugin.pay | Allows using /pay to send money to other players. |
moneyplugin.admin | Allows money.give, money.take and money.balance from console or RCON. |
How the code works
Data model and storage
Balances live in a
Dictionary<string, long>keyed byplayer.UserIDString, serialized tooxide/data/MoneyPlugin.jsonthroughInterface.Oxide.DataFileSystem.LoadDatareads the file inOnServerInitializedand falls back to an empty dictionary if the JSON is corrupt.SaveDatawrites it again, but only when thedirtyflag 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 callsToString()on it before lookup.Config values
PluginConfigholdsStarting Balance,Max Transfer,Currency SymbolandShow Balance On Respawn.LoadConfigcallsLoadDefaultConfigand prints a warning if the file cannot be read, then writes the file back. The starting balance is applied byEnsureAccountthe first time a key is seen, either onOnPlayerConnectedor on the first API write, not on every join.Max Transfercaps/pay; set it to 0 to disable the cap.Show Balance On Respawncontrols the message sent fromOnPlayerRespawned.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 becauseAddBalanceclamps at zero withMath.Max. A calling plugin declares aPluginfield 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.Player commands and permissions
[ChatCommand("balance")]checksmoneyplugin.use, reads the balance and sends theBalancelang message.[ChatCommand("pay")]checksmoneyplugin.pay, requires two arguments, resolves the target withBasePlayer.FindByIDorBasePlayer.FindAwakeOrSleeping, rejects self-payment, parses the amount withlong.TryParse, enforcesMax Transferand the sender's funds, then moves the money withRemoveBalanceandAddBalanceand notifies both players. Every user-facing string comes fromLoadDefaultMessagesand theMsghelper, so no text is hard-coded.Admin console commands
money.give,money.takeandmoney.balanceare declared with the console command attribute and take aConsoleSystem.Arg.IsAdminArgchecksarg.IsAdminand replies with a refusal otherwise; the plugin does not checkmoneyplugin.adminin code, becauseConsoleSystem.Argexposes no player object to test the permission against.money.takeclamps the amount to the current balance before subtracting, so an admin command can never push an account below zero.Saving and unloading safely
OnServerSavecallsSaveData, which returns immediately whendirtyis false.Unloadcalls 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 thedirtyflag inside hooks that fire often, and the only warnings printed are the corrupt-data message fromLoadDataand the invalid-config message fromLoadConfig.
Install it on your server
Save MoneyPlugin.cs
Keep the file name exactly as the class name. Oxide matches them.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.Edit the config if you want
On first load the default config is written tooxide/config/MoneyPlugin.json(Carbon:carbon/configs/MoneyPlugin.json). After editing, reload it:oxide.reload MoneyPlugin c.reload MoneyPlugin # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip: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 versionEconomics bridge
Mirrors balances to the Economics plugin so shop plugins see the same numbers.
Generate this versionTransfer 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.
Edit the prompt on the next screen. 2 free plugins a month, no card.
More economy plugins
- BountiesA scrap bounty system where players fund a pool on a target and the killer collects it, using the OnEntityDeath hook plus a data file forโฆOnServerInitializedOnEntityDeath
- Chat shopA chat-driven shop where players spend scrap from their inventory to buy configured items.
- Daily rewardsAdds a /daily command that lets each player claim one reward from a configurable weighted list every 24 hours, with a streak bonus that scales whileโฆOnServerInitializedOnServerSave
Last updated Oct 26, 2026.
Customize server currency and balances
Customize