DEV Community

Cover image for How to add license keys to a Minecraft plugin in 10 minutes
Kasun Hapangama
Kasun Hapangama

Posted on Edited on

How to add license keys to a Minecraft plugin in 10 minutes

Selling a premium plugin without license keys? Then every copy is the same jar. One buyer can hand it to a whole network, and when it turns up on a leak site you can't tell whose copy it was.

This tutorial adds licensing to a Paper plugin in about ten minutes. Every buyer gets their own key automatically when they download from BuiltByBit, a key works on a limited number of servers, and you can switch off one leaked copy without touching anyone else. It follows BuiltByBit's DRM guidance, so it never takes a paying customer's server down.

I build SunLicense, and I'll use it as the license server here (the free plan covers 3 products). The design works the same with any licensing service.

The design, in four rules

  1. One key per buyer, written into the jar by the store. No keys to send by hand.
  2. A server limit per key, for example two servers per purchase.
  3. Check on startup, re-check every few hours. No phoning home every minute.
  4. Only stop on a clear "no". If the license server can't be reached, keep running.

Rule 4 matters most. BuiltByBit's DRM guidance warns that a plugin which disables itself whenever a server is unreachable can be treated as malicious and removed.

One check, three answers: valid starts the plugin, a clear no disables it, unreachable keeps running

Step 1: Create the product and a policy (2 minutes)

Create a free account (Google, GitHub, Discord or email, no credit card), then:

  1. Products → New product, for example "MyPlugin".
  2. New policy: this is what one purchase gets. Set Max machines to 2. For a plugin, a machine is a server.
  3. API keys → New key. Your plugin uses it to check licenses. Keep it out of git.

The New policy dialog with Max machines set to 2

Step 2: Add the check (5 minutes)

The fastest way: start from the free Paper plugin template. It already has the SDK relocated into your package with Shadow, a console message for every result, the re-check every few hours, an offline-license fallback, and it keeps running when the server can't be reached.

  1. Click Use this template on GitHub, then rename com.example.myplugin.
  2. Put the API key in ~/.gradle/gradle.properties as sunlicenseApiKey=slk_....
  3. Run ./gradlew runServer to start a local Paper server with your plugin.
  4. Put your features in core/CoreFeatures.java. They start once the license is valid.

Adding it to an existing plugin instead? Add the Java SDK (com.sunlicense:sunlicense, current version in the docs), shade and relocate it, and run the check off the main thread:

// BuiltByBit replaces this string with the buyer's key when they download.
private static final String LICENSE_KEY = "%%__SUNLICENSE__%%";
private SunLicense client;
private String serverId;

@Override
public void onEnable() {
    client = new SunLicense.Builder()
            .baseUrl("https://api.sunlicense.com")
            .token(API_KEY)
            .timeout(Duration.ofSeconds(10))
            .build();
    try {
        serverId = serverId();
    } catch (IOException e) {
        serverId = "unknown";
    }
    // Check now, then every 6 hours, on a background thread.
    long sixHours = 20L * 60 * 60 * 6;
    getServer().getScheduler().runTaskTimerAsynchronously(this, this::checkLicense, 0L, sixHours);
    // ...start your plugin
}

private void checkLicense() {
    try {
        SunLicense.Result result = client.validate(LICENSE_KEY, Map.of("host", serverId));
        if (result.isValid()) {
            return;
        }
        if (result.getStatus() == 400) {
            // A clear "no": revoked, suspended, expired or too many servers.
            getLogger().severe("This copy is not licensed. Contact the author.");
            getServer().getScheduler().runTask(this,
                    () -> getServer().getPluginManager().disablePlugin(this));
        } else {
            // A server error is not a "no".
            getLogger().warning("License server answered " + result.getStatus() + ", running anyway.");
        }
    } catch (SunLicense.SunLicenseException e) {
        // Can't reach the license server: keep running, check again later.
        getLogger().warning("License server unreachable, running anyway.");
    }
}

// A random id saved in the plugin folder, so each server counts once.
private String serverId() throws IOException {
    Path file = getDataFolder().toPath().resolve(".server-id");
    if (Files.isRegularFile(file)) {
        return Files.readString(file).trim();
    }
    String id = UUID.randomUUID().toString();
    Files.createDirectories(file.getParent());
    Files.writeString(file, id);
    return id;
}
Enter fullscreen mode Exit fullscreen mode

Two details worth copying: only HTTP 400 counts as a "no" (a 5xx or a timeout never stops a server), and the server id is a stable random id rather than the IP, which is often blank on hosted servers.

Step 3: Let BuiltByBit deliver the keys (2 minutes)

  1. In SunLicense: Integrations → BuiltByBit → Connect. Map your resource ID (the number at the end of your resource URL) to the policy. SunLicense creates the secret and shows the hook URL.
  2. On BuiltByBit: Creator dashboard → Placeholders → Create placeholder:
    • Placeholder: %%__SUNLICENSE__%%
    • Type: External license key
    • URL and secret: from step 1
  3. Apply it to your resource and publish an update. Placeholders only apply to files uploaded after they exist.

The Connect BuiltByBit dialog: resource ID mapped to a policy, secret created for you

Now each download contains that buyer's own key, the same one every time they re-download. Selling on Voxel Shop or SpigotMC instead? Both have their own placeholder integration on the same page.

Step 4: Test it like a buyer (1 minute)

  • Download your resource from BuiltByBit once and check the jar contains a key, not the placeholder string.
  • Start a server: the console says the license is valid.
  • Start a third server with the same key: it's refused (too many servers).
  • Revoke the license in the dashboard: at the next check, the plugin stops with your message.
  • Block the license server (for example in your hosts file) and restart: the plugin keeps running.

What you get

  • A shared key gets refused past the server limit.
  • A leaked jar names its buyer. The license is named after their BuiltByBit user ID, so you revoke that one license and every copy of that jar stops at its next check.
  • Paying customers never notice: one check on startup, a quiet re-check, and it keeps running if your license server has a bad day.

The licenses list: one masked key per buyer, each with its own status

Going further

  • Offline licenses: a signed file for servers without internet access.
  • Seal a core: with the template, ./gradlew build -PsunlicenseProduct=<code> seals a part of your plugin so only a valid license unseals it. Deleting the check then doesn't help a pirate.
  • Obfuscate the release jar: see How to obfuscate a Minecraft plugin.
  • Let your coding agent do it: the product's Setup tab has a prompt for Claude Code, Cursor or Codex that adds all of this to your project and proves it works with a test license.

Licensing, the BuiltByBit integration and obfuscation are all on SunLicense's free plan, with no cut of your sales. See how it works for Minecraft plugins and protect your next release.


This article was written with the help of an AI assistant and reviewed by me before publishing.

Top comments (0)