Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/joshuaKnauber/serpens_addon_market/llms.txt

Use this file to discover all available pages before exploring further.

The Serpens Addon Market runs entirely through a Discord bot written in Python using the discord.py library. Community members interact with the bot by sending plain-text messages in designated Discord channels. The bot guides each user through a short, interactive conversation to collect all required metadata, optionally saves uploaded files to a private Discord channel (using that channel as a file host), writes the finished entry to one of three JSON files, and then automatically commits and pushes the change to GitHub — making the new or updated listing visible on the marketplace within minutes.

End-to-End Architecture

1

Bot starts up and connects to Discord

When the bot process starts, discord.Client initialises and the on_ready event handler fires as soon as the connection to Discord is established. The handler prints Connected to Discord! to the console to confirm a successful login before the bot begins processing any messages.
2

User types a command in the addon-market channel

The bot listens for messages in two allowed channels: #addon-market (channel ID 767853772562366514) and the commands channel (ID 768053288989360128). When a user types upload, update, or remove, the bot recognises the intent and opens a new conversation.
upload
If the user has no active conversation yet and types anything else, the bot replies with a usage reminder that includes a random emoji from the random_emoji() function — a built-in helper that picks at random from a list of 27 emoji names (animals, food, and everyday objects) to make the response feel friendly rather than robotic.
3

Bot opens an interactive conversation

As soon as a trigger command is received, the bot creates a new entry in the in-memory open_entries dictionary keyed by the user’s Discord ID. The entry records the operation type (upload_type) and starts with an empty type field:
open_entries[user_id] = {"upload_type": "upload", "type": ""}
The bot then asks the user to specify a content type: Addon (or A), Snippet (or S), or Package (or P). The user can type Cancel at any point to abort, which removes their entry from open_entries and sends the confirmation message: I canceled your upload! Feel free to try again at any time!
4

User provides metadata step by step

Once a content type is chosen, the bot initialises the JSON template for that type and walks the user through each required field in sequence. For example, when uploading a snippet the bot collects:
  1. title — the display name of the snippet
  2. description — a short explanation of what it does
  3. author — the creator’s name
  4. price — a price string, or Free
  5. File or URL — Yes to upload a file directly, No to provide an external link
  6. Optional .blend example file
For addons, the initial metadata comes from a JSON string that Serpens itself generates and the user pastes into the channel. This JSON already contains fields such as name, category, blender_version, addon_version, external, and blend.
Typing Cancel at any step immediately ends the conversation and removes all in-progress data. No partial entry is ever written to the JSON files.
5

Bot saves uploaded files to the private storage channel

When a user chooses to upload a file directly, the bot saves the attachment locally, re-uploads it to a private Discord channel (ID 785132940278366260) as a message attachment, then deletes the local copy. The permanent cdn.discordapp.com URL of that attachment is stored in the entry’s url or blend_url field.
async def save_file(save_file):
    file_name = save_file.filename
    channel = client.get_channel(785132940278366260)
    await save_file.save(file_name)
    fileobject = discord.File(file_name)
    message = await channel.send(file=fileobject)
    os.system("rm " + file_name)
    return message.attachments[0].url
Accepted file types are .zip or .py for addon/snippet files and .blend for example files. External URLs (e.g. Gumroad, BlenderMarket, GitHub) are stored as-is without any re-upload.
6

Bot writes the completed entry to the correct JSON file

Once all metadata and files are collected, the bot appends the new entry to the appropriate data file using one of three helper functions — add_addon(), add_snippet(), or add_package(). For update operations, the old entry is first removed with remove_addon(), remove_snippet(), or remove_package() before the new one is appended.Each function reads the current JSON, appends or removes the entry, then writes the updated content back to disk using json.dumps with indent=4.
7

Bot deletes the user's message and auto-commits to GitHub

After every message the bot processes in an allowed channel, it calls await message.delete() to remove the user’s message from the public chat. This keeps the #addon-market channel clean and prevents metadata snippets or file attachments from lingering in the visible message history.The bot then runs four shell commands to keep the repository in sync:
git add -A
git commit -m"Serverlog"
git pull
git push
This means every upload, update, or removal triggers an immediate commit. The marketplace data on GitHub is always within seconds of being up to date with what the bot just wrote to disk.

Data Store: The Three JSON Files

All marketplace content lives in three JSON files at the root of the repository. Each file holds an array under a top-level key matching the content type.

addons.json

Each addon entry contains the following fields:
FieldTypeDescription
namestringDisplay name of the addon
descriptionstringShort description
categorystringBlender category (e.g. "3D View", "Node")
authorstringCreator’s name
blender_versionarrayMinimum Blender version as [major, minor, patch]
addon_versionarrayAddon version as [major, minor, patch]
externalbooleantrue if hosted on an external site; false for Discord CDN
urlstringDownload URL (CDN or external)
blendbooleantrue if a .blend source file is included
blend_urlstringURL of the optional .blend file
pricestringPrice string or empty for free
userintegerDiscord user ID of the submitter
serpens_versionintegerTarget Serpens major version (e.g. 3)

snippets.json

Snippet entries share most fields but use title instead of name and always include blend_url:
FieldTypeDescription
titlestringDisplay name of the snippet
descriptionstringShort description
authorstringCreator’s name
pricestringPrice string (most snippets are "Free")
urlstringDownload URL for the .json or .zip snippet file
blend_urlstringOptional URL of a companion .blend file
userintegerDiscord user ID of the submitter
serpens_versionintegerTarget Serpens major version

packages.json

Package entries are the simplest structure, since packages always point to an external download page:
FieldTypeDescription
titlestringDisplay name of the package
descriptionstringShort description
authorstringCreator’s name
pricestringPrice string
urlstringLink to the download page
userintegerDiscord user ID of the submitter
versionintegerTarget Serpens major version

version.json

The version.json file tracks the latest stable release of Serpens itself. It is not managed by the bot but is read by Serpens clients to check for updates.
{
  "version": [3, 1, 2],
  "content": [
    "Outputs for interface nodes",
    "Context Overrides",
    "Bugfixes",
    "See the documentation for a full changelog"
  ]
}

Discord Channels

The bot recognises three specific Discord channels by their numeric IDs:
ChannelIDPurpose
#addon-market767853772562366514Primary channel where users run upload, update, and remove commands
Commands channel768053288989360128Secondary channel that also accepts bot commands
File storage channel785132940278366260Private channel used as a file host; all uploaded attachments are sent here so their Discord CDN URLs can be stored in the JSON files
All user messages in the allowed channels are deleted by the bot after processing (await message.delete()). This keeps the #addon-market channel clean and prevents sensitive metadata snippets from remaining visible in the public chat log.

State Management: the open_entries Dictionary

The bot manages concurrent multi-step conversations using a single in-memory Python dictionary called open_entries. Each key is a Discord user ID (an integer); each value is a plain dict that records where that user is in their current upload, update, or remove flow.
open_entries = {}

# Example state after a user has chosen to upload a snippet
# and has provided a title and description:
open_entries[123456789] = {
    "upload_type": "upload",
    "type": "snippet",
    "json": {
        "title": "My Snippet",
        "description": "Does something useful",
        "price": "",
        "url": "",
        "blend_url": "",
        "author": "",
        "serpens_version": 3,
        "user": 123456789
    }
}
The bot advances through the conversation by checking which fields in open_entries[user_id]["json"] are still empty, filling them in one message at a time. When the conversation is complete — or when the user types Cancel — the entry is removed from open_entries with open_entries.pop(user_id).
Because open_entries is stored in memory rather than on disk, in-progress conversations are lost if the bot process restarts. Users would need to start their upload again from the beginning after an unexpected restart.

Build docs developers (and LLMs) love