<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Kasun Hapangama</title>
    <description>The latest articles on DEV Community by Kasun Hapangama (@ka0).</description>
    <link>https://dev.to/ka0</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1073475%2Fb26491ae-6138-40b4-9391-fd7d78681a48.jpeg</url>
      <title>DEV Community: Kasun Hapangama</title>
      <link>https://dev.to/ka0</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ka0"/>
    <language>en</language>
    <item>
      <title>Your Minecraft plugin leaked. Here's how to find who leaked it and stop the copies</title>
      <dc:creator>Kasun Hapangama</dc:creator>
      <pubDate>Fri, 06 Nov 2026 20:53:52 +0000</pubDate>
      <link>https://dev.to/ka0/your-minecraft-plugin-leaked-heres-how-to-find-who-leaked-it-and-stop-the-copies-1a1d</link>
      <guid>https://dev.to/ka0/your-minecraft-plugin-leaked-heres-how-to-find-who-leaked-it-and-stop-the-copies-1a1d</guid>
      <description>&lt;p&gt;&lt;strong&gt;You search your plugin's name and there it is: your premium jar, free to download on a leak forum.&lt;/strong&gt; It happens to almost every plugin that sells well, and it tends to happen the week of a big release or sale.&lt;/p&gt;

&lt;p&gt;Here's what to do in the first hour, in order, and how to set things up so the next leak takes five minutes instead of a weekend.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Don't run the leaked file
&lt;/h2&gt;

&lt;p&gt;Leak sites are a common way to spread malware: "cracked" jars often carry a backdoor that was added after the leak. Download the file only to inspect it, in a throwaway folder or a VM. Never put it on a server, not even a test one.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Find out whose copy it is
&lt;/h2&gt;

&lt;p&gt;This only works if every buyer got a unique copy. If they did, you have three ways to find the buyer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Look for the key in the jar.&lt;/strong&gt; If you sell on BuiltByBit with an &lt;em&gt;External license key&lt;/em&gt; placeholder, or on Voxel Shop or SpigotMC with their buyer placeholders, the store wrote something unique into each download. A jar is just a zip file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;leak &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;leak &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; unzip &lt;span class="nt"&gt;-q&lt;/span&gt; ../leaked.jar
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"%%__"&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;                                   &lt;span class="c"&gt;# a placeholder nobody replaced?&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rlE&lt;/span&gt; &lt;span class="s2"&gt;"[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}"&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;  &lt;span class="c"&gt;# files that contain a key&lt;/span&gt;
strings path/to/Match.class | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"[A-Z0-9]{4}-"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the placeholder was replaced, you'll find the buyer's key or user ID in a class file or in &lt;code&gt;config.yml&lt;/code&gt;. If the class is hard to read, open it in a decompiler (Recaf, JD-GUI) and look at its constants.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2g6yklt4o3q3iga6hp2o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2g6yklt4o3q3iga6hp2o.png" alt="Find whose copy it is: unzip the jar, search for a key, read it from the class file" width="800" height="345"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Look for a watermark.&lt;/strong&gt; If your downloads carry a mark (a line in the manifest, an archive comment, an ID compiled into a class), read it back the same way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ask your license server.&lt;/strong&gt; Search for the key you found. It points to the license, and the license to the buyer. Its activations tell you more: a key on twelve servers in four countries is a shared key, not a stolen account.&lt;/p&gt;

&lt;p&gt;A careful leaker may have swapped the key for another, or cut the check out entirely. Then the watermark and your license server's logs (which key started showing up on new servers when the leak appeared) are your best leads.&lt;/p&gt;

&lt;p&gt;If every buyer got the same jar, you can't trace this leak. Skip to step 5, and make sure the next version is traceable.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Stop the leaked copies
&lt;/h2&gt;

&lt;p&gt;Revoke the buyer's key. If your plugin checks its license on startup and re-checks every few hours, every copy of that jar stops at its next check, on every server that downloaded it. Your other buyers notice nothing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F977p5ptanu7ih4sg7p6n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F977p5ptanu7ih4sg7p6n.png" alt="Revoke that one key: every copy of that jar stops at its next check" width="800" height="565"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two things make this work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Re-check while running.&lt;/strong&gt; A startup-only check stops new installs, but servers that were already running keep going until they restart. A re-check every few hours reaches them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't punish a network outage.&lt;/strong&gt; Only stop on a clear "revoked" or "suspended" answer, never on a timeout. That keeps you on the right side of BuiltByBit's DRM rules, and keeps paying customers safe when your license server has a bad day.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your plugin has no license check yet, this is the step you can't do today. &lt;a href="https://dev.to/ka0/how-to-add-license-keys-to-a-minecraft-plugin-in-10-minutes-bd6"&gt;How to add license keys to a Minecraft plugin in 10 minutes&lt;/a&gt; fixes that for the next version.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Deal with the buyer
&lt;/h2&gt;

&lt;p&gt;Look at their history before you assume the worst. Sometimes their account was compromised, or a staff member they trusted shared it. Then:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Refund or chargeback?&lt;/strong&gt; If they charged back, revoke and move on. Stores don't always tell your license server about refunds (BuiltByBit's placeholder doesn't), so check.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Report them&lt;/strong&gt; to the marketplace with your evidence: the key, the leak URL, the date.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Give a second chance if it was an accident:&lt;/strong&gt; issue a new key and ask them to keep it private.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5. Get the post taken down
&lt;/h2&gt;

&lt;p&gt;Most leak sites are hosted somewhere that responds to copyright notices. Send a DMCA takedown to the site, or to its host if the site ignores you, with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a link to your original listing,&lt;/li&gt;
&lt;li&gt;the exact leak URL,&lt;/li&gt;
&lt;li&gt;a statement that you own the copyright and didn't authorise the copy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;BuiltByBit has its own DMCA form for copies uploaded there. Expect reposts, and don't spend your week chasing every mirror: revoking the key in step 3 already made those copies useless for anyone who restarts.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Make the next leak traceable
&lt;/h2&gt;

&lt;p&gt;If this leak was hard to trace, fix it for the next version:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;One key per buyer, written into each download&lt;/strong&gt; by your store's placeholder.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A server limit per key&lt;/strong&gt;, so a shared key gets refused on the third server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A startup check with a re-check every few hours&lt;/strong&gt;, that keeps running when your license server can't be reached.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alerts for shared keys:&lt;/strong&gt; one key on many servers, or in two countries an hour apart.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Obfuscation and a sealed core&lt;/strong&gt;, so the check is hard to cut out of the leaked jar. See &lt;a href="https://dev.to/ka0/how-to-obfuscate-a-minecraft-plugin-free-step-by-step-283m"&gt;How to obfuscate a Minecraft plugin&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F932ovckzeaz2jdhuw857.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F932ovckzeaz2jdhuw857.png" alt="Catch the next one early: alerts for a key on too many servers or in two countries at once" width="800" height="525"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How I handle this with SunLicense
&lt;/h2&gt;

&lt;p&gt;I build &lt;a href="https://sunlicense.com/for/minecraft-plugins?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=guides-2026-11&amp;amp;utm_content=plugin-leaked" rel="noopener noreferrer"&gt;SunLicense&lt;/a&gt;, so here's the same process in it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Each BuiltByBit download gets the buyer's key&lt;/strong&gt; through the placeholder. The license is named after their BuiltByBit user ID, so the key in a leaked jar points straight at the buyer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Obfuscated jars stay traceable:&lt;/strong&gt; classes that hold a download placeholder are never string-encrypted, so the key is still readable in step 2.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sentinel&lt;/strong&gt; flags keys being shared (too many servers, impossible travel, a revoked key still in use, a key posted in your Discord) and offers one-click fixes you can undo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Revoke from the dashboard, the CLI or Discord.&lt;/strong&gt; The free Paper template re-checks every few hours and keeps running when SunLicense can't be reached.&lt;/li&gt;
&lt;li&gt;On &lt;strong&gt;Standard&lt;/strong&gt;, downloads from SunLicense carry a watermark, &lt;strong&gt;Whose copy is this?&lt;/strong&gt; names the customer when you upload a leaked file, and the web patrol looks for copies offered online.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The free plan covers 3 products and 100 licenses, with no cut of your sales. &lt;a href="https://sunlicense.com/for/minecraft-plugins?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=guides-2026-11&amp;amp;utm_content=plugin-leaked-end" rel="noopener noreferrer"&gt;See how it works for Minecraft plugins&lt;/a&gt; before your next release, so the next leak takes five minutes.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was written with the help of an AI assistant and reviewed by me before publishing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>minecraft</category>
      <category>java</category>
      <category>security</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>How to protect your FiveM scripts: obfuscate Lua for free</title>
      <dc:creator>Kasun Hapangama</dc:creator>
      <pubDate>Fri, 06 Nov 2026 20:52:49 +0000</pubDate>
      <link>https://dev.to/ka0/how-to-protect-your-fivem-scripts-obfuscate-lua-for-free-221j</link>
      <guid>https://dev.to/ka0/how-to-protect-your-fivem-scripts-obfuscate-lua-for-free-221j</guid>
      <description>&lt;p&gt;&lt;strong&gt;Sell FiveM scripts? Then every buyer can open your &lt;code&gt;.lua&lt;/code&gt; files and read every line.&lt;/strong&gt; Copying your logic, removing your credits or patching out a check takes a text editor and five minutes.&lt;/p&gt;

&lt;p&gt;Obfuscation makes that much harder: names become noise, control flow gets scrambled, and the code still runs. This guide walks through obfuscating a whole FiveM resource for free, without breaking the exports, events and NUI callbacks other resources rely on, and how to test it before you ship.&lt;/p&gt;

&lt;h2&gt;
  
  
  First: do you need it?
&lt;/h2&gt;

&lt;p&gt;Be clear on two things before you start:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tebex asset escrow already encrypts escrowed files.&lt;/strong&gt; If you sell through Tebex with Cfx's asset escrow, the files it encrypts don't need obfuscation (Cfx's own FAQ says so). Obfuscation is for what escrow doesn't cover: files you keep readable with &lt;code&gt;escrow_ignore&lt;/code&gt; that still hold logic, and resources you deliver outside escrow (other stores, commissions, private sales).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Cfx.re forum's Releases section doesn't allow third-party obfuscation.&lt;/strong&gt; If you post a resource there, don't obfuscate that release.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What obfuscation does:&lt;/strong&gt; renames local variables and scrambles the structure of your code, so reading or editing it is slow and painful, while it still runs the same.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it doesn't:&lt;/strong&gt; stop someone determined, or stop a buyer from sharing the resource. It raises the cost of copying your work. For sharing, you need a license key per buyer (more at the end).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbibliw9jj14r25pmdgra.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbibliw9jj14r25pmdgra.png" alt="An illustration of a server-side check before and after obfuscation" width="800" height="370"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Check for Lua 5.4-only syntax
&lt;/h2&gt;

&lt;p&gt;This is the catch that trips most people. FiveM runs Lua 5.4 (&lt;code&gt;lua54 'yes'&lt;/code&gt; in your manifest), but &lt;a href="https://github.com/prometheus-lua/Prometheus" rel="noopener noreferrer"&gt;Prometheus&lt;/a&gt;, the Lua obfuscator used here, reads and writes &lt;strong&gt;Lua 5.1&lt;/strong&gt;. Most FiveM code is 5.1-compatible and works fine. Files that use 5.4-only syntax won't parse:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;5.4-only syntax&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Integer division&lt;/td&gt;
&lt;td&gt;&lt;code&gt;local half = count // 2&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bitwise operators&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;flags &amp;amp; 4&lt;/code&gt;, &lt;code&gt;x &amp;gt;&amp;gt; 1&lt;/code&gt;, &lt;code&gt;~mask&lt;/code&gt;, and bitwise or&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Variable attributes&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;local id &amp;lt;const&amp;gt; = 5&lt;/code&gt;, &lt;code&gt;&amp;lt;close&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Find them before you upload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rnE&lt;/span&gt; &lt;span class="s1"&gt;'//|&amp;lt;const&amp;gt;|&amp;lt;close&amp;gt;|&amp;gt;&amp;gt;|&amp;lt;&amp;lt;|[^~=&amp;lt;&amp;gt;]~[^=]|[^&amp;amp;]&amp;amp;[^&amp;amp;]'&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*.lua'&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expect a few false hits (a &lt;code&gt;//&lt;/code&gt; inside a URL string is fine). Any file with real 5.4 syntax goes on the &lt;strong&gt;Exclusions&lt;/strong&gt; list in step 4, so it ships unchanged.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Create a free account
&lt;/h2&gt;

&lt;p&gt;I build &lt;a href="https://sunlicense.com/for/fivem-scripts?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=guides-2026-11&amp;amp;utm_content=fivem-obfuscate-lua" rel="noopener noreferrer"&gt;SunLicense&lt;/a&gt;, a licensing service for script and plugin sellers, and its free plan includes 3 obfuscation runs a month. &lt;a href="https://sunlicense.com/for/fivem-scripts?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=guides-2026-11&amp;amp;utm_content=fivem-obfuscate-lua-step1" rel="noopener noreferrer"&gt;Sign up&lt;/a&gt; with Google, GitHub, Discord or email. No credit card.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Developers → Obfuscation → Lua
&lt;/h2&gt;

&lt;p&gt;Open &lt;strong&gt;Obfuscation&lt;/strong&gt; under &lt;strong&gt;Developers&lt;/strong&gt; in the sidebar, choose &lt;strong&gt;Lua&lt;/strong&gt;, then &lt;strong&gt;Prometheus (Lua / FiveM)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff8qa277d5zynmf8s3w94.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff8qa277d5zynmf8s3w94.png" alt="Choose an obfuscator: Prometheus (Lua / FiveM)" width="800" height="320"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Zip the resource and upload it
&lt;/h2&gt;

&lt;p&gt;Zip the resource folder (the one with &lt;code&gt;fxmanifest.lua&lt;/code&gt;) and drop it in. A single &lt;code&gt;.lua&lt;/code&gt; file works too. Zips up to 50 MB.&lt;/p&gt;

&lt;p&gt;SunLicense reads your &lt;code&gt;fxmanifest.lua&lt;/code&gt; (or the legacy &lt;code&gt;__resource.lua&lt;/code&gt;) and obfuscates only the scripts it lists under &lt;code&gt;client_scripts&lt;/code&gt;, &lt;code&gt;server_scripts&lt;/code&gt; and &lt;code&gt;shared_scripts&lt;/code&gt;, with globs like &lt;code&gt;server/*.lua&lt;/code&gt; expanded. Everything else ships exactly as it was.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F42bla1xleos3p67z89ji.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F42bla1xleos3p67z89ji.png" alt="What gets obfuscated: the scripts in your manifest. Left untouched: the manifest, html, stream, data files and exclusions" width="800" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Because string literals are kept, &lt;strong&gt;exports, event names and NUI callbacks keep working&lt;/strong&gt;: nothing another resource calls by name changes.&lt;/p&gt;

&lt;p&gt;Three settings matter:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Protection preset&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Medium&lt;/code&gt; (recommended). &lt;code&gt;Strong&lt;/code&gt; protects more but grows and slows the code; &lt;code&gt;Weak&lt;/code&gt; and &lt;code&gt;Minify&lt;/code&gt; are lighter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Lua version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Lua 5.1&lt;/code&gt; for FiveM and RedM (&lt;code&gt;LuaU&lt;/code&gt; is for Roblox)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Exclusions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Files to leave alone: your 5.4 files from step 1, and files buyers should edit, like &lt;code&gt;config.lua&lt;/code&gt; or &lt;code&gt;locales/*.lua&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Step 5: Run it and download the result
&lt;/h2&gt;

&lt;p&gt;Choose &lt;strong&gt;Obfuscate&lt;/strong&gt; and watch the live log. It runs as a background task, so you can close the tab and come back; results are kept for 7 days on the &lt;strong&gt;History&lt;/strong&gt; tab. Every output file is syntax-checked (compiled without being run) before you get it, so a broken file never ships silently.&lt;/p&gt;

&lt;p&gt;You get a zip with the same layout as yours, plus a short report.&lt;/p&gt;

&lt;p&gt;For release scripts, the CLI does the same run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sunlicense obfuscate dist/my-resource.zip &lt;span class="nt"&gt;--engine&lt;/span&gt; prometheus &lt;span class="nt"&gt;-o&lt;/span&gt; dist/obfuscated/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 6: Test it on a real server
&lt;/h2&gt;

&lt;p&gt;Never ship an obfuscated resource you haven't started. Ten minutes on a test server:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Drop the obfuscated folder into &lt;code&gt;resources/&lt;/code&gt; and &lt;code&gt;ensure&lt;/code&gt; it. No errors in the server console or the F8 client console.&lt;/li&gt;
&lt;li&gt;Run every command and open every menu or NUI page.&lt;/li&gt;
&lt;li&gt;Call your exports from another resource, and trigger the events other scripts listen for.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;restart&lt;/code&gt; the resource while players are connected.&lt;/li&gt;
&lt;li&gt;Watch the resource monitor (&lt;code&gt;resmon&lt;/code&gt;) for CPU time compared with the original. If a hot loop got slower, exclude that file or step down a preset.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If something fails, the console error names the file. Add it to Exclusions, or try &lt;code&gt;Weak&lt;/code&gt;, and run again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Obfuscation is half the job
&lt;/h2&gt;

&lt;p&gt;An obfuscated resource that anyone can start is still a resource anyone can share. The other half is &lt;strong&gt;a license key per buyer&lt;/strong&gt;: the server owner puts their key in &lt;code&gt;server.cfg&lt;/code&gt;, the resource checks it on start and every few hours, and a refund on Tebex revokes it automatically. A shared key then shows up on too many servers, and you can switch off exactly one buyer.&lt;/p&gt;

&lt;p&gt;The free &lt;a href="https://github.com/sunlicense-com/example-fivem-resource" rel="noopener noreferrer"&gt;FiveM resource template&lt;/a&gt; has all of that ready (Lua 5.4, the license check, keeps running if the license server can't be reached, and a sealed core that only opens with a valid license). For the general picture, see &lt;a href="https://dev.to/ka0/license-keys-explained-how-they-actually-work-a-beginners-guide-cgh"&gt;License keys explained&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Licensing, the Tebex integration and obfuscation are all on SunLicense's free plan, with no cut of your sales. &lt;a href="https://sunlicense.com/for/fivem-scripts?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=guides-2026-11&amp;amp;utm_content=fivem-obfuscate-lua-end" rel="noopener noreferrer"&gt;See how it works for FiveM scripts&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was written with the help of an AI assistant and reviewed by me before publishing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>lua</category>
      <category>gamedev</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>License keys explained: how they actually work (a beginner's guide)</title>
      <dc:creator>Kasun Hapangama</dc:creator>
      <pubDate>Fri, 06 Nov 2026 20:51:08 +0000</pubDate>
      <link>https://dev.to/ka0/license-keys-explained-how-they-actually-work-a-beginners-guide-cgh</link>
      <guid>https://dev.to/ka0/license-keys-explained-how-they-actually-work-a-beginners-guide-cgh</guid>
      <description>&lt;p&gt;&lt;strong&gt;Selling a plugin, a bot, a script or a desktop app, and not sure how license keys actually work?&lt;/strong&gt; You're not alone. Most guides jump straight to code, and skip the part where you decide what a key should do.&lt;/p&gt;

&lt;p&gt;This guide explains the whole system in plain words: what a key is, how it gets tied to a computer, how your app checks it (online and offline), how trials and expiry work, and what happens when you revoke one. It applies to any language and any store. At the end there's a short code example and the mistakes that hurt paying customers most.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a license key actually is
&lt;/h2&gt;

&lt;p&gt;A license key like &lt;code&gt;K7QX-M2PD-9HWT-R4CN&lt;/code&gt; is just a random, hard-to-guess ID. On its own it means nothing. It points to a &lt;strong&gt;record on a license server&lt;/strong&gt;, and that record is the truth:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;who bought it (the customer),&lt;/li&gt;
&lt;li&gt;what they bought (the product and its rules),&lt;/li&gt;
&lt;li&gt;its status: active, suspended or revoked,&lt;/li&gt;
&lt;li&gt;when it expires, if ever,&lt;/li&gt;
&lt;li&gt;which machines it's activated on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the key difference from the old "serial number" approach, where a key was valid if it passed a math check inside the app. Those get cracked by keygens: once someone reverse-engineers the formula, they can print infinite valid keys. A random key backed by a server can't be generated, only shared, and sharing is something the server can see and stop.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fox92gtk4nnaiqbb3j7v9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fox92gtk4nnaiqbb3j7v9.png" alt="The life of a license key: issued at checkout, activated on a machine, checked on every start, then it expires or is revoked" width="800" height="265"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The four pieces every licensing system has
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Piece&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Product&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The thing you sell&lt;/td&gt;
&lt;td&gt;"MyPlugin", "Acme Editor"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Policy&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The rules one purchase gets&lt;/td&gt;
&lt;td&gt;2 machines, 365 days, subscription&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;License&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One buyer's key, issued from a policy&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;K7QX-…&lt;/code&gt;, active, expires 6 Nov 2027&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Machine&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One computer or server the license runs on&lt;/td&gt;
&lt;td&gt;a server id, a desktop's machine id&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The policy is where most of your decisions live. Change a policy's machine limit and every license on it follows, so you set the rules once instead of per buyer.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgbh537kk0kkj6xwbkbll.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgbh537kk0kkj6xwbkbll.png" alt="A policy in the dashboard: type, max machines, max IPs and duration" width="800" height="590"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Activation and machine limits
&lt;/h2&gt;

&lt;p&gt;A key that works anywhere gets shared. So your app sends two things with every check: the &lt;strong&gt;key&lt;/strong&gt;, and a &lt;strong&gt;machine id&lt;/strong&gt;, a stable ID for the computer or server it runs on (a random ID saved in the app's folder works well; hardware-based IDs change when someone swaps a disk).&lt;/p&gt;

&lt;p&gt;The first time a new machine checks in, it &lt;strong&gt;activates&lt;/strong&gt;: it takes one of the license's slots. With "2 machines", a buyer's laptop and desktop both work. A third computer is refused with a clear "too many machines" answer, and the buyer can free a slot (from a customer portal, a Discord bot, or by asking you) when they move to a new PC.&lt;/p&gt;

&lt;p&gt;Game-server plugins often use an &lt;strong&gt;IP limit&lt;/strong&gt; instead or as well: the key works from a limited number of network addresses, the classic "one server per purchase" rule.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Be generous.&lt;/strong&gt; People reinstall, upgrade and move. A support ticket costs you more than one extra slot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking a key: online first
&lt;/h2&gt;

&lt;p&gt;Most apps check online: on startup, the app asks the license server "is this key valid on this machine?" and gets one of a handful of answers.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Answer&lt;/th&gt;
&lt;th&gt;Means&lt;/th&gt;
&lt;th&gt;What your app should do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Valid&lt;/td&gt;
&lt;td&gt;Active, not expired, machine allowed&lt;/td&gt;
&lt;td&gt;Start normally&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Expired&lt;/td&gt;
&lt;td&gt;Past its end date&lt;/td&gt;
&lt;td&gt;Offer a renewal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Revoked / Suspended&lt;/td&gt;
&lt;td&gt;You switched it off&lt;/td&gt;
&lt;td&gt;Stop, show a support contact&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Too many machines&lt;/td&gt;
&lt;td&gt;The slots are full&lt;/td&gt;
&lt;td&gt;Ask the buyer to free one&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Not found&lt;/td&gt;
&lt;td&gt;No such key&lt;/td&gt;
&lt;td&gt;Ask them to check for typos&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;No answer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Network down, server error, timeout&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Keep running&lt;/strong&gt;, try again later&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That last row is the one beginners get wrong. If your app stops whenever the license server can't be reached, every outage (yours, the buyer's ISP, a firewall) turns paying customers into angry ones. Some marketplaces, BuiltByBit for example, warn that a plugin which disables itself when a server is unreachable may be treated as malicious. &lt;strong&gt;Only stop on a clear "no".&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Re-check every few hours while the app runs, too. A startup-only check never notices a key you revoked yesterday on a server that hasn't restarted in a month.&lt;/p&gt;

&lt;p&gt;Here's the logic in JavaScript (every language looks the same):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;SunLicense&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sunlicense&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SunLicense&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.sunlicense.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;API_KEY&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;checkLicense&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;licenseKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;machineId&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;run&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stop&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// a clear "no": revoked, expired, too many machines...&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;run&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                               &lt;span class="c1"&gt;// a server error is not a "no"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;run&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                               &lt;span class="c1"&gt;// unreachable: keep running, check again later&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Offline license files
&lt;/h2&gt;

&lt;p&gt;Some customers run with no internet at all. For them, a license server can export a &lt;strong&gt;signed license file&lt;/strong&gt;: the license's details (key, expiry, machine limit), plus a signature made with your private key, usually Ed25519. Your app ships the matching &lt;strong&gt;public key&lt;/strong&gt; and verifies the file without a network. Anyone can read the file, but changing a single character breaks the signature.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fizrm9xf0nc6uyzsuzi8h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fizrm9xf0nc6uyzsuzi8h.png" alt="Online check or offline file: online revokes fast, the signed file works with no network" width="800" height="340"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The trade-off is revocation: a file someone already has stays valid until it expires. So give files a short life (a month, say) and treat them as a &lt;strong&gt;fallback&lt;/strong&gt;: check online first, and use the file only when the server can't be reached.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trials, subscriptions and expiry
&lt;/h2&gt;

&lt;p&gt;The policy's &lt;strong&gt;type&lt;/strong&gt; decides how time works:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Perpetual:&lt;/strong&gt; pay once, works forever (usually with updates for a while).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subscription:&lt;/strong&gt; valid until a date; renewing pushes the date forward. An expired key gets an "expired" answer, so your app can show "renew here" instead of a generic error.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trial:&lt;/strong&gt; a short duration (7 or 14 days) and often one machine. When it ends, the same "expired" answer becomes your upgrade prompt.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Revoking, suspending and refunds
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Suspend&lt;/strong&gt; is a pause: the key stops working until you reinstate it. Use it while you look into something.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Revoke&lt;/strong&gt; is permanent: for a leaked key, or a buyer who charged back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refunds&lt;/strong&gt; should revoke automatically. If your store sends refund webhooks (Tebex, Stripe, Lemon Squeezy and others do), connect them, or refunded buyers keep a free copy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because every buyer has their own key, revoking one never touches anyone else.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz2eddueqsp4i21ro5cd0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz2eddueqsp4i21ro5cd0.png" alt="The licenses list: one masked key per buyer, each with its own status" width="800" height="565"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where keys come from
&lt;/h2&gt;

&lt;p&gt;Don't email keys by hand. Connect your store so a key is created at checkout and delivered to the buyer: by email, on a "redeem" page, or written straight into the download (BuiltByBit and Voxel Shop can do that for Minecraft plugins).&lt;/p&gt;

&lt;h2&gt;
  
  
  What license keys don't do
&lt;/h2&gt;

&lt;p&gt;A key check is one &lt;code&gt;if&lt;/code&gt; in your code, and anyone with a decompiler can delete it. Keys stop casual sharing and make leaks traceable; they don't stop a determined cracker. If that worries you, the next layers are obfuscation and sealing part of your code so it only runs with a valid license. I wrote those up in &lt;a href="https://dev.to/ka0/how-to-protect-your-plugin-or-app-from-piracy-step-by-step-fk3"&gt;How to protect your plugin or app from piracy, step by step&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build it or use a service
&lt;/h2&gt;

&lt;p&gt;You can build all of this: a small API, a database of licenses, a signing key for offline files, and a dashboard to revoke from. Plenty of people do. If you'd rather not, I build &lt;a href="https://sunlicense.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=guides-2026-11&amp;amp;utm_content=license-keys-explained" rel="noopener noreferrer"&gt;SunLicense&lt;/a&gt;, which does the pieces above: policies with machine and IP limits, SDKs for Java, JavaScript, Python, .NET, PHP and Lua, signed offline files, store integrations that issue and revoke keys, and a Discord bot for buyers. The free plan covers 3 products and 100 licenses, with no credit card and no cut of your sales.&lt;/p&gt;

&lt;p&gt;Writing a Minecraft plugin? The step-by-step version is &lt;a href="https://dev.to/ka0/how-to-add-license-keys-to-a-minecraft-plugin-in-10-minutes-bd6"&gt;How to add license keys to a Minecraft plugin in 10 minutes&lt;/a&gt;. For anything else, &lt;a href="https://sunlicense.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=guides-2026-11&amp;amp;utm_content=license-keys-explained-end" rel="noopener noreferrer"&gt;see how SunLicense works&lt;/a&gt; and set up your first product.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was written with the help of an AI assistant and reviewed by me before publishing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>security</category>
      <category>tutorial</category>
      <category>programming</category>
    </item>
    <item>
      <title>How to protect your plugin or app from piracy, step by step</title>
      <dc:creator>Kasun Hapangama</dc:creator>
      <pubDate>Fri, 06 Nov 2026 20:31:29 +0000</pubDate>
      <link>https://dev.to/ka0/how-to-protect-your-plugin-or-app-from-piracy-step-by-step-fk3</link>
      <guid>https://dev.to/ka0/how-to-protect-your-plugin-or-app-from-piracy-step-by-step-fk3</guid>
      <description>&lt;p&gt;&lt;strong&gt;Sell a Minecraft plugin, a FiveM script, a Discord bot or a desktop app, and someone will use it without paying.&lt;/strong&gt; Usually it isn't a skilled cracker. It's one buyer sharing their copy with five friends, or a jar reposted on a leak forum the week you launch.&lt;/p&gt;

&lt;p&gt;You can't make piracy impossible, and trying to usually hurts paying customers more than pirates. What you can do is make sharing inconvenient, make leaks traceable, and make paying the easy option. This is the plan I'd follow, in order. Each step is worth doing on its own, so stop wherever the effort stops paying off.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 0: Decide what you're actually fighting
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Threat&lt;/th&gt;
&lt;th&gt;How common&lt;/th&gt;
&lt;th&gt;What stops it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A buyer shares their copy or key&lt;/td&gt;
&lt;td&gt;Very common&lt;/td&gt;
&lt;td&gt;Per-buyer keys and machine limits (steps 1-2)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A jar gets reposted on a leak site&lt;/td&gt;
&lt;td&gt;Common&lt;/td&gt;
&lt;td&gt;Per-buyer keys, watermarks, takedowns (steps 1, 6, 8)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Someone deletes your license check&lt;/td&gt;
&lt;td&gt;Less common, easier with AI editors&lt;/td&gt;
&lt;td&gt;Core unlock and obfuscation (steps 4-5)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A determined cracker rewrites your code&lt;/td&gt;
&lt;td&gt;Rare&lt;/td&gt;
&lt;td&gt;Nothing fully. Make it slow and not worth it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Most losses sit in the first two rows, and those are the cheapest to fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Give every buyer their own key, automatically
&lt;/h2&gt;

&lt;p&gt;A single shared password or license file can't tell anyone apart. Give each purchase its own key, so you can see who's using what and switch off exactly one person.&lt;/p&gt;

&lt;p&gt;Don't do it by hand. Connect your store so keys are created at checkout. On BuiltByBit, an &lt;em&gt;External license key&lt;/em&gt; placeholder writes the buyer's key into the jar they download. Voxel Shop and SpigotMC have their own placeholders, and Tebex, Stripe, Gumroad, Lemon Squeezy and others can create a key through a webhook. Wire refunds in too, so a refunded purchase loses its key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Limit where a key can run
&lt;/h2&gt;

&lt;p&gt;A key that works everywhere is a key that gets shared. Set a limit that fits the product:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Plugins:&lt;/strong&gt; servers per key (1-3 is typical).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Desktop apps:&lt;/strong&gt; machines per key (2-3 covers a laptop and a desktop).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bots or web tools:&lt;/strong&gt; an IP address limit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When the limit is reached, a new machine is refused with a clear message, and the buyer can free a slot themselves. Be generous: a support ticket costs you more than one extra machine.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F240tm7fmqwk5ys1x4o29.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F240tm7fmqwk5ys1x4o29.png" alt="A policy with Max machines and Max IPs per key" width="800" height="590"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Check at startup, and never take a customer down
&lt;/h2&gt;

&lt;p&gt;Check the license when the app starts, then again every few hours. No need to phone home every minute. Above all, &lt;strong&gt;only stop on a clear "no"&lt;/strong&gt; (revoked, suspended, expired, too many machines). If your license server can't be reached, keep running. BuiltByBit's DRM guidance says the same, and plugins that disable themselves when a server is down risk being removed. For a Minecraft plugin, I walk through this check line by line in &lt;a href="https://dev.to/ka0/how-to-add-license-keys-to-a-minecraft-plugin-in-10-minutes-bd6"&gt;How to add license keys to a Minecraft plugin in 10 minutes&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For customers without internet, ship a &lt;strong&gt;signed offline license&lt;/strong&gt;: a file signed with a private key (Ed25519, for example) that your app verifies with the public key, and that expires on a date you set.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Make the check hard to delete
&lt;/h2&gt;

&lt;p&gt;A license check is one &lt;code&gt;if&lt;/code&gt; statement. Anyone with a decompiler, or an AI editor, can delete it. Two things make that pointless:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Core unlock.&lt;/strong&gt; Encrypt a small, essential piece of your code (a core class, default rules, a lookup table) and only decrypt it after a valid license check. Delete the check, and that piece is simply missing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tamper reports.&lt;/strong&gt; Hash your own classes and report a modified copy with the next license check, so you know which license runs a patched build.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 5: Obfuscate your release builds
&lt;/h2&gt;

&lt;p&gt;Rename classes and methods, encrypt strings and scramble control flow, so your check and your core are hard to find and patch. ProGuard gives a safe rename; ProGuard followed by Skidfuscator adds control flow and string encryption. Only obfuscate the build you sell, test it before upload, and keep the mapping file so you can read customers' stack traces. I wrote a step-by-step guide: &lt;a href="https://dev.to/ka0/how-to-obfuscate-a-minecraft-plugin-free-step-by-step-283m"&gt;How to obfuscate a Minecraft plugin&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Watermark every download
&lt;/h2&gt;

&lt;p&gt;Put something in each buyer's copy that names them: their license key in the jar, an ID in the manifest, a mark in the archive. When the jar shows up on a leak site, you know whose it was without guessing. It also deters sharing: people think twice when their copy carries their name.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Watch for sharing
&lt;/h2&gt;

&lt;p&gt;You won't spot a shared key by reading logs. Get alerts for the patterns that matter: one key on many machines at once, a key checking in from two countries an hour apart, a machine trying random keys, a revoked key still in use, a key pasted in your Discord. Then act on the real ones (a customer who travels isn't a pirate).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj1dgbjlk0i5skyjapu7i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj1dgbjlk0i5skyjapu7i.png" alt="Sentinel watches license checks and flags shared and leaked keys" width="800" height="525"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 8: Respond fast when something leaks
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Find the buyer from the watermark or the key in the leaked file.&lt;/li&gt;
&lt;li&gt;Revoke that key. Copies stop at their next check; everyone else is untouched.&lt;/li&gt;
&lt;li&gt;Send a takedown notice (DMCA) to the site hosting it. Most marketplaces also have a DMCA form.&lt;/li&gt;
&lt;li&gt;Revoke refunded and charged-back purchases too, or they keep a free copy.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The full walkthrough, from unzipping the leaked jar to the takedown notice: &lt;a href="https://dev.to/ka0/your-minecraft-plugin-leaked-heres-how-to-find-who-leaked-it-and-stop-the-copies-1a1d"&gt;Your Minecraft plugin leaked. Here's how to find who leaked it and stop the copies&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 9: Make paying easier than pirating
&lt;/h2&gt;

&lt;p&gt;The best anti-piracy feature is a product that's easy to buy. A fair price, a trial, fast updates, real support and a Discord. A pirate gets an old jar and no help. A customer gets the update that fixes their bug today.&lt;/p&gt;

&lt;h2&gt;
  
  
  The whole plan on one page
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmhfwa04htlnlsq7qnosj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmhfwa04htlnlsq7qnosj.png" alt="The plan, layer by layer: keys, limits, a safe check, core unlock, obfuscation, watermarks, alerts, response" width="800" height="440"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How to put it together
&lt;/h2&gt;

&lt;p&gt;You can build each layer yourself: a small key server, a signing key, an obfuscation step in your build, and a dashboard to revoke from. Or use a service. I build &lt;a href="https://sunlicense.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=guides-2026-11&amp;amp;utm_content=anti-piracy-plan" rel="noopener noreferrer"&gt;SunLicense&lt;/a&gt;, which covers the list:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;In SunLicense&lt;/th&gt;
&lt;th&gt;Plan&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1. Per-buyer keys from your store&lt;/td&gt;
&lt;td&gt;BuiltByBit, Voxel Shop, SpigotMC, Tebex, Stripe, Gumroad and more&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2. Machine and IP limits&lt;/td&gt;
&lt;td&gt;Per policy&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3. Safe startup check, offline files&lt;/td&gt;
&lt;td&gt;SDKs for Java, JavaScript, Python, .NET, PHP and Lua; signed Ed25519 files&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4. Core unlock and tamper reports&lt;/td&gt;
&lt;td&gt;Seal up to 512 KB, 7 days offline grace&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5. Obfuscation&lt;/td&gt;
&lt;td&gt;Maximum, ProGuard, yGuard, Skidfuscator; JavaScript, Python, Lua (FiveM)&lt;/td&gt;
&lt;td&gt;Free (3 runs a month)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6. Watermarked downloads&lt;/td&gt;
&lt;td&gt;The buyer's mark in each file&lt;/td&gt;
&lt;td&gt;Standard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7. Sharing alerts&lt;/td&gt;
&lt;td&gt;Sentinel detectors, Discord alerts&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8. Revoke in one click&lt;/td&gt;
&lt;td&gt;Dashboard, CLI, Discord&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It's free for 3 products and 100 licenses, with no credit card and no cut of your sales. &lt;a href="https://sunlicense.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=guides-2026-11&amp;amp;utm_content=anti-piracy-plan-end" rel="noopener noreferrer"&gt;See what SunLicense covers&lt;/a&gt;, then work through the list, starting with step 1.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was written with the help of an AI assistant and reviewed by me before publishing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>gamedev</category>
      <category>java</category>
      <category>indiehackers</category>
    </item>
    <item>
      <title>How to obfuscate a Minecraft plugin (free, step by step)</title>
      <dc:creator>Kasun Hapangama</dc:creator>
      <pubDate>Fri, 06 Nov 2026 20:31:21 +0000</pubDate>
      <link>https://dev.to/ka0/how-to-obfuscate-a-minecraft-plugin-free-step-by-step-283m</link>
      <guid>https://dev.to/ka0/how-to-obfuscate-a-minecraft-plugin-free-step-by-step-283m</guid>
      <description>&lt;p&gt;&lt;strong&gt;Assume your premium plugin will be decompiled the day it goes on sale.&lt;/strong&gt; Anyone can open the jar in a decompiler and read your code like the source, including the license check you spent an evening on.&lt;/p&gt;

&lt;p&gt;Obfuscation won't make that impossible. It turns readable code into a puzzle, so copying your logic or patching out a check takes far more work. The catch: a careless obfuscation pass is the fastest way to ship a plugin that doesn't load. Paper and Spigot find your main class by name and call your event handlers by reflection. Rename the wrong thing and the server throws &lt;code&gt;ClassNotFoundException&lt;/code&gt; at startup.&lt;/p&gt;

&lt;p&gt;This guide shows two ways to do it safely: the quick way, with a free hosted obfuscator that writes the keep rules for you, and the by-hand way with ProGuard. Then a checklist to test the jar before you sell it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh6tkfgz1ypf1kk2rrgct.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh6tkfgz1ypf1kk2rrgct.png" alt="A license check before and after obfuscation, as a decompiler shows it"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What obfuscation does, and what it doesn't
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;It does:&lt;/strong&gt; rename your classes, methods and fields to &lt;code&gt;a&lt;/code&gt;, &lt;code&gt;b&lt;/code&gt;, &lt;code&gt;c&lt;/code&gt;, encrypt string constants, and scramble control flow. A decompiler still produces Java, but it reads like noise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It doesn't:&lt;/strong&gt; stop someone determined, or stop a buyer from sharing the jar. It raises the cost of reading and patching your code. For sharing you need a license key per buyer (more at the end). The two work best together: obfuscation hides the check, and the license makes a shared jar useless.&lt;/p&gt;

&lt;h2&gt;
  
  
  The quick way: free hosted obfuscation
&lt;/h2&gt;

&lt;p&gt;I build &lt;a href="https://sunlicense.com/for/minecraft-plugins?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=guides-2026-11&amp;amp;utm_content=obfuscate-plugin" rel="noopener noreferrer"&gt;SunLicense&lt;/a&gt;, a licensing service for plugin and app sellers. Its free plan includes 3 obfuscation runs a month, which covers a release or two. It runs ProGuard, yGuard and Skidfuscator on its servers, and it reads your jar to find what must keep its name, so you don't write keep rules.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Create a free account
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://sunlicense.com/for/minecraft-plugins?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=guides-2026-11&amp;amp;utm_content=obfuscate-plugin-step1" rel="noopener noreferrer"&gt;Sign up&lt;/a&gt; with Google, GitHub, Discord or email. No credit card.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Developers → Obfuscation → Java
&lt;/h3&gt;

&lt;p&gt;Open &lt;strong&gt;Obfuscation&lt;/strong&gt; under &lt;strong&gt;Developers&lt;/strong&gt; in the sidebar and pick &lt;strong&gt;Java&lt;/strong&gt;. (The same page handles JavaScript, Python and Lua for FiveM, if you sell those too.)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fspokysh7uvzy1iefe6d0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fspokysh7uvzy1iefe6d0.png" alt="The Obfuscation page: choose Java, JavaScript/TypeScript, Lua or Python"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Pick Maximum or ProGuard
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Engine&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;Pick it when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Maximum&lt;/strong&gt; (recommended)&lt;/td&gt;
&lt;td&gt;ProGuard renames, then Skidfuscator adds control flow and string encryption&lt;/td&gt;
&lt;td&gt;You want the strongest protection and will test the result&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ProGuard&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Renames classes, methods and fields&lt;/td&gt;
&lt;td&gt;You want the safe standard, or Maximum broke something&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;yGuard&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Renames only&lt;/td&gt;
&lt;td&gt;You need the most compatible option&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Skidfuscator&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Control flow and strings, keeps names&lt;/td&gt;
&lt;td&gt;You want flow and string protection without renaming&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Start with &lt;strong&gt;Maximum&lt;/strong&gt;. If your plugin leans heavily on reflection (NMS, Gson models, a command framework) and something misbehaves in testing, run the same jar again with &lt;strong&gt;ProGuard&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4rtvswlhrulq6ir70nvj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4rtvswlhrulq6ir70nvj.png" alt="Choose an obfuscator: Maximum, ProGuard, yGuard and Skidfuscator"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Upload the built jar
&lt;/h3&gt;

&lt;p&gt;Build the jar you actually ship (with Shadow, &lt;code&gt;./gradlew shadowJar&lt;/code&gt;) and drop it in. A &lt;code&gt;.jar&lt;/code&gt; or a &lt;code&gt;.zip&lt;/code&gt; of several jars, up to 100 MB, Java up to 25.&lt;/p&gt;

&lt;p&gt;SunLicense reads the jar and keeps the names that must survive: the &lt;code&gt;main&lt;/code&gt; class in &lt;code&gt;plugin.yml&lt;/code&gt;, &lt;code&gt;paper-plugin.yml&lt;/code&gt; or &lt;code&gt;bungee.yml&lt;/code&gt;, &lt;code&gt;velocity-plugin.json&lt;/code&gt;, Fabric and Forge entry points and mixins, &lt;code&gt;META-INF/services&lt;/code&gt;, &lt;code&gt;@EventHandler&lt;/code&gt; and &lt;code&gt;@Subscribe&lt;/code&gt; methods, enums and serialisable types.&lt;/p&gt;

&lt;p&gt;One detail that saves a lot of pain: if you sell on BuiltByBit or Voxel Shop with a download &lt;strong&gt;placeholder&lt;/strong&gt; (a string like &lt;code&gt;%%__SUNLICENSE__%%&lt;/code&gt; the store replaces with the buyer's key), string encryption would hide that string from the store. Classes that hold a placeholder are never string-encrypted, so key injection keeps working.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsvhqf7nu71ynq00qqowh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsvhqf7nu71ynq00qqowh.png" alt="Upload your file: .jar or .zip, up to 100 MB"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Optional:&lt;/strong&gt; &lt;strong&gt;Advanced options&lt;/strong&gt; has a &lt;strong&gt;Keep unrenamed&lt;/strong&gt; list for extra packages or classes (for example &lt;code&gt;com.example.api.**&lt;/code&gt;), and lets you add library jars you compile against if the report mentions missing classes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Run, then download three files
&lt;/h3&gt;

&lt;p&gt;Choose &lt;strong&gt;Obfuscate&lt;/strong&gt; and watch the live log. It runs as a background task, so you can close the page and come back; results are kept for 7 days and stay on the &lt;strong&gt;History&lt;/strong&gt; tab. You get:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The obfuscated jar&lt;/strong&gt;: the one you upload to your store.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The mapping file&lt;/strong&gt;: keep it for every release. When a buyer sends a stack trace full of &lt;code&gt;a.b.c&lt;/code&gt;, ProGuard's &lt;code&gt;retrace&lt;/code&gt; tool turns it back into your real names.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A short report&lt;/strong&gt; on the run, including any warnings.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Releasing often? The same run works from a release script with the CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sunlicense obfuscate build/libs/MyPlugin-1.0-all.jar &lt;span class="nt"&gt;--profile&lt;/span&gt; release &lt;span class="nt"&gt;-o&lt;/span&gt; dist/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The by-hand way: ProGuard
&lt;/h2&gt;

&lt;p&gt;If you'd rather run it yourself, &lt;a href="https://www.guardsquare.com/proguard" rel="noopener noreferrer"&gt;ProGuard&lt;/a&gt; is the safe choice. What you must keep:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Keep&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;The &lt;code&gt;main:&lt;/code&gt; class in &lt;code&gt;plugin.yml&lt;/code&gt; (and &lt;code&gt;paper-plugin.yml&lt;/code&gt;, &lt;code&gt;bungee.yml&lt;/code&gt;, &lt;code&gt;velocity-plugin.json&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;The server loads it by exact name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;@EventHandler&lt;/code&gt; methods&lt;/td&gt;
&lt;td&gt;Found by annotation; shrinking removes "unused" methods&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;ConfigurationSerializable&lt;/code&gt; classes&lt;/td&gt;
&lt;td&gt;Saved configs store the class name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enum &lt;code&gt;values()&lt;/code&gt; / &lt;code&gt;valueOf()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Used when reading config values back&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anything loaded by reflection&lt;/td&gt;
&lt;td&gt;The name is a string in your code&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A starting config for a shaded jar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;-&lt;span class="n"&gt;injars&lt;/span&gt;  &lt;span class="n"&gt;build&lt;/span&gt;/&lt;span class="n"&gt;libs&lt;/span&gt;/&lt;span class="n"&gt;MyPlugin&lt;/span&gt;-&lt;span class="m"&gt;1&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;-&lt;span class="n"&gt;all&lt;/span&gt;.&lt;span class="n"&gt;jar&lt;/span&gt;
-&lt;span class="n"&gt;outjars&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;/&lt;span class="n"&gt;obf&lt;/span&gt;/&lt;span class="n"&gt;MyPlugin&lt;/span&gt;-&lt;span class="m"&gt;1&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="n"&gt;jar&lt;/span&gt;
-&lt;span class="n"&gt;libraryjars&lt;/span&gt; &amp;lt;&lt;span class="n"&gt;java&lt;/span&gt;.&lt;span class="n"&gt;home&lt;/span&gt;&amp;gt;/&lt;span class="n"&gt;jmods&lt;/span&gt;/&lt;span class="n"&gt;java&lt;/span&gt;.&lt;span class="n"&gt;base&lt;/span&gt;.&lt;span class="n"&gt;jmod&lt;/span&gt;(!**.&lt;span class="n"&gt;jar&lt;/span&gt;;!&lt;span class="n"&gt;module&lt;/span&gt;-&lt;span class="n"&gt;info&lt;/span&gt;.&lt;span class="n"&gt;class&lt;/span&gt;)
-&lt;span class="n"&gt;libraryjars&lt;/span&gt; &lt;span class="n"&gt;libs&lt;/span&gt;/&lt;span class="n"&gt;paper&lt;/span&gt;-&lt;span class="n"&gt;api&lt;/span&gt;.&lt;span class="n"&gt;jar&lt;/span&gt;

&lt;span class="c"&gt;# Plugins are loaded by reflection: don't shrink or optimise
&lt;/span&gt;-&lt;span class="n"&gt;dontshrink&lt;/span&gt;
-&lt;span class="n"&gt;dontoptimize&lt;/span&gt;
-&lt;span class="n"&gt;keepattributes&lt;/span&gt; *&lt;span class="n"&gt;Annotation&lt;/span&gt;*,&lt;span class="n"&gt;Signature&lt;/span&gt;,&lt;span class="n"&gt;InnerClasses&lt;/span&gt;,&lt;span class="n"&gt;EnclosingMethod&lt;/span&gt;,&lt;span class="n"&gt;SourceFile&lt;/span&gt;,&lt;span class="n"&gt;LineNumberTable&lt;/span&gt;

-&lt;span class="n"&gt;keep&lt;/span&gt; &lt;span class="n"&gt;public&lt;/span&gt; &lt;span class="n"&gt;class&lt;/span&gt; &lt;span class="n"&gt;com&lt;/span&gt;.&lt;span class="n"&gt;example&lt;/span&gt;.&lt;span class="n"&gt;myplugin&lt;/span&gt;.&lt;span class="n"&gt;MyPlugin&lt;/span&gt; { *; }
-&lt;span class="n"&gt;keepclassmembers&lt;/span&gt; &lt;span class="n"&gt;class&lt;/span&gt; * { @&lt;span class="n"&gt;org&lt;/span&gt;.&lt;span class="n"&gt;bukkit&lt;/span&gt;.&lt;span class="n"&gt;event&lt;/span&gt;.&lt;span class="n"&gt;EventHandler&lt;/span&gt; &amp;lt;&lt;span class="n"&gt;methods&lt;/span&gt;&amp;gt;; }
-&lt;span class="n"&gt;keep&lt;/span&gt; &lt;span class="n"&gt;class&lt;/span&gt; * &lt;span class="n"&gt;implements&lt;/span&gt; &lt;span class="n"&gt;org&lt;/span&gt;.&lt;span class="n"&gt;bukkit&lt;/span&gt;.&lt;span class="n"&gt;configuration&lt;/span&gt;.&lt;span class="n"&gt;serialization&lt;/span&gt;.&lt;span class="n"&gt;ConfigurationSerializable&lt;/span&gt; { *; }
-&lt;span class="n"&gt;keepclassmembers&lt;/span&gt; &lt;span class="n"&gt;enum&lt;/span&gt; * {
    &lt;span class="n"&gt;public&lt;/span&gt; &lt;span class="n"&gt;static&lt;/span&gt; **[] &lt;span class="n"&gt;values&lt;/span&gt;();
    &lt;span class="n"&gt;public&lt;/span&gt; &lt;span class="n"&gt;static&lt;/span&gt; ** &lt;span class="n"&gt;valueOf&lt;/span&gt;(&lt;span class="n"&gt;java&lt;/span&gt;.&lt;span class="n"&gt;lang&lt;/span&gt;.&lt;span class="n"&gt;String&lt;/span&gt;);
}

-&lt;span class="n"&gt;printmapping&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;/&lt;span class="n"&gt;obf&lt;/span&gt;/&lt;span class="n"&gt;mapping&lt;/span&gt;.&lt;span class="n"&gt;txt&lt;/span&gt;
-&lt;span class="n"&gt;dontwarn&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add a &lt;code&gt;-keep&lt;/code&gt; line for every package you reach by reflection. For more strength, run &lt;a href="https://skidfuscator.dev" rel="noopener noreferrer"&gt;Skidfuscator&lt;/a&gt; after ProGuard: turn on string encryption first, test, then control flow, and test again. Exclude the class holding any store placeholder from string encryption.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the jar before you sell it
&lt;/h2&gt;

&lt;p&gt;Never upload an obfuscated jar you haven't started. Five minutes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Start a clean Paper server with only your plugin. No errors in the console.&lt;/li&gt;
&lt;li&gt;Run every command, including tab completion.&lt;/li&gt;
&lt;li&gt;Trigger your main events (join, break a block, open your GUI).&lt;/li&gt;
&lt;li&gt;Restart the server: config and saved data load back.&lt;/li&gt;
&lt;li&gt;Test on every server version listed on your store page.&lt;/li&gt;
&lt;li&gt;Open the jar in a decompiler yourself and check it reads like noise.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If something fails, the error names the class. Keep that package unrenamed, or step down from Maximum to ProGuard, and run again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Obfuscation is half the job
&lt;/h2&gt;

&lt;p&gt;An obfuscated jar that anyone can run is still a jar anyone can share. The other half is &lt;strong&gt;one license key per buyer&lt;/strong&gt;, checked on startup: a shared key then shows up on too many servers, and a leaked jar names its buyer. I wrote that up separately: &lt;a href="https://dev.to/ka0/how-to-add-license-keys-to-a-minecraft-plugin-in-10-minutes-bd6"&gt;How to add license keys to a Minecraft plugin in 10 minutes&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Licensing and obfuscation are both on SunLicense's free plan, with no cut of your sales. &lt;a href="https://sunlicense.com/for/minecraft-plugins?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=guides-2026-11&amp;amp;utm_content=obfuscate-plugin-end" rel="noopener noreferrer"&gt;See how it works for Minecraft plugins&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was written with the help of an AI assistant and reviewed by me before publishing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>minecraft</category>
      <category>java</category>
      <category>security</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>How to add license keys to a Minecraft plugin in 10 minutes</title>
      <dc:creator>Kasun Hapangama</dc:creator>
      <pubDate>Fri, 06 Nov 2026 20:30:22 +0000</pubDate>
      <link>https://dev.to/ka0/how-to-add-license-keys-to-a-minecraft-plugin-in-10-minutes-bd6</link>
      <guid>https://dev.to/ka0/how-to-add-license-keys-to-a-minecraft-plugin-in-10-minutes-bd6</guid>
      <description>&lt;p&gt;&lt;strong&gt;Selling a premium plugin without license keys?&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;I build &lt;a href="https://sunlicense.com/for/minecraft-plugins?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=guides-2026-11&amp;amp;utm_content=license-keys-plugin" rel="noopener noreferrer"&gt;SunLicense&lt;/a&gt;, 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The design, in four rules
&lt;/h2&gt;

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

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr52u91pogsfcamhbkzx9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr52u91pogsfcamhbkzx9.png" alt="One check, three answers: valid starts the plugin, a clear no disables it, unreachable keeps running" width="800" height="320"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Create the product and a policy (2 minutes)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://sunlicense.com/for/minecraft-plugins?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=guides-2026-11&amp;amp;utm_content=license-keys-plugin-step1" rel="noopener noreferrer"&gt;Create a free account&lt;/a&gt; (Google, GitHub, Discord or email, no credit card), then:&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft6nymhutbfydfbv0xqkn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft6nymhutbfydfbv0xqkn.png" alt="The New policy dialog with Max machines set to 2" width="800" height="590"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Add the check (5 minutes)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The fastest way:&lt;/strong&gt; start from the free &lt;a href="https://github.com/sunlicense-com/example-paper-plugin" rel="noopener noreferrer"&gt;Paper plugin template&lt;/a&gt;. 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.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;Use this template&lt;/strong&gt; on GitHub, then rename &lt;code&gt;com.example.myplugin&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Put the API key in &lt;code&gt;~/.gradle/gradle.properties&lt;/code&gt; as &lt;code&gt;sunlicenseApiKey=slk_...&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;./gradlew runServer&lt;/code&gt; to start a local Paper server with your plugin.&lt;/li&gt;
&lt;li&gt;Put your features in &lt;code&gt;core/CoreFeatures.java&lt;/code&gt;. They start once the license is valid.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Adding it to an existing plugin instead?&lt;/strong&gt; Add the Java SDK (&lt;code&gt;com.sunlicense:sunlicense&lt;/code&gt;, &lt;a href="https://docs.sunlicense.com/guide/sdks" rel="noopener noreferrer"&gt;current version in the docs&lt;/a&gt;), shade and relocate it, and run the check off the main thread:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// BuiltByBit replaces this string with the buyer's key when they download.&lt;/span&gt;
&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="no"&gt;LICENSE_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"%%__SUNLICENSE__%%"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;SunLicense&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;serverId&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;@Override&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;onEnable&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SunLicense&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;baseUrl&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://api.sunlicense.com"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;API_KEY&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofSeconds&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;serverId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;serverId&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;IOException&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;serverId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"unknown"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// Check now, then every 6 hours, on a background thread.&lt;/span&gt;
    &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;sixHours&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20L&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;getServer&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getScheduler&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;runTaskTimerAsynchronously&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;checkLicense&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0L&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sixHours&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// ...start your plugin&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

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

&lt;span class="c1"&gt;// A random id saved in the plugin folder, so each server counts once.&lt;/span&gt;
&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;serverId&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;IOException&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Path&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;getDataFolder&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;toPath&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;resolve&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;".server-id"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Files&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isRegularFile&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Files&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;readString&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;trim&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;UUID&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;randomUUID&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;toString&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="nc"&gt;Files&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createDirectories&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getParent&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="nc"&gt;Files&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;writeString&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Let BuiltByBit deliver the keys (2 minutes)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;In SunLicense: &lt;strong&gt;Integrations → BuiltByBit → Connect&lt;/strong&gt;. 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.&lt;/li&gt;
&lt;li&gt;On BuiltByBit: &lt;strong&gt;Creator dashboard → Placeholders → Create placeholder&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;Placeholder: &lt;code&gt;%%__SUNLICENSE__%%&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Type: &lt;strong&gt;External license key&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;URL and secret: from step 1&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Apply it to your resource and &lt;strong&gt;publish an update&lt;/strong&gt;. Placeholders only apply to files uploaded after they exist.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr1oj8cymzm4dwavtxs1w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr1oj8cymzm4dwavtxs1w.png" alt="The Connect BuiltByBit dialog: resource ID mapped to a policy, secret created for you" width="800" height="525"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Test it like a buyer (1 minute)
&lt;/h2&gt;

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

&lt;h2&gt;
  
  
  What you get
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A shared key gets refused&lt;/strong&gt; past the server limit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A leaked jar names its buyer.&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Paying customers never notice:&lt;/strong&gt; one check on startup, a quiet re-check, and it keeps running if your license server has a bad day.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz2eddueqsp4i21ro5cd0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz2eddueqsp4i21ro5cd0.png" alt="The licenses list: one masked key per buyer, each with its own status" width="800" height="565"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Going further
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Offline licenses:&lt;/strong&gt; a signed file for servers without internet access.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seal a core:&lt;/strong&gt; with the template, &lt;code&gt;./gradlew build -PsunlicenseProduct=&amp;lt;code&amp;gt;&lt;/code&gt; seals a part of your plugin so only a valid license unseals it. Deleting the check then doesn't help a pirate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Obfuscate the release jar:&lt;/strong&gt; see &lt;a href="https://dev.to/ka0/how-to-obfuscate-a-minecraft-plugin-free-step-by-step-283m"&gt;How to obfuscate a Minecraft plugin&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Let your coding agent do it:&lt;/strong&gt; the product's &lt;strong&gt;Setup&lt;/strong&gt; 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.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Licensing, the BuiltByBit integration and obfuscation are all on SunLicense's free plan, with no cut of your sales. &lt;a href="https://sunlicense.com/for/minecraft-plugins?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=guides-2026-11&amp;amp;utm_content=license-keys-plugin-end" rel="noopener noreferrer"&gt;See how it works for Minecraft plugins&lt;/a&gt; and protect your next release.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was written with the help of an AI assistant and reviewed by me before publishing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>minecraft</category>
      <category>java</category>
      <category>tutorial</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>Stop Shipping Software Without a License System — Meet SunLicense</title>
      <dc:creator>Kasun Hapangama</dc:creator>
      <pubDate>Wed, 10 Jun 2026 18:26:48 +0000</pubDate>
      <link>https://dev.to/ka0/stop-shipping-software-without-a-license-system-meet-sunlicense-26o8</link>
      <guid>https://dev.to/ka0/stop-shipping-software-without-a-license-system-meet-sunlicense-26o8</guid>
      <description>&lt;p&gt;If you've ever shipped a software product — a Minecraft plugin, a Discord bot, a Java library, anything paid — you know the feeling.&lt;/p&gt;

&lt;p&gt;Someone reposts your download link. A customer's key gets leaked. You have no way to invalidate it, no idea how many installs are out there, and no system to do anything about it.&lt;/p&gt;

&lt;p&gt;License management is one of those unsexy problems every indie dev eventually hits. Most either skip it entirely, roll a janky home-baked solution, or find enterprise tools that cost more per month than the product earns.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://builtbybit.com/resources/sunlicense-license-management-system.58359/?ref=featured" rel="noopener noreferrer"&gt;SunLicense&lt;/a&gt; is a different answer — built by an indie dev, for indie devs, priced as a one-time $14.99.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; I'm Kasun Hapangama, the creator of SunLicense. Stats and review excerpts are pulled directly from the BuiltByBit listing.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  What Is SunLicense?
&lt;/h2&gt;

&lt;p&gt;SunLicense is a &lt;strong&gt;self-hosted license management system&lt;/strong&gt; that ships with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A web admin dashboard&lt;/li&gt;
&lt;li&gt;A customer-facing dashboard (with Discord OAuth2 login)&lt;/li&gt;
&lt;li&gt;A Discord bot for automated license workflows&lt;/li&gt;
&lt;li&gt;A RESTful license API&lt;/li&gt;
&lt;li&gt;A Java API wrapper&lt;/li&gt;
&lt;li&gt;BuiltByBit marketplace integration for fully automated license issuance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's currently at &lt;strong&gt;v1.2.4&lt;/strong&gt;, with 11 updates since launch in January 2025, 212 purchases, and a 5.0 average rating across 16 reviews on BuiltByBit.&lt;/p&gt;




&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;p&gt;The core model is dead simple — intentionally so.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. You create a license key (dashboard or API)
2. The user enters the key in your product
3. Your product calls the SunLicense validation endpoint
4. The server applies your rules → grants or denies access
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rules you can configure per license:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Expiry dates&lt;/li&gt;
&lt;li&gt;Device (HWID) restrictions&lt;/li&gt;
&lt;li&gt;IP restrictions / blacklists&lt;/li&gt;
&lt;li&gt;Temporary licenses&lt;/li&gt;
&lt;li&gt;Max activations&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Integrating Into Your Product
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Java (using the official API wrapper)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Initialize the client&lt;/span&gt;
&lt;span class="nc"&gt;SunLicense&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SunLicenseClient&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"https://your-sunlicense-host.com"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"your-product-secret-key"&lt;/span&gt;
&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Validate at startup&lt;/span&gt;
&lt;span class="nc"&gt;ValidationResult&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;validate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;userInputKey&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;HardwareID&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;generate&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;// auto HWID binding&lt;/span&gt;
&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isValid&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"License invalid: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getReason&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;exit&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// License valid — continue loading your product&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Other Languages (REST API)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /api/v1/license/validate
Content-Type: application/json

{
  "key": "XXXX-XXXX-XXXX-XXXX",
  "hwid": "generated-hardware-id",
  "product_id": "your-product-id"
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"valid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expires_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-12-31T00:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"License validated successfully"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Support guides exist for PHP, Python, JavaScript, Lua, and more — so you're not starting from scratch regardless of your stack.&lt;/p&gt;




&lt;h2&gt;
  
  
  Feature Breakdown
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Core
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Web Admin Dashboard&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Customer Dashboard (Discord OAuth2)&lt;/td&gt;
&lt;td&gt;✅ New in 1.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Discord Bot Integration&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RESTful License API&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Java API Wrapper&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Temporary Licenses&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IP Restrictions&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HWID Restrictions&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unlimited Products &amp;amp; Licenses&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Graphs &amp;amp; Analytics&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dark Theme&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Automation
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Auto License Creation via BuiltByBit Purchase&lt;/td&gt;
&lt;td&gt;✅ New in 1.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto License Creation via Discord Role&lt;/td&gt;
&lt;td&gt;✅ New in 1.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Product Discord Role on License Claim&lt;/td&gt;
&lt;td&gt;✅ New in 1.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DM Discord Commands&lt;/td&gt;
&lt;td&gt;✅ New in 1.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom Discord Embeds&lt;/td&gt;
&lt;td&gt;✅ New in 1.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blacklists (IP / HWID)&lt;/td&gt;
&lt;td&gt;✅ New in 1.1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Security &amp;amp; Ops
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Secure Developer API&lt;/td&gt;
&lt;td&gt;✅ New in 1.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Disaster Mode (Panic Mode)&lt;/td&gt;
&lt;td&gt;✅ New in 1.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Automated + Manual Backups&lt;/td&gt;
&lt;td&gt;✅ New in 1.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;System Health Checks&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blacklisting&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Platform Support
&lt;/h2&gt;

&lt;p&gt;SunLicense ships with integration guides for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Minecraft&lt;/strong&gt; — Plugins, Mods, Server Software&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discord Bots&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FiveM Projects&lt;/strong&gt; &lt;em&gt;(added in 1.1)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Java Projects&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PHP Projects&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Python Projects&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JavaScript Projects&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lua Projects&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Infrastructure: runs on &lt;strong&gt;ARM and x86&lt;/strong&gt;, supports &lt;strong&gt;Pterodactyl Panel&lt;/strong&gt; (a Pterodactyl Egg is included), and &lt;strong&gt;managed hosting&lt;/strong&gt; is available if you'd rather not self-host.&lt;/p&gt;




&lt;h2&gt;
  
  
  Who Should Be Using This
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Marketplace sellers (BuiltByBit, Spigot, etc.)
&lt;/h3&gt;

&lt;p&gt;The BuiltByBit automation is the headline feature. When someone buys your resource, SunLicense auto-generates their key and optionally assigns a Discord role — zero manual steps. If you're actively selling on a marketplace and handling license requests by hand, this alone pays for itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Discord bot developers
&lt;/h3&gt;

&lt;p&gt;The Discord-native workflow is genuinely polished. Customers can claim and check licenses entirely via DM commands. Role-gated access works automatically on claim. Custom embed support means it fits your branding without looking like someone else's bot.&lt;/p&gt;

&lt;h3&gt;
  
  
  Java / backend developers
&lt;/h3&gt;

&lt;p&gt;The Java API wrapper is well-maintained, there's a sample project in the docs, and HWID + IP binding gives you real protection for desktop software without running your own validation infrastructure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Small studios with multiple products
&lt;/h3&gt;

&lt;p&gt;Unlimited products and licenses out of the box. If you're at three or four products and still handling key distribution manually over Discord — this is the inflection point where a system like this pays for itself in hours saved per week.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Honest Take
&lt;/h2&gt;

&lt;p&gt;No tool is perfect, and the reviews are candid about where SunLicense has room to grow.&lt;/p&gt;

&lt;p&gt;The setup process has a learning curve, especially outside the Java ecosystem. One reviewer noted:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Not being able to configure things outside the Java experience makes it harder than it needs to be sometimes."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is fair feedback. The creator has acknowledged it publicly and is active on the Discord server taking suggestions.&lt;/p&gt;

&lt;p&gt;The flip side: &lt;strong&gt;11 updates since January 2025&lt;/strong&gt;, covering things users actually asked for — customer dashboards, automated backups, FiveM support, the secure developer API. That pace of iteration matters a lot when your product's license validation depends on a tool staying maintained.&lt;/p&gt;

&lt;p&gt;One other thing worth knowing: this is a &lt;strong&gt;self-hosted system&lt;/strong&gt;. You're responsible for running the server, or you pay for managed hosting. If zero-ops is a hard requirement, factor that into your evaluation.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Users Are Saying
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"The dashboard is intuitive, the automation works flawlessly, and every feature feels like it was built with real developers' needs in mind. It's rare to find a product that combines such a smooth user experience with this level of power and reliability."&lt;/em&gt;&lt;br&gt;
— &lt;strong&gt;gOOvER&lt;/strong&gt;, ★★★★★&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"By far the best License Management system. The owner even helped me in a private voice call to set everything up — 10/10."&lt;/em&gt;&lt;br&gt;
— &lt;strong&gt;xByNovaTV&lt;/strong&gt;, ★★★★★&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"It's a solid and promising product. The Discord bot is super helpful. You can tell there's real passion behind it. Setup can be a pain outside Java, but despite those bumps, it's still a great tool overall."&lt;/em&gt;&lt;br&gt;
— &lt;strong&gt;bestiebeate&lt;/strong&gt;, ★★★★☆&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"This is the first and probably last license system I have to buy."&lt;/em&gt;&lt;br&gt;
— &lt;strong&gt;workattack&lt;/strong&gt;, ★★★★★&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Pricing
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Base License&lt;/td&gt;
&lt;td&gt;$14.99 one-time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enhanced Support (install &amp;amp; setup)&lt;/td&gt;
&lt;td&gt;+$20.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Source Code&lt;/td&gt;
&lt;td&gt;+$99.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Support Duration&lt;/td&gt;
&lt;td&gt;Lifetime&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Bundles are also available:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SunLicense Bundle&lt;/strong&gt; — Total Protection &amp;amp; Control for Your Software (~$25.48, was $29.98)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SunDevs Developer Bundle&lt;/strong&gt; — All SunDevs products (~$47.96, was $59.96)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🔗 &lt;a href="https://builtbybit.com/resources/sunlicense-license-management-system.58359/?ref=featured" rel="noopener noreferrer"&gt;SunLicense on BuiltByBit&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📖 &lt;a href="https://docs.sundevs.hapangama.com" rel="noopener noreferrer"&gt;Documentation&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💬 &lt;a href="https://discord.gg/sundevs" rel="noopener noreferrer"&gt;Discord Support Server&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;☕ &lt;a href="https://sundevs.hapangama.com" rel="noopener noreferrer"&gt;SunDevs Studio&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Built and maintained by &lt;a href="https://kasun.hapangama.com" rel="noopener noreferrer"&gt;Kasun Hapangama&lt;/a&gt; @ SunDevs · Sri Lanka&lt;/em&gt;&lt;/p&gt;

</description>
      <category>tools</category>
      <category>opensource</category>
      <category>java</category>
      <category>devtools</category>
    </item>
  </channel>
</rss>
