NapNodes

← Discord bot guides

Discord bots · updated 24 October 2026

Discord Giveaway Bot in Python: Enter Button, Random Winners and Rerolls

The most popular giveaway bots belong to someone else: they carry their branding and ads, and some charge for more than one winner. This one you build yourself in discord.py, in a single file: /giveaway posts the prize with an “Enter” button and a countdown, picks the winners at random when it ends and mentions them, and /reroll draws someone else if a winner doesn't reply. Giveaways are saved in a file, so a restart doesn't wipe them.

What the bot does

Anyone who can manage the server types /giveaway with the prize, how many minutes it lasts and how many winners. The bot posts a card with the prize, the time it ends (everyone sees it in their own time zone, with a countdown) and the “Enter” button. Clicking it enters you; clicking it again takes you out. Only the person who clicks sees the reply, so the channel doesn't fill up with “I'm in”.

Every 30 seconds the bot checks whether a giveaway has ended. When one does, it picks the winners among the entrants, mentions them in the channel by replying to the card and removes the button. If a winner doesn't show up, /reroll picks someone else without repeating anyone who already won.

It picks using the system's randomness (Python's secrets module), not a seed someone could guess. And it doesn't need any special intent: buttons and commands arrive as interactions.

Rather not set it up yourself? Discord bot hosting from €1.50 a month, no contract and a full refund within 72 hours. Or create a free Discord bot, no card needed.

1. Create the bot on Discord

  1. Create the application and get the token

    At discord.com/developers/applications, “New Application”; in the “Bot” tab, “Reset Token” and copy the token. It's the bot's password: don't share it or push it to GitHub. If this is your first bot, the whole process, slowly, is in your first bot in Python.

  2. Invite it with just the permissions it needs

    In “OAuth2” → “URL Generator”, tick “bot” and “applications.commands”, and for permissions “View Channels”, “Send Messages”, “Embed Links” and “Read Message History” (without that last one it can't reply to the card or remove its button). the bot permissions calculator explains each one and gives you the link. Leave the “Privileged Gateway Intents” off.

2. The code

Three files in one folder. The token goes in .env, without quotes. You need Python 3.10 or newer. Giveaways are saved in giveaways.json, which the bot creates by itself next to app.py.

requirements.txt
discord.py
python-dotenv
.env
DISCORD_TOKEN=paste-your-token-here
app.py
import json
import os
import secrets
import time
from pathlib import Path

import discord
from discord import app_commands
from discord.ext import tasks
from dotenv import load_dotenv

load_dotenv()  # reads the token from the .env file

# Giveaways are saved next to the bot: a restart doesn't wipe them.
GIVEAWAYS = Path("giveaways.json")
rng = secrets.SystemRandom()  # the system's randomness: nobody can guess who wins


def load() -> dict:
    return json.loads(GIVEAWAYS.read_text("utf-8")) if GIVEAWAYS.exists() else {}


def save(data: dict) -> None:
    GIVEAWAYS.write_text(json.dumps(data, ensure_ascii=False, indent=2), "utf-8")


class Enter(discord.ui.View):
    def __init__(self):
        super().__init__(timeout=None)  # no expiry: the button keeps working after a restart

    @discord.ui.button(label="Enter", emoji="🎉", style=discord.ButtonStyle.success, custom_id="giveaway:enter")
    async def enter(self, interaction: discord.Interaction, button: discord.ui.Button):
        data = load()
        giveaway = data.get(str(interaction.message.id))
        if giveaway is None or giveaway["ended"]:
            await interaction.response.send_message("This giveaway has already ended.", ephemeral=True)
            return
        entrants = giveaway["entrants"]
        if interaction.user.id in entrants:
            entrants.remove(interaction.user.id)
            text = "You've left the giveaway. If you change your mind, click again."
        else:
            entrants.append(interaction.user.id)
            text = f"You're in! {len(entrants)} entered so far. To leave, click again."
        save(data)
        await interaction.response.send_message(text, ephemeral=True)


class Bot(discord.Client):
    def __init__(self):
        super().__init__(intents=discord.Intents.default())
        self.tree = app_commands.CommandTree(self)

    async def setup_hook(self):
        self.add_view(Enter())  # listens again to the buttons already posted
        await self.tree.sync()  # publishes /giveaway and /reroll
        check.start()


bot = Bot()


async def channel_of(giveaway: dict):
    return bot.get_channel(giveaway["channel"]) or await bot.fetch_channel(giveaway["channel"])


async def announce(message_id: str, giveaway: dict, winners: list[int]) -> None:
    """Mentions the winners by replying to the giveaway card."""
    channel = await channel_of(giveaway)
    if winners:
        mentions = ", ".join(f"<@{w}>" for w in winners)
        text = f"🎉 Congratulations, {mentions}! Prize: **{giveaway['prize']}**."
    else:
        text = f"Nobody entered the **{giveaway['prize']}** giveaway."
    card = discord.MessageReference(message_id=int(message_id), channel_id=channel.id, fail_if_not_exists=False)
    await channel.send(text, reference=card)


async def close_card(message_id: str, giveaway: dict) -> None:
    """Removes the button and notes how many entered."""
    try:
        message = await (await channel_of(giveaway)).fetch_message(int(message_id))
        card = message.embeds[0]
        card.color = 0x888888
        card.set_footer(text=f"Ended · {len(giveaway['entrants'])} entrants")
        await message.edit(embed=card, view=None)
    except (discord.HTTPException, IndexError):
        pass  # the card was deleted: the result is already announced


@tasks.loop(seconds=30)
async def check():
    now = time.time()
    for message_id, giveaway in load().items():
        if giveaway["ended"] or giveaway["end"] > now:
            continue
        # First it closes (nobody else can enter), then it picks.
        data = load()
        data[message_id]["ended"] = True
        save(data)
        giveaway = data[message_id]
        entrants = giveaway["entrants"]
        winners = rng.sample(entrants, min(giveaway["winners"], len(entrants)))
        try:
            await announce(message_id, giveaway, winners)
            await close_card(message_id, giveaway)
        except discord.HTTPException as error:
            print(f"Couldn't announce giveaway {message_id}: {error!r}")
        data = load()  # again: meanwhile, people may have entered other giveaways
        data[message_id]["winner_ids"] = winners
        save(data)


@check.before_loop
async def before_check():
    await bot.wait_until_ready()


@bot.tree.command(name="giveaway", description="Starts a giveaway in this channel")
@app_commands.guild_only()
@app_commands.default_permissions(manage_guild=True)
@app_commands.describe(prize="What's being given away", minutes="How long it lasts: from 1 minute to 28 days (40320)", winners="How many win")
async def giveaway(interaction: discord.Interaction, prize: str,
                   minutes: app_commands.Range[int, 1, 40320], winners: app_commands.Range[int, 1, 20] = 1):
    end = int(time.time()) + minutes * 60
    card = discord.Embed(
        title=f"🎁 Giveaway: {prize}",
        # <t:…:R> is a Discord timestamp: "in 2 hours", in each person's own time zone.
        description=f"Click **Enter** to join.\nEnds <t:{end}:R> (<t:{end}:f>).\nWinners: {winners}",
        color=0x5B4AE8,
    )
    card.set_footer(text=f"Hosted by {interaction.user.display_name}")
    await interaction.response.send_message(embed=card, view=Enter())
    message = await interaction.original_response()
    data = load()
    data[str(message.id)] = {
        "channel": interaction.channel_id, "prize": prize, "winners": winners, "end": end,
        "entrants": [], "ended": False, "winner_ids": [],
    }
    save(data)


@bot.tree.command(name="reroll", description="Picks another winner for an ended giveaway")
@app_commands.guild_only()
@app_commands.default_permissions(manage_guild=True)
@app_commands.describe(message_id="Right-click the giveaway card → “Copy Message ID”", count="How many new winners")
async def reroll(interaction: discord.Interaction, message_id: str, count: app_commands.Range[int, 1, 20] = 1):
    message_id = message_id.strip()
    giveaway = load().get(message_id)
    channel = bot.get_channel(giveaway["channel"]) if giveaway else None
    # It has to be from this server: an id from another server doesn't count here.
    if giveaway is None or not giveaway["ended"] or getattr(getattr(channel, "guild", None), "id", None) != interaction.guild_id:
        await interaction.response.send_message("I can't find an ended giveaway with that id on this server.", ephemeral=True)
        return
    # No repeats: whoever already won doesn't go back in.
    left = [p for p in giveaway["entrants"] if p not in giveaway["winner_ids"]]
    if not left:
        await interaction.response.send_message("There's nobody left to pick.", ephemeral=True)
        return
    new = rng.sample(left, min(count, len(left)))
    await interaction.response.send_message("Picked. I'll announce it in the giveaway channel.", ephemeral=True)
    await announce(message_id, giveaway, new)
    data = load()
    data[message_id]["winner_ids"] += new
    save(data)


@bot.event
async def on_ready():
    print(f"Logged in as {bot.user}")


bot.run(os.environ["DISCORD_TOKEN"])

Why nothing is lost on restart

The button doesn't store anything: when someone clicks it, Discord sends the bot the message id and the button's id (custom_id). That's why the view has timeout=None and a fixed custom_id, and setup_hook registers it with add_view at startup: the card for a week-long giveaway keeps working even if the bot restarts halfway through.

The entrants, the end time and the winners go in giveaways.json, keyed by the card's id. If the bot was off when a giveaway was due to end, it ends it as soon as it's back, on the first 30-second round.

3. Try it

Who can use /giveaway and /reroll can be changed without touching the code in “Server Settings” → “Integrations” → your bot: for example, letting the moderator role use them too.

  1. Start the bot

    In a terminal, in the folder: pip install -r requirements.txt and python app.py. When it says “Logged in as…”, you're set. Commands can take a few minutes to show up the first time.

  2. Run a one-minute giveaway

    In a test channel, /giveaway with the prize “Test”, 1 minute and 1 winner. Click “Enter” with your account and with another one (or a friend's). After the minute, the bot replies to the card with the winner and the button disappears.

  3. Reroll the giveaway

    Turn on “Developer Mode” in your Discord settings → “Advanced”, right-click the giveaway card, “Copy Message ID”, and type /reroll with that id: it picks someone else from the entrants.

If something doesn't work

“This interaction failed” when clicking “Enter”: the bot is offline. As soon as it's back, the same button works (no need to run the giveaway again).

The giveaway ends but no winner is posted: the bot is missing “Send Messages” or “Read Message History” in that channel; check the channel's permissions and the console, where it logs “Couldn't announce…”. If the commands don't show up, what to check when they don't appear; if the bot is online but doesn't reply, my bot isn't responding.

The time on the card is each person's own: Discord writes it from a timestamp. If you want to put one in an announcement by hand, the timestamp generator gives it to you ready to paste.

4. Keep it online all the time (for free)

A giveaway that runs for several days only ends on time if the bot is online: if it lives on your computer and you switch it off, nobody can enter and the winner comes out when you switch it back on. It has to be hosted on a server.

On NapNodes: create the free bot (it starts out on Node.js; for this one, go to “Settings” → “Language” and choose “Switch to Python”), upload the three files in “Files” and click “Start”. The libraries in requirements.txt install themselves at startup and app.py is the startup file that comes set. giveaways.json shows up in “Files” next to app.py.

The free bot has 256 MB, more than enough for this bot even with thousands of entrants, and you renew it with one click every week: if you forget, it stays off until you start it. So you don't have to remember, the Mini plan (512 MB) stays online with nothing to renew (Discord bot pricing). The whole setup is in the Discord bot hosting guide.

Ideas to extend it

Only one role can enter (for example, subscribers or people above a level): in enter(), check whether interaction.user has that role with discord.utils.get(interaction.user.roles, name="Subscriber") and, if not, tell them they can't. With the leveling bot, you already have roles by level.

An /end-giveaway command to finish it early: set its end in the past in giveaways.json and the next round closes it. Or combine it with the moderation bot and the welcome bot in a single one: all three fit in the free bot.

Frequently asked questions

How do I run a giveaway on Discord?

With a bot: it posts the prize with a button to enter and, when time's up, picks winners at random among the entrants. The full code in Python, with /giveaway and /reroll, is in this guide.

How does the bot pick the winner?

At random among everyone who clicked “Enter”, using Python's secrets module, which uses the system's randomness. Nobody can know in advance who's going to win.

Can I reroll a giveaway if the winner doesn't reply?

Yes: /reroll with the id of the giveaway card picks someone else from the entrants, without repeating anyone who already won.

What happens if the bot restarts during a giveaway?

Nothing: the entrants and the end time are in giveaways.json and the button keeps working. If it was due to end while the bot was off, it ends it when it's back.

Where do I keep the giveaway bot online all the time?

On a server. NapNodes has a free 256 MB bot that you renew with one click every week, or the Mini plan, which stays online all the time.

Shall we set it up for you?

Discord bot hosting from €1.50 a month, never oversold: your RAM is yours alone. And if you'd rather start without paying, there's a free Discord bot, no card needed.

Free tool: Discord bot permissions calculator. Tick your bot's permissions and get the number and the invite link; or paste a number and see which permissions it grants.

Free tool: Discord timestamp generator. Pick a date, time and time zone and copy the <t:…> so everyone on Discord sees the time in their own zone, or a countdown.

Keep reading

All Discord bot guides, sorted →