Astral Realms Documentation Help

Currencies

AstralEconomy is multi-currency by design: every account row is keyed on (uuid, currency), not just uuid, and the entire currency catalogue is config-driven from currencies.yml. There is no code change needed to add, rename, or retire a currency — only a config edit and /eco reload (see Reloading).

currencies.yml structure

The file is a single currencies map, keyed by an internal id, whose values each deserialize to an EconomyCurrency record. It is loaded alongside config.yml on startup and re-read by /eco reload — see Configuration.

Shipped default:

currencies: default: id: "default" name: "Coins" symbol: "$" default-balance: 1000 transferable: true default: true leaderboard-id: "currency-default" astralys: id: "astralys" name: "Astralys" symbol: "A$" default-balance: 0 transferable: false

default is a transferable, leaderboard-tracked currency starting new accounts at 1000. astralys is a second, non-transferable currency starting at 0 with no leaderboard.

Currency fields

YAML key

Java field

Type

Required

Description

id

id

String

Yes

The currency's identifier, matched case-insensitively wherever a currency is looked up by id (/pay, /eco, /balance <currency>, the EconomyService SPI).

name

name

ComponentWrapper (MiniMessage)

Yes

Display name. Exposed as %currency_name%.

symbol

symbol

ComponentWrapper (MiniMessage)

Yes

Currency symbol. Exposed as %currency_symbol%; conventionally appended directly after a formatted amount (e.g. %amount%%currency_symbol%1 200$).

default-balance

defaultBalance

double

Yes (shipped file always sets it)

Starting balance minted into a newly provisioned account row for this currency — see default-balance.

transferable

transferable

boolean

Yes (shipped file always sets it)

Whether /pay may move this currency — see transferable.

default

defaultCurrency (@Setting("default"))

boolean

No — defaults to false when omitted

Marks this the network's default currency — see The default currency.

leaderboard-id

leaderboardId

String, nullable

No — defaults to null /absent when omitted

Pushes this currency's player-balance changes to an AstralCore leaderboard — see leaderboard-id.

astralys in the shipped file demonstrates that default and leaderboard-id can simply be left out of an entry rather than written as default: false/ with no leaderboard-id key.

The default currency

Exactly one currency should be marked default: true. CurrenciesConfiguration exposes two accessors over the catalogue:

  • findDefaultCurrency() returns an Optional<EconomyCurrency> — empty when no currency is marked default. This is the accessor every player-facing and SPI path uses, so a missing default degrades gracefully instead of throwing:

    • /balance (no argument) — warns the player with balance-no-default-currency and logs an error.

    • /pay — warns the sender with pay-no-default-currency and stops before touching the ledger.

    • Every no-currency overload of the EconomyService SPI (deposit(uuid, amount), withdraw(uuid, amount), getBalance(uuid), hasBalance(uuid, amount), transfer(from, to, amount), …) — resolves currency == null to the default id and fails closed (false/ empty / 0.0) rather than guessing when none is configured.

    • EconomyAPI.loadTown(s)/unloadTown(s) — each town pin operates on the default currency; with none configured, load* is a logged no-op and unload* does nothing.

  • defaultCurrency() returns the EconomyCurrency directly, throwing IllegalStateException("No default currency configured") when none exists. Reserve this for a path where a missing default is genuinely unrecoverable, not one that must degrade gracefully.

EconomyCurrency implements ComplexPlaceholder, so a currency (including the default) also exposes its default flag as %currency_default% — see Currencies as placeholders.

transferable

transferable gates only the player-initiated /pay command: PayCommand checks currency.transferable() and, if false, rejects the payment with pay-untransferable before any ledger call is made.

The admin /eco set//eco add|give//eco take subcommands call TransactionService directly and do not consult transferable at all — an operator can always move an untransferable currency. astralys in the shipped file is untransferable for exactly this reason: it is meant to move only through admin edits (or other plugins via the SPI, which likewise does not check transferable), never player-to-player.

leaderboard-id

When leaderboard-id is set and non-blank, every successful player-account mutation (deposit, withdraw, or either leg of a transfer — a set still updates the leaderboard the same way, since it goes through the same post-mutation hook) pushes the account's new balance to AstralCore's LeaderboardService, under that id, from the already-read post-mutation snapshot (no extra database read):

leaderboardService.update(account, current.leaderboardId(), state.balance().setScale(2, RoundingMode.DOWN).doubleValue());

Two cases are skipped before the leaderboard is ever touched:

  • Town accounts — the leaderboard update short-circuits on any account whose EconomyAccountType is not PLAYER, since a player leaderboard has no meaningful town entry.

  • Currencies with no leaderboard-id (null or blank) — astralys in the shipped file has none, so its balance changes never reach a leaderboard.

A leaderboard update failure is caught and logged as a warning; it never fails the underlying money mutation. The shipped default currency uses leaderboard-id: currency-default.

default-balance

default-balance is the balance minted into a brand-new account row the first time one is provisioned for that currency — never applied to an account that already has a row for it. It is used by:

  • ensureCurrencies(account) — called on player join/first-seen; creates a row for every configured currency the player doesn't already have, each at that currency's default-balance.

  • ensureCurrency(account, currencyId, type) — creates a single missing row (used, for example, when a town account is first provisioned for a specific currency) at that currency's default-balance.

With the shipped file, a new player account starts at 1000 default coins and 0 astralys; a currency added later only affects accounts created (or first-provisioned) after the change — existing account rows are never rewritten to match a new default-balance.

Currencies as placeholders

EconomyCurrency implements AstralCore's ComplexPlaceholder under namespace currency, so anywhere a currency is registered into a placeholder container (every /pay, /balance, and /eco message; the %economy_currency_<id>% chain point — see Placeholders) it exposes its fields as sub-keys:

Sub-key

Resolves to

id

The currency's id string.

name

The name component (MiniMessage-rendered).

symbol

The symbol component (MiniMessage-rendered).

defaultBalance

The default-balance double.

transferable

The transferable boolean.

default

The default boolean (defaultCurrency field).

leaderboardId

The leaderboard-id string, or null.

For example, %currency_symbol% inside a message resolves to the registered currency's symbol — this is how pay-untransferable and the *_SUCCESS/*_FAILURE admin-edit messages render A$ or $ without hardcoding it. See Placeholders for the full %economy_*% namespace this chains from.

Money precision

accounts.balance and transactions.delta are both DECIMAL(19,4) columns — every balance and every signed delta is exact to 4 decimal places, never a double at rest. TransactionService enforces this at the boundary between the plugin's double-based command/SPI surface and the ledger:

  • Deposit / withdraw — the requested magnitude is normalized with BigDecimal.setScale(4, RoundingMode.DOWN). A magnitude that is null, zero, or negative — or a sub-tick amount so small it rounds down to zero at 4 decimal places — is rejected outright as INVALID_AMOUNT before any ledger call. No transaction is ever created for it, so it can never mint or destroy money by rounding.

  • Set — the target balance is normalized the same way (setScale(4, RoundingMode.DOWN), never rounding up, since that would mint money on a set); a negative or null target is likewise rejected as INVALID_AMOUNT.

  • Transfer — the same magnitude validation as deposit/withdraw applies to the transferred amount before either leg is written.

This rounding is a property of every mutation, not of a specific currency — it applies identically to default, astralys, and any currency added later. See Overview for how the guarded transaction itself makes an overdraft structurally impossible.

Last modified: 25 July 2026