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
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.
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.
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.
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.
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.
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.
Dedup Ledger
aiosqlite tracks processed_items so each listing is handled once, and records every purchase - persisted across restarts.
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.
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)
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
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 {}
Wire it up.
- 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.
- 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.
- 03
Run the bot
Launch bot.py - the sniper loop opens the session, loads processed items, and starts scanning the market every 10 seconds.
- 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
For educational use. Respect every platform's terms of service and rate limits.