The Serpens Addon Market runs entirely through a Discord bot written in Python using theDocumentation 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.
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
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.User types a command in the addon-market channel
The bot listens for messages in two allowed channels: 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
#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.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.Bot opens an interactive conversation
As soon as a trigger command is received, the bot creates a new entry in the in-memory The bot then asks the user to specify a content type: Addon (or
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: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!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:
title— the display name of the snippetdescription— a short explanation of what it doesauthor— the creator’s nameprice— a price string, orFree- File or URL —
Yesto upload a file directly,Noto provide an external link - Optional
.blendexample file
name, category, blender_version, addon_version, external, and blend.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 Accepted file types are
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..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.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.Bot deletes the user's message and auto-commits to GitHub
After every message the bot processes in an allowed channel, it calls 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.
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: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:| Field | Type | Description |
|---|---|---|
name | string | Display name of the addon |
description | string | Short description |
category | string | Blender category (e.g. "3D View", "Node") |
author | string | Creator’s name |
blender_version | array | Minimum Blender version as [major, minor, patch] |
addon_version | array | Addon version as [major, minor, patch] |
external | boolean | true if hosted on an external site; false for Discord CDN |
url | string | Download URL (CDN or external) |
blend | boolean | true if a .blend source file is included |
blend_url | string | URL of the optional .blend file |
price | string | Price string or empty for free |
user | integer | Discord user ID of the submitter |
serpens_version | integer | Target Serpens major version (e.g. 3) |
snippets.json
Snippet entries share most fields but usetitle instead of name and always include blend_url:
| Field | Type | Description |
|---|---|---|
title | string | Display name of the snippet |
description | string | Short description |
author | string | Creator’s name |
price | string | Price string (most snippets are "Free") |
url | string | Download URL for the .json or .zip snippet file |
blend_url | string | Optional URL of a companion .blend file |
user | integer | Discord user ID of the submitter |
serpens_version | integer | Target Serpens major version |
packages.json
Package entries are the simplest structure, since packages always point to an external download page:| Field | Type | Description |
|---|---|---|
title | string | Display name of the package |
description | string | Short description |
author | string | Creator’s name |
price | string | Price string |
url | string | Link to the download page |
user | integer | Discord user ID of the submitter |
version | integer | Target Serpens major version |
version.json
Theversion.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.
Discord Channels
The bot recognises three specific Discord channels by their numeric IDs:| Channel | ID | Purpose |
|---|---|---|
#addon-market | 767853772562366514 | Primary channel where users run upload, update, and remove commands |
| Commands channel | 768053288989360128 | Secondary channel that also accepts bot commands |
| File storage channel | 785132940278366260 | Private 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[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.