Skip to content
RustPluginGenerator

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

SellItems.cs
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.

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

Chat and console commands
CommandWhat it doesPermission
/sellSells 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
Permissions
PermissionGrants
sellitems.useGrants access to the /sell and /sell all commands.

How the code works

  1. Config: shortname prices

    The PluginConfig class holds Prices, a Dictionary<string, int> keyed by item shortname, plus GiveScrap and MaxPayoutPerSale. LoadDefaultConfig seeds entries such as rifle.ak, gears and metal.fragments, and the read path in LoadConfig falls 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.

  2. Validating prices on startup

    In OnServerInitialized the plugin clears the private validPrices map and walks config.Prices. Each key is checked with ItemManager.FindItemDefinition; unknown shortnames are reported once with PrintWarning and skipped, while valid entries are copied into validPrices. The public scrap entry is copied straight through. This runs only at startup, so the warning never spams a live server.

  3. Selling the active item

    CmdSell is registered with [ChatCommand("sell")] and first checks sellitems.use via permission.UserHasPermission. With no arguments it reads player.GetActiveItem(), looks up item.info.shortname in validPrices, and removes one unit with player.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.

  4. Selling by shortname

    If the first argument equals all, SellAll requires a second argument and lowercases it. It resolves the shortname with ItemManager.FindItemDefinition, uses player.inventory.GetAmount to count everything owned, and takes the whole amount with player.inventory.Take. Items without a configured price return the NoPrice message instead of being deleted, and an empty count returns InventoryEmpty.

  5. Paying out scrap

    PayScrap multiplies the unit price by the amount sold and clamps it to MaxPayoutPerSale, sending the PayoutCapped message when the cap is hit. If GiveScrap is true, scrap is built with ItemManager.CreateByName("scrap", payout) and handed over with player.GiveItem. If GiveScrap is false and the [PluginReference] Economics plugin is loaded, the amount is instead deposited through Economics.Call("Deposit", ...).

  6. Messages and audit log

    All player text lives in LoadDefaultMessages under keys like Sold, NoPrice, SellAllUsage, InventoryEmpty and PayoutCapped, formatted through a small Msg helper that calls lang.GetMessage. Each sale also goes to LogSale, which writes an entry to the SellItems/sales data file through Interface.Oxide.DataFileSystem.GetDatafile keyed by the current epoch time and player ID, then saves it.

Install it on your server

  1. Save SellItems.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/SellItems.json (Carbon: carbon/configs/SellItems.json). After editing, reload it:oxide.reload SellItems c.reload SellItems # Carbon
  4. Grant the permission

    Give it to every player, or swap default for a group such as vip: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 version
  • Daily sell limit

    Cap how much scrap a player can earn from selling per UTC day, reset at midnight.

    Generate this version
  • Price 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.

Prompt: Create a Rust Oxide plugin named SellItems with /sell and /sell all <shortname> chat commands that pay configured scrap prices per item shortname, give scrap directly by default, and use the sellitems.use permission.
Customize for my server

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

Last updated Oct 26, 2026.

Customize sell items for scrap

Customize