Developer API
On this page(10)
SnCredits exposes a public developer API for other plugins. On the Velocity proxy you get cancellable
events, a query service and price modifiers. On Paper backends you get cancellable menu events,
notifications, price modifiers and the cached balances. Both live in the com.sn.credits.api package
inside the plugin jar. There is no separate artifact.
Only com.sn.credits.api and its subpackages are a stable, kept contract. Everything else in the jar
is obfuscated and internal. Do not call it.
Two surfaces
Every balance, shop, discount and coinflip lives on the proxy. That is where the full API lives too.
| Surface | Platform | Package | What it offers |
|---|---|---|---|
| Proxy API | Velocity | com.sn.credits.api | Cancellable events, the query service and price modifiers. The source of truth. |
| Bridge API | Paper | com.sn.credits.api.bridge | Cancellable menu events, notifications, price modifiers, and the balance and coinflip stats the proxy last pushed for online players. |
/credits pay and creating a coinflip run on the proxy, so only a proxy plugin can cancel them. The
shop and coinflip menus run on the backends, so a Paper plugin can cancel those clicks before they
reach the proxy.
Getting the jar
-
Sign in and download the latest SnCredits jar from SnDevelopment.
-
Install it into your local Maven repository:
mvn install:install-file -Dfile=SnCredits-<version>.jar -DgroupId=com.sn \ -DartifactId=SnCredits -Dversion=<version> -Dpackaging=jar -
Depend on it with
providedscope. Never shade it.
Quick start (Velocity)
Declare the dependency in your velocity-plugin.json:
"dependencies": [{ "id": "sncredits", "optional": true }]Resolve the API when you need it:
SnCreditsAPI api = SnCreditsProvider.get();
if (api != null) {
double balance = api.getBalance(player.getUniqueId());
}SnCreditsProvider.get() returns null while SnCredits is still booting, after it shut down, or when
its license is refused. SnCreditsProvider.isAvailable() answers the same question.
Resolve the reference when you need it. Do not cache it: SnCredits registers it once its database is ready and withdraws it on shutdown.
Master switch
The server owner can turn the events off with api-events.enabled: false in the proxy config.yml.
The proxy pushes the switch to every bridge, so it turns off the Paper events too. Every action then
proceeds as if no plugin had cancelled it. The query service and the price modifiers stay active
either way.
Events
Every event fires on the proxy before anything changes. SnCredits waits for all listeners, then continues. Denying the result aborts the action.
| Event | Fired when | Cancel effect |
|---|---|---|
CreditsPayEvent | A player runs /credits pay, after the amount, target and balance checks pass | No credits move and nothing is logged |
ShopPurchaseEvent | A player confirms a shop purchase, after the prerequisite and balance checks pass | Nothing is charged, recorded or executed |
CoinflipCreateEvent | A player opens a coinflip from the command or the menu, after the bet checks pass | The bet is not taken and no coinflip opens |
CoinflipAcceptEvent | A player accepts a coinflip from the menu, after the balance check passes | No credits move and the coinflip stays open |
Payloads:
| Event | Getters |
|---|---|
CreditsPayEvent | getSender(), getReceiver() (both online Players), getAmount() |
ShopPurchaseEvent | getPlayer(), getServer(), getCategoryId(), getItemId(), getItemName(), getBasePrice(), getPrice() (the price about to be charged, after discounts and price modifiers) |
CoinflipCreateEvent | getPlayer(), getAmount() |
CoinflipAcceptEvent | getAcceptor(), getCoinflip() (a CoinflipView) |
Listen like any Velocity event. Each event implements ResultedEvent<GenericResult>, and
setCancelled(true) is a shorthand for setResult(GenericResult.denied()):
@Subscribe
public void onPurchase(ShopPurchaseEvent event) {
if (event.getPrice() > 50_000 && !event.getPlayer().hasPermission("myplugin.bigspender")) {
event.setCancelled(true);
event.getPlayer().sendMessage(Component.text("You cannot buy items that expensive."));
}
}A cancelled action ends silently. SnCredits sends the player no message, so your plugin should tell the player why.
Event payloads are read-only. Cancelling is the only change a listener can make. Purchases in admin bypass mode charge nothing and fire no event.
Query service (Velocity)
Synchronous methods read in-memory snapshots. They are safe from any thread.
| Method | Returns | Notes |
|---|---|---|
getBalance(UUID) | double | Cached balance of a player loaded on the proxy. 0 when the player is not loaded |
getTopEntries() | List<LeaderboardEntryView> | The cached leaderboard, best first. Exempt players are left out |
getRank(UUID) | int | 1-based rank, or -1 when the player is not on the cached leaderboard |
getShopServers() | Set<String> | Servers that have their own shop file |
getShop(String server) | Optional<ShopView> | Categories and items of one shop. Empty when the server has no shop file |
getEffectivePrice(server, categoryId, itemId) | OptionalDouble | The current price after the biggest active discount. Empty when the item does not exist |
getEffectivePrice(Player, server, categoryId, itemId) | OptionalDouble | The price for one player: the discount, then every proxy price modifier |
registerPriceModifier(plugin, modifier) | void | Adds a proxy price modifier. See Price modifiers |
unregisterPriceModifier(modifier) | void | Removes it |
getDiscounts() | List<DiscountView> | Every active discount |
getActiveCoinflips() | List<CoinflipView> | Coinflips waiting for an opponent |
getApiVersion() | String | The API contract version |
Asynchronous methods return CompletableFuture and complete on a SnCredits database thread.
| Method | Returns | Notes |
|---|---|---|
getBalanceAsync(UUID) | CompletableFuture<Double> | Any player, online or offline. Reads the cache first, then the database |
getRecentTransactions(UUID, int limit) | CompletableFuture<List<TransactionView>> | Newest first. limit is clamped to 1..100 |
Never block on these futures with join() or get() inside an SnCredits event listener. The SQLite
backend uses a single database thread, so blocking there can freeze the plugin. Chain with
thenAccept instead.
Views are immutable records:
| View | Components |
|---|---|
LeaderboardEntryView | rank, uuid, username, balance |
TransactionView | id, uuid, type (a name such as PURCHASE), amount, balanceAfter, details, timestamp |
ShopView | server, categories |
ShopCategoryView | id, displayName, items |
ShopItemView | id, categoryId, displayName, material, price (before discount), requiresAny |
DiscountView | id, server (* for every server), category, item, percent, expiresAt (0 when permanent) |
CoinflipView | id, creatorUuid, creatorName, amount, createdAt |
Treat unknown transaction type names gracefully. A future version may add new ones.
Price modifiers
A price modifier changes what a shop item costs for one player. Register it from a proxy plugin, from a Paper plugin, or from both:
// Velocity: 20% off for MVPs on every server
api.registerPriceModifier(this, ctx ->
ctx.getPlayer().hasPermission("rank.mvp") ? ctx.percentOff(20) : ctx.getPrice());
// Paper: another 10% off on this backend during an event
bridge.registerPriceModifier(this, ctx -> eventRunning ? ctx.percentOff(10) : ctx.getPrice());The price is built in this order:
- The configured price.
- The biggest active shop discount.
- Every proxy modifier, in registration order. The confirm menu receives this price.
- Every bridge modifier on the player's backend, in registration order.
The proxy charges the final price. The menus mark it as a discount when it is below the configured price.
| Context getter | What it holds |
|---|---|
getPlayer() | The player the price is for (a Velocity or a Bukkit Player) |
getServer() | The server whose shop holds the item (proxy context only) |
getCategoryId(), getItemId() | The item |
getBasePrice() | The configured price |
getPrice() | The current price in the chain |
percentOff(int) | The current price minus that percent, floored |
Rules:
- Return
ctx.getPrice()to leave the price unchanged. - Results are floored to whole credits and never go below
0. - A modifier that throws or returns a non-finite value is skipped and logged once.
- Modifiers must be fast and must never block. Proxy modifiers can run on a database thread; bridge modifiers run on the main thread.
- A Paper plugin's modifiers are removed when that plugin disables.
The category menu shows server-wide prices, so proxy modifiers only show from the confirm menu on. Bridge modifiers show in both menus.
The proxy charges the price the confirm menu showed. If a discount or a proxy modifier changes while the menu is open, the purchase is not charged and the confirm menu reopens with the new price.
Bridge modifiers need an up-to-date proxy. Against an older proxy they stay off, so a menu never shows a price the proxy would not charge.
Bridge API (Paper)
Declare the dependency in your plugin.yml. The bridge registers under the name SnCredits-Bridge:
softdepend: [SnCredits-Bridge]Resolve it through the Bukkit ServicesManager, or the provider:
SnCreditsBridgeAPI bridge = SnCreditsBridgeProvider.get();
if (bridge != null && bridge.isBalanceKnown(player.getUniqueId())) {
double balance = bridge.getBalance(player.getUniqueId());
}| Method | Returns | Notes |
|---|---|---|
getBalance(UUID) | double | The balance the proxy last pushed. 0 when nothing is cached yet |
isBalanceKnown(UUID) | boolean | false right after a join, until the proxy answers, and after the player leaves |
getCoinflipStats(UUID) | Optional<CoinflipStatsView> | wins, losses and profit, as last pushed |
getShopPrice(Player, categoryId, itemId) | OptionalDouble | The category menu price for that player: the synced price, then every bridge modifier |
registerPriceModifier(Plugin, modifier) | void | Adds a bridge price modifier. See Price modifiers |
unregisterPriceModifier(modifier) | void | Removes it |
getApiVersion() | String | The bridge contract version, independent of the proxy API version |
Bridge events
Bridge events are normal Bukkit events, fired on the main thread. Listen with @EventHandler.
The cancellable ones fire before the bridge sends the action to the proxy. A cancelled action never reaches the proxy and ends silently.
| Event | Fired when | Cancel effect | Getters |
|---|---|---|---|
ShopItemSelectEvent | A player clicks an item in a shop category menu | No confirm menu opens | getPlayer(), getCategoryId(), getItemId(), getItemName(), getBasePrice(), getPrice() |
ShopPurchaseConfirmEvent | A player presses Confirm in the purchase menu | The menu closes and nothing is charged | the same getters, getPrice() being the price to charge |
CoinflipAcceptRequestEvent | A player clicks a coinflip in the coinflip menu | The coinflip stays open | getPlayer(), getCoinflipId(), getCreatorUuid(), getCreatorName(), getAmount() |
@EventHandler
public void onConfirm(ShopPurchaseConfirmEvent event) {
if (event.getCategoryId().equals("vip") && !event.getPlayer().hasPermission("rank.vip")) {
event.setCancelled(true);
event.getPlayer().sendMessage("Only VIPs can buy from this category.");
}
}The proxy can still refuse an action the bridge sent, for example when the player lacks the credits.
Notification events fire after the fact. They cannot be cancelled.
| Event | Fired when | Getters |
|---|---|---|
BalanceUpdateEvent | The proxy pushes a new balance for a player on this server | getPlayer(), getPreviousBalance() (empty on the first push after a join), getNewBalance() |
ShopPurchaseCompleteEvent | The proxy charged a purchase of a player on this server | getPlayer(), getCategoryId(), getItemId(), getItemName(), getPrice(), getBalanceAfter() |
The bridge only knows players online on that backend. It can lag behind the proxy for a moment. For offline players or authoritative values, use the proxy API.
Versioning
Call getApiVersion() for the API contract version. The proxy API and the bridge API carry separate
versions, both independent of the plugin version. Additions bump the minor component. Existing
members are never removed or changed; deprecated members keep working.