← Trading Systems
Discord ↔ Sierra Chart access

Discord–Sierra Access Bot

A Discord bot that lets community members self‑register their Sierra Chart usernames through a persistent button, then automatically manages access to a custom Sierra Chart DLL study via the Sierra HTTP API. Now in its second generation: a multi‑tenant, license‑gated product where any indicator vendor onboards their own server with a license key.

Python 3.12 discord.py Sierra Chart API Lemon Squeezy licensing Fernet encryption SQLite Docker / Fargate
Live Discord → Sierra flow
Built with agentic AI. Development on this project runs through multi-agent Claude Code workflows — developer → independent-reviewer cycles with evidence gates, batched into unattended runs that execute overnight and resume from committed ledgers. How the agent system works →

Project overview

This project ties together Discord role management and Sierra Chart’s licensing API. A persistent button in a chosen Discord channel launches a modal where users can submit their Sierra Chart username. The bot records a mapping between Discord username and Sierra username in a local SQLite database, and uses Sierra’s CustomDLLStudiesManagement API to grant or revoke access to a compiled study.

Problem solved

Manually onboarding and offboarding users for paid Sierra Chart studies is error‑prone: each change requires editing usernames in Sierra’s control panel, keeping a separate list in Discord, and remembering to revoke access when someone leaves or loses a subscription role.

  • No single place that knows “who in Discord should have access to this Sierra study”.
  • Onboarding relies on DMs and spreadsheets instead of a structured flow.
  • Revoking access when a user leaves or loses a role is easy to forget.

The bot replaces this with a simple, auditable workflow driven directly from Discord and synchronized to Sierra.

Key outcomes

  • One‑click onboarding: members press a button and submit their Sierra username in a modal.
  • Automatic grant/revoke of study access via Sierra’s HTTP API.
  • SQLite database that tracks Discord ↔ Sierra username mappings and access flags.
  • Hooks on member leave and role changes to keep Sierra access in sync with Discord roles.
Discord slash UI Persistent components Sierra Chart API Access automation

How it works

At a high level, the bot exposes a persistent “Fill in Your Sierra Info” button in a configured channel. When users interact with that button, their Sierra username is stored, and the Sierra API is called to add or remove them from the DLL study’s access list. Separate event handlers react to membership and role changes so that Discord and Sierra stay aligned.

Key technologies used

The implementation is intentionally small and focused, built around discord.py and a file‑backed SQLite database, so it can run locally or inside a container.

Representative code excerpt

The heart of the project is a modal submission handler that updates the local mapping table and synchronizes the result to Sierra’s API.

Username capture and Sierra sync

bot.py – simplified modal handler
class InfoModal(ui.Modal, title="Share Your Sierra Username"):
    full_name = ui.TextInput(label="Sierra Chart Username")

    async def on_submit(self, interaction: Interaction):
        discord_user = f"{interaction.user.name}"
        new_username = self.full_name.value

        # Look up existing mappings
        cursor = db_conn.cursor()
        rows = cursor.execute(
            "SELECT mapped_username FROM username_mapping "
            "WHERE discord_username = ? AND username_type = ?",
            (discord_user, "sierra_chart")
        ).fetchall()

        # Insert or update row, then call Sierra API with "add"/"remove" actions
        # depending on whether the username changed.

The production code handles several edge cases: users updating existing usernames, API failures (including rolling back DB rows on error), and composing a human‑readable summary of Sierra API responses back to the user.

v2 — Multi-tenant, licensed product

The single-server bot above proved the workflow; v2 rebuilds it as a product any indicator vendor can use. One bot instance serves many Discord servers, each belonging to a "buyer" who onboards themselves with a license key — turning bespoke automation into per-seat SaaS.

Multi-tenancy & licensing

  • A server admin runs !setup_buyer: a modal collects their Lemon Squeezy license key, Sierra admin credentials, study name, and optional access-granting roles — validated against the licensing API before anything is persisted.
  • Anti-key-sharing at the data layer: the first Discord user to redeem a key owns it; they may reuse it across their own servers, anyone else is rejected.
  • License validation cross-checks product and variant IDs, so a key for a cheaper product cannot unlock this one, with distinct human-readable messages per failure mode.
  • Tenant data model: buyers → guilds → settings → username mappings with a composite primary key and cascade deletes; username_type is already generalized so a second vendor platform needs no migration.

Reliability & security details

  • Sierra admin passwords are encrypted at rest (Fernet), with a documented dev-vs-prod key split.
  • One stable button custom_id for every tenant — the buyer is resolved from where the click happened (guild + channel), sidestepping unbounded component growth.
  • Self-healing UI: on every startup the bot verifies its registration button still exists in each configured channel and reposts it only if the message or component is gone.
  • Compensating writes: if the Sierra API call fails after the local row was inserted, the row is deleted — the database never claims an entitlement the vendor panel doesn't have.
  • Runs as a non-root Docker container targeting AWS Fargate, fully env-driven with fail-fast config validation.

Tenant resolution by location, not by button identity

mydiscordbot_aws/bot.py – persistent button handler
@ui.button(label="Fill in Your Sierra Info", style=ButtonStyle.primary,
           custom_id=PERSISTENT_BUTTON_CUSTOM_ID)
async def open_modal(self, interaction: Interaction, button: ui.Button) -> None:
    # Same custom_id for all servers is intentional: we resolve buyer from WHERE the
    # click happened (guild_id + channel_id), not from the button id.
    row = db_module.get_buyer_for_guild_channel(
        db_conn, interaction.guild.id, interaction.channel.id)
    if not row:
        await interaction.response.send_message(
            "This channel is not set up for Sierra registration. "
            "An admin should run `!setup_buyer` here first.", ephemeral=True)
        return
    await interaction.response.send_modal(
        InfoModal(buyer_id=row["buyer_id"], guild_id=interaction.guild.id))