🔄 Trade Module

Real-time player-to-player item trading — same server or across the entire network


Paper 1.21+ RabbitMQ cross-server SmartInventory GUI Pending-grant delivery
🔄

Overview

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.

✅ What it does

  • Safe two-party item exchange
  • Real-time GUI — see partner's items update live
  • Both must confirm before items swap
  • Works on the same server and across servers via RabbitMQ
  • Items returned if either player cancels or disconnects
  • Pending delivery if a player is offline when items are granted

📁 Files

  • plugins/OreoEssentials/trades/trade.yml — all config
  • No shop files, no databases — pure in-memory + RabbitMQ
  • Pending grants survive server restart via in-memory DAO (cleared on restart)
Note: The Trade module requires RabbitMQ only for cross-server trades. Same-server trades work with zero extra dependencies beyond the plugin itself.

Quick Start

Installation

Drop 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>.

Minimum Working Config

enabled: true

Everything else has sensible defaults. Edit only what you need to change.

trade.yml — Full Reference

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

Color Codes

The title, name, and text fields support legacy Minecraft color codes using & as the prefix:

CodeColorCodeColor
&0Black&8Dark Gray
&1Dark Blue&9Blue
&2Dark Green&aGreen
&3Dark Aqua&bAqua
&4Dark Red&cRed
&5Dark Purple&dLight Purple
&6Gold&eYellow
&7Gray&fWhite
&l Bold&o Italic

GUI Layout

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):

â–ª border
â–ª
â–ª
â–ª
â–ª
â–ª
â–ª
â–ª
â–ª
â–ª
👤 Your Head
â–ª
â–ª
📖 Book
â–ª
â–ª
👤 Their Head
â–ª
â–ª
A slot
A slot
A slot
— ÷ —
B slot
B slot
B slot
â–ª
â–ª
A slot
A slot
A slot
— ÷ —
B slot
B slot
B slot
â–ª
â–ª
A slot
A slot
A slot
— ÷ —
B slot
B slot
B slot
â–ª
â–ª
â–ª
✔ Confirm
â–ª
✖ Cancel
â–ª
Partner status
â–ª
â–ª
Your offer area (9 slots — editable)
Partner's offer area (view-only for you)
Divider (decorative)
Confirm button
Cancel button
Partner's ready status (green = they confirmed)
Player heads (decorative)
Border / locked slots (cannot click)

Slot Details

AreaRow × ColRaw SlotsNotes
Your offer (A)Rows 2–4, Cols 1–319, 20, 21, 28, 29, 30, 37, 38, 399 slots — drag items here freely
Partner's offer (B)Rows 2–4, Cols 5–723, 24, 25, 32, 33, 34, 41, 42, 43View-only — you cannot place items here
DividerRows 2–4, Col 422, 31, 40Decorative — cannot click
Your headRow 1, Col 110Shows your player head
Partner headRow 1, Col 716Shows their player head
Info bookRow 1, Col 413Decorative book item
ConfirmRow 5, Col 247Click to confirm / un-confirm
CancelRow 5, Col 449Click to cancel the trade
Partner statusRow 5, Col 651Green = partner confirmed; Red = not yet

Important Rules About the Offer Area

Editing your offer resets your Confirm status. Any time you add, remove, or change an item in your offer area, your Confirm button is automatically reset to the unconfirmed state. This prevents the classic trick of swapping items after your partner already confirmed.
Your offer area holds up to 9 slots (3×3). You can place any item type and any stack size as long as it fits in one of the 9 slots. There is no built-in item duplication check — stack sizes are limited by normal Minecraft rules.

How a Trade Works — Step by Step

A

Player A sends an invite

Runs /trade PlayerB. The invite is stored in memory with a 60-second timeout. PlayerB sees a chat message.

B

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.

What Happens to Items During the Trade

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.

Item safety guarantee: Items are never destroyed. If the server crashes mid-trade, or a player disconnects, the offer items go back to whoever put them in (or into pending grants).

Cross-Server Trades

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.

How Cross-Server Trades Work

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.

Leader Election

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.

Cross-Server GUI Sync

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.

Version counter: Each state update increments a monotonic version number. Stale packets (version ≤ current) are ignored. This prevents out-of-order packets from rolling back the session state.

What If RabbitMQ Is Down?

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.

Invite System

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.

Invite Flow

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).

Invite Expiry

SettingValueNotes
TTL60 secondsHard-coded in TradeService.INVITE_TTL_SECONDS
Purge check intervalEvery 2 seconds (40 ticks)Background task removes expired invites
Max pending invitesOne per target playerA new invite from A to B replaces the old one
Tip: If you accidentally type the wrong player name and want to cancel your invite, simply wait 60 seconds for it to expire, or send another invite to the same player to refresh it.

Already In a Trade

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.

Confirm & Cancel Flow

Confirming

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.

Cursor requirement: If 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.

What Happens When Both Confirm

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.

Cancelling

Either player can click the Cancel button (Barrier by default, row 5 col 4) at any time while the trade is not locked. This:

Disconnecting / Closing the GUI

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.

Pending Grants

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.

How It Works

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.

Pending grants do not survive server restarts. The 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.

Item Delivery on Join

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).

RabbitMQ Packets (Cross-Server)

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

Trade ID

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)

ItemStack Serialization

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.

Commands

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.

Command Behavior Summary

  • No pending invite from target: Send them an invite. They see a message and have 60 seconds to accept.
  • Pending invite from target already exists: Immediately start the trade (no second message needed).
  • You're already in a trade: Error message — finish or cancel your current trade first.
  • Target is yourself: Error message — you cannot trade with yourself.
  • Target not found (offline on all servers): Error message — player not found.
  • Target is on another server: Invite is routed via RabbitMQ automatically.

Permissions

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

LuckPerms Setup Example

# 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

Player Examples

Example 1 — Basic Same-Server Trade

Steve (server: survival-1)

Steve wants to trade 64 diamonds for Alex's Netherite sword.

/trade Alex
Alex (server: survival-1)

Alex sees: "Steve wants to trade with you! Type /trade Steve to accept."

/trade Steve

✅ 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.

Example 2 — Last-Minute Change of Mind

Steve

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.

This is intentional. Any offer change resets your confirm status, so your partner can always see exactly what they're agreeing to.

Example 3 — Cross-Server Trade

Steve (server: survival-1) & Alex (server: survival-2)

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.

Example 4 — Invite Times Out

Steve

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.

Example 5 — Partner Disconnects

Steve & Alex

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.

Example 6 — Inventory Full on Delivery

Alex

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!

FAQ

Can I disable the trade module without removing the plugin?

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.

How do I change the sounds?

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".

Can a player trade with themselves?

No. The /trade command checks if the sender and target are the same player and returns an error message.

Can I limit trading to a specific distance?

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.

What happens if I put an item on my cursor and close the trade GUI?

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.

Do trades work across different world groups (e.g., survival vs creative)?

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.

What is "debugDeep" / trade debugging?

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.

Can I change the number of offer slots?

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.

What if only one player closes the GUI and the other keeps it open?

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.

Will pending grants survive a server restart?

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.

Can I configure a per-item trade tax or fee?

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.

The trade GUI shows "Trade: " as the title — is that a bug?

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.