- What it is
- Design
- Features
- Installation
- Configuration
- Commands and permissions
- PlaceholderAPI
- Data safety
- Building
- Compatibility
- Project layout
- Contributing notes
- License
- Acknowledgements
Vault3 fills two roles at once:
- A complete economy — balances, transfers, charge requests, leaderboard, GUI menus, loans
- A Vault API provider — registered through the
net.milkbowl.vaultinterfaces
Plugins that declare depend: [Vault] and call net.milkbowl.vault.economy.Economy keep
working exactly as before, but you do not need a separate Vault.jar — the API ships
inside this plugin.
⚠️ For that same reason, do not installVault.jaralongside it. Both declare the plugin nameVault, so Bukkit treats them as duplicates and one will fail to load — and you cannot predict which one.
Most economy plugins are fine on a small server. Things break once traffic picks up, so this project is built around the two failures that actually matter: blocking the main thread and losing money.
A transaction marks the account dirty and returns immediately. Writes happen asynchronously, debounced, and cover only the balances that changed — never the whole table.
On a 200-player server a shop purchase used to mean the main thread waiting on a 200-row batch upsert. Now it is a single row, off-thread.
withdrawPlayer and depositPlayer are single atomic operations on the account, implemented
with ConcurrentHashMap.compute() so the read, the sufficient-funds check and the write all
happen under the same lock.
Two plugins touching the same balance at once — a shop and a daily-reward task, say — cannot overwrite each other. Under a stress test of 8 threads × 25,000 concurrent withdrawals the final balance is exact.
Resolving a player name goes from cheapest to most expensive:
- Exact match against online players
- Local name→UUID cache, populated on join so renames stay correct
- The server's own user cache (
getOfflinePlayerIfCached, called reflectively so older APIs still work) - A full directory scan — at most once every 60 seconds, and its results warm the cache
Failed lookups are negatively cached for 60 seconds, so a name that does not exist cannot trigger repeated scans. In testing, 210,000 lookups triggered a single directory scan.
| Area | Detail |
|---|---|
| Storage | MySQL (HikariCP) or flat file. Drivers download at runtime, nothing to bundle |
| Payments | /pay with a paginated player picker, or direct /pay <player> <amount> |
| Charge requests | Request money from a player. Offline targets receive it on their next join |
| Loans | Optional loan system with installments, interval charging and penalties |
| Leaderboard | /vaultop, paginated |
| Admin | /eco give|take, in-game config editor GUI, /vault reload |
| PlaceholderAPI | %vault_*% and %vault3_*%, plus %vault2_*% as a legacy alias |
| Languages | English, 繁體中文, 简体中文, Español, Français, Deutsch, Nederlands, Polski, Português, Русский, हिन्दी |
| Currency | Symbol, prefix/suffix position, decimal pattern, 1.2k / 3.4m abbreviation |
| Import | One-shot import from Essentials userdata |
- Stop the server cleanly (
/stop— not a forced kill) - Drop the jar into
plugins/ - Start the server;
plugins/Vault/config.ymlis generated - Edit the config, then
/vault reload
If the new jar has a different filename from the old one it will not replace it. Delete the old jar first, or both will load and collide as duplicate plugins.
storage:
# Persist shortly after each transaction. Asynchronous and only writes changed
# rows, so it never blocks the main thread
save_on_transaction: true
# Debounce window in ticks. Lower is safer, higher means fewer round trips
transaction_flush_delay_ticks: 20
# Periodic asynchronous safety net, in seconds. 0 disables it
autosave_seconds: 60
# On startup, re-apply balances a previous shutdown could not write
recover_emergency_dumps: true
use_mysql: true
mysql:
host: localhost
port: 3306
database: vault
username: root
password: ""
params: useSSL=false&serverTimezone=UTC
pool:
max: 10
min_idle: 2
connection_timeout_ms: 10000
idle_timeout_ms: 600000
max_lifetime_ms: 1800000Two keys are easy to get wrong, and getting them wrong fails silently:
- The connection string key is
params, notproperties- Pool settings must be nested under
pool:, not flat undermysql:A wrong key logs no error. It quietly falls back to the default, so the settings look applied when they are not.
language: en # see the messages/ directory
currency:
symbol: "$"
position: suffix # prefix / suffix / none
space: true
abbreviate:
enabled: false # 1.2k / 3.4m
pay_limits:
min: 10
max: 100000
loans:
enabled: false # disables the whole subsystem, scheduler included
update_check: false| Command | Description | Permission |
|---|---|---|
/balance |
Show your balance | vault.balance |
/pay [player] [amount] |
Pay a player, or open the payment menu | vault.pay |
/vaultop [page] |
Richest players | vault.top |
/eco give|take <player> <amount> |
Adjust a balance | vault.eco |
/vault [reload|loan] |
Main menu and admin actions | vault.admin |
/loan |
Loan menu | vault.loan |
vault.pay.bypass_min and vault.pay.bypass_max exempt a player from pay_limits.
%vault_balance% %vault_balance_formatted%
%vault_eco_balance% %vault_eco_balance_formatted%
%vault_eco_balance_fixed% %vault_eco_balance_commas%
%vault_eco_balance_short% %vault_ecobalance<0-8>dp%
%vault_currency_symbol%
%vault_balance_<player>% %vault_balance_formatted_<player>%
%vault_top% %vault_top_<n>%
%vault_top_name_<n>% %vault_top_amount_<n>%
Every placeholder is also available as %vault3_*%, and as %vault2_*% for configs
written against the older identifier.
Every balance is written synchronously, before the connection pool closes. If the database write fails:
- It is retried 3 times, 400 ms apart
- If it still fails, balances are written to
balances-emergency-<timestamp>.yml - The console reports
Saved N player balances before shutdown.
Startup scans for emergency dumps, applies them oldest to newest so a later dump wins, writes
them back to the database, then renames each file to .recovered so it can never be applied
twice. If the write-back still fails the files are kept for the following start.
Controlled by storage.recover_emergency_dumps.
Set it to
falseif several servers share one database — a stale dump from one server could overwrite newer values written by another.
onDisable() does not run, so transactions since the last flush are lost. With the defaults
that window is about one second; tune it with transaction_flush_delay_ticks.
A normal /stop, a panel shutdown and docker stop (SIGTERM) all run the full shutdown save.
Only SIGKILL and the OOM killer skip it.
Requires JDK 21 (modern) or JDK 17 (legacy).
mvn -P modern clean package # → target-java21/vault-3.0.0.jar Java 21 / Spigot 1.21.4
mvn -P legacy clean package # → target-java17/vault-3.0.0.jar Java 17 / Spigot 1.8.8Both profiles produce the same filename but write to different directories, so they never
clobber each other. If you copy them into one place, tell them apart by the class file major
version: 0x3d is Java 17, 0x41 is Java 21.
Two places must be updated together, or /version Vault reports the wrong number:
<version>and the three<jar.finalName>entries inpom.xmlversion:insrc/main/resources/plugin.yml
- Server: Spigot / Paper. Builds are provided for Java 21 and Java 17
- Optional hooks: PlaceholderAPI, LuckPerms, SkinsRestorer, Essentials (import only)
- Never install
Vault.jaras well — see What it is
src/main/java/
├── kevin/vault3/
│ ├── VaultPlugin.java plugin entry point, lifecycle, scheduling
│ ├── economy/ SimpleEconomy — balances, transactions, persistence
│ ├── storage/ Database — MySQL access
│ ├── commands/ balance, pay, vaultop, eco, vault
│ ├── menu/ payment, loan, config editor and charge request GUIs
│ ├── loans/ loan logic and storage
│ ├── placeholder/ PlaceholderAPI expansions
│ ├── importer/ Essentials import
│ ├── i18n/ message loading
│ └── util/ name cache, colour, input sanitising
└── net/milkbowl/vault/
└── Vault.java plugin main class, extends VaultPlugin
src/main/resources/
├── config.yml
├── plugin.yml
└── messages/ 11 languages
These are external contracts. Changing any of them breaks other plugins:
| Item | Why |
|---|---|
name: Vault in plugin.yml |
Other plugins hook in via depend: [Vault] |
main: net.milkbowl.vault.Vault |
Same, and it is the plugin main class |
The net.milkbowl.vault.** package |
Other plugins import these interfaces directly |
PlaceholderAPI ids vault / vault3 / vault2 |
Already written into users' scoreboard configs |
Implementation constraints:
- The transaction path (
withdrawPlayer/depositPlayer) must contain no I/O flushDirty()may only be called from an asynchronous threadsave()/saveAllNow()are blocking, and exist for shutdown, reload and import only- Every balance mutation must be atomic — never
get()thenput()
bstats-bukkit 3.2.1 is bundled at build time and relocated to kevin.vault3.metrics so it
cannot clash with another plugin's copy. Two custom charts are collected: storage backend and
configured language.
Server owners can disable it globally with enabled: false in plugins/bStats/config.yml.
This plugin does not overwrite that setting.
Distributed under AGPL-3.0.
In practice this means: if you modify Vault3 and let people interact with it over a network, you must offer those users the source of your modified version (AGPL-3.0 §13). Running an unmodified copy triggers no obligation.
The jar bundles MilkBowl/VaultAPI
(net.milkbowl.vault.**), which is licensed LGPL-3.0. LGPL-3.0 is GPL-3.0 plus additional
permissions, so it combines cleanly into an AGPL-3.0 work — but that portion remains under its
own licence and its notice must be retained when redistributing this jar.
Other bundled components:
| Component | License |
|---|---|
| MilkBowl/VaultAPI | LGPL-3.0 |
| bStats | MIT |
| Libby | MIT |
- MilkBowl/VaultAPI — the economy, permission and chat interfaces. This API is the shared language of the entire Minecraft economy ecosystem; the fact that Vault3 can stand in for it without touching any other plugin rests entirely on that work.
- bStats — anonymous usage statistics.
- Libby — runtime dependency loading, which keeps HikariCP and the MySQL driver out of the jar.
- The author of the original Vault 2.0 economy plugin — the feature set and configuration structure originate there; the storage layer and transaction path were reimplemented on top of it. Thanks for allowing release under this name.


