Real-time player-to-player item trading — same server or across the entire network
Safe, fair item exchanges between two players — even on different servers
The Trade module lets two players open a shared GUI where each places items they want to offer, then both must click Confirm before anything changes hands. Neither player can receive items until both agree — making cheating or scamming impossible.
plugins/OreoEssentials/trades/trade.yml — all configDrop OreoEssentials.jar into your server's plugins/ folder and start the server once to generate config files.
Open plugins/OreoEssentials/trades/trade.yml and adjust sounds, button materials, and title to your liking.
Make sure enabled: true is set (it is by default).
Give players the oreo.trade permission (or oreo.* for everything).
Done! Players can now run /trade <playername>.
enabled: true
Everything else has sensible defaults. Edit only what you need to change.
Located at plugins/OreoEssentials/trades/trade.yml. Every option is explained below.
# ────────────────────────────────────────────────────────────── # OreoEssentials — Trade Module Config # File: plugins/OreoEssentials/trades/trade.yml # ────────────────────────────────────────────────────────────── # Master switch. Set to false to disable the entire trade module. enabled: true # Title shown at the top of the trade GUI. # <you> → replaced with the viewer's own name # <them> → replaced with the partner's name # Uses legacy color codes (&8, &7, etc.) title: "&8Trade — <you> &7⇄ &8<them>" # Number of rows in the GUI. Keep at 6 — other values are unsupported. rows: 6 # ── Divider ──────────────────────────────────────────────────── # The vertical bar of blocks that separates your offer area from # your partner's area (column 4, rows 2–4). divider: material: "PURPLE_STAINED_GLASS_PANE" # Any Bukkit Material name name: "&5— Trade —" # Hover name for the divider item # ── Buttons ──────────────────────────────────────────────────── buttons: # The green "Confirm" button before you've clicked it confirm: material: "EMERALD_BLOCK" text: "&aConfirm" # The button AFTER you've clicked Confirm — shown to you while waiting # for your partner to also confirm confirmed: material: "LIME_CONCRETE" text: "&aReady ✔" # The red cancel button — closes the trade and returns all items cancel: material: "BARRIER" text: "&cCancel" # ── Labels ───────────────────────────────────────────────────── # Text displayed above each player's offer area (currently used # in the GUI title substitution). labels: you: "&fYou" them: "&fThem" # ── Sounds ───────────────────────────────────────────────────── # All values must be valid Bukkit Sound enum names. # Find the full list at: https://jd.papermc.io/paper/1.21/org/bukkit/Sound.html sounds: open: "UI_BUTTON_CLICK" # Played when GUI opens click: "UI_BUTTON_CLICK" # Played when toggling confirm off confirm: "BLOCK_NOTE_BLOCK_PLING" # Played when you click Confirm cancel: "ENTITY_VILLAGER_NO" # Played when trade is cancelled complete: "ENTITY_PLAYER_LEVELUP" # Played when trade completes successfully # ── Safety ───────────────────────────────────────────────────── # If true, players must empty their cursor (not be holding an item # on their mouse) before they can click the Confirm button. # Prevents accidentally confirming while mid-drag. require-empty-cursor-on-confirm: true # Minimum distance in blocks two players must be from each other # to trade. Set to 0 to allow trading from any distance. # Works for same-server trades only. min-distance-blocks: 0
The title, name, and text fields support legacy Minecraft color codes using & as the prefix:
| Code | Color | Code | Color |
|---|---|---|---|
&0 | Black | &8 | Dark Gray |
&1 | Dark Blue | &9 | Blue |
&2 | Dark Green | &a | Green |
&3 | Dark Aqua | &b | Aqua |
&4 | Dark Red | &c | Red |
&5 | Dark Purple | &d | Light Purple |
&6 | Gold | &e | Yellow |
&7 | Gray | &f | White |
&l Bold | &o Italic | ||
The trade GUI is a 6-row (54-slot) chest inventory. From Player A's perspective it looks like this (Player B sees an identical layout but their own items are on the left and A's items are on the right):
| Area | Row × Col | Raw Slots | Notes |
|---|---|---|---|
| Your offer (A) | Rows 2–4, Cols 1–3 | 19, 20, 21, 28, 29, 30, 37, 38, 39 | 9 slots — drag items here freely |
| Partner's offer (B) | Rows 2–4, Cols 5–7 | 23, 24, 25, 32, 33, 34, 41, 42, 43 | View-only — you cannot place items here |
| Divider | Rows 2–4, Col 4 | 22, 31, 40 | Decorative — cannot click |
| Your head | Row 1, Col 1 | 10 | Shows your player head |
| Partner head | Row 1, Col 7 | 16 | Shows their player head |
| Info book | Row 1, Col 4 | 13 | Decorative book item |
| Confirm | Row 5, Col 2 | 47 | Click to confirm / un-confirm |
| Cancel | Row 5, Col 4 | 49 | Click to cancel the trade |
| Partner status | Row 5, Col 6 | 51 | Green = partner confirmed; Red = not yet |
Player A sends an invite
Runs /trade PlayerB. The invite is stored in memory with a 60-second timeout. PlayerB sees a chat message.
Player B accepts the invite
Runs /trade PlayerA within 60 seconds. If they don't respond in time, the invite expires and A must send it again.
Session created & GUI opens
A TradeSession is created with a unique ID. Both players see the 6-row GUI open simultaneously. The open sound plays for both.
Both players place their items
Each player drags items from their hotbar/inventory into their 9-slot offer area (left side for A, right side for B). Changes appear in real time for the partner.
Both players click Confirm
When you click Confirm, your button turns green (Ready ✔) and your partner sees their status indicator change. If either player changes their offer after confirming, the confirm resets.
Trade completes
The moment both players are confirmed simultaneously, the GUI locks (items cannot be moved), the GUI closes, and items are delivered. A completion sound plays. If a player is offline at delivery time, their items go into pending grants and are delivered on next login.
While the GUI is open, your offer items remain in the GUI inventory — they are not yet removed from your player inventory. The GUI's offer area is editable, so you can drag items directly out of your hotbar/inventory into the offer slots. When the trade completes, those items swap to the other player. If the trade is cancelled for any reason, the items in the offer slots are returned to your inventory.
If Player A is on survival-1 and Player B is on survival-2,
the trade module handles this automatically using RabbitMQ packets.
You do not need to configure anything special — just ensure RabbitMQ is set up in the main config.
Each trade packet is broadcast over a RabbitMQ exchange. Every server on the network subscribes to these packets. When a packet arrives, a server handles it only if it has the relevant player online. This means the "work" is always done on the server that actually has the player.
When both players have confirmed and a trade is ready to finalize, one server must be in charge to prevent double-granting items. This server is called the trade leader.
The leader is always the server that currently hosts Player A (the player who sent the
original invite). The leader server is responsible for calling onFinish(), which grants
items to both players and marks the trade completed. The non-leader server simply delivers items
to its local player when it receives the TradeGrantPacket.
Every time a player changes their offer (adds/removes an item) or clicks Confirm, a
TradeStatePacket is sent over RabbitMQ. The remote server receives it and calls
applyRemoteState() on the local session mirror, which updates the GUI for the
player on the remote server. This keeps both GUIs in sync even across servers.
Cross-server trades will fail to start — the invite will not be delivered to the remote player. Same-server trades are completely unaffected because they never use RabbitMQ.
Before a GUI opens, both players must agree to trade via the invite handshake. This prevents GUIs from being forced open on someone who doesn't want to trade.
Player A runs /trade PlayerB. An Invite object is stored in memory keyed by Player B's UUID, recording who sent it and when.
Player B sees a chat notification: they can accept by running /trade PlayerA.
If Player B runs /trade PlayerA within the TTL window, the invite is consumed and the trade session starts.
If Player A tries to run /trade PlayerB again while an invite is already pending, the pending invite is refreshed (timer reset).
| Setting | Value | Notes |
|---|---|---|
| TTL | 60 seconds | Hard-coded in TradeService.INVITE_TTL_SECONDS |
| Purge check interval | Every 2 seconds (40 ticks) | Background task removes expired invites |
| Max pending invites | One per target player | A new invite from A to B replaces the old one |
A player who is already in an active trade session cannot accept a new invite. The command will tell them they need to finish or cancel their current trade first.
Click the Confirm button (Emerald Block by default, row 5 col 2). Your button will change to the "Ready ✔" appearance (Lime Concrete by default) and your partner's status indicator will turn green.
Clicking Confirm again un-confirms you (toggles). Your button changes back and your partner's status indicator turns red again. You can change your mind as many times as you like before the trade completes.
require-empty-cursor-on-confirm: true (the default),
you must not be holding any item on your mouse cursor when you click Confirm. This prevents accidentally
confirming while mid-drag. Simply drop the cursor item or put it back in your inventory first.
The instant the second player clicks Confirm (and both aReady and bReady
are simultaneously true):
The UI is locked — all offer slots are replaced with gray glass panes and cursors are cleared.
Both players' GUIs close.
onFinish() is called — items are transferred to each player.
A completion sound plays for both players.
The session is removed from memory.
Either player can click the Cancel button (Barrier by default, row 5 col 4) at any time while the trade is not locked. This:
If a player closes the trade GUI by pressing Escape or using /close, it is
treated the same as clicking Cancel — their offer items are returned and the session ends.
In rare cases, a player might not be online on the correct server at the exact moment a trade finalizes (e.g., they were in the middle of switching servers during a cross-server trade). The pending grants system handles this gracefully.
When the trade leader tries to grant items to a player but that player is not online locally, the items are stored as a pending grant tied to the player's UUID.
The pending grant is held in memory by InMemoryPendingGrantsDao.
When the player next logs in (fires PlayerJoinEvent), the plugin checks for pending grants.
Any pending grants are delivered immediately on join, before the player can do anything else.
Delivered grants are marked in a dedup set so they cannot be delivered twice.
InMemoryPendingGrantsDao
stores data in RAM only. If the server restarts between a trade finalizing and the player logging in,
the pending grant is lost. For production servers with frequent restarts, consider implementing a
persistent PendingGrantsDao backed by a database.
Delivery happens in TradeService.onJoin(PlayerJoinEvent). Items are added to the player's
inventory. If the inventory is full, excess items are dropped at the player's feet (same as normal
Minecraft overflow behavior).
These packets are sent over RabbitMQ between servers. You don't need to configure them — this section is for server administrators who want to understand what's happening on the network.
| Packet | Direction | Purpose | Key Fields |
|---|---|---|---|
| TradeInvitePacket | A's server → B's server | Delivers the trade invite to Player B's server | fromId, fromName, toId, toName, tradeId |
| TradeStartPacket | Broadcast to all servers | Signals that both players accepted and the session should open | tradeId, aId, bId, aServer, bServer |
| TradeStatePacket | Either server → partner's server | Syncs offer contents and ready flags after every change | tradeId, offerA[], offerB[], readyA, readyB, version |
| TradeGrantPacket | Leader → non-leader server | Tells the remote server to give items to its local player | tradeId, recipientId, items[] |
| TradeCancelPacket | Either server → partner's server | Cancels the trade and returns items on the remote server | tradeId, reason |
| TradeClosePacket | Either server → partner's server | Tells the remote server to close the GUI for its player | tradeId |
Each trade has a unique Trade ID (a UUID) computed by TradeIds.computeTradeId().
This ID is deterministic and symmetric — it produces the same UUID regardless of whether you
pass (A, B) or (B, A). This means both servers always agree on the trade ID
without needing to communicate it first.
// Simplified pseudocode of how the trade ID is computed:
// 1. Sort the two UUIDs alphabetically by their string representations
// 2. Pack the 32 bytes of both UUIDs into a byte array
// 3. Return UUID.nameUUIDFromBytes(thoseBytes)
Item arrays are serialized using Bukkit's built-in BukkitObjectOutputStream (Java serialization)
and then encoded to Base64 for transport over RabbitMQ. This preserves all item data including
enchantments, NBT tags, custom model data, and display names exactly as-is.
| Command | Permission | Description |
|---|---|---|
/trade <player> |
oreo.trade |
Send a trade invite to <player>.If they have already sent you an invite, this accepts it instead. |
| Permission | Default | Description |
|---|---|---|
oreo.trade |
All players (op) | Allows using /trade — sending and accepting trade invites |
oreo.* |
Operators | Grants all OreoEssentials permissions including oreo.trade |
# Give all players access to trade /lp group default permission set oreo.trade true # Or give everything to a VIP group /lp group vip permission set oreo.* true
Steve wants to trade 64 diamonds for Alex's Netherite sword.
Alex sees: "Steve wants to trade with you! Type /trade Steve to accept."
✅ The GUI opens for both. Steve drags 64 diamonds into his 9-slot area. Alex drags her Netherite Sword into her area. Both click Confirm. Trade completes — Steve gets the sword, Alex gets the diamonds.
Steve placed 32 iron and clicked Confirm. Then decides he wants to add more.
Steve drags 32 more iron into the offer area — his Confirm resets to unconfirmed automatically. He must click Confirm again.
Steve runs /trade Alex on survival-1. The invite travels via RabbitMQ to survival-2 where Alex receives it. Alex runs /trade Steve on survival-2.
Both GUIs open on their respective servers. Every item change is synced in real time. When both confirm,
the leader server (survival-1, since Steve sent the invite) grants items to Steve locally
and sends a TradeGrantPacket to survival-2 for Alex.
Steve runs /trade Alex but Alex is AFK. After 60 seconds, the invite expires.
Steve can run /trade Alex again to send a new invite at any time.
Both have the GUI open. Alex disconnects unexpectedly.
The session is cancelled. Steve's offer items are returned to his inventory. Alex's offer items are sent as pending grants and delivered to her inventory the next time she logs in.
Alex's inventory is completely full when the trade completes.
The items Steve was offering are added to Alex's inventory. Any items that don't fit are dropped on the ground at Alex's feet (standard Minecraft overflow behavior). Alex should make room before trading!
Yes. Set enabled: false in trades/trade.yml and restart (or reload) the server. The /trade command will not work and no sessions can be created.
Edit the sounds: section in trades/trade.yml. Use exact Bukkit Sound enum names — find the full list in the Paper Javadocs. Example: confirm: "ENTITY_EXPERIENCE_ORB_PICKUP".
No. The /trade command checks if the sender and target are the same player and returns an error message.
Yes. Set min-distance-blocks to the maximum number of blocks apart two players can be. For example, min-distance-blocks: 20 means players must be within 20 blocks of each other. Set to 0 to disable the check (default).
Note: This check only applies to same-server trades. Cross-server trades are not affected.
If require-empty-cursor-on-confirm: true, the UI lock phase clears your cursor automatically when the trade completes. If you close the GUI yourself while holding an item on the cursor, Minecraft's normal behavior applies — the item is returned to your inventory or dropped.
Trades work as long as both players are on Minecraft servers in the same network connected via RabbitMQ. The trade module does not care about world names or game modes — item granting simply adds items to the player's inventory.
Set tradedebug: true in your main config.yml to enable verbose trade logging. This prints every offer slot change, version bump, and state transition to the server console. Only enable this for troubleshooting — it is very noisy.
Not via config — the layout is hard-coded to 9 slots per player (a 3×3 area). Changing rows: 6 is not supported and will break the GUI.
As soon as one player closes the trade GUI (or disconnects), the trade is cancelled. The other player's GUI closes automatically and all offer items are returned. A trade requires both players to be actively engaged.
No. The default InMemoryPendingGrantsDao stores data in RAM. A restart will clear all pending grants. In practice this is only relevant for cross-server trades where one player disconnects at the exact moment of finalization — a very rare edge case. If this is critical for your server, a persistent database-backed DAO can be implemented by overriding getPendingGrantsDao() in the plugin.
No. The trade module is purely item-for-item with no currency or fee system built in. If you need trade taxes, you would need to implement custom logic.
No — the title is configurable via the title: key in trade.yml. The default is &8Trade — <you> &7⇄ &8<them> where <you> and <them> are replaced with actual player names at runtime. If your title shows "Trade: " literally, check that you haven't accidentally deleted the title: line from the config.