How to make a Rust chat shop plugin (scrap currency)
A chat-driven shop where players spend scrap from their inventory to buy configured items. It registers the `/shop` and `/buy` chat commands and needs no Oxide hooks at all.
Small servers often want a shop without a vending machine network or a UI plugin, but the base game has no chat command that sells items for scrap. The usual workaround is a plugin that reads prices from a config file and moves items between the player's inventory and a hidden balance of scrap.
This plugin does exactly that. /shop prints a paginated price list built from the Items section of the config. /buy <id> [amount] checks that the player has the scrap in their main inventory, takes it with Take, validates that the item fits, then hands over the goods with GiveItem. Nothing is stored in a database: the scrap leaves the inventory and the item arrives, so currencies and persistence stay in the base game. Permissions can gate the shop and its admin command so normal players never see admin text.
The plugin code
using System.Linq;
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("ScrapShop", "RustPluginGenerator", "1.0.0")]
[Description("Chat shop that sells configured items for scrap from the player's inventory.")]
public class ScrapShop : RustPlugin
{
private const string PermUse = "scrapshop.use";
private const string PermAdmin = "scrapshop.admin";
private const int ScrapId = -932201673;
private PluginConfig config;
private class ShopItem
{
[JsonProperty("Id")]
public int Id { get; set; }
[JsonProperty("Shortname")]
public string Shortname { get; set; } = "scrap";
[JsonProperty("DisplayName")]
public string DisplayName { get; set; } = "Item";
[JsonProperty("Price")]
public int Price { get; set; } = 10;
[JsonProperty("MaxAmount")]
public int MaxAmount { get; set; } = 100;
}
private class PluginConfig
{
[JsonProperty("Items per page")]
public int ItemsPerPage { get; set; } = 10;
[JsonProperty("Items")]
public List<ShopItem> Items { get; set; } = new List<ShopItem>
{
new ShopItem { Id = 1, Shortname = "metal.refined", DisplayName = "High Quality Metal", Price = 30, MaxAmount = 50 },
new ShopItem { Id = 2, Shortname = "rifle.ak", DisplayName = "Assault Rifle", Price = 500, MaxAmount = 1 }
};
}
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>
{
["NoPermission"] = "You do not have permission to use the shop.",
["ShopHeader"] = "Shop page {0}/{1} - use /buy <id> [amount]",
["ShopLine"] = "#{0} {1} - {2} scrap each (max {3})",
["ShopEmpty"] = "The shop has no items configured.",
["InvalidItem"] = "No shop item with that id.",
["InvalidAmount"] = "Amount must be a positive number.",
["NoFunds"] = "You need {0} scrap but have {1}.",
["NoSpace"] = "You do not have enough inventory space for {0}x {1}.",
["PurchaseComplete"] = "You bought {0}x {1} for {2} scrap.",
["Reloaded"] = "Config reloaded with {0} shop items.",
["UnknownShortname"] = "Item '{0}' does not exist on this server."
}, 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(PermAdmin, this);
}
[ChatCommand("shop")]
private void ShopCommand(BasePlayer player, string command, string[] args)
{
if (player == null) return;
if (!permission.UserHasPermission(player.UserIDString, PermUse))
{
SendReply(player, Msg("NoPermission", player.UserIDString));
return;
}
if (config.Items == null || config.Items.Count == 0)
{
SendReply(player, Msg("ShopEmpty", player.UserIDString));
return;
}
int perPage = Mathf.Max(1, config.ItemsPerPage);
int pages = Mathf.CeilToInt((float)config.Items.Count / perPage);
int page = 1;
if (args != null && args.Length > 0) int.TryParse(args[0], out page);
page = Mathf.Clamp(page, 1, pages);
SendReply(player, Msg("ShopHeader", player.UserIDString, page, pages));
int start = (page - 1) * perPage;
int end = Mathf.Min(start + perPage, config.Items.Count);
for (int i = start; i < end; i++)
{
ShopItem entry = config.Items[i];
if (entry == null) continue;
SendReply(player, Msg("ShopLine", player.UserIDString, entry.Id, entry.DisplayName, entry.Price, entry.MaxAmount));
}
}
[ChatCommand("buy")]
private void BuyCommand(BasePlayer player, string command, string[] args)
{
if (player == null) return;
if (!permission.UserHasPermission(player.UserIDString, PermUse))
{
SendReply(player, Msg("NoPermission", player.UserIDString));
return;
}
if (args == null || args.Length < 1)
{
SendReply(player, Msg("ShopHeader", player.UserIDString, 1, 1));
return;
}
int id;
if (!int.TryParse(args[0], out id))
{
SendReply(player, Msg("InvalidItem", player.UserIDString));
return;
}
ShopItem entry = config.Items?.Find(x => x != null && x.Id == id);
if (entry == null)
{
SendReply(player, Msg("InvalidItem", player.UserIDString));
return;
}
int amount = 1;
if (args.Length > 1 && !int.TryParse(args[1], out amount))
{
SendReply(player, Msg("InvalidAmount", player.UserIDString));
return;
}
if (amount <= 0)
{
SendReply(player, Msg("InvalidAmount", player.UserIDString));
return;
}
if (entry.MaxAmount > 0 && amount > entry.MaxAmount) amount = entry.MaxAmount;
ItemDefinition def = ItemManager.FindItemDefinition(entry.Shortname);
if (def == null)
{
SendReply(player, Msg("UnknownShortname", player.UserIDString, entry.Shortname));
return;
}
int cost = entry.Price * amount;
int held = player.inventory.GetAmount(ScrapId);
if (held < cost)
{
SendReply(player, Msg("NoFunds", player.UserIDString, cost, held));
return;
}
if (FreeSpace(player, def) < amount)
{
SendReply(player, Msg("NoSpace", player.UserIDString, amount, def.displayName.english));
return;
}
var collected = new List<Item>();
player.inventory.Take(collected, ScrapId, cost);
Item item = ItemManager.CreateByItemID(def.itemid, amount);
if (item == null)
{
SendReply(player, Msg("UnknownShortname", player.UserIDString, entry.Shortname));
return;
}
player.GiveItem(item);
SendReply(player, Msg("PurchaseComplete", player.UserIDString, amount, def.displayName.english, cost));
}
private int FreeSpace(BasePlayer player, ItemDefinition def)
{
int free = 0;
ItemContainer[] containers = { player.inventory.containerMain, player.inventory.containerBelt };
foreach (ItemContainer container in containers)
{
if (container == null) continue;
for (int i = 0; i < container.capacity; i++)
{
Item slot = container.GetSlot(i);
if (slot == null) free += def.stackable;
else if (slot.info.itemid == def.itemid) free += Mathf.Max(0, def.stackable - slot.amount);
}
}
return free;
}
[ConsoleCommand("scrapshop.reload")]
private void ReloadCommand(ConsoleSystem.Arg arg)
{
if (arg == null) return;
BasePlayer player = arg.Player();
if (player != null && !permission.UserHasPermission(player.UserIDString, PermAdmin))
{
SendReply(player, Msg("NoPermission", player.UserIDString));
return;
}
LoadConfig();
string message = Msg("Reloaded", player?.UserIDString, config.Items?.Count ?? 0);
if (player != null) SendReply(player, message);
else arg.ReplyWith(message);
}
}
}Config file
Created automatically on first load at oxide/config/ScrapShop.json.
{
"Items per page": 10,
"Items": [
{
"Id": 1,
"Shortname": "metal.refined",
"DisplayName": "High Quality Metal",
"Price": 30,
"MaxAmount": 50
},
{
"Id": 2,
"Shortname": "rifle.ak",
"DisplayName": "Assault Rifle",
"Price": 500,
"MaxAmount": 1
}
]
}Commands and permissions
| Command | What it does | Permission |
|---|---|---|
/shop [page] | Lists configured items with their id, shortname, price and stack size. | scrapshop.use |
/buy <id> [amount] | Buys `amount` of the item with the given id, paying in scrap from the main inventory. | scrapshop.use |
scrapshop.reload | Console/RCON command that reloads the config file and prints the item count. | scrapshop.admin |
| Permission | Grants |
|---|---|
scrapshop.use | Allows using /shop and /buy. |
scrapshop.admin | Allows running the scrapshop.reload console command. |
How the code works
Config shape
The config is a list of
ShopItementries under theItemskey. Each entry has anId(the number players type in/buy), aShortname, aDisplayName,Pricein scrap and an optionalMaxAmountthat caps a single purchase.LoadDefaultConfigseeds two example items so the file is never empty.LoadConfigreads the file, catches bad JSON, and falls back to defaults with a warning rather than crashing on startup.Listing the catalogue
The
[ChatCommand("shop")]method reads the optional page argument, slices the item list withItemsPerPagefrom config and sends one line per item throughSendReply. Each line comes from theShopLinemessage key with the id, shortname, price and cap substituted. Prices are shown as scrap so players know the currency without reading docs.Validating a purchase
BuyCommandparses the id and amount, finds the matchingShopItem, clamps the amount toMaxAmountand rejects zero or negative values. It then resolves the item definition withItemManager.FindItemDefinitionand computes the total cost asPrice * amount. If the definition is missing, it sendsInvalidItemand stops before touching the inventory.Taking scrap and giving the item
The plugin uses
player.inventory.GetAmount(ScrapId)to confirm funds, thenplayer.inventory.Take(collect, ScrapId, cost)with aList<Item>to remove the scrap. A slight delay of one tick withNextTickgives the container time to update beforeGiveItem, so the new item lands in a free slot instead of dropping on the floor.PurchaseCompleteconfirms the transaction in chat.Inventory space checks
Before charging, the plugin sums the free stack capacity of the main belt and backpack. If the total free space is smaller than the purchase amount it replies with
NoSpaceand stops. This is a simple guard: it comparesstackableagainstGetAmountper container, so players who are full do not lose scrap.Reloading and permissions
Initregisters bothscrapshop.useandscrapshop.admin. The[ConsoleCommand("scrapshop.reload")]handler callsLoadConfigand replies with the new count, so admins can edit the file and refresh without a restart. Each chat command checkspermission.UserHasPermissionfirst; players withoutscrapshop.useget aNoPermissionmessage.
Install it on your server
Save ScrapShop.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/ScrapShop.json(Carbon:carbon/configs/ScrapShop.json). After editing, reload it:oxide.reload ScrapShop c.reload ScrapShop # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip:oxide.grant group default scrapshop.use c.grant group default scrapshop.use # Carbon
Variations
Each one opens the generator with a ready prompt you can tweak.
Different currency
Swap scrap for a second item such as low grade fuel or metal fragments.
Generate this versionServer-side cooldown
Add a per-player buy cooldown to stop spam and scripted buying.
Generate this versionDaily limits
Cap the amount a player can buy per real day.
Generate this version
FAQ
How do I install this on Oxide or Carbon?
Drop ScrapShop.cs into oxide/plugins, wait for compilation, then edit oxide/config/ScrapShop.json. Carbon loads the same file from its plugin folder. After editing config, run scrapshop.reload or restart the server.
Does the shop write a database?
No. Payment happens by taking real scrap out of the player's inventory and the item is created on the spot. There is no balance to persist, so a plugin reload or server restart never loses or duplicates currency.
Why does /buy say I have no space when I have room?
The check counts free stack space in the main inventory and belt only. Vending machines, boxes or the hotbar do not count. Sell smaller amounts or empty a slot. The NoSpace message in the lang file can be edited to explain this.
Can I restrict the shop to certain players?
Yes. The scrapshop.use permission controls /shop and /buy. Grant it with oxide.grant group default scrapshop.use for everyone or assign it to a VIP group only, then leave it off the default group for normal players.
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
- 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
- Death penaltyMoves a configurable percentage of a dead player's carried scrap to their killer using OnPlayerDeath, with min/max clamps and separate handling forโฆOnServerInitializedOnPlayerDeath
Last updated Oct 26, 2026.
Customize chat shop
Customize