For the complete documentation index, see llms.txt. This page is also available as Markdown.

🖥️For Developers

NetworkEconomy exposes a stable, fully asynchronous API plus two Bukkit events. Everything in the com.ohalee.networkeconomy.api package is the public surface; anything outside it is internal and may change.

Getting the API

Depend on NetworkEconomy in your plugin.yml so load order is guaranteed:

depend: [NetworkEconomy]      # or softdepend if the hook is optional

Then grab the instance from your own onEnable:

NetworkEconomyAPI economy = NetworkEconomyProvider.get();

With softdepend, guard the call:

if (NetworkEconomyProvider.isAvailable()) {
    NetworkEconomyAPI economy = NetworkEconomyProvider.get();
}

The API is also registered with the Bukkit ServicesManager. get() throws IllegalStateException if NetworkEconomy is not loaded yet, and the provider is cleared when the plugin disables.

Threading model

economy.getBalance(uuid).thenAccept(balance -> {
    // off the main thread!
    Bukkit.getScheduler().runTask(plugin, () -> {
        player.sendMessage("You have " + balance);
    });
});

This is deliberate: all operations are backed by an authoritative SQL store using atomic conditional updates, which is exactly what makes concurrent writes from different network nodes safe.

Fast reads for UI

For scoreboards, tab lists and anything that needs a number now, use the cached read. It performs no I/O and returns empty if the player is not cached (normally meaning they are not on this server).

Currencies

Currency is immutable and equality is by id. Useful members:

Member
Description

getId()

Lowercase config key

getDisplayName() / getDisplayNamePlural()

Human-readable names

getSymbol() / isSymbolBefore()

Symbol and its position

getDecimals()

Fractional digits

getStartingBalance()

Balance for new accounts

isPayable()

Whether /pay accepts it

isDefault()

Whether this is the Vault-exposed currency

normalize(BigDecimal)

Round to this currency's precision (half-up)

format(BigDecimal)

Format with grouping and symbol, e.g. $1,250.00

Reading balances

Mutating balances

reason is stored on the transaction row (up to 128 characters) and shown in the history GUI. Use something identifiable — "shop:diamond_sword", "quest:reward:12" — so the audit log stays useful.

transfer moves money inside one database transaction: either both sides apply or neither does. Use it instead of a withdraw-then-deposit pair.

EconomyResult

Member
Description

status()

The outcome — see below

isSuccess()

true when the operation was persisted

newBalance()

Balance after the operation, or the current balance on failure

message()

Optional detail, null on success

Status
Meaning

SUCCESS

Completed and persisted

INSUFFICIENT_FUNDS

Not enough money for a withdrawal or transfer

ACCOUNT_NOT_FOUND

The player has never joined the network

CANCELLED

A PreTransactionEvent handler vetoed it

ERROR

Unexpected failure (SQL, connectivity, …)

History and listings

getHistory returns newest first. getTopBalances returns highest first and covers the whole network. lookupUuid resolves a name case-insensitively from the accounts table, so it works for any player who has ever joined — no Mojang API call, no main-thread stall.

TransactionRecord is a record with: id, player, currencyId, type, amount (always positive), balanceAfter, counterparty (nullable, transfers only), reason, serverId and timestamp.

BalanceEntry is a record with uuid, name (nullable) and balance.

TransactionType

Type
Meaning

DEPOSIT

Credit added (Vault deposit, plugin reward, …)

WITHDRAW

Debit removed (Vault withdraw, shop purchase, …)

TRANSFER_OUT

Outgoing side of a player-to-player transfer

TRANSFER_IN

Incoming side of a player-to-player transfer

SET

Balance overwritten to an absolute value

RESET

Balance reset to the currency's starting amount

Events

Both events are asynchronous — they fire off the main thread. Handlers must be thread-safe and must not touch the Bukkit API directly.

PreTransactionEvent

Fired before a change is committed, and cancellable. Cancelling aborts the operation with EconomyResult.Status.CANCELLED.

Exposes getPlayer(), getCurrency(), getType(), getAmount() and getReason().

BalanceUpdateEvent

Fired after a change has been committed and cached — including changes that originated on another network node and arrived over Redis. Informational; it cannot be cancelled.

Exposes getPlayer(), getCurrency(), getOldBalance(), getNewBalance(), getType() and isRemote().

isRemote() is the hook you want for keeping network-wide UI in sync: it lets you refresh a scoreboard when a player earns money on a server they are not currently on.

Using Vault instead

If you already target Vault, nothing changes — NetworkEconomy registers as the Vault Economy provider at the highest priority and your plugin becomes network-wide for free. Two caveats:

  • Vault exposes only the default currency.

  • Vault's API is synchronous, so the bridge blocks briefly (5-second timeout) on the underlying async operation to return an accurate response. Banks are not supported and return NOT_IMPLEMENTED.

Prefer the native async API for anything latency-sensitive.

Database schema

Read-only integrations can query the tables directly. All three use the configured table-prefix (default ne_) in place of {prefix}.

{prefix}accounts

Column
Type
Notes

uuid

CHAR(36) (PK)

Player unique id

name

VARCHAR(16)

Last known name, indexed (idx_name)

first_seen

BIGINT

Epoch millis

last_seen

BIGINT

Epoch millis

{prefix}balances

Column
Type
Notes

uuid

CHAR(36)

Part of PK (uuid, currency)

currency

VARCHAR(32)

Currency id

balance

DECIMAL(20,4)

Exact fixed-point balance

version

BIGINT

Monotonic, bumped on every change

Indexed by idx_currency_balance (currency, balance), which is what makes /baltop cheap.

{prefix}transactions

Append-only audit log — rows are never updated or deleted.

Column
Type
Notes

id

BIGINT (PK)

Auto-increment

uuid

CHAR(36)

Affected account

currency

VARCHAR(32)

Currency id

type

VARCHAR(16)

TransactionType name

amount

DECIMAL(20,4)

Always positive

balance_after

DECIMAL(20,4)

Balance immediately after the change

counterparty

CHAR(36)

Other party for transfers, else NULL

reason

VARCHAR(128)

Caller-supplied reason

server_id

VARCHAR(64)

Origin node's server-id

created_at

BIGINT

Epoch millis

Indexed by idx_uuid_time (uuid, created_at), idx_currency and idx_created.

Redis message format

Broadcasts are compact pipe-delimited strings on the configured channel — no JSON dependency:

reason is intentionally omitted to keep broadcasts small. Receivers apply an update only if its version is newer than the cached one, which makes delivery idempotent and out-of-order-safe, and skip messages stamped with their own serverId.

Testing notes

The plugin's own test suite runs the real storage layer against a real MariaDB and the real sync layer against a real Redis — no mocks, because the properties under test (atomicity, lock ordering, pub/sub delivery) only exist in a real engine.

Both integration classes skip themselves cleanly when no server is reachable, so the build still succeeds without a database. Override the endpoints with -Dne.test.host=…, -Dne.test.port=…, -Dne.test.redis.host=….

The concurrency tests hammer a single row from many threads as a stand-in for many network nodes, and are written so that replacing the atomic statements with naive read-modify-write logic makes them fail — verified by temporarily doing exactly that, which let 20 concurrent withdrawals of 10 all succeed against a balance of 100.

Last updated