How to make a Rust sell items plugin (scrap economy)
Adds /sell and /sell all <shortname> chat commands that remove items from a player's inventory and pay configured scrap prices per shortname. It uses item shortnames from your config and the Oxide chat command API; no Rust hooks are required.
Most Rust servers have no way to convert surplus loot into scrap. Players end up hoarding components and weapons they will never use, and traders or vending machines only cover a few hand-picked items. A command-driven sell system solves this without touching the world: the admin sets a scrap price per item shortname in the config, and players type /sell to cash out the held item or /sell all <shortname> to dump whole stacks.
The plugin below reads a Prices dictionary from oxide/config/SellItems.json, validates that each configured shortname actually exists with ItemManager.FindItemDefinition, and refuses anything that has no price. Payouts are scrap items given with player.GiveItem unless GiveScrap is false and Economics is loaded, in which case the balance is deposited through the plugin reference. Every sale is written to a data file so you can audit payouts.
The plugin code
using System.Linq;
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Plugins;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Sell Items", "RustPluginGenerator", "1.0.0")]
[Description("Sells items from a player's inventory for scrap at configured per-shortname prices")]
public class SellItems : RustPlugin
{
private const string PermUse = "sellitems.use";
[PluginReference] private Plugin Economics;
private PluginConfig config;
private readonly Dictionary<string, int> validPrices = new Dictionary<string, int>();
private class PluginConfig
{
[JsonProperty("Give scrap directly")]
public bool GiveScrap { get; set; } = true;
[JsonProperty("Maximum scrap per single sale")]
public int MaxPayoutPerSale { get; set; } = 5000;
[JsonProperty("Prices")]
public Dictionary<string, int> Prices { get; set; } = new Dictionary<string, int>
{
["scrap"] = 0,
["rifle.ak"] = 200,
["smg.2"] = 80,
["pistol.semiauto"] = 50,
["metal.fragments"] = 1,
["metal.refined"] = 6,
["sulfur"] = 2,
["charcoal"] = 1,
["gears"] = 10,
["sheetmetal"] = 4
};
}
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>
{
["Sold"] = "Sold {0} x {1} for {2} scrap.",
["Nothing"] = "You are not holding an item.",
["NoPrice"] = "{0} cannot be sold on this server.",
["InventoryEmpty"] = "You have no {0} to sell.",
["SellAllUsage"] = "Usage: /sell all <shortname>",
["PayoutCapped"] = "Payout capped at {0} scrap for this sale.",
["NoPermission"] = "You do not have permission to sell items."
}, this);
}
private void Init()
{
permission.RegisterPermission(PermUse, this);
}
private void OnServerInitialized()
{
validPrices.Clear();
foreach (var entry in config.Prices)
{
if (entry.Key == "scrap")
{
validPrices[entry.Key] = entry.Value;
continue;
}
if (ItemManager.FindItemDefinition(entry.Key) == null)
{
PrintWarning($"Unknown item shortname in prices: {entry.Key}");
continue;
}
validPrices[entry.Key] = entry.Value;
}
}
private string Msg(string key, string userId = null, params object[] args)
{
return string.Format(lang.GetMessage(key, this, userId), args);
}
[ChatCommand("sell")]
private void CmdSell(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 > 0 && args[0].Equals("all", StringComparison.OrdinalIgnoreCase))
{
SellAll(player, args);
return;
}
Item active = player.GetActiveItem();
if (active == null || active.info == null)
{
SendReply(player, Msg("Nothing", player.UserIDString));
return;
}
string shortname = active.info.shortname;
int price;
if (!validPrices.TryGetValue(shortname, out price) || price <= 0)
{
SendReply(player, Msg("NoPrice", player.UserIDString, active.info.displayName.english));
return;
}
int amount = 1;
player.inventory.Take(null, active.info.itemid, amount);
PayScrap(player, price * amount, shortname, amount);
}
private void SellAll(BasePlayer player, string[] args)
{
if (args.Length < 2)
{
SendReply(player, Msg("SellAllUsage", player.UserIDString));
return;
}
string shortname = args[1].ToLowerInvariant();
int price;
if (!validPrices.TryGetValue(shortname, out price) || price <= 0)
{
var def = ItemManager.FindItemDefinition(shortname);
string label = def != null ? def.displayName.english : shortname;
SendReply(player, Msg("NoPrice", player.UserIDString, label));
return;
}
var itemDef = ItemManager.FindItemDefinition(shortname);
if (itemDef == null) return;
int owned = player.inventory.GetAmount(itemDef.itemid);
if (owned <= 0)
{
SendReply(player, Msg("InventoryEmpty", player.UserIDString, itemDef.displayName.english));
return;
}
player.inventory.Take(null, itemDef.itemid, owned);
PayScrap(player, price * owned, shortname, owned);
}
private void PayScrap(BasePlayer player, int total, string shortname, int amount)
{
int payout = Mathf.Max(0, total);
if (config.MaxPayoutPerSale > 0 && payout > config.MaxPayoutPerSale)
{
payout = config.MaxPayoutPerSale;
SendReply(player, Msg("PayoutCapped", player.UserIDString, payout));
}
if (payout <= 0)
{
SendReply(player, Msg("Sold", player.UserIDString, amount, shortname, 0));
return;
}
if (!config.GiveScrap && Economics != null)
{
Economics.Call("Deposit", player.UserIDString, (double)payout);
}
else
{
Item scrap = ItemManager.CreateByName("scrap", payout);
if (scrap != null) player.GiveItem(scrap);
}
LogSale(player, shortname, amount, payout);
SendReply(player, Msg("Sold", player.UserIDString, amount, shortname, payout));
}
private void LogSale(BasePlayer player, string shortname, int amount, int payout)
{
try
{
var store = Interface.Oxide.DataFileSystem.GetDatafile("SellItems/sales");
string stamp = Facepunch.Math.Epoch.Current + "_" + player.userID.Get();
store[stamp] = new Dictionary<string, object>
{
["shortname"] = shortname,
["amount"] = amount,
["payout"] = payout,
["time"] = Facepunch.Math.Epoch.Current
};
Interface.Oxide.DataFileSystem.SaveDatafile("SellItems/sales");
}
catch
{
// Logging failures must not block a sale
}
}
}
}Hooks used: OnServerInitialized
Config file
Created automatically on first load at oxide/config/SellItems.json.
{
"Give scrap directly": true,
"Maximum scrap per single sale": 5000,
"Prices": {
"scrap": 0,
"rifle.ak": 200,
"smg.2": 80,
"pistol.semiauto": 50,
"metal.fragments": 1,
"metal.refined": 6,
"sulfur": 2,
"charcoal": 1,
"gears": 10,
"sheetmetal": 4
}
}Commands and permissions
| Command | What it does | Permission |
|---|---|---|
/sell | Sells one unit of the item in your active hotbar slot at its configured scrap price. | sellitems.use |
/sell all <shortname> | Sells every inventory stack of the given shortname at its configured price. | sellitems.use |
| Permission | Grants |
|---|---|
sellitems.use | Grants access to the /sell and /sell all commands. |
How the code works
Config: shortname prices
The
PluginConfigclass holdsPrices, aDictionary<string, int>keyed by item shortname, plusGiveScrapandMaxPayoutPerSale.LoadDefaultConfigseeds entries such asrifle.ak,gearsandmetal.fragments, and the read path inLoadConfigfalls back to those defaults if the file fails to parse. Because keys are shortnames, no item IDs are hard-coded and the file stays readable for admins.Validating prices on startup
In
OnServerInitializedthe plugin clears the privatevalidPricesmap and walksconfig.Prices. Each key is checked withItemManager.FindItemDefinition; unknown shortnames are reported once withPrintWarningand skipped, while valid entries are copied intovalidPrices. The publicscrapentry is copied straight through. This runs only at startup, so the warning never spams a live server.Selling the active item
CmdSellis registered with[ChatCommand("sell")]and first checkssellitems.useviapermission.UserHasPermission. With no arguments it readsplayer.GetActiveItem(), looks upitem.info.shortnameinvalidPrices, and removes one unit withplayer.inventory.Take(null, active.info.itemid, 1). Selling one at a time keeps partial stacks intact; the player repeats the command to cash out more.Selling by shortname
If the first argument equals
all,SellAllrequires a second argument and lowercases it. It resolves the shortname withItemManager.FindItemDefinition, usesplayer.inventory.GetAmountto count everything owned, and takes the whole amount withplayer.inventory.Take. Items without a configured price return theNoPricemessage instead of being deleted, and an empty count returnsInventoryEmpty.Paying out scrap
PayScrapmultiplies the unit price by the amount sold and clamps it toMaxPayoutPerSale, sending thePayoutCappedmessage when the cap is hit. IfGiveScrapis true, scrap is built withItemManager.CreateByName("scrap", payout)and handed over withplayer.GiveItem. IfGiveScrapis false and the[PluginReference]Economics plugin is loaded, the amount is instead deposited throughEconomics.Call("Deposit", ...).Messages and audit log
All player text lives in
LoadDefaultMessagesunder keys likeSold,NoPrice,SellAllUsage,InventoryEmptyandPayoutCapped, formatted through a smallMsghelper that callslang.GetMessage. Each sale also goes toLogSale, which writes an entry to theSellItems/salesdata file throughInterface.Oxide.DataFileSystem.GetDatafilekeyed by the current epoch time and player ID, then saves it.
Install it on your server
Save SellItems.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/SellItems.json(Carbon:carbon/configs/SellItems.json). After editing, reload it:oxide.reload SellItems c.reload SellItems # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip:oxide.grant group default sellitems.use c.grant group default sellitems.use # Carbon
Variations
Each one opens the generator with a ready prompt you can tweak.
Sell everything at once
Add a /sell all with no shortname variant that empties every priced item from the inventory in one pass.
Generate this versionDaily sell limit
Cap how much scrap a player can earn from selling per UTC day, reset at midnight.
Generate this versionPrice list command
A /selllist command that prints configured shortnames and prices in pages of nine.
Generate this version
FAQ
How do I install this plugin on Oxide or Carbon?
Drop SellItems.cs into oxide/plugins, then run oxide.reload SellItems from the server console. Carbon loads the same file from its plugins folder. The config appears at oxide/config/SellItems.json on first load; edit it and reload to apply new prices.
Why does /sell say the item has no price?
The item's shortname is not a key in the Prices dictionary, so it was never copied into the working validPrices map. Add it with the exact shortname, for example supply.signal or gears, then reload the plugin. Copy the shortname from an item spawn command to avoid typos.
Does the bare /sell sell whole stacks?
No. /sell removes one unit from the active hotbar slot and pays the unit price, so partial stacks stay intact. Use /sell all <shortname> if you want to liquidate every matching item in the inventory in a single command.
Does this need an economy plugin?
No. GiveScrap defaults to true and the plugin hands over real scrap items. Set it to false and install Economics to deposit into balances instead; when Economics is not loaded the plugin falls back to giving scrap items so no sale is lost.
Where are the sale logs stored?
Each sale is written by LogSale to the SellItems/sales data file under oxide/data. Entries are keyed by epoch time and player ID and record the shortname, quantity, payout and timestamp, so you can reconcile payouts after a support request.
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 sell items for scrap
Customize