ASYNC · AIOHTTP · AIOSQLITE

DonutSMP
Discord Bot

An async-first sniper for the DonutSMP market. It scans LZT listings every 10 seconds, scores each account against live DonutSMP stats, and either alerts your channel or fires a retry-backed auto-buy - all from one tight Python core.

Scan interval
10s
Buy retries
100x
Pages per scan
3pg
01Overview

A two-layer sniper, built on a clean async core.

SniperBot runs a 10s loop over the LZT market. New listings get scored against live DonutSMP stats - money, playtime, and shards - then split into two layers: alert-worthy matches post to your channel, and the strictest ones can fire an automatic buy.

Underneath, LZTDonutAPI wraps both APIs behind one async context manager: a shared aiohttp session, an aiosqlite ledger, and a lock that paces every search to the 1s rate limit.

  • 01

    sniper_loop()

    Every 10s, scan up to 3 pages of fresh LZT listings, skipping anything already processed.

  • 02

    get_donut_stats()

    Score each account on live DonutSMP money, playtime, and shards.

  • 03

    Alert layer

    Matches that clear MIN_MONEY, MIN_PLAYTIME, or MIN_SHARDS post an embed with a Buy button.

  • 04

    Auto-buy layer

    If enabled and under AUTO_BUY_MAX_PRICE, fast_buy fires automatically on the strictest matches.

02Features

Engineered for the snipe.

Two-Layer Sniping

Layer one alerts on any listing that clears your money, playtime, or shard floors. Layer two auto-buys only the strictest matches under a price cap.

sniper_loop()

Fast Buy with Retries

Attempts a purchase up to 100 times, sleeping on too_many_requests and bailing cleanly on terminal errors. Every win lands in the SQLite ledger.

fast_buy()

Rate-limited Multi-Page Search

An asyncio lock paces requests 1s apart, and search_minecraft_multi_page sweeps several pages in sequence without ever tripping the limit.

search_minecraft_multi_page()

DonutSMP Stats Integration

Pulls live money, playtime, and shards from the DonutSMP v1 API with a Bearer token, then scores every candidate against your thresholds.

get_donut_stats()

Interactive Buy Button

Alerts ship with a Discord Buy Now button gated to ALLOWED_BUYER_IDS, with live label and color updates from Purchasing to Purchased or Failed.

discord.ui.View

Dedup Ledger

aiosqlite tracks processed_items so each listing is handled once, and records every purchase - persisted across restarts.

aiosqlite
03Source

Read it, copy it, ship it.

The whole bot lives in three files. Set your environment variables, drop these in, and it is ready to scan and snipe.

bot.py
import os
import asyncio
import discord
import aiosqlite
import config
from discord.ext import commands, tasks
from lzt_donut_api import LZTDonutAPI

class BuyButton(discord.ui.View):
    def __init__(self, api: LZTDonutAPI, item_id: str, price: float):
        super().__init__(timeout=None)
        self.api = api
        self.item_id = item_id
        self.price = price

    @discord.ui.button(label="Buy Now", style=discord.ButtonStyle.green, custom_id="buy_button")
    async def buy(self, interaction: discord.Interaction, button: discord.ui.Button):
        if interaction.user.id not in config.ALLOWED_BUYER_IDS:
            return await interaction.response.send_message("You are not authorized to perform this purchase.", ephemeral=True)

        await interaction.response.defer()
        button.disabled = True
        button.label = "Purchasing..."
        await interaction.edit_original_response(view=self)

        success = await self.api.fast_buy(self.item_id, self.price)

        if success:
            button.label = "Purchased!"
            button.style = discord.ButtonStyle.grey
            embed = interaction.message.embeds[0]
            embed.color = discord.Color.grey()
            embed.title = f"[PURCHASED] {embed.title}"
            await interaction.edit_original_response(embed=embed, view=self)
        else:
            button.label = "Failed"
            button.style = discord.ButtonStyle.red
            button.disabled = False
            await interaction.edit_original_response(view=self)

class SniperBot(commands.Bot):
    def __init__(self):
        intents = discord.Intents.default()
        intents.message_content = True
        super().__init__(command_prefix="!", intents=intents)
        self.api = LZTDonutAPI(lzt_token=config.LZT_TOKEN, donut_token=config.DONUT_TOKEN)
        self.processed_items = set()
        self.db_path = "bot_data.db"

    async def init_db(self):
        async with aiosqlite.connect(self.db_path) as db:
            await db.execute("""
                CREATE TABLE IF NOT EXISTS processed_items (
                    item_id TEXT PRIMARY KEY,
                    timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
                )
            """)
            await db.commit()
            async with db.execute("SELECT item_id FROM processed_items") as cursor:
                async for row in cursor:
                    self.processed_items.add(row[0])
        print(f"Loaded {len(self.processed_items)} processed items.")

    async def mark_processed(self, item_id: str):
        self.processed_items.add(item_id)
        async with aiosqlite.connect(self.db_path) as db:
            await db.execute("INSERT OR IGNORE INTO processed_items (item_id) VALUES (?)", (item_id,))
            await db.commit()

    async def setup_hook(self):
        await self.api.__aenter__()
        await self.init_db()
        self.sniper_loop.start()

    async def on_ready(self):
        print(f"Bot logged in as {self.user}")

    @tasks.loop(seconds=10)
    async def sniper_loop(self):
        channel = self.get_channel(config.CHANNEL_ID)
        if not channel: return

        try:
            # Multi-page scanning optimized for 1s delay
            items = await self.api.search_minecraft_multi_page({"pmin": 1, "category_id": 1}, pages=3)
            for item in items:
                item_id = str(item.get("item_id"))
                if item_id in self.processed_items: continue

                await self.mark_processed(item_id)

                username = item.get("username", "Unknown")
                price = float(item.get("price", 0))

                # Fetch DonutSMP Stats
                stats = await self.api.get_donut_stats(username)
                money = stats.get("money", 0)
                playtime = stats.get("playtime", 0)
                shards = stats.get("shards", 0)

                # Layer 1: Alert Criteria (Money OR Playtime OR Shards)
                is_alert_match = (money >= config.MIN_MONEY) or \
                                 (playtime >= config.MIN_PLAYTIME_SECONDS) or \
                                 (shards >= config.MIN_SHARDS)

                if is_alert_match:
                    # Layer 2: Auto-Buy Criteria (Price AND strict thresholds)
                    meets_strict_thresholds = (money >= config.AUTO_BUY_MIN_MONEY) or \
                                             (playtime >= config.AUTO_BUY_MIN_PLAYTIME_SECONDS) or \
                                             (shards >= config.AUTO_BUY_MIN_SHARDS)

                    should_auto_buy = config.AUTO_BUY_ENABLED and \
                                      (price <= config.AUTO_BUY_MAX_PRICE) and \
                                      meets_strict_thresholds

                    embed = discord.Embed(
                        title=f"Sniper Match: {username}",
                        url=f"https://lzt.market/{item_id}/",
                        color=discord.Color.gold()
                    )
                    embed.add_field(name="Price", value=f"{price} RUB", inline=True)
                    embed.add_field(name="Money", value=f"${money:,}", inline=True)
                    embed.add_field(name="Playtime", value=f"{playtime // 86400}d", inline=True)
                    embed.add_field(name="Shards", value=f"{shards:,}", inline=True)
                    embed.set_footer(text=f"Item ID: {item_id}")

                    if should_auto_buy:
                        purchase_success = await self.api.fast_buy(item_id, price)
                        if purchase_success:
                            embed.title = f"[AUTO-BOUGHT] {username}"
                            embed.color = discord.Color.green()
                            await channel.send(embed=embed)
                        else:
                            embed.title = f"[AUTO-BUY FAILED] {username}"
                            embed.color = discord.Color.red()
                            await channel.send(embed=embed, view=BuyButton(self.api, item_id, price))
                    else:
                        # Alert only (no auto-buy)
                        await channel.send(embed=embed, view=BuyButton(self.api, item_id, price))

        except Exception as e:
            print(f"Loop error: {e}")

    @sniper_loop.before_loop
    async def before_sniper_loop(self):
        await self.wait_until_ready()

if __name__ == "__main__":
    bot = SniperBot()
    bot.run(config.DISCORD_BOT_TOKEN)
config.py
import os
from dotenv import load_dotenv

load_dotenv()

# API Keys & Discord
DISCORD_BOT_TOKEN = os.getenv("DISCORD_BOT_TOKEN")
LZT_TOKEN = os.getenv("LZT_TOKEN")
DONUT_TOKEN = os.getenv("DONUT_TOKEN")
CHANNEL_ID = int(os.getenv("CHANNEL_ID", 0))
ALLOWED_BUYER_IDS = [int(i.strip()) for i in os.getenv("ALLOWED_BUYER_IDS", "").split(",") if i.strip()]

# 1. Alert Criteria (General matches sent to Discord)
MIN_MONEY = int(os.getenv("MIN_MONEY", 10_000_000))
MIN_PLAYTIME_DAYS = int(os.getenv("MIN_PLAYTIME_DAYS", 10))
MIN_SHARDS = int(os.getenv("MIN_SHARDS", 10))

# 2. Auto-Buy Criteria (Strict thresholds for automatic purchase)
AUTO_BUY_ENABLED = os.getenv("AUTO_BUY_ENABLED", "False").lower() == "true"
AUTO_BUY_MAX_PRICE = float(os.getenv("AUTO_BUY_MAX_PRICE", 900.0))
AUTO_BUY_MIN_MONEY = int(os.getenv("AUTO_BUY_MIN_MONEY", 100_000_000))
AUTO_BUY_MIN_PLAYTIME_DAYS = int(os.getenv("AUTO_BUY_MIN_PLAYTIME_DAYS", 30))
AUTO_BUY_MIN_SHARDS = int(os.getenv("AUTO_BUY_MIN_SHARDS", 50))

# Derived constants
MIN_PLAYTIME_SECONDS = MIN_PLAYTIME_DAYS * 24 * 60 * 60
AUTO_BUY_MIN_PLAYTIME_SECONDS = AUTO_BUY_MIN_PLAYTIME_DAYS * 24 * 60 * 60
lzt_donut_api.py
import asyncio
import aiohttp
import aiosqlite
import time
from typing import List, Dict, Any, Optional

class LZTDonutAPI:
    """
    Integrates LZT Market and DonutSMP APIs for Turki's Discord bot.
    Requirements: Async-first, aiohttp, aiosqlite, rate-limited LZT searches,
    fast-buy with retries, and DonutSMP stats.
    """

    def __init__(self, lzt_token: str, donut_token: str, db_path: str = "bot_data.db"):
        self.lzt_token = lzt_token
        self.donut_token = donut_token
        self.db_path = db_path
        self.session: Optional[aiohttp.ClientSession] = None
        self._last_lzt_search = 0.0
        self._lzt_search_lock = asyncio.Lock()

    async def __aenter__(self):
        self.session = aiohttp.ClientSession()
        async with aiosqlite.connect(self.db_path) as db:
            await db.execute("""
                CREATE TABLE IF NOT EXISTS purchases (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    item_id TEXT,
                    price REAL,
                    timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
                )
            """)
            await db.commit()
        return self

    async def __aexit__(self, exc_type, exc, tb):
        if self.session:
            await self.session.close()

    async def _lzt_request(self, method: str, endpoint: str, **kwargs) -> Dict[str, Any]:
        url = f"https://api.lzt.market/{endpoint}"
        headers = kwargs.get("headers", {})
        headers["Authorization"] = f"Bearer {self.lzt_token}"
        kwargs["headers"] = headers

        async with self.session.request(method, url, **kwargs) as resp:
            return await resp.json()

    async def search_minecraft(self, params: Dict[str, Any]) -> List[Dict[str, Any]]:
        """
        LZT Search with rate limiting (now 1s delay per request).
        """
        async with self._lzt_search_lock:
            now = time.time()
            elapsed = now - self._last_lzt_search
            if elapsed < 1.0:
                await asyncio.sleep(1.0 - elapsed)

            result = await self._lzt_request("GET", "mc", params=params)
            self._last_lzt_search = time.time()
            return result.get("items", [])

    async def search_minecraft_multi_page(self, base_params: Dict[str, Any], pages: int = 3) -> List[Dict[str, Any]]:
        """
        Scans multiple pages sequentially to respect the 1s rate limit.
        """
        all_items = []
        for page in range(1, pages + 1):
            params = base_params.copy()
            params["page"] = page
            items = await self.search_minecraft(params)
            if not items:
                break
            all_items.extend(items)
        return all_items

    async def fast_buy(self, item_id: str, price: float) -> bool:
        """
        LZT fast-buy with retry_request up to 100 retries.
        """
        for attempt in range(100):
            data = {
                "price": price,
                "retry_request": 1
            }
            res = await self._lzt_request("POST", f"{item_id}/fast-buy", data=data)

            if "item" in res or res.get("status") == "ok":
                async with aiosqlite.connect(self.db_path) as db:
                    await db.execute("INSERT INTO purchases (item_id, price) VALUES (?, ?)", (item_id, price))
                    await db.commit()
                return True

            if res.get("error") == "too_many_requests":
                await asyncio.sleep(1)
            else:
                break
        return False

    async def get_donut_stats(self, username: str) -> Dict[str, Any]:
        """
        DonutSMP stats lookup using Bearer token.
        """
        url = f"https://api.donutsmp.net/v1/stats/{username}"
        headers = {"Authorization": f"Bearer {self.donut_token}"}
        async with self.session.get(url, headers=headers) as resp:
            if resp.status == 200:
                return await resp.json()
            return {}
04Quick start

Wire it up.

install dependencies
$ pip install discord.py aiohttp aiosqlite python-dotenv
  1. 01

    Set your environment

    Put your Discord, LZT, and DonutSMP tokens plus CHANNEL_ID and ALLOWED_BUYER_IDS in a .env file - config.py loads it for you.

  2. 02

    Tune your thresholds

    Set the alert floors (MIN_MONEY, MIN_PLAYTIME_DAYS, MIN_SHARDS) and the stricter auto-buy limits to match your budget.

  3. 03

    Run the bot

    Launch bot.py - the sniper loop opens the session, loads processed items, and starts scanning the market every 10 seconds.

  4. 04

    Snipe or auto-buy

    Matches post to your channel with a Buy button. Flip AUTO_BUY_ENABLED to true and the strictest hits buy themselves.

DonutSMP Bot

built by @iturkii

iturkii

For educational use. Respect every platform's terms of service and rate limits.