Consumer Guide
Use an economy from your plugin: deposit, withdraw, transfer, events.
This guide is for plugin developers who want to spend, check, or move money: shops, jobs, rewards, casinos, quests, anything that consumes an economy. If you are implementing the economy itself, see the Provider Guide.
1. Depend on the API
Add conduit-api as a compileOnly dependency. The actual classes are provided at runtime by the installed Conduit plugin, so you never bundle them.
repositories {
maven("https://repo.alaz.so/releases")
}
dependencies {
compileOnly("so.alaz.conduit:conduit-api:0.3.4")
}Want the latest beta?
Pre-release builds are published as snapshots in a separate repository. To use the bleeding-edge 0.3.4 build, point at the snapshots repo instead:
repositories {
maven("https://repo.alaz.so/snapshots")
}
dependencies {
compileOnly("so.alaz.conduit:conduit-api:0.3.4")
}Declare Conduit as a dependency in your paper-plugin.yml:
name: MyPlugin
version: "1.0.0"
main: com.example.MyPlugin
api-version: "26.1"
dependencies:
server:
Conduit:
load: BEFORE
required: true # use false if your plugin should still load without an economy2. Resolve the economy
You almost never need the onEnable dance Vault forced on you. Use whenProviderAvailable: it runs your code as soon as a provider exists, whether that is before or after your plugin enables.
import so.alaz.conduit.api.Conduit;
import so.alaz.conduit.api.economy.Economy;
Conduit.whenProviderAvailable(Economy.class, economy -> {
// economy is ready and dispatch-decorated (validation + events + interceptors)
});Other resolution options:
Conduit.getEconomy(); // active Economy, or throws ProviderNotFoundException
Conduit.findEconomy(); // Optional<Economy>, empty if none registered
Conduit.isInitialized(); // false before Conduit enables / after it disablesUse getEconomy() when an economy is mandatory and you want to fail loudly. Use findEconomy() when you can degrade gracefully. Use whenProviderAvailable(...) in startup paths to avoid any ordering assumptions.
3. Move money and handle the result
Every mutation returns CompletableFuture<EconomyResult>. EconomyResult is sealed, so you handle each case explicitly.
import so.alaz.conduit.api.result.EconomyResult;
import java.math.BigDecimal;
economy.deposit(playerUuid, new BigDecimal("100.00"), "daily reward")
.thenAccept(result -> {
switch (result) {
case EconomyResult.Success s ->
getLogger().info("New balance: " + economy.format(s.newBalance()));
case EconomyResult.InsufficientFunds f ->
getLogger().warning("Had " + f.balance() + ", needed " + f.requested());
case EconomyResult.AccountNotFound nf ->
getLogger().warning("No account for " + nf.uuid());
case EconomyResult.CurrencyNotSupported c ->
getLogger().warning("Unsupported currency: " + c.currency().id());
case EconomyResult.Rejected r ->
getLogger().info("Vetoed by policy: " + r.reason());
case EconomyResult.ProviderError e ->
getLogger().severe("Backend error: " + e.message());
}
});If you only care about success:
economy.deposit(uuid, amount, "reward")
.thenAccept(r -> r.ifSuccess(s ->
player.sendMessage("You received " + economy.format(amount))));r.success() returns Optional<EconomyResult.Success> if you prefer that style.
Reducing boilerplate in your own plugin
The resolve-and-handle pattern is worth wrapping once in your plugin so call sites stay short. Keep the wrapper async and BigDecimal (never add a blocking or double variant):
public final class Econ {
public static CompletableFuture<EconomyResult> deposit(UUID uuid, BigDecimal amount, String reason) {
CompletableFuture<EconomyResult> out = new CompletableFuture<>();
Conduit.whenProviderAvailable(Economy.class, economy ->
economy.deposit(uuid, amount, reason).whenComplete((r, err) -> {
if (err != null) out.completeExceptionally(err); else out.complete(r);
}));
return out;
}
}Then call Econ.deposit(uuid, amount, "reward").thenAccept(...) everywhere.
4. The operations you have
On the base Economy:
| Operation | Returns | Notes |
|---|---|---|
hasAccount(uuid) | CompletableFuture<Boolean> | |
createAccount(uuid) / deleteAccount(uuid) | EconomyResult | |
getBalance(uuid) | CompletableFuture<Balance> | Balance(owner, currency, amount) |
getBalances(collection) | CompletableFuture<Map<UUID,Balance>> | batch read |
deposit(uuid, amount[, reason]) | EconomyResult | amount > 0 |
withdraw(uuid, amount[, reason]) | EconomyResult | amount > 0 |
set(uuid, amount) | EconomyResult | amount >= 0 |
transfer(from, to, amount[, reason]) | EconomyResult | atomic, amount > 0 |
canDeposit/canWithdraw(uuid, amount) | CompletableFuture<Boolean> | requires ECONOMY_PREFLIGHT |
format(amount) | String | currency-aware formatting |
Prefer transfer over manual withdraw-then-deposit. transfer is atomic: the classic "withdraw succeeded, deposit failed, money vanished" bug cannot happen.
5. Use capabilities before calling gated methods
import so.alaz.conduit.api.capability.Capability;
if (economy.supports(Capability.ECONOMY_PREFLIGHT)) {
economy.canWithdraw(uuid, price).thenAccept(ok -> { /* ... */ });
}For structural features, ask the registry:
import so.alaz.conduit.api.economy.BankingEconomy;
Conduit.getRegistry().getProvider(BankingEconomy.class)
.ifPresent(bank -> bank.getBankBalance("spawn_bank").thenAccept(...));See Capabilities and Extension Interfaces.
6. React to money movement with events
Conduit fires post-commit events. Listen to them like any Bukkit event:
@EventHandler
public void onTxn(EconomyTransactionEvent event) {
Transaction t = event.transaction();
// analytics, logging, quest progress, achievements...
}Events are non-cancellable: the money already moved. To block an operation before it happens, register an interceptor instead.
7. Attribute your plugin (optional but recommended)
Bind a CallerToken so events and audits know the money came from you:
CallerToken token = Conduit.getRegistry().registerCaller(this); // once, in onEnable
CallerToken.runWith(token, () -> economy.deposit(uuid, amount, "shop sale"));Without this, your transactions are attributed to CallerToken.ANONYMOUS. See Caller Identity for the threading rules.
8. Threading reminders
- Continuations on the default
CompletableFutureexecutor are fine; do not block them with.join()in production. - If you hop to your own executor with
thenApplyAsync(fn, myExecutor), theCallerTokendoes not propagate automatically. Rebind withCallerToken.runWith(...)or useCallerToken.propagating(executor). - Bukkit API calls (sending messages, world edits) must happen on the main/region thread. Marshal back with your scheduler before touching the world from a future callback.
Where to next
- How-To Recipes: concrete "how do I X" answers.
- Migrating from Vault: side-by-side porting.
- API Reference: every type at a glance.