Overview
AstralEconomy is the multi-currency economy plugin for the AstralRealms network. It provides player and town balances, an admin edit command, a player-to-player pay command, %economy_*% placeholders, and a developer-facing service for every other plugin that needs to move money.
The MariaDB database is the sole money-safety authority. Every balance mutation runs as one guarded SQL transaction (UPDATE accounts SET balance = balance + ? ... WHERE balance + ? >= 0) that atomically checks funds and applies the delta at the storage layer — there is no in-memory balance, no lease, and no write-behind. A negative balance is structurally impossible: an overdrawn withdrawal, /eco take, transfer leg, or SPI call simply fails the guard and the transaction reports INSUFFICIENT_FUNDS with nothing written.
Currencies
Currencies are entirely config-driven, defined in currencies.yml. Each entry is a display name, a symbol, a starting balance, a transferable flag, and an optional leaderboard id — and gets its own balance row per account (accounts is keyed on (id, currency), not just id). The shipped defaults:
id | name | symbol | default-balance | transferable | default | leaderboard-id |
|---|---|---|---|---|---|---|
| Coins |
| 1000 |
|
|
|
| Astralys |
| 0 |
| — | — |
Exactly one currency should be marked default: true — it is what /pay, /balance (no argument), and every no-currency SPI/API overload resolve to. See Currencies for the full config shape.
Core surfaces at a glance
Surface | Summary |
|---|---|
| Admin |
| Player-initiated transfer of the default currency, respecting the currency's |
| Self-service balance check — no argument reports the default currency, |
|
|
| The framework-wide, |
| This plugin's own static facade for pinning town balances into the cache. |
See Commands and Developer API for the full surface.
Reads: an in-memory cache, DB on miss
Balance reads are served from a Caffeine AsyncLoadingCache keyed on (uuid, currency), loading from the database on a miss. Two populations behave differently:
Pinned entries — online players (warmed on join, dropped on quit) and towns registered through
EconomyAPI.loadTown/loadTowns— never TTL-expire and carry cache weight0, so they are exempt from size-based eviction. They are refreshed in place after a local mutation or a cross-server hint, so a pinned account is never left cold.Everyone else (offline lookups) keeps the configured
cache.read-ttl(default5s) and is capped bycache.max-size(default500000) entries.
The synchronous placeholder/action-bar read path (cachedBalance) never blocks: it returns the cached value or zero, and self-heals a cold pinned account by kicking off an async re-warm in the background. Anything that needs a guaranteed-correct figure (/balance, /eco balance, the SPI's getBalance) goes through the asynchronous, authoritative balance()/lookup() path instead, which loads from the database on a miss.
Cross-server model
Each mutation (deposit, withdraw, set, transfer) is its own independent, retried database transaction — retried up to 4 attempts on a transient InnoDB deadlock/serialization failure or a MariaDB snapshot-isolation conflict (error 1020), and idempotent on its transaction id so a retried or replayed attempt never double-applies. On success the acting server:
Writes the just-committed
(balance, version)straight into its own cache (a version-guarded write-through — an out-of-order write can never clobber a fresher value already cached).Broadcasts a
BalanceInvalidatePacketon messaging channeleconomy.balance, carrying only the(uuid, currency)cache key — never an amount — so every other server drops (or, if pinned, reloads) that entry and re-reads the database before its own read-ttl would have expired it anyway.
The hint is best-effort: if it's lost, nothing is wrong — the receiving server simply keeps serving a stale cached value until its read-ttl naturally expires, at which point the next read re-loads from the database and self-corrects. No packet ever carries money; the database is always re-consulted for the real value.
Action-bar notifications
A player's net balance change is shown on their action bar, network-wide, regardless of which server actually applied the mutation. Each successful player-account deposit/withdraw/transfer leg (a set has no meaningful delta and never notifies) is handed to a debounce window per player: changes merge into one net figure per currency over a 500 ms window, then render as a single line (e.g. +1 200$ -50A$). If the player is online on the acting server the change accumulates locally; otherwise it is broadcast as a BalanceNotifyPacket on messaging channel economy.notify — carrying a delta for display only, never applied to a balance — so whichever server actually holds the player shows it. The rendered line is wrapped in an AstralHud background before being sent via sendActionBar.
Ledger & history
Every applied mutation writes one row (two, for a transfer — one debit, one credit) to transactions: the transaction id, the account and currency, the signed delta, the resulting balance_after, the acting server_id, and a timestamp. This is an append-only audit trail — it is never read to compute a balance and pruning it never touches accounts. A background task (TransactionCleanupTask) periodically deletes rows older than cleanup.retention (default 14d) every cleanup.interval (default 6h); the whole job can be turned off with cleanup.enabled: false. See Configuration.
Dependencies
Dependency | Required | Notes |
|---|---|---|
Paper 1.21+ | Yes |
|
AstralCore | Yes | Configuration loading, commands, placeholders, messaging, the |
AstralHud | Yes | Backgrounds for the action-bar balance-change notification. |
MariaDB / MySQL | Yes | Sole money-safety authority; connection configured in |
Where to go next
Installation — deploying the plugin and its database.
Configuration —
config.ymlcache and cleanup settings.Currencies — defining and tuning
currencies.yml.Commands —
/eco,/pay,/balancein full.Messages — the message-key catalogue.
Placeholders — the
%economy_*%namespace.Developer API —
EconomyService,EconomyAPI, and integrating from another plugin.