GroupeZ Docs
AddonszAuctionHouseStats

Developer API

Read zAuctionHouseStats leaderboards, player statistics and item prices from your own plugin

zAuctionHouseStats exposes a small, read-only API through the Bukkit ServicesManager. It lets your plugin read the same leaderboards, player statistics and item prices that power the addon's commands, placeholders and inventories.

All public types live in the fr.maxlego08.stats.api package:

TypeKindDescription
StatsManagerInterfaceEntry point: leaderboards, player statistics, item prices
LeaderboardMetricEnumThe 6 available leaderboards
StatsPeriodEnumCalendar windows: DAY, WEEK, MONTH, ALL
LeaderboardEntryRecordOne row of a leaderboard
PlayerStatsSnapshotRecordFull statistics sheet of a player
ItemPriceStatsRecordSales count, average and median price of an item

The API is stable across the 2.x versions: no constant of LeaderboardMetric or StatsPeriod is removed in 2.x.

Dependency

zAuctionHouseStats is a commercial addon: its build publishes to the private GroupeZ repository (https://repo.groupez.dev/private), which requires credentials. If you do not have access to it, compile against the plugin jar directly.

Compiling against the jar

Put zAuctionHouseStats.jar in a libs/ folder of your project.

Gradle (Kotlin DSL)

dependencies {
    compileOnly(files("libs/zAuctionHouseStats.jar"))
}

Maven

<dependency>
    <groupId>fr.maxlego08.stats</groupId>
    <artifactId>zauctionhousestats</artifactId>
    <version>2.0.0</version>
    <scope>system</scope>
    <systemPath>${project.basedir}/libs/zAuctionHouseStats.jar</systemPath>
</dependency>

From the private repository

If your account has access to the private repository:

repositories {
    maven {
        name = "groupezPrivate"
        url = uri("https://repo.groupez.dev/private")
        credentials(PasswordCredentials::class)
    }
}

dependencies {
    compileOnly("fr.maxlego08.stats:zauctionhousestats:2.0.0")
}

Always use compileOnly (Gradle) or provided / system (Maven): never shade zAuctionHouseStats into your plugin. The classes are provided at runtime by the installed addon.

plugin.yml

Declare the addon as a dependency so that it is enabled before your plugin:

depend:
  - zAuctionHouseStats
# or, if your plugin can run without it:
softdepend:
  - zAuctionHouseStats

Getting the StatsManager

The StatsManager is registered in the ServicesManager at the very end of the addon's startup (after the database, caches, menus, placeholders and commands are ready), so the API is never exposed in a half-initialized state. It is unregistered when the addon is disabled.

import fr.maxlego08.stats.api.StatsManager;
import org.bukkit.Bukkit;
import org.bukkit.plugin.RegisteredServiceProvider;
import org.bukkit.plugin.java.JavaPlugin;

public class MyPlugin extends JavaPlugin {

    private StatsManager statsManager;

    @Override
    public void onEnable() {
        RegisteredServiceProvider<StatsManager> registration =
            Bukkit.getServicesManager().getRegistration(StatsManager.class);

        if (registration == null) {
            getLogger().warning("zAuctionHouseStats not found, statistics features disabled.");
            return;
        }

        this.statsManager = registration.getProvider();
        getLogger().info("Hooked into zAuctionHouseStats!");
    }
}

If zAuctionHouseStats fails to start (for example when its configuration cannot be satisfied), it disables itself and never registers the service: getRegistration then returns null.

Threading guarantees

Every method of StatsManager returns a CompletableFuture and is served from the addon's caches. The contract is:

GuaranteeDetails
Never blockingNo call runs synchronous SQL or blocks the calling thread. You can call the API from the main thread, a region thread or an async thread.
Warm cacheThe returned future is already completed.
Stale valueWhen the cached value is older than its TTL, the old value is returned immediately (already-completed future) and a background refresh is triggered.
Cold cacheThe future completes when the first asynchronous load finishes.
Coalesced loadsSimultaneous requests for the same key share a single database query (50 cold calls = 1 query).
Possibly staleValues can be up to one cache TTL old (see the caches section of the configuration and Data source below).
Defensive copiesEach caller receives its own copy of the future: calling cancel, complete or orTimeout on it never affects other callers.
Completion threadFutures complete on the addon's storage thread (zAHStats-Storage), not on the main thread.

Switch back to the right thread

Because futures complete on the zAHStats-Storage thread, you must switch back to the main / region / entity thread before touching the game state (inventories, entities, worlds). On Folia, use the entity scheduler of the player, for example through zAuctionHouse's scheduler: auctionPlugin.getScheduler().runAtEntity(player, task -> ...). Never call join() or get() on these futures from the main thread.

Cache lifetimes used by the API:

MethodCache TTL keyDefault
getLeaderboardcaches.leaderboard-ttl-seconds60
getPlayerStatscaches.player-stats-ttl-seconds60
getItemPriceStatisticscaches.material-price-ttl-seconds3600

Failures and shutdown

SituationResult
A refresh fails while a value is already cachedThe previous value is kept and returned; a warning is logged and the next retry happens after a full TTL.
The very first load of a key failsThe future completes exceptionally. Until a full TTL has elapsed, further calls for that key return a failed future (the database is not hammered).
The addon is shutting downIn-flight futures complete immediately with the last cached value, or exceptionally with a CancellationException when no value was ever loaded.

Always handle the exceptional case, for example with exceptionally(...) or whenComplete(...).

StatsManager

public interface StatsManager {

    CompletableFuture<List<LeaderboardEntry>> getLeaderboard(LeaderboardMetric metric, StatsPeriod period,
                                                             String economyName);

    CompletableFuture<PlayerStatsSnapshot> getPlayerStats(UUID uniqueId);

    CompletableFuture<ItemPriceStats> getItemPriceStatistics(String economyName, Material material);

    CompletableFuture<ItemPriceStats> getItemPriceStatistics(String economyName, String material,
                                                             Integer customModelData, StatsPeriod period);
}

getLeaderboard

CompletableFuture<List<LeaderboardEntry>> getLeaderboard(LeaderboardMetric metric, StatsPeriod period, String economyName)

Returns the top-N of a leaderboard, where N is leaderboards.size (default 10, bounded to [1, 100]). The returned list is immutable and sorted by rank.

ParameterDescription
metricThe leaderboard, see LeaderboardMetric. Required (NullPointerException if null).
periodThe calendar window, see StatsPeriod. null = ALL.
economyNameThe zAuctionHouse economy name (surrounding spaces are trimmed). null or empty = the default economy for money metrics, all economies combined for count metrics. A non-empty name always restricts the leaderboard to that economy.

Special cases:

SituationResult
The metric is listed in leaderboards.disabledFuture completed with an empty list, no query
A money metric is requested without economy and no default economy can be resolvedFuture completed with an empty list
The leaderboard has fewer players than NA shorter list

The default economy is leaderboards.default-economy, or zAuctionHouse's default economy when that key is empty. Money metrics are always computed for one economy: amounts from different economies are never added together.

getPlayerStats

CompletableFuture<PlayerStatsSnapshot> getPlayerStats(UUID uniqueId)

Returns the statistics sheet of a player, see PlayerStatsSnapshot. uniqueId is required (NullPointerException if null). A player without any recorded statistics returns an empty snapshot (isEmpty() is true), never null.

Online players are pre-loaded when they join, so their snapshot is usually already cached.

getItemPriceStatistics (Material)

CompletableFuture<ItemPriceStats> getItemPriceStatistics(String economyName, Material material)

Price statistics of a material over all time, without any custom model data filter. Equivalent to getItemPriceStatistics(economyName, material.name(), null, StatsPeriod.ALL).

ParameterDescription
economyNameEconomy name. null or empty = default economy.
materialThe Bukkit material. Required (NullPointerException if null).

getItemPriceStatistics (full)

CompletableFuture<ItemPriceStats> getItemPriceStatistics(String economyName, String material,
                                                         Integer customModelData, StatsPeriod period)
ParameterDescription
economyNameEconomy name. null or empty = default economy.
materialBukkit material name, case-insensitive ("diamond" = "DIAMOND"). null or blank = filter on customModelData only, across all materials.
customModelDataFilter on the custom model data of the sold item. null = no filter.
periodCalendar window. null = ALL.
SituationResult
material is null / blank and customModelData is nullIllegalArgumentException thrown immediately (not through the future)
No economy can be resolvedFuture completed with an empty ItemPriceStats (count 0, average and median 0.00)
No matching saleFuture completed with an empty ItemPriceStats

Prices are computed on the listing price of completed sales. In the DAY / WEEK / MONTH windows, only sales made since midnight of the first day of the window (in statistics.timezone) are counted.

Types

LeaderboardMetric

ConstantValue rankedMoney metricDescription
TOP_SELLERS_MONEYSum of money received by the sellerYesMoney actually earned by sellers, net of taxes
TOP_BUYERS_MONEYSum of money paid by the buyerYesMoney actually spent by buyers, taxes included
TOP_SELLERS_COUNTNumber of salesNoCompleted sales as a seller
TOP_BUYERS_COUNTNumber of purchasesNoCompleted purchases as a buyer
TOP_TAX_PAIDSum of taxes paidYesTaxes borne by the player (purchase tax share + listing tax)
BIGGEST_SALEMaximum single saleYesBiggest single sale received as a seller

Methods:

MethodReturn typeDescription
isMoney()booleantrue for money metrics: an economy is always required (default economy if none is given), and the value can be formatted with zAuctionHouse's economy format
getAggregation()LeaderboardMetric.AggregationSUM or MAX (MAX only for BIGGEST_SALE)
getAllTimeColumn()StringInternal database column used for the ALL period
getDailyColumn()StringInternal database column used for DAY / WEEK / MONTH, null for BIGGEST_SALE

StatsPeriod

Periods are calendar windows, computed in the timezone configured by statistics.timezone (empty = server timezone):

ConstantWindow
DAYToday, since midnight
WEEKSince Monday (ISO week)
MONTHSince the 1st of the current month
ALLAll time

Methods:

MethodReturn typeDescription
isWindowed()booleantrue for every constant except ALL
minEpochDay(ZoneId zone)longFirst epoch-day included in the window. Throws IllegalStateException for ALL
minEpochDay(Clock clock, ZoneId zone)longSame, with an injected clock
minEpochDay(LocalDate today)longFirst epoch-day of the window containing today. Throws IllegalStateException for ALL
windowStart(Clock clock, ZoneId zone)InstantMidnight of the first day of the window. Throws IllegalStateException for ALL

LeaderboardEntry

public record LeaderboardEntry(int rank, UUID uuid, String name, BigDecimal value) {}
FieldTypeDescription
rankintPosition, from 1 to N. Ranks are positional (1, 2, 3…): ties are never shared and are broken by UUID
uuidUUIDThe player
nameStringLast known name of the player, or leaderboards.unknown-name (default ?) if unknown
valueBigDecimalThe ranked value. Scale 2 for money metrics, scale 0 for counts

PlayerStatsSnapshot

public record PlayerStatsSnapshot(UUID uniqueId, String name, Map<String, PlayerEconomyStats> byEconomy,
                                  Map<PeriodKey, PlayerPeriodStats> periods, long loadedAt) {}
FieldTypeDescription
uniqueIdUUIDThe player
nameStringLast known name, null if unknown
byEconomyMap<String, PlayerEconomyStats>All-time totals, keyed by economy name (immutable)
periodsMap<PeriodKey, PlayerPeriodStats>Totals of the DAY, WEEK and MONTH windows, keyed by (period, economy) (immutable)
loadedAtlongEpoch milliseconds of the load
MethodReturn typeDescription
economy(String economyName)Optional<PlayerEconomyStats>All-time totals of an economy, empty if the player never used it
period(StatsPeriod period, String economyName)Optional<PlayerPeriodStats>Totals of a window for an economy, empty if there was no activity in that window
isEmpty()booleantrue if the player has no recorded statistics

PlayerStatsSnapshot.PeriodKey

public record PeriodKey(StatsPeriod period, String economyName) {}

period is DAY, WEEK or MONTH (never ALL, which is served by byEconomy).

PlayerStatsSnapshot.PlayerEconomyStats

All-time totals of one economy.

FieldTypeDescription
itemsListedlongItems put up for sale
itemsSoldlongItems sold
itemsBoughtlongItems bought
itemsExpiredlongListings that expired without being sold
itemsRemovedlongListings removed from sale
moneyEarnedBigDecimalMoney earned as a seller, net of taxes
moneySpentBigDecimalMoney spent as a buyer, taxes included
taxPaidBigDecimalTaxes paid
biggestSaleBigDecimalBiggest single sale received
firstSaleAtInstantFirst sale, null if never
lastSaleAtInstantLast sale, null if never
firstPurchaseAtInstantFirst purchase, null if never
lastPurchaseAtInstantLast purchase, null if never

PlayerStatsSnapshot.PlayerPeriodStats

Totals of one window (DAY / WEEK / MONTH) for one economy.

FieldTypeDescription
itemsSoldlongItems sold
itemsBoughtlongItems bought
itemsListedlongItems put up for sale
moneyEarnedBigDecimalMoney earned, net of taxes
moneySpentBigDecimalMoney spent, taxes included
taxPaidBigDecimalTaxes paid

ItemPriceStats

public record ItemPriceStats(long count, BigDecimal average, BigDecimal median) {}
FieldTypeDescription
countlongExact number of matching sales
averageBigDecimalAverage price (scale 2), 0 if no sale
medianBigDecimalMedian price (scale 2), computed on the most recent sales, up to statistics.material-prices.median-sample-limit (default 50000); 0 if no sale
MethodReturn typeDescription
isEmpty()booleantrue if no sale matches (count == 0)

Examples

Display the top sellers of the day

import fr.maxlego08.stats.api.LeaderboardEntry;
import fr.maxlego08.stats.api.LeaderboardMetric;
import fr.maxlego08.stats.api.StatsManager;
import fr.maxlego08.stats.api.StatsPeriod;
import fr.maxlego08.zauctionhouse.api.AuctionPlugin;
import org.bukkit.entity.Player;

public void sendDailyTop(AuctionPlugin auctionPlugin, StatsManager stats, Player player) {
    stats.getLeaderboard(LeaderboardMetric.TOP_SELLERS_MONEY, StatsPeriod.DAY, null) // default economy
        .whenComplete((entries, throwable) -> {
            // Back on the player's thread before sending messages / touching the game state
            auctionPlugin.getScheduler().runAtEntity(player, task -> {
                if (throwable != null) {
                    player.sendMessage("Statistics are unavailable right now.");
                    return;
                }
                if (entries.isEmpty()) {
                    player.sendMessage("No sales today.");
                    return;
                }
                for (LeaderboardEntry entry : entries) {
                    player.sendMessage("#" + entry.rank() + " " + entry.name() + " - " + entry.value());
                }
            });
        });
}

Read a player's statistics

import fr.maxlego08.stats.api.PlayerStatsSnapshot;
import fr.maxlego08.stats.api.StatsPeriod;

stats.getPlayerStats(player.getUniqueId()).thenAccept(snapshot -> {
    if (snapshot.isEmpty()) return;

    // All-time totals for the "vault" economy
    snapshot.economy("vault").ifPresent(total -> {
        long sold = total.itemsSold();
        java.math.BigDecimal earned = total.moneyEarned();
        // ...
    });

    // This week's totals for the same economy
    snapshot.period(StatsPeriod.WEEK, "vault").ifPresent(week -> {
        long boughtThisWeek = week.itemsBought();
        // ...
    });
});

Get the average price of an item

import fr.maxlego08.stats.api.ItemPriceStats;
import fr.maxlego08.stats.api.StatsPeriod;
import org.bukkit.Material;

// All time, default economy
stats.getItemPriceStatistics(null, Material.DIAMOND).thenAccept(prices -> {
    if (prices.isEmpty()) return; // never sold
    java.math.BigDecimal average = prices.average();
    java.math.BigDecimal median = prices.median();
    long sales = prices.count();
});

// This month, "vault" economy, only items with custom model data 1001
stats.getItemPriceStatistics("vault", "PAPER", 1001, StatsPeriod.MONTH)
    .thenAccept(prices -> { /* ... */ });

Never call join() or get() on these futures from the main thread: on a cold cache, it would wait for a database query. Always chain with thenAccept, whenComplete, etc., then switch back to the right thread.

Data source: zAuctionHouse events

zAuctionHouseStats does not expose events of its own. It collects its data by listening (at MONITOR priority, without ever modifying them) to zAuctionHouse V4 events. The dedicated statistics events live in the fr.maxlego08.zauctionhouse.api.event.events.stats package:

EventFired whenUsed for
AuctionItemListedEventAn item has been successfully listed for saleListings, listing tax
AuctionItemPurchasedEventA purchase has been fully committed (once per purchase, on the buyer's server)Sales, money earned/spent, taxes, item prices, leaderboards
AuctionMoneyClaimEventA player has claimed pending moneyMoney claims per economy
AuctionOpenEventA player opens (or comes back to) the main auction inventoryActivity counter

These four events are observational only (not cancellable) and are delivered synchronously on the main thread (the global region thread on Folia).

The addon also listens to existing zAuctionHouse events:

EventPackageUsed for
AuctionExpireEventfr.maxlego08.zauctionhouse.api.event.eventsListings that expired without being sold (only items of the LISTED storage)
AuctionRemoveListedItemEventfr.maxlego08.zauctionhouse.api.event.events.removeListings removed from sale (credited to the seller, even when an admin removes the item)
AuctionRemoveExpiredItemEventfr.maxlego08.zauctionhouse.api.event.events.removeKeeping player names up to date
AuctionRemovePurchasedItemEventfr.maxlego08.zauctionhouse.api.event.events.removeKeeping player names up to date

Event handlers never touch the database: they only queue the data, which is written asynchronously in batches (see flush.interval-seconds in the configuration). Consequences for the API:

  • A PlayerStatsSnapshot that is already cached on the server where the activity happened is updated immediately in memory, without any query.
  • After each flush, the caches of the affected players are refreshed from the database (this also picks up activity recorded by other servers of a cluster), and cached item prices that had no sale yet are refreshed when that material is sold.
  • Leaderboards and other cached item prices pick up new activity when their cache TTL expires.

The statistics events require a recent zAuctionHouse V4 build. With collection.mode: AUTO, if the installed zAuctionHouse does not provide them, the addon can fall back to reading the zAuctionHouse logs table instead (only when collection.polling.enabled is true). This is a degraded mode: amounts are approximate (listing price, taxes unknown) and expirations and auction openings are not recorded. The API is the same in both modes. See Installation and Configuration.

To listen to these events yourself, see the zAuctionHouse Events page.

On this page

GroupeZ MCP

Documentation in your AI

MCP lets your AI search GroupeZ documentation and read current examples to help you configure your plugins.

Read-only · No documentation API key needed

1. Copy the server URL

Transport: HTTP (Streamable HTTP). Authentication: none. Your tool uses its own model and subscription limits.

  1. Open Plugins in ChatGPT on the web, then + → Add custom MCP server.
  2. Name it ‘GroupeZ Documentation’, paste the URL above and choose ‘No authentication’. Create and install the plugin.
  3. In a conversation, type @ and select the plugin before asking. If needed, enable search, get_page and list_pages in its settings.
Official setup guide

3. Ask a question

Use GroupeZ MCP to help me create a zMenu inventory. Search relevant pages, read the examples and cite your sources.
Other clients and troubleshooting

For clients using mcpServers, adapt this example to their format:

{
  "mcpServers": {
    "groupez-docs": {
      "url": ""
    }
  }
}

Available tools are search, get_page and list_pages. Refresh the connector’s tools after updates. If your AI cannot access them, check that the connector is enabled in the conversation and your account allows custom MCP servers. Opening the URL in a browser is not an MCP test.