How to add license keys to a discord.py bot
Short answer: pip install sunlicense, read the buyer's key from the environment, check it in setup_hook (in a thread, so the event loop isn't blocked), and close the bot when SunLicense says no. A network error is not a no: keep running and check again later.
import asyncio, os, urllib.error
import discord
from sunlicense import SunLicense
licensing = SunLicense(base_url="https://api.sunlicense.com", token=API_KEY) # API key from your build, never git
class MyBot(discord.Client):
async def setup_hook(self):
try:
r = await asyncio.to_thread(licensing.validate, os.environ["LICENSE_KEY"], host=machine_id())
if r.status == 400: # EXPIRED, REVOKED, TOO_MANY_MACHINES...
print("License rejected:", (r.data or {}).get("code"))
await self.close()
except (urllib.error.URLError, TimeoutError) as e:
print("SunLicense unreachable, running anyway:", e)
MyBot(intents=discord.Intents.default()).run(os.environ["DISCORD_TOKEN"])validate returns (valid, status, data): a 400 is a rejection with its code in data; network failures raise.
There's no Python bot template yet: the discord.js template shows the same behaviour in full (re-checks, an offline file, masked keys, a /license command) and ports line for line.
Or let your AI agent do it: paste the setup prompt into Claude Code, Cursor or Codex in the bot's folder. It adds the check and proves it with a test license.
Free plan: 3 products, 100 licenses, no credit card. Create a free account.
Step by step
In the dashboard: a product, a policy (1 machine = one running copy per license) and an API key, which can only validate licenses.
Install:
pip install sunlicense(no dependencies beyond the standard library; it works with nextcord, disnake, py-cord and hikari too).Keep the API key out of git. The buyer's
.envholdsDISCORD_TOKENandLICENSE_KEY; your API key goes into the build you hand out (a generated_build.pyyour packaging script writes), never the repository.Machine id: hash something stable and send it as
host:pythonimport hashlib, platform, socket def machine_id(): raw = f"{platform.system()}|{platform.machine()}|{socket.gethostname()}" return hashlib.sha256(raw.encode()).hexdigest()In Docker set a fixed
hostname:, or the id changes with every container. Choosing a machine id.Don't block the event loop. The SDK is synchronous (
urllib);asyncio.to_threadkeeps the gateway heartbeat going while it waits.Results: 200 runs, 400 closes with the reason (mask the key in logs), anything else runs and retries in about 10 minutes, or uses an offline license.
Re-check every few hours with
discord.ext.tasks(@tasks.loop(hours=6)), closing on a 400, so a revoked key reaches bots that are already running.
Selling it
Gumroad, Lemon Squeezy, Payhip, Stripe, Tebex and the other stores issue a key per purchase and revoke it on a refund. The SunLicense Discord bot can give buyers their key and a role in your support server.
Going further
- Obfuscation for the Python you ship, and protection to make part of the bot run only with a valid license.
- Selling a command-line tool instead? See Python CLI.
- discord.js and JDA versions of this guide. More on licensing Discord bots.
