Compatibility
Minecraft: Java Edition
Platforms
Links
Tags
Creators
Details
MultiBank
A secure, feature-rich economy plugin for Paper/Purpur 26.3 servers (2.0.0 runs on 26.2, 1.x on 1.21.x).
MultiBank replaces EssentialsX Economy and acts as your server's main economy system, with GUI support, multi-account management, a Vault bridge for legacy plugins, and a clean developer API.
Features
- Multi-account system: Personal + Group/Joint accounts with roles (OWNER, MANAGER, MEMBER, VIEWER)
- GUI interface: Full-featured inventory GUI via
/mb— pay players, manage accounts, view history, accept invites, view top balances - Vault bridge: Legacy plugins (shops, auction houses, etc.) work seamlessly via Vault Economy provider
- Developer API: Exposed via Bukkit
ServicesManagerwith asyncCompletableFuturemethods - Custom Events:
BalanceChangeEvent,TransferEvent,AccountCreateEvent,PrimaryAccountChangeEvent,AccountFrozenEvent - Atomic transactions: No duplication, no double-spend, no race conditions — all DB operations are transactional
- SQLite/MySQL: Configurable storage backend with HikariCP connection pooling
- Localization: English (
en_US) + Portuguese (pt_PT), easily extensible - Rate limiting: Configurable per-player operation throttling
- Long cents storage: Balances stored as
longcents internally — no floating-point precision loss
Banking modules
Four optional modules, on by default, each switchable in config.yml without losing its data.
- Savings accounts (
/poupanca): interest paid on the lowest balance held during the period, so money parked a moment before the tick earns nothing. - Term deposits (
/poupanca prazo): capital locked in escrow for a fixed term and out of the player's balance, at several times the open savings rate. The accruing interest stays hidden until it matures — and cancelling early returns exactly what went in, to the cent. - Loans (
/emprestimo): borrow from the State, or from other players through an offer board where capital is escrowed at offer time and acceptance is a database compare-and-swap. Defaulting hands the balance to garnishment, which collects it from the borrower's future income. - Certificates (
/certificados): savings and treasury certificates modelled on the Portuguese instruments. Subscribing lends money to the State, which puts it to work. - The State (
/estado): a public treasury balance sheet and the community projects it funds. Delivering a project destroys the money — the economy's one deliberate sink.
No module creates currency. Every credit has a matching debit: interest is paid out of the
treasury, loans move treasury or escrow money, and if the State cannot afford a payment it is not
made rather than minted. /mb admin audit reconciles the escrow accounts against the contracts they
back and reports any drift.
Installation
- Download
MultiBank-2.1.0+mc26.3.jar - Place it in your server's
plugins/folder - (Optional) Place
Vault.jarinplugins/for legacy plugin support - Start the server —
config.ymlandlang/files will be generated - Edit
plugins/MultiBank/config.ymlto customize currency, database, pay limits, etc. - Use
/mb admin reloadto apply config changes without restart
Commands
Player Commands
| Command | Aliases | Description |
|---|---|---|
/mb |
— | Opens the main GUI |
/money [player] |
/bal, /saldo, /balance |
Check balance (primary account) |
/pay <player> <amount> [fromAccount] |
/pagar |
Pay another player |
/pay confirm |
— | Confirm a pending large payment |
/pay cancel |
— | Cancel a pending payment |
/topbal [page] |
/top, /baltop, /topmoney |
View richest accounts |
Account Management (/mb account)
| Command | Description |
|---|---|
/mb account list |
List all your accounts |
/mb account info <accountId> |
View account details |
/mb account create <name> |
Create a group/joint account |
/mb account rename <accountId> <name> |
Rename an account |
/mb account setprimary <accountId> |
Set your primary account |
/mb account invite <accountId> <player> |
Invite a player to your group account |
/mb account accept [inviteId] |
Accept an invite (latest if no ID) |
/mb account kick <accountId> <player> |
Remove a member from your group account |
/mb account role <accountId> <player> <OWNER|MANAGER|MEMBER|VIEWER> |
Change a member's role |
/mb account leave <accountId> |
Leave a group account (cannot leave if owner) |
Savings and term deposits (/poupanca, /savings)
| Command | Description |
|---|---|
/poupanca |
Savings and term deposit GUI |
/poupanca abrir [name] |
Open a savings account |
/poupanca depositar <amount> [savingsId] |
Move money into savings |
/poupanca levantar <amount> [savingsId] |
Take money out |
/poupanca info |
Your accounts, interest base and time to the next payment |
/poupanca taxa |
The current rate and how it is calculated |
/poupanca prazo tipos |
Term deposits on offer |
/poupanca prazo abrir <type> <amount> |
Lock capital away for a fixed term |
/poupanca prazo |
Your locked positions |
/poupanca prazo cancelar <id> |
Cancel or collect — capital always returned in full |
Interest is paid on the lowest balance held during the period, so capital that was not at risk for the whole period earns nothing. A term deposit's accrued interest is hidden until it matures.
Loans (/emprestimo, /loan)
| Command | Description |
|---|---|
/emprestimo |
Credit dashboard |
/emprestimo simular <amount> <periods> |
Quote a State loan before committing |
/emprestimo pedir <amount> <periods> |
Borrow from the State |
/emprestimo oferecer <amount> <rate%> <periods> |
Offer a loan to other players (escrows the capital) |
/emprestimo mercado |
Browse open offers |
/emprestimo aceitar <offerId> |
Take an offer |
/emprestimo cancelar <offerId> |
Withdraw your own offer |
/emprestimo pagar <loanId> [amount] |
Repay; no amount settles the balance |
/emprestimo meus |
Loans you owe and loans you granted |
/emprestimo credito |
Your score, record and credit line |
Instalments are collected automatically. Missing too many defaults the loan, and the balance is then collected as a share of the borrower's incoming money until it is cleared.
Certificates (/certificados, /certs)
| Command | Description |
|---|---|
/certificados |
Certificate GUI |
/certificados series |
Series on offer, with rates and terms |
/certificados subscrever <series> <amount> |
Lend the amount to the State |
/certificados resgatar <certId> |
Redeem |
/certificados meus |
Your holdings |
The State (/estado, /state)
| Command | Description |
|---|---|
/estado |
Treasury and projects GUI |
/estado info |
Balance, obligations, what is lent out, solvency |
/estado projetos |
Community projects and their progress |
/estado propor <goal> <name…> |
Propose a project |
/estado votar <projectId> |
Vote for a proposal |
/estado financiar <projectId> <amount> |
Contribute |
Admin Commands (/mb admin)
| Command | Description | Permission |
|---|---|---|
/mb admin give <player|accountId> <amount> [reason] |
Give money | multibank.admin.give |
/mb admin take <player|accountId> <amount> [reason] |
Take money | multibank.admin.take |
/mb admin set <player|accountId> <amount> [reason] |
Set balance | multibank.admin.set |
/mb admin freeze <player|accountId> <on|off> |
Freeze/unfreeze account | multibank.admin.freeze |
/mb admin reload |
Reload config & lang files | multibank.admin.reload |
/mb admin treasury info |
Treasury balance sheet | multibank.admin.treasury |
/mb admin treasury grant <amount> [reason] |
Seed the treasury (mints; recorded as such) | multibank.admin.treasury |
/mb admin treasury burn <amount> [reason] |
Destroy treasury money | multibank.admin.treasury |
/mb admin project open <goal> <name…> |
Open a project for contributions | multibank.admin.project |
/mb admin project complete <id> |
Mark delivered — spends and destroys the money | multibank.admin.project |
/mb admin project cancel <id> |
Cancel and refund every contributor | multibank.admin.project |
/mb admin audit |
Reconcile escrow against contracts; report drift | multibank.admin.audit |
Console and RCON replies for these are written to the server log: the lookups answer from a database thread, by which time RCON has already closed out the request.
The treasury starts empty. Until it holds money it cannot lend or pay interest — either seed it with
/mb admin treasury grant, or let it fill from certificate subscriptions and loan fees.
OP Override Commands (/mb op)
These commands are restricted to server OPs (isOp()) and console only — no permission nodes, pure OP check.
They always target the player's personal account, regardless of their current primary account setting.
| Command | Description | Example |
|---|---|---|
/mb op set <player> <amount> |
Hard-set a player's personal balance | /multibank op set username 4000 |
/mb op give <player> <amount> |
Give money to a player's personal account | /mb op give username 250 |
/mb op take <player> <amount> |
Take money from a player's personal account | /mb op take username 100 |
Rules:
- Amount supports decimals (e.g.,
4000.50), converted to cents internally - Works for offline players (UUID lookup via
Bukkit.getOfflinePlayer) - All operations are atomic (single DB transaction)
- Every action is logged to console and transaction history with reason
"OP override" - Take behavior when balance would go negative is configurable:
clamp-negative-to-zero: true(default) — takes only what the player has, balance becomes 0clamp-negative-to-zero: false— denies the operation entirelyallow-negative-after-take: true— allows negative balance (overrides clamp)
Examples:
/multibank op set Steve 4000 → Sets Steve's personal balance to $4,000.00
/mb op give Alex 250.50 → Gives Alex $250.50 into personal account
/mb op take Notch 100 → Takes $100.00 from Notch's personal account
Permissions
| Permission | Description | Default |
|---|---|---|
multibank.use |
Use /mb GUI and basic commands |
All players |
multibank.balance |
Check balances | All players |
multibank.pay |
Use /pay |
All players |
multibank.top |
View top balances | All players |
multibank.account.create |
Create group accounts | All players |
multibank.admin |
Access admin subcommands | OP only |
multibank.admin.give |
/mb admin give |
OP only |
multibank.admin.take |
/mb admin take |
OP only |
multibank.admin.set |
/mb admin set |
OP only |
multibank.admin.freeze |
/mb admin freeze |
OP only |
multibank.admin.reload |
/mb admin reload |
OP only |
multibank.savings |
Savings accounts and term deposits | All players |
multibank.loan |
Loan commands and GUI | All players |
multibank.loan.borrow |
Take a loan or accept an offer | All players |
multibank.loan.lend |
Post a lending offer | All players |
multibank.cert |
Certificates | All players |
multibank.state |
View the treasury and projects | All players |
multibank.project.vote |
Vote on proposed projects | All players |
multibank.project.propose |
Propose a project | All players |
multibank.admin.treasury |
/mb admin treasury |
OP only |
multibank.admin.project |
/mb admin project |
OP only |
multibank.admin.audit |
/mb admin audit |
OP only |
Nodes are registered in code at enable time. Paper ignores the permissions: block of
paper-plugin.yml, and an unregistered node silently defaults to OP — which is what had made
multibank.account.create staff-only despite the config implying otherwise.
Roles (Group Accounts)
| Role | Deposit | Withdraw | Transfer | Invite | Kick | Set Roles | Rename | Set Primary | View |
|---|---|---|---|---|---|---|---|---|---|
| OWNER | |||||||||
| MANAGER | |||||||||
| MEMBER | |||||||||
| VIEWER |
Vault Bridge
MultiBank automatically registers as a Vault Economy provider if Vault is present on the server. This means any plugin that uses Vault's Economy API (shops, auction houses, job plugins, etc.) will transparently use MultiBank balances.
How it works:
Economy.getBalance(player)→ reads the player's personal account balanceEconomy.depositPlayer(player, amount)→ deposits into personal account (atomic)Economy.withdrawPlayer(player, amount)→ withdraws from personal account (atomic)Economy.has(player, amount)→ checks personal account balanceEconomy.createPlayerAccount(player)→ auto-creates personal account with starting balance- Bank operations → return
NOT_IMPLEMENTED(use MultiBank's group accounts instead)
Startup logs:
"Vault bridge enabled — MultiBank is now the economy provider."— success"Vault not found; bridge disabled."— Vault not on server (this is fine)
Configuration: set vault.enabled: false in config.yml to disable the bridge.
Thread safety: All Vault operations use timeout-guarded async DB calls. The bridge is safe to call from any thread.
Developer API
Package
pt.henrique.multibank.api.MultiBankApi
Events are in:
pt.henrique.multibank.api.event.*
Getting the API
import pt.henrique.multibank.api.MultiBankApi;
// In your plugin's onEnable():
MultiBankApi api = getServer().getServicesManager().load(MultiBankApi.class);
if(api ==null){
getLogger().
warning("MultiBank not found! Economy features disabled.");
return;
}
API Methods
All methods return CompletableFuture for async execution. Chain with .thenAccept() / .thenCompose() for non-blocking use.
Account Resolution
CompletableFuture<Account> getPersonalAccount(UUID playerId)
CompletableFuture<Optional<Account>> getAccountById(String accountId)
CompletableFuture<List<Account>> listAccounts(UUID playerId)
CompletableFuture<Account> getPrimaryAccount(UUID playerId)
CompletableFuture<Void> setPrimaryAccount(UUID playerId, String accountId)
Balance Operations
CompletableFuture<Long> getBalanceCents(String accountId)
CompletableFuture<TransactionResult> deposit(String accountId, long cents, String reason, UUID actor)
CompletableFuture<TransactionResult> withdraw(String accountId, long cents, String reason, UUID actor)
CompletableFuture<TransactionResult> transfer(String fromAccountId, String toAccountId, long cents, String reason, UUID actor)
Group Management
CompletableFuture<String> createGroupAccount(UUID owner, String name)
CompletableFuture<Void> inviteMember(String accountId, UUID inviter, UUID target)
CompletableFuture<Void> setMemberRole(String accountId, UUID actor, UUID target, Role role)
Security
boolean isFrozen(String accountId)
Example Usage
// Get a player's primary balance
api.getPrimaryAccount(playerUuid).thenAccept(account -> {
long cents = account.getBalanceCents();
getLogger().info("Balance: " + (cents / 100.0));
});
// Deposit money
api.deposit(accountId, 5000L, "Shop purchase refund", actorUuid)
.thenAccept(result -> {
if (result.isSuccess()) {
getLogger().info("Deposit OK, txId=" + result.transaction().txId());
}
});
// Transfer between accounts (atomic)
api.transfer(fromId, toId, 10000L, "Trade deal", actorUuid)
.thenAccept(result -> {
if (result.isSuccess()) {
// Transfer complete — both balances updated atomically
} else {
// result.status() tells you why: INSUFFICIENT_FUNDS, ACCOUNT_FROZEN, etc.
}
});
// Create a group account
api.createGroupAccount(ownerUuid, "My Faction Bank").thenAccept(accountId -> {
getLogger().info("Created group account: " + accountId);
});
// Check if an account is frozen
boolean frozen = api.isFrozen(accountId);
Custom Events
All events are in pt.henrique.multibank.api.event and are fired on the main server thread.
| Event | Cancellable | Fields |
|---|---|---|
BalanceChangeEvent |
accountId, oldBalance, newBalance, amount, actor, reason |
|
TransferEvent |
fromAccountId, toAccountId, amount, actor, reason |
|
AccountCreateEvent |
accountId, type, owner, name |
|
PrimaryAccountChangeEvent |
playerId, oldAccountId, newAccountId |
|
AccountFrozenEvent |
accountId, frozen, actor |
import pt.henrique.multibank.api.event.BalanceChangeEvent;
@EventHandler
public void onBalanceChange(BalanceChangeEvent event) {
getLogger().info("Account " + event.getAccountId()
+ " balance changed by " + event.getAmount()
+ " cents. Reason: " + event.getReason());
}
Adding MultiBank as a Dependency
In your plugin's build.gradle.kts:
repositories {
// Add the repo where MultiBank is hosted, or use a local jar
}
dependencies {
compileOnly(files("libs/MultiBank-1.0.0.jar"))
}
In your paper-plugin.yml:
dependencies:
server:
MultiBank:
load: BEFORE
required: true # or false if optional
join-classpath: true
Configuration
See plugins/MultiBank/config.yml for all options:
| Section | Key options |
|---|---|
language |
en_US, pt_PT (or any custom lang file) |
starting-balance |
Starting money for new accounts (default: 100.00) |
currency.* |
Symbol, position, decimals, separators, name |
database.* |
sqlite (default) or mysql with host/port/credentials |
pay.* |
Enable/disable, min/max amounts, confirmation threshold |
accounts.* |
Overdraft, invite expiry, max group accounts |
admin.op-override.* |
Enable/disable OP override, allow console, negative balance rules |
vault.enabled |
Enable/disable Vault bridge |
top.* |
Mode (personal/all/primary), page size |
security.* |
Rate limits, GUI debounce |
Database Schema
| Table | Purpose |
|---|---|
accounts |
Account ID, type, owner UUID, name, frozen flag, created timestamp |
balances |
Account ID → balance in cents (long) |
account_members |
Account ID + member UUID + role |
primary_accounts |
Player UUID → their primary account ID |
invites |
Invite ID, account, target, inviter, expiry |
transactions |
Full audit log: txId, timestamp, type, from/to accounts, amount, actor, reason, success |
schema_version |
Migration bookkeeping; upgrades are additive and idempotent |
savings_state |
Per-account accrual watermark, low-water balance and interest earned |
interest_accruals |
One row per (account, period) credited — the idempotency key for savings |
loan_offers |
Peer-to-peer offers and their escrow state |
loans |
Contracts: principal, outstanding, arrears, rate, term, schedule position, status |
loan_charges |
One row per (loan, period) charged — the idempotency key for instalments |
garnishments |
Defaulted balances being collected from a debtor's income |
credit_profiles |
Repayment record and score |
certificates |
Subscribed certificates and term deposits, with their custody |
cert_accruals |
One row per (certificate, period) accrued |
projects, project_contributions, project_votes |
Community projects, contributions and votes |
Security Notes
- Atomic transactions: All economy operations (deposit, withdraw, transfer, set) use database transactions — no partial updates possible
- No double-spend: Transfer operations debit and credit both accounts in a single atomic transaction
- SQLite WAL mode: Enabled for concurrent read/write safety with busy timeout
- Rate limiting: Configurable per-player operation throttle (default: 30/min) prevents abuse
- GUI debouncing: Click debounce (default: 250ms) prevents double-click exploits
- Frozen accounts: Cannot send or receive any funds until unfrozen by admin
- UUID-based identity: All internal operations use UUID — never trusts player names
- Long cents storage: Balances stored as
longcents — no floating-point precision issues - Transaction audit log: Every operation generates a unique
txIdUUID and is logged with actor, reason, and timestamp - Non-negative enforcement: Balances cannot go negative unless
allow-overdraftis enabled - Vault timeout safety: Vault bridge operations use 10-second timeout to prevent server hangs
- In-database arithmetic: every balance change is computed inside the SQL statement under the row's write lock. A read-modify-write in Java lets two threads observe the same balance and both write — the lost-update duplication this plugin was patched for
- Idempotent accrual: interest and instalments claim their period through a primary key, so a scheduler misfire, a bulk catch-up or a manual re-run pays each period exactly once
- Monotonic time: accrual watermarks advance by whole periods from their previous value, never to the wall clock; a clock moved backwards yields zero periods, and catch-up is clamped
- Escrow before commitment: a lending offer debits the lender when it is posted, so it can never be backed by money since spent, and one balance cannot back two offers
- Compare-and-swap contracts: accepting an offer, redeeming a certificate and completing a project all flip a row from an expected state; concurrent attempts produce one winner
- Anti-arbitrage validation: a configuration where an interest-bearing product out-earns the cost of borrowing disables that product at startup rather than being accepted, and holding debt blocks investing — which also closes the 0%-loan-from-a-friend variant
- Ledger reconciliation:
/mb admin auditchecks each escrow account against the contracts it backs and reports drift, which is the footprint any duplication bug leaves behind
Building from Source
Two targets from one source tree:
git clone <repo-url>
cd MultiBank
./gradlew build # 26.3 (default) — runs the test suite
./gradlew -Pmc=262 build # 26.2
./gradlew -Pmc=1211 build # 1.21.11
Output: build/libs/MultiBank-<version>+mc26.3.jar (or +mc26.2, +mc1.21.11).
Every target emits Java 21 bytecode. The 26.x APIs are compiled to class file 69 and can only be
read by JDK 25, hence the JDK 25 toolchain; put one in /opt/jdk-25 or adjust
org.gradle.java.installations.paths.
The tests exercise the banking logic against a real SQLite database, including a randomised 400-operation run that asserts the money supply never changes.
Requirements
- Paper or Purpur 26.3 (2.0.0 for 26.2, 1.x for 1.21.x)
- Java 25
- (Optional) VaultUnlocked for legacy plugin economy support
License
GPL-3.0 — see LICENSE.


